Recent

Author Topic: Multi Lineseries  (Read 1989 times)

mpknap

  • Jr. Member
  • **
  • Posts: 56
Re: Multi Lineseries
« Reply #15 on: March 13, 2019, 06:02:30 am »
The code below is part of a larger program. But it's a chart engine that you can run and check.

As at the beginning of the thread, I asked about the array of chart. This part has been resolved.
Now I have a problem with ChartToolset1DataPointClick. This is probably related to Array. I want to display the value of the clicked point.
Using the code from the tutorial, an error pops up.

Another small question. Is it possible to do the text of LeftAxis.Marks, was displayed only on the existing Series?
LeftAxis is "user_number". At the beginning, I create 15000 Series because there are so many users, but I want to describe only those displayed on Chart1.
Code: Pascal  [Select]
  1. unit Unit1;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. interface
  6.  
  7. uses
  8.   Classes, SysUtils, FileUtil, TAGraph, TAIntervalSources, TASources, Forms,
  9.   Controls, Graphics, Dialogs, StdCtrls, ComCtrls, TATools,
  10.   dateutils, tatypes, TASeries, Types, TACustomSeries;
  11.  
  12. type
  13.  
  14.   { TForm1 }
  15.  
  16.   TForm1 = class(TForm)
  17.     Chart1: TChart;
  18.     ChartToolset1: TChartToolset;
  19.     ChartToolset1DataPointClickTool1: TDataPointClickTool;
  20.     DateTimeIntervalChartSource1: TDateTimeIntervalChartSource;
  21.     Label1: TLabel;
  22.     Label2: TLabel;
  23.     ListChartSource1: TListChartSource;
  24.     StatusBar1: TStatusBar;
  25.     procedure ChartToolset1DataPointClickTool1AfterMouseDown(ATool: TChartTool;
  26.       APoint: TPoint);
  27.     procedure FormCreate(Sender: TObject);
  28.   private
  29.  
  30.   public
  31.  
  32.   end;
  33.  
  34. type
  35.   hit = record
  36.     time: int64;
  37.     lat, long, user_n, user_name: string;
  38.   end;
  39.  
  40. var
  41.   Form1: TForm1;
  42.   Series_ar: array[1..15000] of TLineSeries;
  43.  
  44.  
  45. implementation
  46.  
  47. {$R *.lfm}
  48.  
  49. { TForm1 }
  50.  
  51. procedure TForm1.FormCreate(Sender: TObject);
  52. var
  53.   Detection: array[1..15000] of hit;
  54.   f: textfile;
  55.   i, j: integer;
  56.   cc: int64;
  57.   Read_Line: string;
  58.   dt: tdatetime;
  59. begin
  60.   AssignFile(f, '01.01.2019.txt');
  61.   reset(f);
  62.   i := 1;
  63.   while not EOF(f) do
  64.   begin
  65.     readln(f, cc);
  66.     Detection[i].time := cc;
  67.  
  68.     readln(f, Read_Line);
  69.     Detection[i].lat := Read_Line;
  70.  
  71.     readln(f, Read_Line);
  72.     Detection[i].long := Read_Line;
  73.  
  74.     readln(f, Read_Line);
  75.     Detection[i].user_n := Read_Line;
  76.  
  77.     readln(f, Read_Line);
  78.  
  79.     readln(f, Read_Line);
  80.     Detection[i].user_name := Read_Line;
  81.  
  82.     readln(f, Read_Line);
  83.     Inc(i);
  84.   end;
  85.   closefile(f);
  86.   for j := Low(Series_ar) to High(Series_ar) do
  87.   begin
  88.     Series_ar[j] := TLineSeries.Create(Chart1);
  89.     Series_ar[j].ShowPoints := True;
  90.     Series_ar[j].Pointer.Brush.Color := clRed;
  91.     Series_ar[j].Pointer.Pen.Color := clBlack;
  92.     Series_ar[j].Pointer.Style := psCircle;
  93.   end;
  94.   for j := 1 to High(Series_ar) do
  95.   begin
  96.     if Detection[j].user_n <> '' then
  97.     begin
  98.       DT := unixToDateTime((Detection[j].time) div 1000);
  99.  
  100.       Series_ar[StrToInt(Detection[j].user_n)].AddXy(dt,
  101.         StrToInt(Detection[j].user_n), 'a');
  102.       Series_ar[StrToInt(Detection[j].user_n)].Title :=
  103.         (Detection[j].user_name) + ' (' + (Detection[j].user_n) + ')';
  104.       Chart1.AddSeries(Series_ar[StrToInt(Detection[j].user_n)]);
  105.     end;
  106.  
  107.   end;
  108.  
  109. end;
  110.  
  111. procedure TForm1.ChartToolset1DataPointClickTool1AfterMouseDown(ATool: TChartTool;
  112.   APoint: TPoint);
  113. var
  114.   x, y: double;
  115. begin
  116.   with ATool as TDatapointClickTool do
  117.     if (Series is TLineSeries) then
  118.       with TLineSeries(Series) do
  119.       begin
  120.         x := GetXValue(PointIndex);
  121.         y := GetYValue(PointIndex);
  122.         Statusbar1.SimpleText := Format('%s: x = %f, y = %f', [Title, x, y]);
  123.       end
  124.     else
  125.       Statusbar1.SimpleText := '';
  126. end;
  127.  
  128. end.                  

wp

  • Hero Member
  • *****
  • Posts: 5454
Re: Multi Lineseries
« Reply #16 on: March 13, 2019, 07:30:27 pm »
Now I have a problem with ChartToolset1DataPointClick. This is probably related to Array. I want to display the value of the clicked point.
Using the code from the tutorial, an error pops up.
Which error? Which code? (Sorry, I wrote so many tutorials, I really don't know by heart which one covers the datapoint click tool). Does the problem occur also with the code that you show in the attachment?

The DataPointClickTool in the attachment works for me, but only in principle. Because using the DataPointCLICKTool implies that you really perform a CLICK, not a key press. When I set the Shift of the tool to ssLeft instead of ssShift then everything works fine. Of course you can combine both ([ssLeft, ssShift]) to trigger the tool by a SHIFT-Click event.

Is it possible to do the text of LeftAxis.Marks, was displayed only on the existing Series?
Regarding the 150000 series I would introduce a stringlist to which each user found during reading is added avoiding duplicates. After reading you can set the length of the series array exactly to the length of the stringlist in order to have a series for each user.

I would also use a dynamic array for the detections because you are wasting memory when the actual data file contains less then 150000 events or run into problems when it contains more events.

And in oder to facilitate reading your code for others I'd recommend that you stick to conventions, i.e. preceed type with a "T", i.e. "THit" instead of "hit".

These ideas are contained in this code segment:
Code: Pascal  [Select]
  1. type
  2.   TForm1 = class(TForm)
  3.   private
  4.     Series_ar: array of TLineSeries;  
  5.   ...
  6. type
  7.   THit = record
  8.     time: int64;
  9.     lat, long, user_n, user_name: string;
  10.   end;
  11. ...
  12. procedure TForm1.FormCreate(Sender: TObject);
  13. const
  14.   BLOCK_SIZE = 1000;
  15. var
  16.   Detection: array of THit;
  17.   f: textfile;
  18.   i, j: integer;
  19.   cc: int64;
  20.   Read_Line: string;
  21.   dt: tdatetime;
  22.   Users: TStringList;
  23. begin
  24.   Users := TStringList.Create;
  25.   Users.Duplicates := dupIgnore;
  26.   Users.Sorted := true;
  27.  
  28.   AssignFile(f, '01.01.2019.txt');
  29.   reset(f);
  30.   i := 0;
  31.   SetLength(Detection, 0);
  32.   while not EOF(f) do
  33.   begin
  34.     // To avoid excessive copying of data allocate memory for the Detection array only in blocks of 1000 elements.
  35.     if i mod BLOCK_SIZE = 0 then
  36.       SetLength(Detection, Length(Detection) + BLOCK_SIZE);
  37.  
  38.     readln(f, cc);
  39.     Detection[i].time := cc;
  40.  
  41.     readln(f, Read_Line);
  42.     Detection[i].lat := Read_Line;
  43.  
  44.     readln(f, Read_Line);
  45.     Detection[i].long := Read_Line;
  46.  
  47.     readln(f, Read_Line);
  48.     Detection[i].user_n := Read_Line;
  49.  
  50.     readln(f, Read_Line);
  51.  
  52.     readln(f, Read_Line);
  53.     Detection[i].user_name := Read_Line;
  54.  
  55.     Users.Add(Detection[i].user_n);
  56.  
  57.     readln(f, Read_Line);
  58.     Inc(i);
  59.   end;
  60.   closefile(f);
  61.   // Now we know the exact length of the Detection array and can adjust its length accordingly.
  62.   SetLength(Detection, i);
  63.  
  64.   // The stringlist "UserNames" contains the list of all user names. Each user
  65.   // name should have its own line series.
  66.   SetLength(Series_ar, Users.Count);
  67.   for j := 0 to Users.Count-1 do
  68.   begin
  69.     Series_ar[j] := TLineSeries.Create(Chart1);
  70.     Series_ar[j].ShowPoints := True;
  71.     Series_ar[j].Pointer.Brush.Color := clRed;
  72.     Series_ar[j].Pointer.Pen.Color := clBlack;
  73.     Series_ar[j].Pointer.Style := psCircle;
  74.     Series_ar[j].Title := '';
  75.     Chart1.AddSeries(Series_ar[j]);
  76.   end;
  77.  
  78.   // Populate the series with the detection times, on a per-user base.
  79.   for i := 0 to High(Detection) do begin
  80.     j := Users.IndexOf(Detection[i].User_n);
  81.     if j = -1 then
  82.       continue;
  83.     DT := UnixToDateTime(Detection[i].time div 1000);
  84.     Series_ar[j].AddXY(dt, StrToInt(Detection[i].user_n), 'a');
  85.     if Series_ar[j].Title = '' then
  86.       Series_ar[j].Title := Detection[i].user_name + ' (' + Detection[i].user_n + ')';
  87.   end;
  88.  
  89.   Users.Free;
  90. end;            
Lazarus trunk / fpc 3.0.4 / all 32-bit on Win-10

mpknap

  • Jr. Member
  • **
  • Posts: 56
Re: Multi Lineseries
« Reply #17 on: March 13, 2019, 08:20:24 pm »
 
Quote
Which error? Which code? (Sorry, I wrote so many tutorials, I really don't know by heart which one covers the datapoint click tool). Does the problem occur also with the code that you show in the attachment?
 

Error after clicking on the series point, screenshot in attachment.

Quote
Which code?
Code: Pascal  [Select]
  1. procedure TForm1.ChartToolset1DataPointClickTool1AfterMouseDown(ATool: TChartTool;
  2.   APoint: TPoint);
  3. var
  4.   x, y: double;
  5. begin
  6.   with ATool as TDatapointClickTool do
  7.     if (Series is TLineSeries) then
  8.       with TLineSeries(Series) do
  9.       begin
  10.         x := GetXValue(PointIndex);
  11.         y := GetYValue(PointIndex);
  12.         Statusbar1.SimpleText := Format('%s: x = %f, y = %f', [Title, x, y]);
  13.       end
  14.     else
  15.       Statusbar1.SimpleText := '';
  16. end;

Other tips for which I thank you, I will check tomorrow

wp

  • Hero Member
  • *****
  • Posts: 5454
Re: Multi Lineseries
« Reply #18 on: March 13, 2019, 10:59:40 pm »
Error 219 is "invalid type-cast": the parameter ATool does not seem to be a TDatapointClickTool. Is this in the code that you posted? Maybe you incidentally assigned it to another chart tool.
Lazarus trunk / fpc 3.0.4 / all 32-bit on Win-10

mpknap

  • Jr. Member
  • **
  • Posts: 56
Re: Multi Lineseries
« Reply #19 on: March 14, 2019, 06:45:07 am »
Hmmm, everything works. The problem was in a larger program. In the code added above on the forum, sShift is actually badly marked, and the error image(attachment) is from a large program.

Thanks for the great tips about Arrays. This will solve a lot of problems.

What else interests me? LeftAxis description.
Can it be only at user.n height? And it would be best if it was User_name string. Because when you zoom, you can see the values between them and they are not needed.

wp

  • Hero Member
  • *****
  • Posts: 5454
Re: Multi Lineseries
« Reply #20 on: March 15, 2019, 12:38:43 pm »
In the previous version the y values were equal to the user number ("user_n"). This means that space will be reserved on the y axis also for those users without Detection values.

You can fix this by using the stringlist index of the user for y instead; the stringlist was constructed to contain only those users with detections. The stringlist, however, is available only temporarily, but since each stringlist item corresponds to a lineseries in your case the stringlist index is equal to the series index in the Series_ar array.

Thus, you can use the event OnMarkToText of the chart's LeftAxis to convert the series index (being the axis label primarily) to a nice text given by the series' Title.

Since the y values run from bottom to top and the user list is sorted alphabetically you will find "A" at the bottom and "Z" at the top. To reverse the order you can simply set the "Inverted" property of the chart's LeftAxis to "true".

Code: Pascal  [Select]
  1. uses
  2.   lconvencoding;   // for conversion of names in data file to UTF8
  3.  
  4. procedure TForm1.FormCreate(Sender: TObject);
  5. const
  6.   BLOCK_SIZE = 1000;
  7. var
  8.   Detection: array of THit;
  9.   f: textfile;
  10.   i, j: integer;
  11.   cc: int64;
  12.   Read_Line: string;
  13.   dt: tdatetime;
  14.   Users: TStringList;
  15. begin
  16.   Users := TStringList.Create;
  17.   Users.Duplicates := dupIgnore;
  18.   Users.Sorted := true;
  19.  
  20.   AssignFile(f, '01.01.2019.txt');
  21.   reset(f);
  22.   i := 0;
  23.   SetLength(Detection, 0);
  24.   while not EOF(f) do
  25.   begin
  26.     if i mod BLOCK_SIZE = 0 then
  27.       SetLength(Detection, Length(Detection) + BLOCK_SIZE);
  28.  
  29.     readln(f, cc);
  30.     Detection[i].time := cc;
  31.  
  32.     readln(f, Read_Line);
  33.     Detection[i].lat := Read_Line;
  34.  
  35.     readln(f, Read_Line);
  36.     Detection[i].long := Read_Line;
  37.  
  38.     readln(f, Read_Line);
  39.     Detection[i].user_n := Read_Line;
  40.  
  41.     readln(f, Read_Line);
  42.  
  43.     readln(f, Read_Line);
  44.     Detection[i].user_name := CP1251ToUTF8(Read_Line);  
  45.     // The data file seems to be encoded in CP1251 and must be converted to UTF8
  46.     // at least here for the user name.
  47.  
  48.     Users.Add(Detection[i].user_name);  
  49.     // This is modified to use the user_name instead of user_n in the previous version.
  50.     // The advantage is that the user list is sorted alphabetically by name
  51.  
  52.     readln(f, Read_Line);
  53.     Inc(i);
  54.   end;
  55.   closefile(f);
  56.   SetLength(Detection, i);
  57.  
  58.   // The stringlist "UserNames" contains the list of all user names. Each user
  59.   // name should have its own line series.
  60.   Chart1.DisableRedrawing;
  61.   try
  62.     SetLength(Series_ar, Users.Count);
  63.     for j := 0 to Users.Count-1 do
  64.     begin
  65.       Series_ar[j] := TLineSeries.Create(Chart1);
  66.       Series_ar[j].ShowPoints := True;
  67.       Series_ar[j].Pointer.Brush.Color := clRed;
  68.       Series_ar[j].Pointer.Pen.Color := clBlack;
  69.       Series_ar[j].Pointer.Style := psCircle;
  70.       Series_ar[j].Title := '';
  71.       Chart1.AddSeries(Series_ar[j]);
  72.     end;
  73.  
  74.     // Populate the series with the detection times, on a per-user base.
  75.     for i := 0 to High(Detection) do begin
  76.       j := Users.IndexOf(Detection[i].User_name);
  77.       if j = -1 then
  78.         continue;
  79.  
  80.       DT := UnixToDateTime(Detection[i].time div 1000);
  81.       Series_ar[j].AddXY(dt, j);  // use the series index as y value
  82.       if Series_ar[j].Title = '' then
  83.         Series_ar[j].Title := Detection[i].user_name + ' (' + Detection[i].user_n + ')';
  84.     end;
  85.   finally
  86.     Chart1.EnableRedrawing;
  87.   end;
  88.  
  89.   Users.Free;
  90. end;
  91.  
  92. procedure TForm1.Chart1AxisList0MarkToText(var AText: String; AMark: Double);
  93. var
  94.   idx: Integer;
  95. begin
  96.   // AMark is the y value to which the axis wants to place a label. But the y values are equal to the
  97.   // series indexes in our case. Therefore, we can select the label text to be the series title which
  98.   // contains the user_name
  99.   idx := round(AMark);  
  100.   if (idx < 0) or (idx >= Length(Series_ar)) then
  101.     AText := ''
  102.   else
  103.     AText := Series_ar[idx].Title;
  104. end;
Lazarus trunk / fpc 3.0.4 / all 32-bit on Win-10

mpknap

  • Jr. Member
  • **
  • Posts: 56
Re: Multi Lineseries
« Reply #21 on: March 16, 2019, 07:02:04 am »
Ok, I will test and check it soon

Question. The graph is drawn in memory and later displayed on the screen. Is it possible to be drawn in real time on the screen? I would even like to delay the loop to make it look like an animated presentation. ;)

wp

  • Hero Member
  • *****
  • Posts: 5454
Re: Multi Lineseries
« Reply #22 on: March 16, 2019, 12:26:19 pm »
Question. The graph is drawn in memory and later displayed on the screen. Is it possible to be drawn in real time on the screen? I would even like to delay the loop to make it look like an animated presentation. ;)
  • Move the chart- and data-related code from the OnCreate method to a separate method, such as "LoadData".
    Call "LoadData" in the OnActivate method of the form.
  • In "LoadData", remove the Chart1.DisableRedrawing and .EnableRedrawing calls.
  • At the end of the i loop in "LoadData" (which adds the data to the series) put a call to "Application.ProcessMessages" which executes the message loop containing the chart repainting requests created when data are added to a series. Since loading several thousand data points this way will be very slow I recommend to call Application.ProcessMessages only after some block of data points, like this
Code: Pascal  [Select]
  1.     // Populate the series with the detection times, on a per-user base.
  2.     for i := 0 to High(Detection) do begin
  3.       j := Users.IndexOf(Detection[i].User_name);
  4.       if j = -1 then
  5.         continue;
  6.  
  7.       DT := UnixToDateTime(Detection[i].time div 1000);
  8.       Series_ar[j].AddXY(dt, j);
  9.       if Series_ar[j].Title = '' then
  10.         Series_ar[j].Title := Detection[i].user_name + ' (' + Detection[i].user_n + ')';
  11.  
  12.       if j mod 20 = 0 then            // <---- ADDED
  13.         Application.ProcessMessages;  // <---- ADDED
  14.     end;  
« Last Edit: March 16, 2019, 12:28:46 pm by wp »
Lazarus trunk / fpc 3.0.4 / all 32-bit on Win-10