Difference between revisions of "TAChart Tutorial: Userdefined ChartSource"

From Free Pascal wiki
Jump to navigationJump to search
Line 10: Line 10:
 
== Preparation ==
 
== Preparation ==
 
=== Data ===
 
=== Data ===
The site [http://www.census.gov/population/international/data/worldpop/tool_population.php www.census.gov] contains a wealth of demographic data. The link leads you directly to the data
+
The site [http://www.census.gov www.census.gov] contains a wealth of demographic data. The [http://www.census.gov/population/international/data/worldpop/tool_population.php link] leads you directly to the data
 
that we want to plot: world population as a function of age and gender.
 
that we want to plot: world population as a function of age and gender.
  
Go 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". 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".
+
Go 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"''. 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"''.
 
 
Alternatively, you find the data also in the Source Code section of this tutorial.
 
  
 
Open Lazarus and create a new project. At first we have to load the data to make them available for plotting.
 
Open Lazarus and create a new project. At first we have to load the data to make them available for plotting.
  
We define a TPopulationRecord and an array type TPopulationArray to hold the data of the population table:
+
We define a <code>TPopulationRecord</code> and an array of type <code>TPopulationArray</code> to hold the data of the population table:
  
 
<source>
 
<source>
Line 37: Line 35:
 
</source>
 
</source>
  
Now let's read the file. The easiest way is to load the lines of the population file to a stringlist. The values in each line are separated by tab characters (<code>#9</code>). Therefore, we assign each line to the <code>DelimitedText</code> property of another stringlist which uses tab as a delimiter. This automatically splits each line into the column values. Then we convert the items of the second stringlist to numbers that we
+
Now let's read the file. The easiest way is to load the population file to a stringlist. The values in each line are separated by tab characters (<code>#9</code>). Therefore, we assign each line to <code>DelimitedText</code> of another stringlist which uses tab as a delimiter. This automatically splits each line into the column values. Then we convert the items of the second stringlist to numbers that we store in the array <code>PopulationData</code> (we have to be careful with the thousand separator present in the data file). Have a look at the procedure <code>LoadPopulationData</code> in the [[TAChart_Tutorial:_Userdefined_ChartSource#Source_code|Source Code]] section of this tutorial. Call this method in the <code>OnLoad</code> event handler of the form.
store in the array <code>PopulationData</code>. Have a look at the procedure <code>LoadPopulationData</code> in the [[Source_code|Source Code]] section of this tutorial. Call this method in the OnLoad event handler of the form.
 
  
Compile to check for obvious errors. You may get a runtime error that the data file cannot be found. This is because our <code>LoadPopulationData</code> method assumes that the file resides in the same directory as the exe file. Copy "population.txt" into that folder, and compile again. Now the program should run fine, you will see an unspectacularly empty form, though.
+
Compile to check for obvious errors. You may get a runtime error that the data file cannot be found. This is because our <code>LoadPopulationData</code> method assumes that the file resides in the same directory as the exe file. Copy ''"population.txt"'' into that folder, and compile again. Now the program should run fine, you will see an unspectacularly empty form, though.
  
 
=== Adding a data selector ===
 
=== Adding a data selector ===
The file contains various categories: total, male, or femal population, and male-to-female ratio. Why not display these data in our chart?  
+
The file contains various categories: total, male, and femal population, as well as male-to-female ratio. Why not display these data in our chart?  
 
 
To prepare for this, let us add a panel to the form, align it to <code>alTop</code>, and delete its caption.
 
  
Add a combobox to this panel, set its <code>Style</code> to <code>csDropdownList</code>, add the strings "Total population", "Male population", "Female population", and "Ratio male/female" to the <code>Items</code> property, and set <code>ItemIndex</code> to 0.
+
* To prepare for this, let us add a '''panel''' to the form, align it to <code>alTop</code>, and delete its caption.
 +
* Add a '''combobox''' to this panel, set its <code>Style</code> to <code>csDropdownList</code>, add the strings "Total population", "Male population", "Female population", and "Ratio male/female" to the <code>Items</code> property, and set <code>ItemIndex</code> to 0.
  
 
=== Preparing TChart ===
 
=== Preparing TChart ===
Line 54: Line 50:
 
* Add a <code>TChart</code> component to the form, align it to <code>alClient</code>.  
 
* Add a <code>TChart</code> component to the form, align it to <code>alClient</code>.  
 
* Set the chart's <code>BackColor</code> to <code>clWhite</code> and the <code>Grid.Color</code> of each axis to <code>clSilver</code>.
 
* Set the chart's <code>BackColor</code> to <code>clWhite</code> and the <code>Grid.Color</code> of each axis to <code>clSilver</code>.
* Activate visibility of the axis titles by setting <code>LeftAxis.Visible</code> and <code>BottomAxis.Visible</code> to <code>true</code>.
+
* Get axis titles shown by setting <code>LeftAxis.Visible</code> and <code>BottomAxis.Visible</code> to <code>true</code>.
* Set the caption of the bottom axis to "Age", leave the caption of the left axis empty - we will later replace it at runtime by the selected item of the combobox.
+
* 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).
 
* Use the text "World population" as the chart's title.
 
* Use the text "World population" 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 our data reference 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.
+
* Maybe it is a good idea to display our data reference in the [[TAChart_documentation#Title_and_footer|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.
  
 
[[file:TAChart_Population2.png]]
 
[[file:TAChart_Population2.png]]
Line 70: Line 66:
  
 
* Set its <code>SeriesColor</code> to <code>clSkyBlue</code>. This is the fill color of the area series.
 
* Set its <code>SeriesColor</code> to <code>clSkyBlue</code>. This is the fill color of the area series.
* Hide the vertical connecting lines by setting the <code>AreaLinesPen</code> to <code>psClear</code>. Later, when the series has data, you can should around with this and other properties to see what they do.
+
* Hide the vertical connecting lines by setting the <code>AreaLinesPen</code> to <code>psClear</code>. Later, when the series has data, you should play around with this and other properties to see what they do.
  
 
== UserDefined chart source ==
 
== UserDefined chart source ==
Line 131: Line 127:
 
Not too bad, some flaws, though.
 
Not too bad, some flaws, though.
  
* 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 <code>Format</code> function in some FPC versions. If you have that issue as well, simply change the property <code>LeftAxis.Marks.Format</code>. This string is passed to the <code>Format</code> function to convert 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.  
+
* 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 <code>Format</code> function for large numbers in some FPC versions. If you have that issue as well, simply change the property <code>LeftAxis.Marks.Format</code>. This string is passed to the <code>Format</code> function to convert 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. However, be aware that the "0" before the "n" cuts off any decimals.
 
* 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 <code>Margin</code> property 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 a bit annoying. So just set <code>Chart.Margin.Left</code> and <code>Chart.Margin.Bottom</code> to 0.
 
* 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 <code>Margin</code> property 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 a bit annoying. So just set <code>Chart.Margin.Left</code> and <code>Chart.Margin.Bottom</code> to 0.
 
* Ah, and the combobox is not working yet. We need to assign an event handler for <code>OnSelect</code>. What does it have to do? Well, when a new 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 correct data automatically. OK - let's call <code>Chart1.Invalidate</code>:
 
* Ah, and the combobox is not working yet. We need to assign an event handler for <code>OnSelect</code>. What does it have to do? Well, when a new 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 correct data automatically. OK - let's call <code>Chart1.Invalidate</code>:
Line 146: Line 142:
 
No. 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 <code>UserDefinedChartSource.Reset</code>. That's it: we replace the <code>Invalidate</code> by the <code>Reset</code>.
 
No. 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 <code>UserDefinedChartSource.Reset</code>. That's it: we replace the <code>Invalidate</code> by the <code>Reset</code>.
  
And we can fix another issue: the caption of the y axis should change when a new category is selected. For this, we define a constant array of axis labels for the four possible selections, and assign the correct one to the chart's <code>LeftAxis.Title.Caption</code>.
+
And we can fix another issue: the caption of the y axis should change when a new category is selected. For this, we copy the text of the selected combobox item to the chart's <code>LeftAxis.Title.Caption</code>. We also have to consider here that values in the 4th item "Ratio male/female" are in a completely different range as the other ones. Therefore we have to adjust the <code>LeftAxis.Marks.Format</code> accordingly:
  
 
<source>
 
<source>
const
 
  AXIS_TITLES: Array[0..3] of String = (
 
    'Total population', 'Male population', 'Female population', 'Male/female (%)');
 
 
 
procedure TForm1.ComboBox1Select(Sender: TObject);
 
procedure TForm1.ComboBox1Select(Sender: TObject);
 
begin
 
begin
   Chart1.LeftAxis.Title.Caption := AXIS_TITLES[Combobox1.ItemIndex];
+
   Chart1.LeftAxis.Title.Caption := Combobox1.Items[Combobox1.ItemIndex];
 
   UserDefinedChartSource1.Reset;
 
   UserDefinedChartSource1.Reset;
 
end;
 
end;
Line 297: Line 289:
 
           Male := StrToFloat(StripThousandSep(trim(List2[2])));
 
           Male := StrToFloat(StripThousandSep(trim(List2[2])));
 
           Female := StrToFloat(StripThousandSep(trim(List2[3])));
 
           Female := StrToFloat(StripThousandSep(trim(List2[3])));
           Ratio := StrToFloat(trim(List2[4]))/100;
+
           Ratio := StrToFloat(trim(List2[4]));
 
         end;
 
         end;
 
         inc(j);
 
         inc(j);
Line 370: Line 362:
 
         'Male population'
 
         'Male population'
 
         'Female population'
 
         'Female population'
         'Ratio male/female'
+
         'Ratio male/female (%)'
 
       )
 
       )
 
       OnSelect = ComboBox1Select
 
       OnSelect = ComboBox1Select

Revision as of 20:57, 12 August 2012

Introduction

In this tutorial we want to create a plot the age distribution of world population. In doing this, you will learn

  • how to work with an area searies
  • and, most of all, how to use a user-defined chart source

As usual, we assume that you are familiar with Lazarus and the ObjectPascal language and have some basic knowledge of the TAChart package; if you don't you should work through the Getting Started tutorial before continuing.

Preparation

Data

The site www.census.gov contains a wealth of demographic data. The link leads you directly to the data that we want to plot: world population as a function of age and gender.

Go 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". 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".

Open Lazarus and create a new project. At first we have to load the data to make them available for plotting.

We define a TPopulationRecord and an array of type TPopulationArray to hold the data of the population table:

type
  TPopulationRecord = record
    Age: Integer;
    Total: Double;
    Male: Double;
    Female: Double;
    Ratio: Double;
  end;

  TPopulationArray = array of TPopulationRecord;

var
  PopulationData: TPopulationArray;

Now let's read the file. The easiest way is to load the population file to a stringlist. The values in each line are separated by tab characters (#9). Therefore, we assign each line to DelimitedText of another stringlist which uses tab as a delimiter. This automatically splits each line into the column values. Then we convert the items of the second stringlist to numbers that we store in the array PopulationData (we have to be careful with the thousand separator present in the data file). Have a look at the procedure LoadPopulationData in the Source Code section of this tutorial. Call this method in the OnLoad event handler of the form.

Compile to check for obvious errors. You may get a runtime error that the data file cannot be found. This is because our LoadPopulationData method assumes that the file resides in the same directory as the exe file. Copy "population.txt" into that folder, and compile again. Now the program should run fine, you will see an unspectacularly empty form, though.

Adding a data selector

The file contains various categories: total, male, and femal population, as well as male-to-female ratio. Why not display these data in our chart?

  • To prepare for this, let us add a panel to the form, align it to alTop, and delete its caption.
  • Add a 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, and set ItemIndex to 0.

Preparing TChart

Now we are finished with preparations, and it's time for charting...

  • Add a TChart component to the form, align it to alClient.
  • Set the chart's BackColor to clWhite and the Grid.Color of each axis to clSilver.
  • Get axis titles shown by setting LeftAxis.Visible and BottomAxis.Visible to true.
  • 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).
  • Use the text "World population" as the chart's title.
  • Set these font styles to fsBold.
  • Maybe it is a good idea to display our data reference in the 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.

TAChart Population2.png

Creating an area series

This time, we want to display our data as an area series. This type of series connects the data points as in a line series, but also fills the area underneath by a given color.

So, add a 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:

  • Set its SeriesColor to clSkyBlue. This is the fill color of the area series.
  • Hide the vertical connecting lines by setting the AreaLinesPen to psClear. Later, when the series has data, you should play around with this and other properties to see what they do.

UserDefined chart source

Now, how do we get our data to the area series?

The first idea might be iterate through our PopulationArray and copy the data to the series by means of AddXY calls, like this:

var
  i: Integer;
begin
  for i:=0 to High(PopulationArray) do
    Chart1AreaSeries1.AddXY(PopulationArray[i].Age, PopulationArray[i].Total);
end;

This disadvantage of this approach is that would have the data in memory twice: in the PopulationArray 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.

TAChart LogAx Tutorial2.png

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;
    // ...
  end;

The fields X and Y are the coordinates of the data point.

Now we can write the event handler for OnGetChartDataItem as follows:

procedure TForm1.UserDefinedChartSource1GetChartDataItem(
  ASource: TUserDefinedChartSource; AIndex: Integer; var AItem: TChartDataItem);
begin
  AItem.X := PopulationData[AIndex].Age;
  case Combobox1.ItemIndex of
    0: AItem.Y := PopulationData[AIndex].Total;
    1: AItem.Y := PopulationData[AIndex].Male;
    2: AItem.Y := PopulationData[AIndex].Female;
    3: AItem.Y := PopulationData[AIndex].Ratio;
  end;
end;

AIndex is the index of the datapoint which is queried. Since both chart source and our PopulationArray 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 give provide a multitude of data.

There are still some important things to do:

  • Tell the series to use the UserDefinedChartSource instead of the built-in source. For this purpose, we point the series' property Source to UserDefinedChartSource1.
  • 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 to this procedure which sets PointsNumber correctly.
  • 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.

Compile.

TAChart Population4.png

Not too bad, some flaws, though.

  • 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 "%0.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" before the "n" cuts off any decimals.
  • 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 Margin property 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 a bit annoying. So just set Chart.Margin.Left and Chart.Margin.Bottom to 0.
  • Ah, and the combobox is not working yet. We need to assign an event handler for OnSelect. What does it have to do? Well, when a new 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 correct data automatically. OK - let's call Chart1.Invalidate:
procedure TForm1.ComboBox1Select(Sender: TObject);
begin
  Chart1.Invalidate;
end;

When you compile and play with different selections of the combobox you will find that the series does nicely update to the selected category. But the axis is frozen. Is there a bug?

No. 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.

And we can fix another issue: the caption of the y axis should change when a new category is selected. For this, we copy the text of the selected combobox item to the chart's LeftAxis.Title.Caption. We also have to consider here that values in the 4th item "Ratio male/female" are in a completely different range as the other ones. Therefore we have to adjust the LeftAxis.Marks.Format accordingly:

procedure TForm1.ComboBox1Select(Sender: TObject);
begin
  Chart1.LeftAxis.Title.Caption := Combobox1.Items[Combobox1.ItemIndex];
  UserDefinedChartSource1.Reset;
end;

Now switching between data categories by means of the combobox is working.

A minor flaw left: Since our data do not contain a 0 for the y axis the automatic axis scaling of the chart hides the zero of the y axis. How can we get it back? 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. Therefore, we have our zero back.

TAChart Population5.png

But there is another 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 (AxisPen.Visible = true. After after doing this, the x axis is still broken. This is a cause 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. When you compile now the chart is perfet.

TAChart Population6.png

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, TASources, TASeries, Forms, Controls,
  Graphics, Dialogs, ExtCtrls, StdCtrls, TACustomSource;

type

  { TForm1 }

  TForm1 = class(TForm)
    Chart1: TChart;
    Chart1AreaSeries1: TAreaSeries;
    ComboBox1: TComboBox;
    Panel1: TPanel;
    UserDefinedChartSource1: TUserDefinedChartSource;
    procedure ComboBox1Select(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure UserDefinedChartSource1GetChartDataItem(
      ASource: TUserDefinedChartSource; AIndex: Integer;
      var AItem: TChartDataItem);
  private
    { private declarations }
    procedure LoadPopulationData;
  public
    { public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

type
  TPopulationRecord = record
    Age: Integer;
    Total: Double;
    Male: Double;
    Female: Double;
    Ratio: Double;
  end;

  TPopulationArray = array of TPopulationRecord;

var
  PopulationData: TPopulationArray;

const
  AXIS_TITLES: Array[0..3] of String = (
    'Total population', 'Male population', 'Female population', 'Male/female (%)');

{ TForm1 }

procedure TForm1.LoadPopulationData;

  function StripThousandSep(const s: String): String;
  var
    i: Integer;
  begin
    Result := s;
    for i:=Length(Result) downto 1 do
      if Result[i] = ',' then
        Delete(Result, i, 1);
  end;

const
  FILENAME = 'population.txt';
var
  List1, List2: TStringList;
  i, j, n: Integer;
  s: String;
  ds: char;
begin
  ds := FormatSettings.DecimalSeparator;
  List1 := TStringList.Create;
  try
    List1.LoadFromFile(FILENAME);
    n := List1.Count;
    SetLength(PopulationData, n-2);
    FormatSettings.DecimalSeparator := '.';
    List2 := TStringList.Create;
    try
      List2.Delimiter := #9;
      List2.StrictDelimiter := true;
      j := 0;
      for i:=2 to n-1 do begin
        List2.DelimitedText := List1[i];
        s := List1[i];
        with PopulationData[j] do begin
          if i < n-1 then
            Age := StrToInt(trim(List2[0]))
          else
            Age := 100;
          Total := StrToFloat(StripThousandSep(trim(List2[1])));
          Male := StrToFloat(StripThousandSep(trim(List2[2])));
          Female := StrToFloat(StripThousandSep(trim(List2[3])));
          Ratio := StrToFloat(trim(List2[4]));
        end;
        inc(j);
      end;
      UserDefinedChartSource1.PointsNumber := Length(PopulationData);
    finally
      List2.Free;
    end;
  finally
    FormatSettings.DecimalSeparator := ds;
    List1.Free;
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  LoadPopulationData;
  Chart1.LeftAxis.Title.Caption := AXIS_TITLES[Combobox1.ItemIndex];
end;

procedure TForm1.ComboBox1Select(Sender: TObject);
begin
  Chart1.LeftAxis.Title.Caption := AXIS_TITLES[Combobox1.ItemIndex];
  UserDefinedChartSource1.Reset;
end;

procedure TForm1.UserDefinedChartSource1GetChartDataItem(
  ASource: TUserDefinedChartSource; AIndex: Integer; var AItem: TChartDataItem);
begin
  AItem.X := PopulationData[AIndex].Age;
  case Combobox1.ItemIndex of
    0: AItem.Y := PopulationData[AIndex].Total;
    1: AItem.Y := PopulationData[AIndex].Male;
    2: AItem.Y := PopulationData[AIndex].Female;
    3: AItem.Y := PopulationData[AIndex].Ratio;
  end;
end;

end.
object Form1: TForm1
  Left = 270
  Height = 360
  Top = 151
  Width = 480
  Caption = 'World population'
  ClientHeight = 360
  ClientWidth = 480
  OnCreate = FormCreate
  Position = poScreenCenter
  LCLVersion = '0.9.31'
  object Panel1: TPanel
    Left = 0
    Height = 36
    Top = 0
    Width = 480
    Align = alTop
    ClientHeight = 36
    ClientWidth = 480
    TabOrder = 0
    object ComboBox1: TComboBox
      Left = 12
      Height = 23
      Top = 5
      Width = 196
      ItemHeight = 15
      ItemIndex = 0
      Items.Strings = (
        'Total population'
        'Male population'
        'Female population'
        'Ratio male/female (%)'
      )
      OnSelect = ComboBox1Select
      Style = csDropDownList
      TabOrder = 0
      Text = 'Total population'
    end
  end
  object Chart1: TChart
    Left = 0
    Height = 324
    Top = 36
    Width = 480
    AxisList = <    
      item
        Grid.Color = clSilver
        Alignment = calBottom
        AxisPen.Visible = True
        Minors = <>
        Title.LabelFont.Style = [fsBold]
        Title.Visible = True
        Title.Caption = 'Age'
      end    
      item
        Grid.Color = clSilver
        AxisPen.Visible = True
        Marks.Format = '%.0n'
        Marks.Style = smsCustom
        Minors = <>
        Title.LabelFont.Orientation = 900
        Title.LabelFont.Style = [fsBold]
        Title.Visible = True
      end>
    BackColor = clWhite
    Extent.UseYMin = True
    Foot.Alignment = taLeftJustify
    Foot.Brush.Color = clBtnFace
    Foot.Font.Color = clBlue
    Foot.Text.Strings = (
      'Source:'
      'http://www.census.gov/population/international/data/worldpop/tool_population.php'
    )
    Foot.Visible = True
    Margins.Left = 0
    Margins.Bottom = 0
    Title.Brush.Color = clBtnFace
    Title.Font.Color = clBlue
    Title.Font.Style = [fsBold]
    Title.Text.Strings = (
      'World population'
    )
    Title.Visible = True
    Align = alClient
    ParentColor = False
    object Chart1AreaSeries1: TAreaSeries
      AreaBrush.Color = clSkyBlue
      AreaLinesPen.Style = psClear
      Source = UserDefinedChartSource1
    end
  end
  object UserDefinedChartSource1: TUserDefinedChartSource
    OnGetChartDataItem = UserDefinedChartSource1GetChartDataItem
    left = 552
    top = 152
  end
end