Difference between revisions of "TAChart Tutorial: ListChartSource, Logarithmic Axis, Fitting"

From Free Pascal wiki
(Source code)
m (Fixed syntax highlighting; deleted category included in page template)
 
(37 intermediate revisions by 8 users not shown)
Line 1: Line 1:
 +
{{TAChart_Tutorial:_ListChartSource,_Logarithmic_Axis,_Fitting}}
 +
 
== Introduction ==
 
== Introduction ==
After doing the first steps with TAChart in the [[TAChart tutorial: Getting started|Getting Started]] tutorial, here is another tutorial. This one will be more advanced. It will cover the aspects
+
[[File:TAChart_LogAx_Tutorial15.png]]
 +
 
 +
Do you know [[wikipedia:Moore's_law|Moore's law]]? It is in front of you...
  
* How to apply a user-defined chart source
+
When you are reading this you are probably sitting in front of your desktop computer or have your notebook on your knees, or you are holding your smartphone in your hand. A tremendous progress of microtechnology has been happening during the last decades and is still on-going. This is due to miniaturization of electronic devices allowing to pack ever more functions onto a single silicon chip. And this is what Moore's law is about: the number of transistors on a chip has doubled approximately every two years.
* How to create a logarithmic axis
 
* How to fit an equation to data
 
  
In order to have some meaningful data we will have a look at the development of integrated circuits. The reference [http://www.intel.com/pressroom/kits/quickreffam.htm www.intel.com/pressroom/kits/quickreffam.htm] contains a list of microprocessors, their date of market introduction and their number of transistors per chip. We want to plot the transistor count as a function of the year of market introduction, and we will verify [[wikipedia:Moore's_Law|"Moore's law"]] saying that the transistor count doubles approximately every two years.
+
In this tutorial we will take published data of an important manufacturer of microprocessors and use TAChart to plot the number of transistors versus the year of market introduction of the products. From these data we will try to confirm Moore's law.
  
This tutorial will require some math knowledge. You should be familiar with logarithms.
+
You can find the data on the site [http://www.intel.com/pressroom/kits/quickreffam.htm www.intel.com/pressroom/kits/quickreffam.htm] which contains a list of microprocessors, their date of market introduction, and their transistor count per chip.
 +
 
 +
When working through this example you will learn
 +
 
 +
* how to '''enter data at designtime'''
 +
* how to create a '''point series with marks above each data point'''
 +
* how to '''create a logarithmic axis'''
 +
* how to '''fit an equation to data'''.
 +
 
 +
You should have some basic knowledge of how to work with TAChart; if you don't you should have a look at the [[TAChart Tutorial: Getting started|Getting started tutorial]] of TAChart. Of course, you must be familiar with [[Lazarus]] and [[Object Pascal]]. And you should not be too afraid of math, you'll need logarithms and the exponential function.
  
 
== Preparation ==
 
== Preparation ==
Line 14: Line 25:
 
=== Setting up the chart ===
 
=== Setting up the chart ===
 
* Create a new project.
 
* Create a new project.
* Since we will have some long axis labels make the form a bit bigger. I am using 540 x 320 pixels here.  
+
* Since we will get some long axis labels increase the size of the form to something like 540 x 320 pixels.  
* Add a <code>TChart</code> component, align it to alClient, set its <code>BackColor</code> to <code>clWhite</code> and the <code>Grid.Color</code> of each axis to <code>clSilver</code>.
+
* Add a <code>TChart</code> component, align it to <code>alClient</code>, set its <code>BackColor</code> to <code>clWhite</code> and the <code>Grid.Color</code> of each axis to <code>clSilver</code>.
 
* Add a title to the x axis ("Year of market introduction") and to the y axis ("Number of transistors").  
 
* Add a title to the x axis ("Year of market introduction") and to the y axis ("Number of transistors").  
 
* Use the text "Progress in Microelectronics" as the chart's title.  
 
* Use the text "Progress in Microelectronics" as the chart's title.  
 
* Set these font styles to <code>fsBold</code>.  
 
* Set these font styles to <code>fsBold</code>.  
* Maybe it is a good idea to display above reference for our data in the footer - the chart property <code>Foot</code> can be used for that. Note that the property editor of <code>Foot.Text</code> (as well as that of <code>Title.Text</code>) allows to enter linefeeds for multi-lined titles.
+
* Show a reference for our data in the footer. Use the chart's property [[TAChart_documentation#Title_and_footer|<code>Foot</code>]] for that. Note that the property editor of <code>Foot.Text</code> (as well as that of <code>Title.Text</code>) allows to enter multi-lined titles.
 
[[File:TAChart_LogAx_Tutorial1.png]]
 
[[File:TAChart_LogAx_Tutorial1.png]]
  
=== Data ===
+
=== Creating a point series ===
For simplicity, we hard-code the data into our form. Of course it would be more flexible to read the data from a file, but this is a tutorial on TAChart, not on reading data files.  
+
We want to draw each TDataRecord as a single data point - this is called "point series" in other charting programs. The TAChart series editor does not offer this type of series because [[TAChart_documentation#Line_series|<code>TLineSeries</code>]] can do this as well. We only have to set its property <code>ShowPoints</code> to <code>true</code> and to turn off the connecting lines (<code>LineType</code> = <code>ltNone</code>). The symbols are determined by the property <code>Pointer</code>.  
  
For the data, we declare a TDataRecord
+
So, add a LineSeries to the form and set the properties to create a point series. Additionally, let's set the <code>Pointer.Brush.Color</code> to <code>clRed</code>, and the <code>Pointer.Style</code> to <code>psCircle</code> to draw a red circle at each data point.
<source>
 
type
 
  TDataRecord = record
 
    Year: double;
 
    Processor: string;
 
    TransistorCount: double;
 
  end;
 
</source>
 
and the data simply are stored in an array of these records:
 
<source>
 
const
 
  MAXDATA = 10;
 
  Data: array[0..MAXDATA] of TDataRecord = (
 
    (Year:1972; Processor:'4004'; TransistorCount:2300),
 
    (Year:1974; Processor:'8080'; TransistorCount:6000),
 
    (Year:1978; Processor:'8086'; TransistorCount:29000),
 
    (Year:1982; Processor:'80286'; TransistorCount:134000),
 
    (Year:1986; Processor:'80386'; TransistorCount:275000),
 
    (Year:1989; Processor:'80486'; TransistorCount:1.2E6),
 
    (Year:1993; Processor:'Pentium'; TransistorCount:3.1E6),
 
    (Year:1997; Processor:'Pentium II'; TransistorCount:7.5E6),
 
    (Year:2001; Processor:'Xeon'; TransistorCount:42E6),
 
    (Year:2006; Processor:'Core Duo'; TransistorCount:152E6),
 
    (Year:2009; Processor:'Core i7'; TransistorCount:731E6)
 
  );
 
</source>
 
The original table contains much more data, if you want to add more, don't forget to update MAXDATA, the upper index of the array. It should be mentioned that the original table shows the date of market introduction by month and year - for simplicity, I skipped the month and rounded the date to the next nearest calendar year.
 
 
 
=== Creating a point series ===
 
We want to draw each TDataRecord as a single data point - this is called "PointSeries" in other charting programs. The TAChart series editor does not show this type of series because TLineSeries can do this as well. We only have to set its property <code>ShowPoints</code> to <code>true</code> and to turn off the connecting lines (<code>LineType</code> = <code>ltNone</code>). The symbols are determined by the property <code>Pointer</code>.  
 
  
So, add a LineSeries to the form and set the properites to create a point series. Additionally, let's set the <code>Pointer.Brush.Color</code> to <code>clRed</code>, and the <code>Pointer.Style</code> to <code>psCircle</code> to draw a red circle at each data point.
+
== Entering data ==
 +
=== ListChartSource ===
 +
There are many ways that data could be entered into the chart. We will use a [[TAChart_documentation#List_source|ListChartSource]] today. Usually data are loaded into this source dynamically at run-time, for example after reading a file. In our project, however, we want to enter them at design-time by means of the data point editor of the ListChartSource. This has the main advantage that all our changes that we do with the chart are displayed immediately, there is no need to compile the project. As you will see, however, there are some drawbacks as well.
  
== The TUserDefinedChartSource ==
+
The ListChartSource stores the data to be plotted in a list - that's where its name comes from. The list items are so-called <code>TChartDataItem</code> objects containing for each data point:
 +
* the <code>x</code> and <code>y</code> coordinates,
 +
* a <code>Text</code> to be used for data marks,
 +
* a <code>Color</code> that overrides the SeriesColor,
 +
* as well as a <code>YList</code> which contains additional y values which are needed in some specialized series types.
  
In the [[TAChart tutorial: Getting started|Getting Started]] tutorial, we added the data directly to the built-in chart source of the series. However, then we would have the data in memory twice: in the array shown above, and in the series - not so good: We would be wasting memory, and we would have to synchronize both storages when data points are added, deleted or edited.  
+
The ListChartSource is the same type of chart source that is used by many series classes internally. Having it as a separate component, however, has the big advantage that the same chart source can be used again for other series - we will need this feature later when applying the FitSeries.
  
 
[[File:TAChart_LogAx_Tutorial2.png|left]]
 
[[File:TAChart_LogAx_Tutorial2.png|left]]
To avoid this, we add a <code>TUserDefinedChartSource</code> to the form, it is the third icon from the left in the Chart component palette. This chart source is made to interface to any kind of data storage. You just have to write an event handler for <code>OnGetChartDataItem</code> in order to define the data. For this purpose, the event takes a <code>var</code> parameter <code>AItem</code> of type <code>TChartDataItem</code> that is defined as follows (with elements omitted that are not needed here):
+
We start by adding a TListChartSource to our form. This is the second icon in the chart component palette. Connect the series to this chart source by pointing its property <code>Source</code> to the new <code>ListChartSource1</code>.
  
<source>
+
=== The ListChartSource data points editor ===
type
+
[[File:TAChart_LogAx_Tutorial16.png]]
  TChartDataItem = object
 
    X, Y: Double;
 
    Color: TChartColor;
 
    Text: String;
 
    // ...
 
  end; 
 
</source>
 
  
<code>X</code> and <code>Y</code> indicate the coordinates of the data point. The field <code>Text</code> can be used to assign a string to each data point. This string can be displayed in the chart above each data point. For this purpose, the series has a property <code>Marks</code> which determines how this string is composed. <code>Marks.Style</code> selects whether the <code>x</code> or <code>y</code> value, or the <code>Text</code> label is displayed (<code>smsXValue</code>, <code>smsValue</code>, <code>smsLabel</code>, respectively), and there are even more options.  
+
Clicking at the ellipsis button next to the property <code>DataPoints</code> opens the datapoints editor of the chart source. This is a string grid into which you can enter the data. Type in the data from the preceding image. They are an excerpt from the web site mentioned in the introduction. This site shows the time of market introduction by year and month - for simplicity, I skipped the month and rounded to the nearest calendar year. Enter the years into column '''X'''. Column '''Y''' will get the number of transistors per chip. The name of each microprocessor will go into column '''Text'''. Since we do not want to give each data point an individual color we leave the column '''Color''' alone - but, of course, you can experiment with that feature.
  
In our case, it may be a good idea to show the processor name above each data point. So we set the series' <code>Marks.Style</code> to <code>smsLabel</code>.
+
When you close the datapoints editor the series is automatically updated and displays the current data set. Wow!
  
Marks usually have a white connecting line to the data point. Since our chart background is white as well these connecting lines are not visible. Open the series property <code>Marks.LinkPen</code> and set its color to <code>clGray</code>.
+
=== Displaying datapoint marks ===
 +
Why don't we see the processor names that we entered in the grid? [[TAChart_documentation#Marks|Data point marks]] are turned off by default. Select the series and go to <code>Marks</code>. Look at the subproperty <code>Style</code>, it is <code>smsNone</code> which means "off". Open the dropdown list. You will see a variety of options how the data points can be labeled. Play with these settings to learn what they mean. Here we want to use <code>smsLabel</code> which displays the <code>Text</code> field of the <code>TChartDataItem</code>. In order to assign '''Marks.Style''' through source code, <code>uses ... TAChartUtils</code> shall be added.
  
We could even give each data point an individual color by assigning a corresponding value to the property <code>Color</code>, but we don't want to use this feature here.
+
There is also a connecting line between the label and the datapoint. Its color is white by default, that's why we cannot see it on the white background. The property name for this line is <code>LinkPen</code>. You may want to set the <code>LinkPen.Color</code> to <code>clGray</code>.
  
Now we can write the event handler for <code>OnGetChartDataItem</code> as follows:
+
The position of the marks is OK here, but you should know that the series has a property [[TAChart_documentation#Mark_positions_and_attachment|<code>MarkPositions</code>]] to control positioning to some degree.
  
<source>
+
[[File:TAChart_LogAx_Tutorial3.png]]
procedure TForm1.UserDefinedChartSource1GetChartDataItem(
 
  ASource: TUserDefinedChartSource; AIndex: Integer; var AItem: TChartDataItem);
 
begin
 
  AItem.X := Data[AIndex].Year;
 
  AItem.Y := Data[AIndex].TransistorCount;
 
  AItem.Text := Data[AIndex].Processor;
 
end;
 
</source>
 
 
 
<code>AIndex</code> is the index of the datapoint which is queried. Since both chart source and our data array begin at index 0 we just look into our data array at this index and copy the data to the corresponding elements of the <code>AItem</code>.
 
 
 
There are still some important things to do:
 
* Tell the <code>UserDefinedChartSource</code> how many data points the external data array contains. We have to enter this number in the property <code>PointsNumber</code> of the UserDefinedChartSource. In our case, this is the value of <code>MAXDATA+1</code> (+1 because counting starts at 0), i.e. 11.
 
* Tell the series to use the UserDefinedChartSource instead of the built-in source. For this purpose, we point the series' propery <code>Source</code> to <code>UserDefinedChartSource1</code>.
 
* Since the UserDefinedChartSource does not know anything about the structure of the external data we have to notify it whenever data are available or have been changed. We do this by calling <code>UserDefinedChartSource1.Reset</code> at an appropriate place. Sometimes you are lucky that some other method may have done this already. But keep in mind that whenever a chart with a UserDefinedChartSource does not behave as you expect there is a chance that you may have forgotten to call <code>Reset</code>. Since our data are hardcoded in the project source the form's <code>OnCreate</code> event handler is a good place to do this:
 
 
 
<source>
 
procedure TForm1.FormCreate(Sender: TObject);
 
begin
 
  UserDefinedChartSource1.Reset;
 
end;
 
</source>
 
  
Now we can compile the project for the first time.  
+
Maybe we compile the project at this time.
  
[[File:TAChart_LogAx_Tutorial3.png]]
+
There is nothing new - we saw everything already at design-time. This is a great advantage of working with the datapoints editor of a ListChartSource.
  
As you can see, there are two things which can be improved at this stage:
+
There are two things which can be improved at this stage:
* The point labels are cut off at the edges of the chart. We can fix this by increasing the chart's <code>Margin.Left</code> and <code>Margin.Right</code> to 24. <code>Margin</code> defines the space surrounding the inner plot area which is kept free from plotting. There is also a property <code>MarginExternal</code> which surrounds the outer border of the chart and can be used to change the distance to neighboring controls.
+
* The point labels are cut off at the edges of the chart. We can fix this by increasing the chart's <code>Margin.Left</code> and <code>Margin.Right</code> to 24. [[TAChart_documentation#Margins|<code>Margin</code>]] defines the space surrounding the inner plot area to keep it free from data points. There is also a property [[TAChart_documentation#Margins|<code>MarginExternal</code>]] which surrounds the outer border of the chart and can be used to change the distance to neighboring controls.
* Since almost all data points are crowded at the bottom of the chart the plot not very meaningful. This is why we need a '''logarithmic axis'''.
+
* Since almost all data points are crowded at the bottom of the chart the plot is not very meaningful. This is why we need a '''logarithmic axis'''.
  
== Using a logarithmic axis ==
+
== Setting up a logarithmic axis ==
Plotting data on a logarithmic axis means that the logarithms of the data values are plotted, not the values directly. The y values in our diagram, for example, range between 2300 and 731 millions. When we calculate the (decadic) logarithms, the range is only between about 3.3 and 7.1 - in such a diagram the data can be distinguished much easier.
+
Plotting data on a logarithmic axis means that the logarithms of the data values are plotted, not the values directly. The y values in our diagram, for example, range between 2300 and 731 million. When we calculate the ([[wikipedia:Common_logarithm|decadic]]/log<sub>10</sub>) logarithms, the range is only between about 3.3 and 7.1 - in such a diagram the data can be distinguished much easier.
  
 
=== TChartTransformations and LogarithmicAxisTransform ===
 
=== TChartTransformations and LogarithmicAxisTransform ===
 
[[File:TAChart_LogAx_Tutorial4.png|left]]
 
[[File:TAChart_LogAx_Tutorial4.png|left]]
Calculating the logarithm can be done by TAChart automatically. In fact, there is an entire group of components for this and other axis transformations: <code>TChartAxisTransformations</code>.  
+
Calculating the logarithm can be done by TAChart automatically. The main advantage is that the original data units are drawn on the axis while the logarithms are used for plotting.  
  
An axis transformation is a function which maps "real world" data in units as displayed on the axis (''"axis coordinates"'') to internal units that are common to all series in the same chart (''"graph coordinates"''). The axis coordinate of the transistor count of the 4004 processor, for example, is 2300, the graph coordinate is the logarithm of that number, i.e. log<sub>10</sub>(2300) = 3.36.
+
In fact, there is an entire group of components for axis transformations: [[TAChart_documentation#Axis_transformations|<code>TChartAxisTransformations</code>]]. An axis transformation is a function which maps "real world" data in units as displayed on the axis (''"axis coordinates"'') to internal units that are common to all series in the same chart (''"graph coordinates"''). The axis coordinate of the transistor count of the 4004 processor, for example, is 2300, the graph coordinate is the logarithm of that number, i.e. log<sub>10</sub>(2300) = 3.36.
  
TAChart provides a variety of transforms. In addition to the logarithm transform, there is a '''linear transform''' which allows to multiply the data by a factor and to add an offset. The '''auto-scaling''' transform is useful when several independently scaled series have to be drawn on the same axis. The '''user-defined transform''' allows to apply any arbitrary transformation.
+
TAChart provides a variety of transforms. In addition to the logarithm transform, there is a [[TAChart_documentation#Linear_and_logarithmic_transformation|linear transform]] which allows to multiply the data by a factor and to add an offset. The [[TAChart_documentation#Auto-scaling_transformation|auto-scaling transform]] is useful when several independently scaled series have to be drawn on the same axis. The [[TAChart_documentation#User-defined_source|user-defined transform]] allows to apply any arbitrary transformation.
  
 
[[File:TAChart_LogAx_Tutorial5.png]]
 
[[File:TAChart_LogAx_Tutorial5.png]]
Line 133: Line 92:
 
Add a <code>TAChartTransformations</code> component to the form, double-click on it (or right-click on it in the object tree and select "Edit axis transformations"), click on "Add" and select "Logarithmic". This will create a <code>ChartAxisTransformations1LogarithmAxisTransform1</code> component - what a name! Anyway, there's a high chance that you will not have to type it...  
 
Add a <code>TAChartTransformations</code> component to the form, double-click on it (or right-click on it in the object tree and select "Edit axis transformations"), click on "Add" and select "Logarithmic". This will create a <code>ChartAxisTransformations1LogarithmAxisTransform1</code> component - what a name! Anyway, there's a high chance that you will not have to type it...  
  
In the object inspector, you will see only a few properties - the most important one is <code>Base</code>. This is the base of the logarithm to be calculated. Change it to 10, since we want to calculate the decadic logarithms.
+
In the object inspector, you will see only a few properties - the most important one is <code>Base</code>. This is the base of the logarithm to be calculated. Change it to 10, since we want to calculate the [[wikipedia:Common_logarithm|decadic logarithms]] - this case comprises 99% of all logarithmic charts.
 +
 
 +
A side-note: it is very important that you '''save the project at this time'''. Why? You will see in a minute...
 +
 
 +
Now we must identify the axis which is to be transformed. For this purpose each axis has a property <code>Transformations</code>. In our case, the huge numbers are plotted on the y axis. So, go to the left axis and set its property <code>Transformations</code> to <code>ChartAxisTransformations1</code>.  
  
Now we must identify the axis which is to be transformed. For this purpose each axis has a property <code>Transformations</code>. In our case, the huge numbers are plotted on the y axis. So, go to the left axis and connect it to <code>ChartAxisTransformations1</code> by assigning the <code>Transformations</code> property accordingly. Ignore the strange axis labels for the moment.  
+
[[File:TAChart_LogAx_Tutorial17.png]]
  
Compile the program. Oh, it crashes due to a floating point exception. What is wrong? Well, TChart may contain several axes, and the series does not yet know which axis it belongs to. Normally, this is not a problem, but when transformations are involved the series' properties <code>AxisIndexX</code> and <code>AxisIndexY</code> have to be set properly. <code>AxisIndexX</code> is the index of the x axis in the chart's <code>AxisList</code>, <code>AxisIndexY</code> accordingly. When you look at the object tree you will see that the left axis has index 0 and the bottom axis has index 1. So we have to set <code>AxisIndexX = 1</code> and <code>AxisIndexY = 0</code>. (To be exact, we can leave the <code>AxisIndexX</code> unchanged since it is not involved in a transformation. But let us be complete to avoid unnecessary debugging sessions in the future...)
+
Oh - what's that? An error message pops up reporting a range check error. Hopefully you had saved the project. If you click on Cancel Lazarus will shut down, everything will be lost. If you click on OK suddenly the chart is gone, it reappears when you click somewhere, but the faulty axis is hidden ...
  
Now the program compiles and runs. The data points spread nicely across the y axis range. But what's wrong with the y axis labels? And the years on the x axis are too close and partly overlap.
+
Are you getting desperate?
 +
 
 +
This is the main disadvantage of working at design-time. If something goes wrong, there is no debugger, no indication of what caused the trouble. Since components are compiled into Lazarus you would have to debug the IDE. Sounds complicated...
 +
 
 +
Let's sit down and think what we did. We assigned the logarithmic transform to the y axis. The transform does not yet have any connection to the data, therefore the data are still in "real" units, their maximum is 731 million. But the transform "thinks" that the data are already in graph units (logarithms). When it calculates the axis labels in axis units it takes hundreds of millions to the power of 10! That's what causes the range check error. However, the situation is not always so dramatic; the least thing that could happen is that the data are not transformed, but the axis is.
 +
 
 +
What can we do against that? Each series has properties <code>AxisIndexX</code> and <code>AxisIndexY</code>. The transform can use this information to calculate the logarithms of the correct coordinates before the axis labels are updated. This solves our problem.
 +
 
 +
So, click on Cancel to shutdown Lazarus. Restart and reload the project. In the saved state, the transformation is not yet connected to the axis.
 +
 
 +
Now have a look at the object tree above the object inspector and you will see that the left axis has index 0. So set the series' <code>AxisIndexY</code> to this value. Although not necessary it may be a good idea to disarm also the x axis by assigning its index 1 to the <code>AxisIndexX</code> of the series - who can guarantee that we won't transform the x axis in the future?
 +
 
 +
After this is done you can set the <code>LeftAxis.Transformation</code> to <code>ChartTransformations1</code> without the range check error.
 +
 
 +
With the logarithm transform activated the data points spread nicely across the y axis range now. But what's wrong with the y axis labels? And the years on the x axis are too close and partly overlap.
  
 
[[File:TAChart_LogAx_Tutorial6.png]]
 
[[File:TAChart_LogAx_Tutorial6.png]]
Line 146: Line 123:
  
 
=== Automatic finding of axis labels ===
 
=== Automatic finding of axis labels ===
For automatic label positioning, each axis has a property <code>Intervals</code> which gives access to several, partly mutually excluding parameters - please see [[TAChart_documentation]] for an explanation. In case of the logarithmic axis the issue is usually caused by the fact that the option <code>aipGraphCoordinates</code> is not set. This option, if set, enforces calculation of the tick intervals for the transformed data ("graph coordinates"), not the "real world" data ("axis coordinates"). So, set <code>aipGraphCoordinates</code> in the <code>LeftAxis.Intervals.Options</code> and compile again.
+
For automatic label positioning, each axis has a property [[TAChart_documentation#Axis_intervals|<code>Intervals</code>]] which gives access to several, partly mutually excluding parameters - please see [[TAChart_documentation#Axis_intervals|TAChart documentation]] for an explanation. In case of the logarithmic axis the issue is usually caused by the fact that the option <code>aipGraphCoordinates</code> is not set. This option, if set, enforces calculation of the tick intervals for the transformed data ("graph coordinates"), not the "real world" data ("axis coordinates"). So, set <code>aipGraphCoordinates</code> in the <code>LeftAxis.Intervals.Options</code>. The labels jump to more evenly distributed locations.
  
 
[[File:TAChart_LogAx_Tutorial7.png]]
 
[[File:TAChart_LogAx_Tutorial7.png]]
Line 153: Line 130:
  
 
You can improve the quality of label presentation in the following way:
 
You can improve the quality of label presentation in the following way:
* Moderately increase the <code>Intervals.Tolerance</code> to something like 5.
+
* Increase the <code>Intervals.Tolerance</code>. This allows for varying spacing between tick marks.
* Adjust the range in which the label distance can vary. This is defined by the properties <code>Intervals.MaxLength</code> and <code>Intervals.MinLength</code>. The optimum value depends on the size of the chart and on the range of the data. In our example project, good labels are obtained by setting these properties to 100 and 50, respectively.  
+
* Adjust the range, in pixels, in which the label distance can vary. This is defined by the properties <code>Intervals.MaxLength</code> and <code>Intervals.MinLength</code>. The optimum value depends on the size of the chart and on the range of the data. In our example project, good labels are obtained by setting these properties to 100 and 50, respectively. Usually <code>Intervals.MaxLength</code> provides better results.
  
 
In the same way, the overlapping year labels of the x axis can be addressed. Just increase the <code>BottomAxis.Intervals.MaxLength</code> to 70.
 
In the same way, the overlapping year labels of the x axis can be addressed. Just increase the <code>BottomAxis.Intervals.MaxLength</code> to 70.
  
What is left now is the "1" that appears at the y axis between "10000000" and "1E009". This is due to a bug of some FPC versions. If you have that issue like me, simply change the property <code>LeftAxis.Marks.Format</code>. This string is passed to the <code>Format</code> function to convert the numbers to strings. The format specifier "%0.0n" for example avoids that conversion error and, additionally, adds nice thousand separators to the labels which makes them much more readable.  
+
What is left now is the "1" that appears at the y axis between "10000000" and "1E009". This is due to a bug in some FPC versions. If you have that as well, simply change the property <code>LeftAxis.Marks.Format</code>. This string is passed to the <code>Format</code> function to convert the numbers to strings. The format specifier "%0.0n" for example avoids that conversion error and, additionally, adds nice thousand separators to the labels which makes them much more readable.  
  
 
[[File:TAChart_LogAx_Tutorial8.png]]
 
[[File:TAChart_LogAx_Tutorial8.png]]
  
 
=== Manual finding of axis labels ===
 
=== Manual finding of axis labels ===
This is the best we can do with automatic label positioning. It is not perfect because when we increase the height of the window the half-decade values may appear, or the label interval may be two decads as in above figure.
+
This is the best we can do with automatic label positioning. It is not perfect because when we increase the height of the window, or zoom in, the half-decade values may appear, or the label interval may be two decades as in above figure.
  
 
If you are not happy with that you have to use '''manual axis label selection'''. For this purpose, each axis has a property <code>Source</code> which can be linked to a ListChartSource containing only the allowed axes labels. So when this chart source contains only full-decade labels there is no risk of half-decade labels or omitting every other label. On the other hand, when you zoom into the chart you may come to a point where no labels are visible any more.  
 
If you are not happy with that you have to use '''manual axis label selection'''. For this purpose, each axis has a property <code>Source</code> which can be linked to a ListChartSource containing only the allowed axes labels. So when this chart source contains only full-decade labels there is no risk of half-decade labels or omitting every other label. On the other hand, when you zoom into the chart you may come to a point where no labels are visible any more.  
  
Add a <code>TListChartSource</code> to the form, and populate it in the <code>FormCreate</code> event:
+
Add a second ListChartSource to the form. You can use the DataPoints editor again to enter the full-decade numbers. This has the advantage that you can complete most of this project without writing a single line of code!
 +
 
 +
But you could also populate the listsource easily in the <code>FormCreate</code> event:
  
<source>
+
<syntaxhighlight lang=pascal>
 
procedure TForm1.FormCreate(Sender: TObject);
 
procedure TForm1.FormCreate(Sender: TObject);
 
const
 
const
Line 180: Line 159:
 
   for i:=MIN to MAX do begin
 
   for i:=MIN to MAX do begin
 
     value := Power(10, i);
 
     value := Power(10, i);
     ListChartSource1.Add(value, value);
+
     ListChartSource2.Add(value, value);
 
   end;
 
   end;
 
end;
 
end;
</source>
+
</syntaxhighlight>
  
In this procedure, an integer between 0 and 12 is taken to the power of 10, and the result is stored in the ListChartSource by means of its <code>Add</code> procedure. The first parameter in this call is the x, the second parameter the y value of the TChartDataItem stored in the chart source. We pass the result to both x and y values which is some kind of overkill, but it has the advantage that we'd already have labels if we'd once decide to draw the x axis logarithmically as well. Similarly, the range of labels between 1E0 and 1E12 is a bit generous for the same reason of flexibility.
+
This procedure adds powers of 10 in a wide enough range to the ListChartSource by means of its method <code>Add</code>.  
  
Connect <code>ListChartSource1</code> to <code>LeftAxis.Marks.Source</code> to activate the manual labels of the ListChartSource. You should also remove all flags from the <code>Options</code> property. Otherwise automatic tick finding will still be active to some degree.
+
Connect <code>ListChartSource1</code> to <code>LeftAxis.Marks.Source</code> to activate the manual labels of the ListChartSource. You should also remove all flags from the <code>Options</code> property. Otherwise automatic tick finding will still be active to some degree. If you did not use the DataPoints editor you must compile to see the effect.  
  
 
[[File:TAChart_LogAx_Tutorial9.png]]
 
[[File:TAChart_LogAx_Tutorial9.png]]
Line 199: Line 178:
  
 
== Fitting ==
 
== Fitting ==
Now let's look for a relation between the data, i.e. we want to find a mathematical formula which is able to describe the dependence of transistor count on market introduction year.This is called "fitting": we select a forumula with parameters and adjust the parameters such that the deviation to the data is at minumum.
+
Now let's look for a relation between the data, i.e. we want to find a mathematical formula which describes the dependence of transistor count on market introduction year. This is called "fitting": we select a formula with parameters and adjust the parameters such that the deviation to the data is as small as possible.
  
TAChart does not contain a full-fledged fitting engine. It just "borrows" the fitting routines from the FPC numerical library ([[numlib]]). Therefore, TAChart cannot address all variants of fitting, but it covers the most important case, fitting of a [[wikipedia:Polynomial|polynomial]] by means of the [[wikipedia:least squares|linear least squares technique]]. This is about the level available to Excel users when they add a "trend line" to their chart.
+
TAChart does not contain a full-fledged fitting engine. It uses the fitting routines from the FPC numerical library [[numlib]]. Therefore, TAChart cannot address all variants of fitting, but it covers the most important case, fitting of a [[wikipedia:Polynomial|polynomial]] by means of the [[wikipedia:Least squares|linear least squares technique]]. This is about the level available to Excel users when they add a "trend line" to their chart.
  
 
=== TFitSeries ===
 
=== TFitSeries ===
  
TAChart provides a specialized <code>TFitSeries</code> for fitting. This series has a property <code>FitEquation</code> which defines the formula that is used:
+
TAChart provides a specialized [[TAChart_documentation#Fit_series|<code>TFitSeries</code>]] for fitting. This series has a property <code>FitEquation</code> which defines the formula that is used:
  
* <code>fePolynomial</code>: y = a<sub>0</sub> + a<sub>1</sub> x + ... + a<sub>n</sub> x<sup>n</sup>. Specify the number of fitting parameters a<sub>i</sub> by the property <code>ParamCount</code> = n + 1.
+
* <code>fePolynomial</code>: <i>y</i> = <i>b</i><sub>0</sub> + <i>b</i><sub>1</sub><i>x</i> + <i>b</i><sub>2</sub><i>x</i><sup>2</sup> + &hellip; + <i>b<sub>n</sub>x</i><i><sup>n</sup></i>. Specify the number of fitting parameters <i>a<sub>i</sub></i> by the property <code>ParamCount</code> = <i>n</i> + 1.
* <code>feLinear</code>: y = a + b x - this is a special case of the general polynomial with n=1 and fitting parameters a and b. It is made available as a separate item because straight lines define the most important fitting conditions.
+
* <code>feLinear</code>: <i>y</i> = <i>a</i> + <i>bx</i> -- this is a special case of the general polynomial with <i>n</i> = 1 and fitting parameters <i>a</i> and <i>b</i>. It is made available as a separate item because straight lines define the most important fitting conditions.
* <code>feExp</code>: y = a e<sup>b x</sup> - This equation can also be reduced to the polynomial case although this is not straightforward to see. But take the (natural) logarithm of this equation, and you get to ln(y) = a + b x. Now when we fit ln(y) instead of y we have the linear case again.
+
* <code>feExp</code>: <i>y</i> = <i>a</i> * <i>e<sup>bx</sup></i> -- This equation can also be reduced to the polynomial case although this is not straightforward to see. But take the (natural) logarithm of this equation, and you get to ln(<i>y</i>) = <i>a</i> + <i>bx</i>. Now when we fit ln(<i>y</i>) instead of <i>y</i> we have the linear case again.
* <code>fePower</code>: y = a x<sup>b</sup>. Again, this can be reduced to a linear equation by a logarithmic transformation.
+
* <code>fePower</code>: <i>y</i> = <i>a</i> * <i>x<sup>b</sup></i>. Again, this can be reduced to a linear equation by a logarithmic transformation.
  
 
[[File:TAChart_LogAx_Tutorial11.png]]
 
[[File:TAChart_LogAx_Tutorial11.png]]
Line 216: Line 195:
 
Enough of theory. Let's add a FitSeries to the chart: double-click on the chart, and in the series editor click on "Add" and select the entry "Least squares fit series" from the dropdown list.
 
Enough of theory. Let's add a FitSeries to the chart: double-click on the chart, and in the series editor click on "Add" and select the entry "Least squares fit series" from the dropdown list.
  
At first, we need to tell the fit series where it finds its data. For this purpose, we connect the series' <code>Source</code> with the <code>UserDefinedChartSource1</code> as we had done with the line series. You see: the same chart source can be used for several series.
+
At first, we need to tell the fit series where it finds its data. For this purpose, we connect the series' <code>Source</code> with <code>ListChartSource1</code> as we had done with the line series. You see: the same chart source can be used for several series.
  
Moreover, don't forget to set <code>AxisIndexX</code> and <code>AxisIndexY</code> to the axis index of the bottom and left axes as we did with the line series. If you don't your program will crash most probably.
+
You hopefully remember the disaster above with the AxisIndex. So, set the <code>AxisIndexY</code> to the index of the left axis as we did with the line series.  
  
 
Which one of the four <code>FitEquation</code> possibilities do we select? Well, the data look like lying on a straight line. So let's select <code>feLinear</code>.
 
Which one of the four <code>FitEquation</code> possibilities do we select? Well, the data look like lying on a straight line. So let's select <code>feLinear</code>.
Line 226: Line 205:
 
Oops... We see the black fitted curve, but it does not "fit" at all. And we wanted a straight line, but we get a twisted curve. How can this be?
 
Oops... We see the black fitted curve, but it does not "fit" at all. And we wanted a straight line, but we get a twisted curve. How can this be?
  
The reason is the logarithmic transform that we applied to the y data. Therefore, our plot shows the logarithms, but the fit takes the "raw" data. We are effectively fitting the straight line to the data in the first screen shot of this tutorial where the log transform had not yet been introduced - it is clear that the line would not "fit". And when the fitted function is drawn the log transform distorts the straight line to the twisted curve that we see.
+
The reason is the logarithmic transform that we applied to the y data. Therefore, our plot shows the logarithms, but the fit takes the "raw" data. We are effectively fitting the straight line to the data in the screenshot in the section [[#Displaying_datapoint_marks|Displaying datapoint marks]] where the log transform had not yet been introduced - it is clear that the line would not "fit" there. And when the fitted function is drawn the log transform distorts the straight line to the twisted curve that we see.
  
 
On the other hand, if the '''log data''' follow a straight line our fitting law is not linear, but exponential. Let's set <code>FitEquation</code> to <code>feExp</code> and try again.
 
On the other hand, if the '''log data''' follow a straight line our fitting law is not linear, but exponential. Let's set <code>FitEquation</code> to <code>feExp</code> and try again.
Line 234: Line 213:
 
Ah - much better!
 
Ah - much better!
  
Now we know that the exponential law, y = a e<sup>b x</sup>, is a good description of our data. But how do we get the fitting parameters <code>a</code> and <code>b</code>?
+
Now we know that the exponential law, <i>y</i> = <i>a</i> * <i>x<sup>b</sup></i>, is a good description of our data. But how do we get the fitting parameters <i>a</i> and <i>b</i>?
  
 
=== Fit results ===
 
=== Fit results ===
The fit series has a public array property <code>Param</code> which contains the fitting parameters. <code>a</code> is in <code>Params[0]</code>, and <code>b</code> is in <code>Params[1]</code>. Of course, these values are correct only when a valid fit has been performed. How do we know that? Well, the fit series provides an event <code>OnFitComplete</code> that is generated when the fit complete successfully. That's where we can evaluate the obtained fit parameters. As an example, let's display the fit results in a message:
+
The fit series has a public array property <code>Param</code> which contains the fitting parameters. <code>a</code> is in <code>Params[0]</code>, and <code>b</code> is in <code>Params[1]</code>. Of course, these values are correct only when a valid fit has been performed. How do we know that? Well, the fit series provides an event <code>OnFitComplete</code> that is generated when the fit complete successfully. That's where we can evaluate the obtained fit parameters. As an example, let's display the fit results in a message along with the fit equation:
  
<source>
+
<syntaxhighlight lang=pascal>
 
procedure TForm1.Chart1FitSeries1FitComplete(Sender: TObject);
 
procedure TForm1.Chart1FitSeries1FitComplete(Sender: TObject);
 
begin
 
begin
 
   with Chart1FitSeries1 do
 
   with Chart1FitSeries1 do
     ShowMessage(Format('Fit result: a = %g, b = %g', [Param[0], Param[1]]));
+
     ShowMessage(Format(
 +
      'Fit result: a = %g, b = %g', [
 +
      Param[0], Param[1]
 +
    ]));
 
end;   
 
end;   
</source>
+
</syntaxhighlight>
  
 
And that's what we get:
 
And that's what we get:
Line 251: Line 233:
 
[[File:TAChart_LogAx_Tutorial14.png]]
 
[[File:TAChart_LogAx_Tutorial14.png]]
  
Now we want to calculate the time until the number of transistors on a chip is doubled. Let's say that there are y<sub>1</sub> and y<sub>2</sub> transistors on a chip at times x<sub>1</sub> and x<sub>2</sub>, respectively. Knowing our exponential relationship (y = a e<sup>bx</sup>), we can calculate the ratio
+
Now we want to calculate the time T until the number of transistors on a chip is doubled. As an exercise try to show that  
 
 
y<sub>2</sub> / y<sub>1</sub> = exp (b (x<sub>2</sub> - x<sub>1</sub>))
 
  
which must be 2 since we are interested in doubling. After taking the (natural) log from both sides and abbreviating x<sub>2</sub> - x<sub>1</sub> by the doubling time T, we get the final result
+
T = ln(2) / b
  
T = x<sub>2</sub> - x<sub>1</sub> = ln(2) / b
+
It would be nice to show the doubling time as an additional line of the chart title. For this, we modify the <code>OnFitComplete</code> event handler as follows:
  
It would be nice to show this as an additional line of the chart title. For this, we modify the OnFitComplete event handler as follows:
+
<syntaxhighlight lang=pascal>
 
 
<source>
 
 
procedure TForm1.Chart1FitSeries1FitComplete(Sender: TObject);
 
procedure TForm1.Chart1FitSeries1FitComplete(Sender: TObject);
 
begin
 
begin
Line 269: Line 247:
 
   ));
 
   ));
 
end;   
 
end;   
</source>
+
</syntaxhighlight>
  
 
[[File:TAChart_LogAx_Tutorial15.png]]
 
[[File:TAChart_LogAx_Tutorial15.png]]
Line 276: Line 254:
  
 
== Source code ==
 
== Source code ==
<source>
+
=== Project file ===
 +
<syntaxhighlight lang=pascal>
 
program project1;
 
program project1;
  
Line 296: Line 275:
 
   Application.CreateForm(TForm1, Form1);
 
   Application.CreateForm(TForm1, Form1);
 
   Application.Run;
 
   Application.Run;
end.
+
end.        
</source>            
+
</syntaxhighlight>      
 
+
   
<source>
+
=== Unit1.pas ===
 +
<syntaxhighlight lang=pascal>
 
unit Unit1;
 
unit Unit1;
  
Line 321: Line 301:
 
     ChartAxisTransformations1LogarithmAxisTransform1: TLogarithmAxisTransform;
 
     ChartAxisTransformations1LogarithmAxisTransform1: TLogarithmAxisTransform;
 
     ListChartSource1: TListChartSource;
 
     ListChartSource1: TListChartSource;
     UserDefinedChartSource1: TUserDefinedChartSource;
+
     ListChartSource2: TListChartSource;
 
     procedure Chart1FitSeries1FitComplete(Sender: TObject);
 
     procedure Chart1FitSeries1FitComplete(Sender: TObject);
 
     procedure FormCreate(Sender: TObject);
 
     procedure FormCreate(Sender: TObject);
    procedure UserDefinedChartSource1GetChartDataItem(
 
      ASource: TUserDefinedChartSource; AIndex: Integer;
 
      var AItem: TChartDataItem);
 
 
   private
 
   private
 
     { private declarations }
 
     { private declarations }
Line 342: Line 319:
 
uses
 
uses
 
   math;
 
   math;
 
type
 
  TDataRecord = record
 
    Year: double;
 
    Processor: string;
 
    TransistorCount: double;
 
  end;
 
 
const
 
  MAXDATA = 10;
 
  // Selected data from http://www.intel.com/pressroom/kits/quickreffam.htm
 
  // The date of market introduction is rounded to the next calendar year.
 
  Data: array[0..MAXDATA] of TDataRecord = (
 
    (Year:1972; Processor:'4004'; TransistorCount:2300),
 
    (Year:1974; Processor:'8080'; TransistorCount:6000),
 
    (Year:1978; Processor:'8086'; TransistorCount:29000),
 
    (Year:1982; Processor:'80286'; TransistorCount:134000),
 
    (Year:1986; Processor:'80386'; TransistorCount:275000),
 
    (Year:1989; Processor:'80486'; TransistorCount:1.2E6),
 
    (Year:1993; Processor:'Pentium'; TransistorCount:3.1E6),
 
    (Year:1997; Processor:'Pentium II'; TransistorCount:7.5E6),
 
    (Year:2001; Processor:'Xeon'; TransistorCount:42E6),
 
    (Year:2006; Processor:'Core Duo'; TransistorCount:152E6),
 
    (Year:2009; Processor:'Core i7'; TransistorCount:731E6)
 
  );
 
  
 
{ TForm1 }
 
{ TForm1 }
 
procedure TForm1.UserDefinedChartSource1GetChartDataItem(
 
  ASource: TUserDefinedChartSource; AIndex: Integer; var AItem: TChartDataItem);
 
begin
 
  AItem.X := Data[AIndex].Year;
 
  AItem.Y := Data[AIndex].TransistorCount;
 
  AItem.Text := Data[AIndex].Processor;
 
end;
 
  
 
procedure TForm1.FormCreate(Sender: TObject);
 
procedure TForm1.FormCreate(Sender: TObject);
Line 386: Line 330:
 
   value: double;
 
   value: double;
 
begin
 
begin
  UserDefinedChartSource1.Reset;
 
 
   for i:=MIN to MAX do begin
 
   for i:=MIN to MAX do begin
 
     value := IntPower(10, i);
 
     value := IntPower(10, i);
     ListChartSource1.Add(value, value);
+
     ListChartSource2.Add(value, value);
 
   end;
 
   end;
 +
  Chart1FitSeries1.ExecFit;
 
end;
 
end;
  
 
procedure TForm1.Chart1FitSeries1FitComplete(Sender: TObject);
 
procedure TForm1.Chart1FitSeries1FitComplete(Sender: TObject);
 
begin
 
begin
 +
  {
 +
  with Chart1FitSeries1 do
 +
    ShowMessage(Format(
 +
      'Fit result: a = %g, b = %g', [
 +
      Param[0], Param[1]
 +
    ]));
 +
  }
 
   Chart1.Title.Text.Add(Format(
 
   Chart1.Title.Text.Add(Format(
 
     'The number of transistors doubles every %.0f years',
 
     'The number of transistors doubles every %.0f years',
Line 402: Line 353:
  
 
end.
 
end.
</source>
+
</syntaxhighlight>
  
<source>
+
=== Unit1.lfm ===
 +
<syntaxhighlight lang=pascal>
 
object Form1: TForm1
 
object Form1: TForm1
   Left = 60
+
   Left = 244
   Height = 320
+
   Height = 356
   Top = 201
+
   Top = 193
   Width = 540
+
   Width = 552
 
   Caption = 'Form1'
 
   Caption = 'Form1'
   ClientHeight = 320
+
   ClientHeight = 356
   ClientWidth = 540
+
   ClientWidth = 552
 
   OnCreate = FormCreate
 
   OnCreate = FormCreate
 
   LCLVersion = '1.1'
 
   LCLVersion = '1.1'
 
   object Chart1: TChart
 
   object Chart1: TChart
 
     Left = 0
 
     Left = 0
     Height = 320
+
     Height = 356
 
     Top = 0
 
     Top = 0
     Width = 540
+
     Width = 552
 
     AxisList = <     
 
     AxisList = <     
 
       item
 
       item
 
         Grid.Color = clSilver
 
         Grid.Color = clSilver
        Intervals.MaxLength = 100
 
        Intervals.MinLength = 50
 
        Intervals.Options = [aipGraphCoords]
 
        Intervals.Tolerance = 5
 
 
         Marks.Format = '%0:.0n'
 
         Marks.Format = '%0:.0n'
         Marks.Source = ListChartSource1
+
         Marks.Source = ListChartSource2
 
         Marks.Style = smsCustom
 
         Marks.Style = smsCustom
 
         Minors = <         
 
         Minors = <         
Line 445: Line 393:
 
       item
 
       item
 
         Grid.Color = clSilver
 
         Grid.Color = clSilver
         Intervals.MaxLength = 70
+
         Intervals.MaxLength = 60
 
         Alignment = calBottom
 
         Alignment = calBottom
 
         Minors = <>
 
         Minors = <>
Line 476: Line 424:
 
       Marks.LinkPen.Color = clGray
 
       Marks.LinkPen.Color = clGray
 
       Marks.Style = smsLabel
 
       Marks.Style = smsLabel
      AxisIndexX = 1
 
 
       AxisIndexY = 0
 
       AxisIndexY = 0
 
       LineType = ltNone
 
       LineType = ltNone
 
       Pointer.Brush.Color = clRed
 
       Pointer.Brush.Color = clRed
      Pointer.HorizSize = 5
 
 
       Pointer.Style = psCircle
 
       Pointer.Style = psCircle
      Pointer.VertSize = 5
 
 
       ShowPoints = True
 
       ShowPoints = True
       Source = UserDefinedChartSource1
+
       Source = ListChartSource1
 
     end
 
     end
 
     object Chart1FitSeries1: TFitSeries
 
     object Chart1FitSeries1: TFitSeries
Line 492: Line 437:
 
       OnFitComplete = Chart1FitSeries1FitComplete
 
       OnFitComplete = Chart1FitSeries1FitComplete
 
       ParamCount = 2
 
       ParamCount = 2
       Source = UserDefinedChartSource1
+
       Source = ListChartSource1
 
     end
 
     end
 
   end
 
   end
   object UserDefinedChartSource1: TUserDefinedChartSource
+
   object ListChartSource1: TListChartSource
     OnGetChartDataItem = UserDefinedChartSource1GetChartDataItem
+
     DataPoints.Strings = (
     PointsNumber = 11
+
      '1972|2300|?|4004'
     left = 168
+
      '1974|6000|?|8080'
     top = 104
+
      '1978|29000|?|8086'
 +
      '1982|134000|?|80286'
 +
      '1986|275000|?|80386'
 +
      '1989|1200000|?|80486'
 +
      '1993|3100000|?|Pentium'
 +
      '1997|7500000|?|Pentium II'
 +
      '2001|42000000|?|Xeon'
 +
      '2006|152000000|?|Core Duo'
 +
      '2009|731000000|?|Core i7'
 +
     )
 +
     left = 240
 +
     top = 40
 
   end
 
   end
 
   object ChartAxisTransformations1: TChartAxisTransformations
 
   object ChartAxisTransformations1: TChartAxisTransformations
     left = 168
+
     left = 243
     top = 56
+
     top = 96
 
     object ChartAxisTransformations1LogarithmAxisTransform1: TLogarithmAxisTransform
 
     object ChartAxisTransformations1LogarithmAxisTransform1: TLogarithmAxisTransform
 
       Base = 10
 
       Base = 10
 
     end
 
     end
 
   end
 
   end
   object ListChartSource1: TListChartSource
+
   object ListChartSource2: TListChartSource
     left = 168
+
     left = 243
     top = 168
+
     top = 176
 
   end
 
   end
end
+
end
</source>
+
</syntaxhighlight>
 
 
[[Category:Tutorials]]
 

Latest revision as of 02:31, 29 February 2020

English (en) suomi (fi)

Introduction

TAChart LogAx Tutorial15.png

Do you know Moore's law? It is in front of you...

When you are reading this you are probably sitting in front of your desktop computer or have your notebook on your knees, or you are holding your smartphone in your hand. A tremendous progress of microtechnology has been happening during the last decades and is still on-going. This is due to miniaturization of electronic devices allowing to pack ever more functions onto a single silicon chip. And this is what Moore's law is about: the number of transistors on a chip has doubled approximately every two years.

In this tutorial we will take published data of an important manufacturer of microprocessors and use TAChart to plot the number of transistors versus the year of market introduction of the products. From these data we will try to confirm Moore's law.

You can find the data on the site www.intel.com/pressroom/kits/quickreffam.htm which contains a list of microprocessors, their date of market introduction, and their transistor count per chip.

When working through this example you will learn

  • how to enter data at designtime
  • how to create a point series with marks above each data point
  • how to create a logarithmic axis
  • how to fit an equation to data.

You should have some basic knowledge of how to work with TAChart; if you don't you should have a look at the Getting started tutorial of TAChart. Of course, you must be familiar with Lazarus and Object Pascal. And you should not be too afraid of math, you'll need logarithms and the exponential function.

Preparation

Setting up the chart

  • Create a new project.
  • Since we will get some long axis labels increase the size of the form to something like 540 x 320 pixels.
  • Add a TChart component, align it to alClient, set its BackColor to clWhite and the Grid.Color of each axis to clSilver.
  • Add a title to the x axis ("Year of market introduction") and to the y axis ("Number of transistors").
  • Use the text "Progress in Microelectronics" as the chart's title.
  • Set these font styles to fsBold.
  • Show a reference for our data in the footer. Use the chart's property Foot for that. Note that the property editor of Foot.Text (as well as that of Title.Text) allows to enter multi-lined titles.

TAChart LogAx Tutorial1.png

Creating a point series

We want to draw each TDataRecord as a single data point - this is called "point series" in other charting programs. The TAChart series editor does not offer this type of series because TLineSeries can do this as well. We only have to set its property ShowPoints to true and to turn off the connecting lines (LineType = ltNone). The symbols are determined by the property Pointer.

So, add a LineSeries to the form and set the properties to create a point series. Additionally, let's set the Pointer.Brush.Color to clRed, and the Pointer.Style to psCircle to draw a red circle at each data point.

Entering data

ListChartSource

There are many ways that data could be entered into the chart. We will use a ListChartSource today. Usually data are loaded into this source dynamically at run-time, for example after reading a file. In our project, however, we want to enter them at design-time by means of the data point editor of the ListChartSource. This has the main advantage that all our changes that we do with the chart are displayed immediately, there is no need to compile the project. As you will see, however, there are some drawbacks as well.

The ListChartSource stores the data to be plotted in a list - that's where its name comes from. The list items are so-called TChartDataItem objects containing for each data point:

  • the x and y coordinates,
  • a Text to be used for data marks,
  • a Color that overrides the SeriesColor,
  • as well as a YList which contains additional y values which are needed in some specialized series types.

The ListChartSource is the same type of chart source that is used by many series classes internally. Having it as a separate component, however, has the big advantage that the same chart source can be used again for other series - we will need this feature later when applying the FitSeries.

TAChart LogAx Tutorial2.png

We start by adding a TListChartSource to our form. This is the second icon in the chart component palette. Connect the series to this chart source by pointing its property Source to the new ListChartSource1.

The ListChartSource data points editor

TAChart LogAx Tutorial16.png

Clicking at the ellipsis button next to the property DataPoints opens the datapoints editor of the chart source. This is a string grid into which you can enter the data. Type in the data from the preceding image. They are an excerpt from the web site mentioned in the introduction. This site shows the time of market introduction by year and month - for simplicity, I skipped the month and rounded to the nearest calendar year. Enter the years into column X. Column Y will get the number of transistors per chip. The name of each microprocessor will go into column Text. Since we do not want to give each data point an individual color we leave the column Color alone - but, of course, you can experiment with that feature.

When you close the datapoints editor the series is automatically updated and displays the current data set. Wow!

Displaying datapoint marks

Why don't we see the processor names that we entered in the grid? Data point marks are turned off by default. Select the series and go to Marks. Look at the subproperty Style, it is smsNone which means "off". Open the dropdown list. You will see a variety of options how the data points can be labeled. Play with these settings to learn what they mean. Here we want to use smsLabel which displays the Text field of the TChartDataItem. In order to assign Marks.Style through source code, uses ... TAChartUtils shall be added.

There is also a connecting line between the label and the datapoint. Its color is white by default, that's why we cannot see it on the white background. The property name for this line is LinkPen. You may want to set the LinkPen.Color to clGray.

The position of the marks is OK here, but you should know that the series has a property MarkPositions to control positioning to some degree.

TAChart LogAx Tutorial3.png

Maybe we compile the project at this time.

There is nothing new - we saw everything already at design-time. This is a great advantage of working with the datapoints editor of a ListChartSource.

There are two things which can be improved at this stage:

  • The point labels are cut off at the edges of the chart. We can fix this by increasing the chart's Margin.Left and Margin.Right to 24. Margin defines the space surrounding the inner plot area to keep it free from data points. There is also a property MarginExternal which surrounds the outer border of the chart and can be used to change the distance to neighboring controls.
  • Since almost all data points are crowded at the bottom of the chart the plot is not very meaningful. This is why we need a logarithmic axis.

Setting up a logarithmic axis

Plotting data on a logarithmic axis means that the logarithms of the data values are plotted, not the values directly. The y values in our diagram, for example, range between 2300 and 731 million. When we calculate the (decadic/log10) logarithms, the range is only between about 3.3 and 7.1 - in such a diagram the data can be distinguished much easier.

TChartTransformations and LogarithmicAxisTransform

TAChart LogAx Tutorial4.png

Calculating the logarithm can be done by TAChart automatically. The main advantage is that the original data units are drawn on the axis while the logarithms are used for plotting.

In fact, there is an entire group of components for axis transformations: TChartAxisTransformations. An axis transformation is a function which maps "real world" data in units as displayed on the axis ("axis coordinates") to internal units that are common to all series in the same chart ("graph coordinates"). The axis coordinate of the transistor count of the 4004 processor, for example, is 2300, the graph coordinate is the logarithm of that number, i.e. log10(2300) = 3.36.

TAChart provides a variety of transforms. In addition to the logarithm transform, there is a linear transform which allows to multiply the data by a factor and to add an offset. The auto-scaling transform is useful when several independently scaled series have to be drawn on the same axis. The user-defined transform allows to apply any arbitrary transformation.

TAChart LogAx Tutorial5.png

Add a TAChartTransformations component to the form, double-click on it (or right-click on it in the object tree and select "Edit axis transformations"), click on "Add" and select "Logarithmic". This will create a ChartAxisTransformations1LogarithmAxisTransform1 component - what a name! Anyway, there's a high chance that you will not have to type it...

In the object inspector, you will see only a few properties - the most important one is Base. This is the base of the logarithm to be calculated. Change it to 10, since we want to calculate the decadic logarithms - this case comprises 99% of all logarithmic charts.

A side-note: it is very important that you save the project at this time. Why? You will see in a minute...

Now we must identify the axis which is to be transformed. For this purpose each axis has a property Transformations. In our case, the huge numbers are plotted on the y axis. So, go to the left axis and set its property Transformations to ChartAxisTransformations1.

TAChart LogAx Tutorial17.png

Oh - what's that? An error message pops up reporting a range check error. Hopefully you had saved the project. If you click on Cancel Lazarus will shut down, everything will be lost. If you click on OK suddenly the chart is gone, it reappears when you click somewhere, but the faulty axis is hidden ...

Are you getting desperate?

This is the main disadvantage of working at design-time. If something goes wrong, there is no debugger, no indication of what caused the trouble. Since components are compiled into Lazarus you would have to debug the IDE. Sounds complicated...

Let's sit down and think what we did. We assigned the logarithmic transform to the y axis. The transform does not yet have any connection to the data, therefore the data are still in "real" units, their maximum is 731 million. But the transform "thinks" that the data are already in graph units (logarithms). When it calculates the axis labels in axis units it takes hundreds of millions to the power of 10! That's what causes the range check error. However, the situation is not always so dramatic; the least thing that could happen is that the data are not transformed, but the axis is.

What can we do against that? Each series has properties AxisIndexX and AxisIndexY. The transform can use this information to calculate the logarithms of the correct coordinates before the axis labels are updated. This solves our problem.

So, click on Cancel to shutdown Lazarus. Restart and reload the project. In the saved state, the transformation is not yet connected to the axis.

Now have a look at the object tree above the object inspector and you will see that the left axis has index 0. So set the series' AxisIndexY to this value. Although not necessary it may be a good idea to disarm also the x axis by assigning its index 1 to the AxisIndexX of the series - who can guarantee that we won't transform the x axis in the future?

After this is done you can set the LeftAxis.Transformation to ChartTransformations1 without the range check error.

With the logarithm transform activated the data points spread nicely across the y axis range now. But what's wrong with the y axis labels? And the years on the x axis are too close and partly overlap.

TAChart LogAx Tutorial6.png

Finding axis labels is a non-trivial task, in particular when transformations are active that heavily distort the axis intervals. Unfortunately, logarithmic axes belong to that group. Basically, there are two ways to control label positioning, an automatic and a manual way.

Automatic finding of axis labels

For automatic label positioning, each axis has a property Intervals which gives access to several, partly mutually excluding parameters - please see TAChart documentation for an explanation. In case of the logarithmic axis the issue is usually caused by the fact that the option aipGraphCoordinates is not set. This option, if set, enforces calculation of the tick intervals for the transformed data ("graph coordinates"), not the "real world" data ("axis coordinates"). So, set aipGraphCoordinates in the LeftAxis.Intervals.Options. The labels jump to more evenly distributed locations.

TAChart LogAx Tutorial7.png

Depending on the size of your form you may get quite nice, or not so good labels. If you resize the form you will see some "crooked" labels jump in.

You can improve the quality of label presentation in the following way:

  • Increase the Intervals.Tolerance. This allows for varying spacing between tick marks.
  • Adjust the range, in pixels, in which the label distance can vary. This is defined by the properties Intervals.MaxLength and Intervals.MinLength. The optimum value depends on the size of the chart and on the range of the data. In our example project, good labels are obtained by setting these properties to 100 and 50, respectively. Usually Intervals.MaxLength provides better results.

In the same way, the overlapping year labels of the x axis can be addressed. Just increase the BottomAxis.Intervals.MaxLength to 70.

What is left now is the "1" that appears at the y axis between "10000000" and "1E009". This is due to a bug in some FPC versions. If you have that as well, simply change the property LeftAxis.Marks.Format. This string is passed to the Format function to convert the numbers to strings. The format specifier "%0.0n" for example avoids that conversion error and, additionally, adds nice thousand separators to the labels which makes them much more readable.

TAChart LogAx Tutorial8.png

Manual finding of axis labels

This is the best we can do with automatic label positioning. It is not perfect because when we increase the height of the window, or zoom in, the half-decade values may appear, or the label interval may be two decades as in above figure.

If you are not happy with that you have to use manual axis label selection. For this purpose, each axis has a property Source which can be linked to a ListChartSource containing only the allowed axes labels. So when this chart source contains only full-decade labels there is no risk of half-decade labels or omitting every other label. On the other hand, when you zoom into the chart you may come to a point where no labels are visible any more.

Add a second ListChartSource to the form. You can use the DataPoints editor again to enter the full-decade numbers. This has the advantage that you can complete most of this project without writing a single line of code!

But you could also populate the listsource easily in the FormCreate event:

procedure TForm1.FormCreate(Sender: TObject);
const
  MIN = 0;
  MAX = 12;
var
  i: Integer;
  value: double;
begin
  for i:=MIN to MAX do begin
    value := Power(10, i);
    ListChartSource2.Add(value, value);
  end;
end;

This procedure adds powers of 10 in a wide enough range to the ListChartSource by means of its method Add.

Connect ListChartSource1 to LeftAxis.Marks.Source to activate the manual labels of the ListChartSource. You should also remove all flags from the Options property. Otherwise automatic tick finding will still be active to some degree. If you did not use the DataPoints editor you must compile to see the effect.

TAChart LogAx Tutorial9.png

Minor tick marks

Very often minor tick marks are placed between the major tick marks. TAChart allows to add several sets of minor ticks to each axis. We only need one here. Go to LeftAxis and click on the ellipsis button next to the property Minors. This opens the editor for Chart1.AxisList[0].Minors. Click on "Add" and on the "M" in the list below. Now you can adjust the parameters in the object inspector to get "good" minor ticks. If the major ticks on a logarithmic axis are at full decades then the minor ticks usually are at 2, 3, 4,..., 8, 9, and, of course, powers of 10. This can be achieved easily by turning off all Intervals.Options except for aipUseCount and setting Intervals.Count = 9. Of course, this makes sense only when the major labels are fixed at full decades like in the manual approach above.

Usually the plot gets too crowded by the minor grid which appears now, you should set the minor's Grid.Visible to false.

TAChart LogAx Tutorial10.png

Fitting

Now let's look for a relation between the data, i.e. we want to find a mathematical formula which describes the dependence of transistor count on market introduction year. This is called "fitting": we select a formula with parameters and adjust the parameters such that the deviation to the data is as small as possible.

TAChart does not contain a full-fledged fitting engine. It uses the fitting routines from the FPC numerical library numlib. Therefore, TAChart cannot address all variants of fitting, but it covers the most important case, fitting of a polynomial by means of the linear least squares technique. This is about the level available to Excel users when they add a "trend line" to their chart.

TFitSeries

TAChart provides a specialized TFitSeries for fitting. This series has a property FitEquation which defines the formula that is used:

  • fePolynomial: y = b0 + b1x + b2x2 + … + bnxn. Specify the number of fitting parameters ai by the property ParamCount = n + 1.
  • feLinear: y = a + bx -- this is a special case of the general polynomial with n = 1 and fitting parameters a and b. It is made available as a separate item because straight lines define the most important fitting conditions.
  • feExp: y = a * ebx -- This equation can also be reduced to the polynomial case although this is not straightforward to see. But take the (natural) logarithm of this equation, and you get to ln(y) = a + bx. Now when we fit ln(y) instead of y we have the linear case again.
  • fePower: y = a * xb. Again, this can be reduced to a linear equation by a logarithmic transformation.

TAChart LogAx Tutorial11.png

Enough of theory. Let's add a FitSeries to the chart: double-click on the chart, and in the series editor click on "Add" and select the entry "Least squares fit series" from the dropdown list.

At first, we need to tell the fit series where it finds its data. For this purpose, we connect the series' Source with ListChartSource1 as we had done with the line series. You see: the same chart source can be used for several series.

You hopefully remember the disaster above with the AxisIndex. So, set the AxisIndexY to the index of the left axis as we did with the line series.

Which one of the four FitEquation possibilities do we select? Well, the data look like lying on a straight line. So let's select feLinear.

TAChart LogAx Tutorial12.png

Oops... We see the black fitted curve, but it does not "fit" at all. And we wanted a straight line, but we get a twisted curve. How can this be?

The reason is the logarithmic transform that we applied to the y data. Therefore, our plot shows the logarithms, but the fit takes the "raw" data. We are effectively fitting the straight line to the data in the screenshot in the section Displaying datapoint marks where the log transform had not yet been introduced - it is clear that the line would not "fit" there. And when the fitted function is drawn the log transform distorts the straight line to the twisted curve that we see.

On the other hand, if the log data follow a straight line our fitting law is not linear, but exponential. Let's set FitEquation to feExp and try again.

TAChart LogAx Tutorial13.png

Ah - much better!

Now we know that the exponential law, y = a * xb, is a good description of our data. But how do we get the fitting parameters a and b?

Fit results

The fit series has a public array property Param which contains the fitting parameters. a is in Params[0], and b is in Params[1]. Of course, these values are correct only when a valid fit has been performed. How do we know that? Well, the fit series provides an event OnFitComplete that is generated when the fit complete successfully. That's where we can evaluate the obtained fit parameters. As an example, let's display the fit results in a message along with the fit equation:

procedure TForm1.Chart1FitSeries1FitComplete(Sender: TObject);
begin
  with Chart1FitSeries1 do
    ShowMessage(Format(
      'Fit result: a = %g, b = %g', [
      Param[0], Param[1]
    ]));
end;

And that's what we get:

TAChart LogAx Tutorial14.png

Now we want to calculate the time T until the number of transistors on a chip is doubled. As an exercise try to show that

T = ln(2) / b

It would be nice to show the doubling time as an additional line of the chart title. For this, we modify the OnFitComplete event handler as follows:

procedure TForm1.Chart1FitSeries1FitComplete(Sender: TObject);
begin
  Chart1.Title.Text.Add(Format(
    'The number of transistors doubles every %.0f years',
    [ln(2) / Chart1FitSeries1.Param[1]]
  ));
end;

TAChart LogAx Tutorial15.png

Wow! This is Moore's law: "The number of transistors per chip doubles every two years"...

Source code

Project file

program project1;

{$mode objfpc}{$H+}

uses
  {$IFDEF UNIX}{$IFDEF UseCThreads}
  cthreads,
  {$ENDIF}{$ENDIF}
  Interfaces, // this includes the LCL widgetset
  Forms, Unit1, tachartlazaruspkg
  { you can add units after this };

{$R *.res}

begin
  //RequireDerivedFormResource := True;
  Application.Initialize;
  Application.CreateForm(TForm1, Form1);
  Application.Run;
end.

Unit1.pas

unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, TAGraph, TASeries, TASources, Forms, Controls,
  Graphics, Dialogs, TACustomSource, TATransformations, TAFuncSeries;

type

  { TForm1 }

  TForm1 = class(TForm)
    Chart1: TChart;
    Chart1FitSeries1: TFitSeries;
    Chart1LineSeries1: TLineSeries;
    ChartAxisTransformations1: TChartAxisTransformations;
    ChartAxisTransformations1LogarithmAxisTransform1: TLogarithmAxisTransform;
    ListChartSource1: TListChartSource;
    ListChartSource2: TListChartSource;
    procedure Chart1FitSeries1FitComplete(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { private declarations }
  public
    { public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

uses
  math;

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
const
  MIN = 0;
  MAX = 12;
var
  i: Integer;
  value: double;
begin
  for i:=MIN to MAX do begin
    value := IntPower(10, i);
    ListChartSource2.Add(value, value);
  end;
  Chart1FitSeries1.ExecFit;
end;

procedure TForm1.Chart1FitSeries1FitComplete(Sender: TObject);
begin
  {
  with Chart1FitSeries1 do
    ShowMessage(Format(
      'Fit result: a = %g, b = %g', [
      Param[0], Param[1]
    ]));
  }
  Chart1.Title.Text.Add(Format(
    'The number of transistors doubles every %.0f years',
    [ln(2) / Chart1FitSeries1.Param[1]]
  ));
end;

end.

Unit1.lfm

object Form1: TForm1
  Left = 244
  Height = 356
  Top = 193
  Width = 552
  Caption = 'Form1'
  ClientHeight = 356
  ClientWidth = 552
  OnCreate = FormCreate
  LCLVersion = '1.1'
  object Chart1: TChart
    Left = 0
    Height = 356
    Top = 0
    Width = 552
    AxisList = <    
      item
        Grid.Color = clSilver
        Marks.Format = '%0:.0n'
        Marks.Source = ListChartSource2
        Marks.Style = smsCustom
        Minors = <        
          item
            Grid.Visible = False
            Intervals.Count = 9
            Intervals.MinLength = 5
            Intervals.Options = [aipUseCount]
          end>
        Title.LabelFont.Orientation = 900
        Title.LabelFont.Style = [fsBold]
        Title.Visible = True
        Title.Caption = 'Number of transistors'
        Transformations = ChartAxisTransformations1
      end    
      item
        Grid.Color = clSilver
        Intervals.MaxLength = 60
        Alignment = calBottom
        Minors = <>
        Title.LabelFont.Style = [fsBold]
        Title.Visible = True
        Title.Caption = 'Year of market introduction'
      end>
    BackColor = clWhite
    Foot.Alignment = taLeftJustify
    Foot.Brush.Color = clBtnFace
    Foot.Font.Color = clBlue
    Foot.Text.Strings = (
      'Source:'
      'http://www.intel.com/pressroom/kits/quickreffam.htm'
    )
    Foot.Visible = True
    Margins.Left = 24
    Margins.Right = 24
    Title.Brush.Color = clBtnFace
    Title.Font.Color = clBlue
    Title.Font.Style = [fsBold]
    Title.Text.Strings = (
      'Progress in Microelectronics'
    )
    Title.Visible = True
    Align = alClient
    ParentColor = False
    object Chart1LineSeries1: TLineSeries
      Marks.Format = '%2:s'
      Marks.LinkPen.Color = clGray
      Marks.Style = smsLabel
      AxisIndexY = 0
      LineType = ltNone
      Pointer.Brush.Color = clRed
      Pointer.Style = psCircle
      ShowPoints = True
      Source = ListChartSource1
    end
    object Chart1FitSeries1: TFitSeries
      AxisIndexX = 1
      AxisIndexY = 0
      FitEquation = feExp
      OnFitComplete = Chart1FitSeries1FitComplete
      ParamCount = 2
      Source = ListChartSource1
    end
  end
  object ListChartSource1: TListChartSource
    DataPoints.Strings = (
      '1972|2300|?|4004'
      '1974|6000|?|8080'
      '1978|29000|?|8086'
      '1982|134000|?|80286'
      '1986|275000|?|80386'
      '1989|1200000|?|80486'
      '1993|3100000|?|Pentium'
      '1997|7500000|?|Pentium II'
      '2001|42000000|?|Xeon'
      '2006|152000000|?|Core Duo'
      '2009|731000000|?|Core i7'
    )
    left = 240
    top = 40
  end
  object ChartAxisTransformations1: TChartAxisTransformations
    left = 243
    top = 96
    object ChartAxisTransformations1LogarithmAxisTransform1: TLogarithmAxisTransform
      Base = 10
    end
  end
  object ListChartSource2: TListChartSource
    left = 243
    top = 176
  end
end