Sign in Join
Wiki copytemporary

TAChart Tutorial: Userdefined ChartSource: what changed

Revision 1 to 2. Removed lines have a minus sign; added lines have a plus sign.

OldNewLine
11## Introduction
22
33![](/wiki-images/4/40/TAChart_Population6.png)
44
55In this tutorial we use [[TAChart]] to plot the **age distribution of world population**. In doing this, you will learn
66
77- how to work with an [[TAChart documentation|area series]]
88- and, most of all, how to apply a [[TAChart documentation|user-defined chart source]]
99
1010As usual, we assume that you are familiar with Lazarus and the [[Object Pascal]] language and have some basic knowledge of the TAChart package that you would acquire by working through the [[TAChart Tutorial: Getting started|Getting Started]] tutorial.
1111
1212## Preparation
1313
1414### Data
1515
1616The site [www.census.gov](http://www.census.gov) contains a wealth of demographic data. This [link](http://www.census.gov/population/international/data/worldpop/tool_population.php) leads you directly to the data that we want to plot: world population as a function of age and gender.
1717
1818Go to that page, select a year and click *"submit"*. In the lower part of the window, you will see a table with the columns *"Age"*, *"Both Sexes Population"*, *"Male population"*, *"Female population"*, and *"Sex ratio"*. Use the mouse to select the data in the table (including header) and copy them to the clipboard. Paste the data into a text editor and save as *"population.txt"*.
1919
2020Open Lazarus and create a new project. At first we have to load the data to make them available for plotting.
2121
2222We define a `TPopulationRecord` and an [[Array|array]] of type `TPopulationArray` to hold the data of the population table:
2323
2424``` pascal
2525type
2626 TPopulationRecord = record
2727 Age: Integer;
2828 Total: Double;
2929 Male: Double;
3030 Female: Double;
3131 Ratio: Double;
3232 end;
3333
3434 TPopulationArray = array of TPopulationRecord;
3535```
3636
3737Now let's read the file. Have a look at the [[Unit|unit]] `population.pas` in the [[TAChart Tutorial: Userdefined ChartSource|Source Code]] section of this tutorial. You will find there a
3838
3939``` pascal
4040procedure LoadPopulationData(const AFilename:string; var Data:TPopulationArray)
4141```
4242
4343which does the file reading and stores the data in a `TPopulationArray`.
4444
4545Just include the unit `population` in your [[Uses|uses]] list to get access to this [[Procedure|procedure]]. Add a [[Variable|variable]] `PopulationData` of type `TPopulationArray` to the [[Private|private]] section of the main form. Now we have everything ready to call `LoadPopulationData` from the form's OnCreate event handler. This will read our data file into the array `PopulationData`.
4646
4747``` pascal
4848uses
4949 ..., population;
5050
5151type
5252 TForm1 = class(TForm)
5353 // ...
5454 private
5555 PopulationData : TPopulationArray;
5656 // ...
5757 end;
5858
5959const
6060 POPULATION_FILE = 'population.txt';
6161
6262procedure TForm1.FormCreate(Sender: TObject);
6363begin
6464 LoadPopulationData(POPULATION_FILE, PopulationData);
6565end;
6666```
6767
6868Compile to check for obvious errors. You may get a [[runtime error]] on that the data file cannot be found. This is because our data file is assumed to reside in the same directory as the exe file. To fix this, either change the [[Constant|constant]] `POPULATION_FILE` to the correct storage location, or copy *"population.txt"* to the exe folder. Now the program should run fine, you will see an unspectacularly empty form, though.
6969
7070### Combobox as category selector
7171
7272The file contains various categories: total, male, and femal population, as well as male-to-female ratio. Why not display these data in our chart?
7373
7474To prepare for this,
7575
7676- add a **[[TPanel|panel]]** to the form, align it to `alTop`, and delete its caption.
7777- Add a **[[TComboBox|combobox]]** to this panel, set its `Style` to `csDropdownList`, add the strings "Total population", "Male population", "Female population", and "Ratio male/female (%)" to the `Items` [[Property|property]], and set `ItemIndex` to 0.
7878
7979### Preparing TChart
8080
8181Now we are finished with preparations, and it's time for charting...
8282
8383- Add a `TChart` component to the form, align it to `alClient`.
8484- Set the chart's `BackColor` to `clWhite` and the `Grid.Color` of each axis to `clSilver`.
8585- Get axis titles shown by setting `LeftAxis.Visible` and `BottomAxis.Visible` to `true`.
8686- Set the caption of the bottom axis to "Age", and that of the left axis to "Total population" (or leave it empty - we will later replace it at [[runtime]] by the selected item of the combobox).
8787- Use the text "World population" as the chart's title.
8888- Set these font styles to `fsBold`.
8989- Maybe it is a good idea to display our data reference in the [[TAChart documentation|footer]] - the chart property `Foot` can be used for that. Note that the property editor of `Foot.Text` (as well as that of `Title.Text`) allows to enter linefeeds for multi-lined titles.
9090
9191![](/wiki-images/6/61/TAChart_Population2.png)
9292
9393## Creating an area series
9494
9595This time, we want to display our data as an [area series](https://en.wikipedia.org/wiki/Area_chart). This type of series connects the data points as in a line series, but also fills the area underneath by a given color. It is very common to display demographic data, like a [population pyramid](https://en.wikipedia.org/wiki/Population_pyramid), in this way.
9696
9797So, add a [[TAChart documentation|TAreaSeries]] to the chart. As usual, you don't see it at this time because it does not yet have any data. But we can prepare some properties:
9898
9999- Set its `SeriesColor` to `clSkyBlue`. This is the fill color of the area series.
100100- Hide the vertical connecting lines by setting the `AreaLinesPen` to `psClear`.
101101- If you want you can also change `ConnectType` to `ctStepXY` or `ctStepYX` to get a step effect.
102102
103103Later, when the series has data, you should play around with these and other properties to see what they do.
104104
105105## User-defined chart source
106106
107107Now, how do we get our data to the area series?
108108
109109The first idea might be iterate through our `PopulationData` array and copy the data to the series by means of `AddXY` calls, like this:
110110
111111``` pascal
112112var
113113 i: Integer;
114114begin
115115 for i:=0 to High(PopulationData) do
116116 Chart1AreaSeries1.AddXY(PopulationData[i].Age, PopulationData[i].Total);
117117end;
118118```
119119
120120The disadvantage of this approach is that we would have the data in memory twice: in the `PopulationData` array 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.
121121
122122![](/wiki-images/e/e0/TAChart_Population3.png) To avoid this, we add a [[TAChart documentation|TUserDefinedChartSource]] to the form, it is the third or fourth icon from the left in the [[Chart tab|Chart component palette]]. This chart source is made to interface to any kind of data storage, the source itself does not store any data. You just have to write an event handler for `OnGetChartDataItem` in order to transfer the data. For this purpose, the event takes a [[Variable parameter|var parameter]] `AItem` of type `TChartDataItem` that is defined as follows (with elements omitted that are not needed here):
123123
124124``` pascal
125125type
126126 TChartDataItem = object
127127 X, Y: Double;
128128 // ...
129129 end;
130130```
131131
132132The fields `X` and `Y` are the coordinates of the data point.
133133
134134Now we write the event handler for `OnGetChartDataItem`:
135135
136136``` pascal
137137procedure TForm1.UserDefinedChartSource1GetChartDataItem(
138138 ASource: TUserDefinedChartSource; AIndex: Integer; var AItem: TChartDataItem);
139139begin
140140 AItem.X := PopulationData[AIndex].Age;
141141 case Combobox1.ItemIndex of
142142 0: AItem.Y := PopulationData[AIndex].Total;
143143 1: AItem.Y := PopulationData[AIndex].Male;
144144 2: AItem.Y := PopulationData[AIndex].Female;
145145 3: AItem.Y := PopulationData[AIndex].Ratio;
146146 end;
147147end;
148148```
149149
150150`AIndex` is the index of the datapoint which is queried. Since both chart source and our `PopulationData` 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`. Note that we pick the y coordinate according to what is selected in the combobox. In this way, the UserDefinedChartSource can provide a multitude of data.
151151
152152There are still some important things to do:
153153
154154- Tell the series to use the UserDefinedChartSource instead of the built-in source. For this purpose, we point the series' property `Source` to `UserDefinedChartSource1`.
155155- Tell the UserDefinedChartSource how many data points the external data array contains. We have to enter this number in the property `PointsNumber` of the UserDefinedChartSource. The data count is known after our procedure `LoadPopulationData` has finished. So we add a line which sets `PointsNumber` correctly to the `FormCreate` method after `LoadPopulationData` is called.
156156- 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 call `Reset`. To see what happens let's "forget" to call `Reset` for the moment.
157157
158158## Fine-tuning
159159
160160![](/wiki-images/4/48/TAChart_Population4.png)
161161
162162Compile the project in this stage. Not too bad, some flaws, though.
163163
164164- The labels of the y axis are strange. Why do they jump from 80000000 to 1, and then to 12? This seems to be due to a bug of the `Format` function for large numbers in some FPC versions. If you have that issue as well, simply change the property `LeftAxis.Marks.Format`. This string is passed to the `Format` function to convert numbers to strings. The format specifier "%.0n" for example avoids that conversion error and, additionally, adds nice thousand separators to the labels which makes them much more readable. However, be aware that the "0" cuts off any decimals. Therefore, you should use this format specifier only for large numbers.
165165- There is a small gap between the y axis and the filled area series. Similarly, the filled area reaches a bit below the x axis. This is caused by the property `Margins` of the chart. This property is usually quite helpful to create an empty area near the plot axes, free from overlapping symbols and marks labels. But here, it is unfavorable. So just set `Chart.Margins.Left` and `Chart.Margins.Bottom` to 0.
166166- Ah, and the combobox is not working yet. We need to assign a handler for its `OnSelect` event. What does it have to do? Well, when another combobox item has been selected the UserDefinedChartSource reports a different y value to the series - that's what we want. But how we get the `OnGetChartDataItem` being called? Just by redrawing the chart! When the series is redrawn it always has to query the data from the chart source, so it will get the updated data automatically. OK - let's call `Chart1.Invalidate`:
167167
168168``` pascal
169169procedure TForm1.ComboBox1Select(Sender: TObject);
170170begin
171171 Chart1.Invalidate;
172172end;
173173```
174174
175175When you compile and select another combobox item you will find that the series does nicely update to the selected category. But the axis is frozen, it should rescale automatically. Is this a bug?
176176
177177No. A few lines above, it was said that whenever a chart with a UserDefinedChartSource does not behave as expected there is a chance that you may have forgotten to call `UserDefinedChartSource.Reset`. That's it: we replace the `Invalidate` by the `Reset`.
178178
179179And we can fix another issue: the caption of the y axis should change whenever a different category is selected. For this, we copy the text of the selected combobox item to the chart's `LeftAxis.Title.Caption`. We add a corresponding line the the combobox OnSelect event handler:
180180
181181``` pascal
182182procedure TForm1.ComboBox1Select(Sender: TObject);
183183begin
184184 Chart1.LeftAxis.Title.Caption := Combobox1.Items[Combobox1.ItemIndex];
185185 UserDefinedChartSource1.Reset;
186186end;
187187```
188188
189189Now switching of data categories by the combobox is working fine.
190190
191191A minor flaw left: Since our y data do not contain a 0 the y axis does not begin at 0 due to the automatic axis scaling. But it would be nice if there were a zero. For this, we must partially turn off automatic axis scaling. The `Chart.Extent` defines a rectangle for the minimum and maximum x and y values. When we set `Chart.Extent.UseYMin` to `true`, the y axis minimum is taken from the value of `Chart.Extent.YMin`, and this is 0 by default. Therefore, we have our zero back.
192192
193193![](/wiki-images/1/13/TAChart_Population5.png)
194194
195195But there is another small problem now: the lowest grid line is drawn on top of the x axis. First of all, the black lines that we see as axes are not the axes themselves, but belong to the frame drawn around the plotting area by default. You can test this by setting `Chart1.Frame.Visible` temporarily to `false`. For the axes, there is a property `AxisPen` where the "true" axis line can be turned on by `AxisPen.Visible = true`. After after doing this, the x axis is still a dashed line. This is caused by the drawing sequence. By default, the y axis, along with its grid, is drawn *after* the x axis. If you want to have the x axis drawn first you go to the object tree and drag the x axis above the y axis.
196196
197197When you compile the chart is perfect.
198198
199199![](/wiki-images/4/40/TAChart_Population6.png)
200200
201201![](/wiki-images/f/fc/TAChart_Population7.png)
202202
203203## Source code
204204
205205For Lazarus v2.1+ you can find the [[Source code|source code]] of this tutorial project in folder **components/tachart/tutorials/population1** of your Lazarus installation. For older versions, copy and paste the following code to the corresponding files.
206206
207207### Project file
208208
209209``` pascal
210210program project1;
211211
212212{$mode objfpc}{$H+}
213213
214214uses
215215 {$IFDEF UNIX}{$IFDEF UseCThreads}
216216 cthreads,
217217 {$ENDIF}{$ENDIF}
218218 Interfaces, // this includes the LCL widgetset
219219 Forms, Unit1, tachartlazaruspkg, population
220220 { you can add units after this };
221221
222222{$R *.res}
223223
224224begin
225225 RequireDerivedFormResource := True;
226226 Application.Initialize;
227227 Application.CreateForm(TForm1, Form1);
228228 Application.Run;
229229end.
230230```
231231
232232### population.pas
233233
234234``` pascal
235235unit population;
236236
237237{$mode objfpc}{$H+}
238238
239239interface
240240
241241uses
242242 Classes, SysUtils;
243243
244244type
245245 TPopulationRecord = record
246246 Age: Integer;
247247 Total: Double;
248248 Male: Double;
249249 Female: Double;
250250 Ratio: Double;
251251 end;
252252
253253type
254254 TPopulationArray = array of TPopulationRecord;
255255
256256procedure LoadPopulationData(const AFileName: String; var Data: TPopulationArray);
257257
258258implementation
259259
260260procedure LoadPopulationData(const AFileName: String; var Data: TPopulationArray);
261261
262262 function StripThousandSep(const s: String): String;
263263 var
264264 i: Integer;
265265 begin
266266 Result := s;
267267 for i:=Length(Result) downto 1 do
268268 if Result[i] = ',' then
269269 Delete(Result, i, 1);
270270 end;
271271
272272var
273273 List1, List2: TStringList;
274274 i, j, n: Integer;
275275 s: String;
276276 ds: char;
277277begin
278278 ds := FormatSettings.DecimalSeparator;
279279 List1 := TStringList.Create;
280280 try
281281 List1.LoadFromFile(AFileName);
282282 n := List1.Count;
283283 SetLength(Data, n-2);
284284 FormatSettings.DecimalSeparator := '.';
285285 List2 := TStringList.Create;
286286 try
287287 List2.Delimiter := #9;
288288 List2.StrictDelimiter := true;
289289 j := 0;
290290 for i:=2 to n-1 do begin
291291 List2.DelimitedText := List1[i];
292292 s := List1[i];
293293 with Data[j] do begin
294294 if i < n-1 then
295295 Age := StrToInt(trim(List2[0]))
296296 else
297297 Age := 100;
298298 Total := StrToFloat(StripThousandSep(trim(List2[1])));
299299 Male := StrToFloat(StripThousandSep(trim(List2[2])));
300300 Female := StrToFloat(StripThousandSep(trim(List2[3])));
301301 Ratio := StrToFloat(trim(List2[4]));
302302 end;
303303 inc(j);
304304 end;
305305 finally
306306 List2.Free;
307307 end;
308308 finally
309309 FormatSettings.DecimalSeparator := ds;
310310 List1.Free;
311311 end;
312312end;
313313
314314end.
315315```
316316
317317### Unit1.pas
318318
319319``` pascal
320320unit Unit1;
321321
322322{$mode objfpc}{$H+}
323323
324324interface
325325
326326uses
327327 Classes, SysUtils, FileUtil, TAGraph, TASources, TASeries, Forms, Controls,
328328 Graphics, Dialogs, ExtCtrls, StdCtrls, population, TACustomSource;
329329
330330type
331331
332332 { TForm1 }
333333
334334 TForm1 = class(TForm)
335335 Chart1: TChart;
336336 Chart1AreaSeries1: TAreaSeries;
337337 ComboBox1: TComboBox;
338338 Panel1: TPanel;
339339 UserDefinedChartSource1: TUserDefinedChartSource;
340340 procedure ComboBox1Select(Sender: TObject);
341341 procedure FormCreate(Sender: TObject);
342342 procedure UserDefinedChartSource1GetChartDataItem(
343343 ASource: TUserDefinedChartSource; AIndex: Integer;
344344 var AItem: TChartDataItem);
345345 private
346346 { private declarations }
347347 PopulationData: TPopulationArray;
348348 public
349349 { public declarations }
350350 end;
351351
352352var
353353 Form1: TForm1;
354354
355355implementation
356356
357357{$R *.lfm}
358358
359359const
360360 POPULATION_FILE = 'population.txt';
361361
362362{ TForm1 }
363363
364364procedure TForm1.FormCreate(Sender: TObject);
365365begin
366366 LoadPopulationData(POPULATION_FILE, PopulationData);
367367 UserDefinedChartSource1.PointsNumber := Length(PopulationData);
368368 Chart1.LeftAxis.Title.Caption := Combobox1.Items[Combobox1.ItemIndex];
369369end;
370370
371371procedure TForm1.ComboBox1Select(Sender: TObject);
372372begin
373373 Chart1.LeftAxis.Title.Caption := Combobox1.Items[Combobox1.ItemIndex];
374374 UserDefinedChartSource1.Reset;
375375end;
376376
377377procedure TForm1.UserDefinedChartSource1GetChartDataItem(
378378 ASource: TUserDefinedChartSource; AIndex: Integer; var AItem: TChartDataItem);
379379begin
380380 AItem.X := PopulationData[AIndex].Age;
381381 case Combobox1.ItemIndex of
382382 0: AItem.Y := PopulationData[AIndex].Total;
383383 1: AItem.Y := PopulationData[AIndex].Male;
384384 2: AItem.Y := PopulationData[AIndex].Female;
385385 3: AItem.Y := PopulationData[AIndex].Ratio;
386386 end;
387387end;
388388
389389end.
390390```
391391
392392### Unit1.lfm
393393
394-``` pascal
394+``` lfm
395395object Form1: TForm1
396396 Left = 342
397397 Height = 360
398398 Top = 128
399399 Width = 480
400400 Caption = 'World population'
401401 ClientHeight = 360
402402 ClientWidth = 480
403403 OnCreate = FormCreate
404404 Position = poScreenCenter
405405 LCLVersion = '1.1'
406406 object Panel1: TPanel
407407 Left = 0
408408 Height = 36
409409 Top = 0
410410 Width = 480
411411 Align = alTop
412412 ClientHeight = 36
413413 ClientWidth = 480
414414 TabOrder = 0
415415 object ComboBox1: TComboBox
416416 Left = 12
417417 Height = 23
418418 Top = 5
419419 Width = 196
420420 ItemHeight = 15
421421 ItemIndex = 0
422422 Items.Strings = (
423423 'Total population'
424424 'Male population'
425425 'Female population'
426426 'Ratio male/female (%)'
427427 )
428428 OnSelect = ComboBox1Select
429429 Style = csDropDownList
430430 TabOrder = 0
431431 Text = 'Total population'
432432 end
433433 end
434434 object Chart1: TChart
435435 Left = 0
436436 Height = 324
437437 Top = 36
438438 Width = 480
439439 AxisList = <
440440 item
441441 Grid.Visible = False
442442 Alignment = calRight
443443 AxisPen.Visible = True
444444 Marks.Visible = False
445445 Minors = <>
446446 end
447447 item
448448 Grid.Color = clSilver
449449 Alignment = calBottom
450450 AxisPen.Visible = True
451451 Minors = <>
452452 Title.LabelFont.Style = [fsBold]
453453 Title.Visible = True
454454 Title.Caption = 'Age'
455455 end
456456 item
457457 Grid.Color = clSilver
458458 AxisPen.Visible = True
459459 Marks.Format = '%.0n'
460460 Marks.Style = smsCustom
461461 Minors = <>
462462 Title.LabelFont.Orientation = 900
463463 Title.LabelFont.Style = [fsBold]
464464 Title.Visible = True
465465 end>
466466 BackColor = clWhite
467467 Extent.UseYMin = True
468468 Foot.Alignment = taLeftJustify
469469 Foot.Brush.Color = clBtnFace
470470 Foot.Font.Color = clBlue
471471 Foot.Text.Strings = (
472472 'Source:'
473473 'http://www.census.gov/population/international/data/worldpop/tool_population.php'
474474 )
475475 Foot.Visible = True
476476 Margins.Left = 0
477477 Margins.Right = 0
478478 Margins.Bottom = 0
479479 Title.Brush.Color = clBtnFace
480480 Title.Font.Color = clBlue
481481 Title.Font.Style = [fsBold]
482482 Title.Text.Strings = (
483483 'World population'
484484 )
485485 Title.Visible = True
486486 Align = alClient
487487 ParentColor = False
488488 object Chart1AreaSeries1: TAreaSeries
489489 AreaBrush.Color = clSkyBlue
490490 AreaLinesPen.Style = psClear
491491 Source = UserDefinedChartSource1
492492 end
493493 end
494494 object UserDefinedChartSource1: TUserDefinedChartSource
495495 OnGetChartDataItem = UserDefinedChartSource1GetChartDataItem
496496 left = 552
497497 top = 152
498498 end
499499end
500500```
501501
502502---
503503*From the Free Pascal wiki, page "TAChart Tutorial: Userdefined ChartSource", as it stood on 2019-11-16 after a change by Djzepi, out of the wiki's dump of 2024-05-26. Copyright stays with the author who wrote it, according to the old wiki's [[Licensing]] page. If you have concerns about this material, [message the admin](/wiki-contact).*

Back to history