Difference between revisions of "TAChart Tutorial: ListChartSource, Logarithmic Axis, Fitting"
(→Data) |
|||
Line 61: | Line 61: | ||
== The TUserDefinedChartSource == | == The TUserDefinedChartSource == | ||
− | + | 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. | |
[[File:TAChart_LogAx_Tutorial2.png|left]] | [[File:TAChart_LogAx_Tutorial2.png|left]] | ||
Line 76: | Line 76: | ||
</source> | </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 | + | <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. |
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>. | 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>. | ||
Line 115: | Line 115: | ||
As you can see, there are two things which can be improved at this stage: | As you can see, 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. | + | * 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. |
− | * Since almost all data points are crowded at the bottom of the chart the plot not very meaningful. This is | + | * 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'''. |
== Using a logarithmic axis == | == Using a logarithmic axis == | ||
Line 123: | Line 123: | ||
=== 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> | + | 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>. |
− | 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. | + | 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. | ||
[[File:TAChart_LogAx_Tutorial5.png]] | [[File:TAChart_LogAx_Tutorial5.png]] | ||
Line 135: | Line 137: | ||
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. | 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. | ||
− | 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>. | + | 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...) |
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. | 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. | ||
Line 141: | Line 143: | ||
[[File:TAChart_LogAx_Tutorial6.png]] | [[File: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. | + | 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 === | === 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 <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. |
[[File:TAChart_LogAx_Tutorial7.png]] | [[File:TAChart_LogAx_Tutorial7.png]] | ||
Line 150: | Line 152: | ||
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. | 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 | + | You can improve the quality of label presentation in the following way: |
− | * Moderately increase the <code>Intervals.Tolerance</code> | + | * Moderately increase the <code>Intervals.Tolerance</code> to something like 5. |
− | * Adjust the range the distance | + | * 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. |
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. | ||
Line 163: | Line 165: | ||
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 the half-decade values may appear, or the label interval may be two decads 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 <code>TListChartSource</code> to the form, and populate it in the <code>FormCreate</code> event: | ||
Line 183: | Line 185: | ||
</source> | </source> | ||
− | 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 Add procedure. The first parameter in this call is the x, the second parameter the y value of the TChartDataItem. 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. | + | 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. |
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. | ||
Line 190: | Line 192: | ||
=== Minor tick marks === | === 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 <code>LeftAxis</code> and click on the ellipsis button next to the property <code>Minors</code>. This opens the editor for <code>Chart1.AxisList[0].Minors</code>. 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 < | + | 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 <code>LeftAxis</code> and click on the ellipsis button next to the property <code>Minors</code>. This opens the editor for <code>Chart1.AxisList[0].Minors</code>. 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 <code>Intervals.Options</code> except for <code>aipUseCount</code> and setting <code>Intervals.Count = 9</code>. 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 <code>Grid.Visible</code> to <code>false</code>. | Usually the plot gets too crowded by the minor grid which appears now, you should set the minor's <code>Grid.Visible</code> to <code>false</code>. | ||
Line 199: | Line 201: | ||
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 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. | ||
− | TAChart does not contain a full-fledged fitting engine. It just "borrows" the fitting routines | + | 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. |
=== TFitSeries === | === TFitSeries === | ||
− | TAChart provides a specialized TFitSeries for fitting. This series has a property <code>FitEquation</code> which defines the formula that is used: | + | TAChart provides a specialized <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>: 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>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>: 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>feExp</code>: y = a e<sup>x</sup> - This equation can also be reduced to the polynomial case | + | * <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>fePower</code>: y = a x<sup>b</sup>. Again, this can be reduced to a linear equation by a logarithmic transformation. | * <code>fePower</code>: y = a x<sup>b</sup>. Again, this can be reduced to a linear equation by a logarithmic transformation. | ||
[[File:TAChart_LogAx_Tutorial11.png]] | [[File:TAChart_LogAx_Tutorial11.png]] | ||
− | Enough of theory. Let's add a | + | 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 | + | 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. |
− | Moreover, don't forget to set AxisIndexX and AxisIndexY 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. | + | 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. |
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 222: | Line 224: | ||
[[File:TAChart_LogAx_Tutorial12.png]] | [[File: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 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 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. | ||
− | 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. |
[[File:TAChart_LogAx_Tutorial13.png]] | [[File:TAChart_LogAx_Tutorial13.png]] | ||
Line 235: | Line 237: | ||
=== Fit results === | === Fit results === | ||
− | The fit series has a public array property <code> | + | 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: |
<source> | <source> | ||
Line 249: | Line 251: | ||
[[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 | |
y<sub>2</sub> / y<sub>1</sub> = exp (b (x<sub>2</sub> - x<sub>1</sub>)) | y<sub>2</sub> / y<sub>1</sub> = exp (b (x<sub>2</sub> - x<sub>1</sub>)) | ||
− | which must | + | 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 = x<sub>2</sub> - x<sub>1</sub> = ln(2) / b | T = x<sub>2</sub> - x<sub>1</sub> = ln(2) / b | ||
Line 271: | Line 273: | ||
[[File:TAChart_LogAx_Tutorial15.png]] | [[File:TAChart_LogAx_Tutorial15.png]] | ||
− | Wow! This is Moore's law: "The number of transistors per chip doubles every two | + | Wow! This is Moore's law: "The number of transistors per chip doubles every two years"... |
== Source code == | == Source code == |
Revision as of 18:16, 11 August 2012
Introduction
After doing the first steps with TAChart in the Getting Started tutorial, here is another tutorial. This one will be more advanced. It will cover the aspects
- How to apply a user-defined chart source
- 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 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 "Moore's law" saying that the transistor count doubles approximately every two years.
This tutorial will require some math knowledge. You should be familiar with logarithms.
Preparation
Setting up the chart
- 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.
- Add a
TChart
component, align it to alClient, set itsBackColor
toclWhite
and theGrid.Color
of each axis toclSilver
. - 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
. - Maybe it is a good idea to display above reference for our data in the footer - the chart property
Foot
can be used for that. Note that the property editor ofFoot.Text
(as well as that ofTitle.Text
) allows to enter linefeeds for multi-lined titles.
Data
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.
For the data, we declare a TDataRecord
type
TDataRecord = record
Year: double;
Processor: string;
TransistorCount: double;
end;
and the data simply are stored in an array of these records:
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)
);
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 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 properites 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.
The TUserDefinedChartSource
In the 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.
To avoid this, we add a TUserDefinedChartSource
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 OnGetChartDataItem
in order to define the data. For this purpose, the event takes a var
parameter AItem
of type TChartDataItem
that is defined as follows (with elements omitted that are not needed here):
type
TChartDataItem = object
X, Y: Double;
Color: TChartColor;
Text: String;
// ...
end;
X
and Y
indicate the coordinates of the data point. The field Text
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 Marks
which determines how this string is composed. Marks.Style
selects whether the x
or y
value, or the Text
label is displayed (smsXValue
, smsValue
, smsLabel
, respectively), and there are even more options.
In our case, it may be a good idea to show the processor name above each data point. So we set the series' Marks.Style
to smsLabel
.
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 Marks.LinkPen
and set its color to clGray
.
We could even give each data point an individual color by assigning a corresponding value to the property Color
, but we don't want to use this feature here.
Now we can write the event handler for OnGetChartDataItem
as follows:
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;
AIndex
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 AItem
.
There are still some important things to do:
- Tell the
UserDefinedChartSource
how many data points the external data array contains. We have to enter this number in the propertyPointsNumber
of the UserDefinedChartSource. In our case, this is the value ofMAXDATA+1
(+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
Source
toUserDefinedChartSource1
. - 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
UserDefinedChartSource1.Reset
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 callReset
. Since our data are hardcoded in the project source the form'sOnCreate
event handler is a good place to do this:
procedure TForm1.FormCreate(Sender: TObject);
begin
UserDefinedChartSource1.Reset;
end;
Now we can compile the project for the first time.
As you can see, 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
andMargin.Right
to 24.Margin
defines the space surrounding the inner plot area which is kept free from plotting. There is also a propertyMarginExternal
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.
Using 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.
TChartTransformations and LogarithmicAxisTransform
Calculating the logarithm can be done by TAChart automatically. In fact, there is an entire group of components for this and other 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.
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.
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 connect it to ChartAxisTransformations1
by assigning the Transformations
property accordingly. Ignore the strange axis labels for the moment.
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 AxisIndexX
and AxisIndexY
have to be set properly. AxisIndexX
is the index of the x axis in the chart's AxisList
, AxisIndexY
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 AxisIndexX = 1
and AxisIndexY = 0
. (To be exact, we can leave the AxisIndexX
unchanged since it is not involved in a transformation. But let us be complete to avoid unnecessary debugging sessions in the future...)
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.
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
and compile again.
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:
- Moderately increase the
Intervals.Tolerance
to something like 5. - Adjust the range in which the label distance can vary. This is defined by the properties
Intervals.MaxLength
andIntervals.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.
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 of some FPC versions. If you have that issue like me, 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.
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.
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 TListChartSource
to the form, and populate it 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);
ListChartSource1.Add(value, value);
end;
end;
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 Add
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.
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.
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
.
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.
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 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 = a0 + a1 x + ... + an xn. Specify the number of fitting parameters ai by the propertyParamCount
= n + 1.feLinear
: 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.feExp
: y = a eb x - 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.fePower
: y = a xb. Again, this can be reduced to a linear equation by a logarithmic transformation.
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 the UserDefinedChartSource1
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 AxisIndexX
and AxisIndexY
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.
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
.
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.
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.
Ah - much better!
Now we know that the exponential law, y = a eb x, 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:
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:
Now we want to calculate the time until the number of transistors on a chip is doubled. Let's say that there are y1 and y2 transistors on a chip at times x1 and x2, respectively. Knowing our exponential relationship (y = a ebx), we can calculate the ratio
y2 / y1 = exp (b (x2 - x1))
which must be 2 since we are interested in doubling. After taking the (natural) log from both sides and abbreviating x2 - x1 by the doubling time T, we get the final result
T = x2 - x1 = ln(2) / b
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:
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;
Wow! This is Moore's law: "The number of transistors per chip doubles every two years"...
Source code
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.
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;
UserDefinedChartSource1: TUserDefinedChartSource;
procedure Chart1FitSeries1FitComplete(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure UserDefinedChartSource1GetChartDataItem(
ASource: TUserDefinedChartSource; AIndex: Integer;
var AItem: TChartDataItem);
private
{ private declarations }
public
{ public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.lfm}
uses
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 }
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);
const
MIN = 0;
MAX = 12;
var
i: Integer;
value: double;
begin
UserDefinedChartSource1.Reset;
for i:=MIN to MAX do begin
value := IntPower(10, i);
ListChartSource1.Add(value, value);
end;
end;
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;
end.
object Form1: TForm1
Left = 60
Height = 320
Top = 201
Width = 540
Caption = 'Form1'
ClientHeight = 320
ClientWidth = 540
OnCreate = FormCreate
LCLVersion = '1.1'
object Chart1: TChart
Left = 0
Height = 320
Top = 0
Width = 540
AxisList = <
item
Grid.Color = clSilver
Intervals.MaxLength = 100
Intervals.MinLength = 50
Intervals.Options = [aipGraphCoords]
Intervals.Tolerance = 5
Marks.Format = '%0:.0n'
Marks.Source = ListChartSource1
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 = 70
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
AxisIndexX = 1
AxisIndexY = 0
LineType = ltNone
Pointer.Brush.Color = clRed
Pointer.HorizSize = 5
Pointer.Style = psCircle
Pointer.VertSize = 5
ShowPoints = True
Source = UserDefinedChartSource1
end
object Chart1FitSeries1: TFitSeries
AxisIndexX = 1
AxisIndexY = 0
FitEquation = feExp
OnFitComplete = Chart1FitSeries1FitComplete
ParamCount = 2
Source = UserDefinedChartSource1
end
end
object UserDefinedChartSource1: TUserDefinedChartSource
OnGetChartDataItem = UserDefinedChartSource1GetChartDataItem
PointsNumber = 11
left = 168
top = 104
end
object ChartAxisTransformations1: TChartAxisTransformations
left = 168
top = 56
object ChartAxisTransformations1LogarithmAxisTransform1: TLogarithmAxisTransform
Base = 10
end
end
object ListChartSource1: TListChartSource
left = 168
top = 168
end
end