TAChart Tutorial: ListChartSource, Logarithmic Axis, Fitting
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
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. In a susequent tutorial we will fit an exponential function to the data and verify "Moore's law" saying that the transistor count doubles every two years.
Preparation
Setting up the chart
- Create a new project.
- Resize the main form to 540 x 480 pixels.
- Add a
TAChart
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.
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:
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 use the correct value for MAXDATA, the upper index of the array. It should be mentioned that the 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.
Using the TUserDefinedChartSource
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.
TUserDefinedChartSource
Normally, we would add the data to the built-in chart source of this series, as we did in the "Getting Started" tutorial. 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 displayed. Style
determines 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
.
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 set 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.
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. - Since almost all data points are crowded at the bottom of the chart the plot not very meaningful. This is when the logarithmic axis comes into play.
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.
Calculating the logarithm can be done by TAChart automatically. In fact, there is an entire group of components for this and other axis transformations: TATransformations
. A transformation is a function which maps "real world" data in units as displayed on the axis ("axis coordinates") to 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(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
.
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?