[SOLVED] Analog to onClickSeries event lazarus / 55-TAChart ---------------------------------------------------------------------- axline · 2020-04-22 06:16 Hello, folks! I'm moving an ancient Delphi project to Lazarus so it's all about TeeChart -> TAChart transition. This got me by surprise that there's no analog to onClickSeries event in TAChart. The closest thing I've found is described in https://forum.lazarus.freepascal.org/index.php/topic,47644.0.html. Didn't manage to make it work for my needs, though. The task is dead simple: there's a TAChart with a bunch of TLineSeries (a lot of them, up to thousands). No data points are shown (and no needed), just lines. Each TLineSeries has its own unique name. How do I find that name by clicking on the chart? I tried to process onPointClick event of the ChartToolsetDataPointClickTool but it didn't quite work as I had no actual data points shown So the main question: how do I find the name of the TLineSeries I'm clicking on? Any help will be highly appreciated. ---------------------------------------------------------------------- wp · 2020-04-22 21:20 Looks like I forgot this feature when I did the more general usage of ChartTools some time ago... Anyway, now it's done, see Laz trunk r63048-50. If you do not use trunk but a release version of Laz 2.0+, I think you can patch your own version this way (make backup copies of the modified files in case something goes wrong): Open the file components/tachart/tageometry.pas (path relative to your Lazarus installation), and in the interface part, find function PointLineDist and add the directive "overload". Then add this declaration afterwards: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i function PointLineDist(const P, A,B: TPoint; out Q: TPoint; out Inside: Boolean): Integer; overload;In the implementation part add somewhere Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i function PointLineDist(const P, A,B: TPoint; out Q: TPoint;  out Inside: Boolean): Integer;var  v, w: TPoint;  dot: Int64;  lv: Integer;  aq, bq: Integer;begin  if A = B then begin    Result := PointDist(A, P);  Inside := false;    Q := A;  end else begin    v := B - A;    w := P - A;    dot := Int64(v.x) * w.x + Int64(v.y) * w.y;    lv := PointDist(A, B);    Q := (v * dot) div lv;    Result := PointDist(Q, w);     // Check whether the projection point Q is inside the A-B line.    // In this case the lengths AQ and BQ are shorter than AB.  aq := sqr(Q.x) + sqr(Q.y);     // note: Q is seen from A, not from origin.    bq := PointDist(v, Q);    Inside := (aq <= lv) and (bq <= lv);    Q := Q + A;  end;end;Now open TASeries.pas in the same directory. In the interface part, public section of TLineSeries, add: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i     function GetNearestPoint(const AParams: TNearestPointParams;  out AResults: TNearestPointResults): Boolean; override;And in the implementation part, add: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i function TLineSeries.GetNearestPoint(const AParams: TNearestPointParams;  out AResults: TNearestPointResults): Boolean;var  pointIndex, levelIndex: Integer;  ip1, ip2, q: TPoint;  d, dmin: Integer;  isInside: Boolean;  ext: TDoubleRect;begin  Result := false;  AResults.FDist := sqr(AParams.FRadius) + 1;  AResults.FIndex := -1;  AResults.FXIndex := 0;  AResults.FYIndex := 0;   Result := inherited;   if Result or (LineType <> ltFromPrevious) or     not ((nptCustom in AParams.FTargets) and (nptCustom in ToolTargets))  then    exit;   with Extent do begin    ext.a := AxisToGraph(a);    ext.b := AxisToGraph(b);  end;  NormalizeRect(ext);  // Do not do anything if the series extent does not intersect CurrentExtent.  if not RectIntersectsRect(ext, ParentChart.CurrentExtent) then    exit;   // Iterate through all points of the series and - if nptYList is in Targets -  // at all stack levels.  PrepareGraphPoints(ext, true);  dmin := AResults.FDist;  for levelIndex := 0 to Source.YCount-1 do begin    if levelIndex > 0 then      UpdateGraphPoints(levelIndex, FStacked);    ip1 := ParentChart.GraphToImage(FGraphPoints[0]);    for pointIndex := 1 to FUpBound - FLoBound do begin      ip2 := ParentChart.GraphToImage(FGraphPoints[pointIndex]);      d := PointLineDist(AParams.FPoint, ip1, ip2, q, isInside);      if isInside and (d < dmin) then begin        dmin := d;        AResults.FIndex := -1; //pointIndex + FLoBound;        AResults.FYIndex := levelIndex;      AResults.FImg := q;        AResults.FValue := ParentChart.ImageToGraph(q);      end;      ip1 := ip2;    end;    if not ((nptYList in AParams.FTargets) and (nptYList in ToolTargets)) then      break;  end;   if dmin < AResults.FDist then  begin    AResults.FDist := d;    Result := true;  end;end; Rebuild the IDE ("Tools" > "Build Lazarus with profile..."). After a while Lazarus restarts, and you have the new feature. How to apply it? Very easy... Yes, you must add a TChartTools component on the form and link it to the Toolset property of the Chart. Doubleclick on the ChartTools component and add a DatapointClickTool. Set its Shift property to ssLeft to activate the tool with a left mouse click. TLineSeries (like other series) has a property ToolTargets which identifies the elements of the data point record which are checked by the tool. The option nptCustom has not been used by TLineSeries so far, but with the new version it is. Now the series detects the mouse not only on the data points, but also on the line segments between them (provided they are shown). To get a message about which series was clicked you must write a handler of the OnPointClick event of the DatapointClickTool. The event has the Tool as parameter, however, in a very generic way. you must cast it to TDataPointClickTool to get further information; the series is provided as property Series of the DataPointClickTool, again as a very generic series. Cast it to TLineSeries (or more general, TChartSeries) to the title of the series from that same property. Here is a bit more general example which displays also the clicked point: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i uses  TACustomSeries;  // for TChartSeries { TForm1 } procedure TForm1.ChartToolset1DataPointClickTool1PointClick(ATool: TChartTool;  APoint: TPoint);var  msg: String;  tool: TDataPointClickTool;  series: TChartSeries;begin  tool := ATool as TDataPointClickTool;  if tool.Series is TChartSeries then  begin    series := TChartSeries(tool.Series);    if tool.PointIndex = -1 then      msg := Format('Series "%s" clicked on segment at x = %.3f, y = %.3f', [      series.Title,        tool.NearestGraphPoint.X,        tool.NearestGraphPoint.Y      ])    else      msg := Format('Series "%s" click on data pont #%d with x = %.3f, y=%.3f', [        series.Title,        tool.PointIndex,        series.XValue[tool.PointIndex],        series.YValue[tool.PointIndex]      ]);    ShowMessage(msg);  end;end; See the attached demo. There is also a demo in your TAChart folder demo/datatools which shows that all the other data tools also can handle the segment-part of the series now. The datapoint click tool is described in more detail in this tutorial: https://wiki.lazarus.freepascal.org/TAChart_Tutorial:_Chart_Tools ---------------------------------------------------------------------- axline · 2020-04-23 16:09 Oh, that's great! Thanks a lot, wp!  You saved my day I've done everything with my Lazarus 2.0.6 installation and your example worked just fine! There's a small issue with my project though. I create series dynamically and cannot set the flags to ToolTargets. Whenever I set Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i ToolTargets := [nptPoint, nptYList, nptCustom] I get an exception saying "Identifier not found". In which module are those constants declared? I'm attaching your slightly modified example with dynamic TLineSeries creation to illustrate this. One more (unrelated question): how do I change the cursor to crHandPoint while hovering over a TLineSeries? It is even possible? ---------------------------------------------------------------------- wp · 2020-04-23 17:57 The npt* identifiers are declared in unit TAChartTools. A trick how to find out: Uncomment the ToolTargets := ... line again. Ctrl+click on the word "ToolTargets" - this opens the unit in which the proprty ToolTargets is published in TLineLseries. Ctrl-Click on "ToolTargets" in this unit again - now the IDE opens units TACustomSeries where the default value of ToolTargets was changed in TBasicChartSeries. You see: "property ToolTargets default [nptPoint, nptYList]". Cltr-Click on nptPoint - this opens TAChartUtils where nptPoint is declared for the very first time. This is a super-cool method to navigate inside source code. The cursor is defined in the tool. Select the DataPointClickTool in the object inspector (the easiest way is to find it in the object tree at the top), and see the Cursor proprerty. As you will notice the cursor changes only when the tool is active, and this is when the mouse button is pressed. You probably want the cursor to change already when the mouse hovers above the curve. You can add another datapoint tool to the toolset, but leave the Shift property empty (i.e. no mouse button pressed) and only set the Cursor (plus the GrabRadius to have equal sensitivities). However, most tools need some action to start. Use a TDataPointHintTool which can be triggered by mouse movement alone. Alternatively you could also use a TDataPointCrosshairTool which does not allow to change the cursor but draws a crosshair snapping to the curve and following it (you may want set size to some decent value like 12). Returning to the DataPointHint tool, you could also highlight the currently "selected" curve by drawing it with a thicker line. Use the tool's OnAfterMouseMove event and call this code: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i uses  TACustomSeries,  TAEnumerators;procedure TForm1.HighlightSeries(ASeries: TBasicChartSeries);var  ser: TCustomChartSeries;  w: Integer;begin  for ser in CustomSeries(Chart1) do    if ser is TLineSeries then    begin      if ser = ASeries then w := 3 else w := 1;      TLineSeries(ser).LinePen.Width := w;    end;end; procedure TForm1.ChartToolset1DataPointHintTool1AfterMouseMove(  ATool: TChartTool; APoint: TPoint);begin  HighlightSeries(TDatapointHintTool(ATool).Series);end; ---------------------------------------------------------------------- axline · 2020-04-24 06:44 Wow, didn't know about this trick with finding dependencies  Did that and the links led me to the unit TAChartUtils. The example now works exactly the way I'd like it to. Cool! Thank you very much, wp!