Search code examples
delphiannotationsteechartdelphi-xe3onmousemove

TeeChart: Fast way to show series values at current mouse position


I want to show the values of all series at the current mouse position if the cursor is on the chart. Exactly as it is displayed in this figure:

Target Display

To accomplish this behavior I used an TAnnotationTool and the OnMouseMove event. Additionally I use a TCursorTool with Style := cssVertical and FollowMouse := True to draw a vertical line at the current mouve position. Unfortunately this solution is very slow. If the series count is greater than 10 the user already could observe that the annotation run after the mouse with a lag of about 500ms. During my investigation of this issue, I found out that this part of the MouseMoveEvent is the bottleneck:

chtMain  : TChart; 
FValAnno : TAnnotationTool;
...
TfrmMain.chtMainMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer)
var
  HasData : Boolean;
  AnnoLst : TStrings;
begin
  ...  
  if HasData then
    Self.FValAnno.Text := AnnoLst.Text
  else
    Self.FValAnno.Text := 'No data';
  //
  if (X < Self.chtMain.Width - Self.FValAnno.Width - 5) then
    Self.FValAnno.Shape.Left := X + 10
  else
    Self.FValAnno.Shape.Left := X - Self.FValAnno.Width - 15;
  //
  if (Y < Self.chtMain.Height - Self.FValAnno.Height - 5) then
    Self.FValAnno.Shape.Top := Y + 10
  else
    Self.FValAnno.Shape.Top := Y - Self.FValAnno.Height - 15;
  //
  if (FX >= Self.chtMain.BottomAxis.IStartPos) and
    (FX <= Self.chtMain.BottomAxis.IEndPos) and
    (FY >= Self.chtMain.LeftAxis.IStartPos) and
    (FY <= Self.chtMain.LeftAxis.IEndPos) then
    Self.FValAnno.Active := True
  else
    Self.FValAnno.Active := False;
  ...
end;

If I use the code above the vertical line and the annotation run after the cursor by about 500ms at a series count of 100. The lag increases the higher the series count is. On the other hand if I do not use the annotation code the vertical line run after only by a lag of about 100ms.

Is there any other tool to accomplish this benaviour much faster with the TChart components? Or are there any properties I can play with to make this faster?

Thanks in advance for your support!

EDIT: Example code to reproduce this issue

  1. Create a new VCL Project
  2. Drop a TChart component and a checkbox on the form
  3. Create the FormCreate for the form and the MouseMoveEvent for the chart
  4. Switch to the code view an insert the following code:

Code:

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, VclTee.TeeGDIPlus,
  VCLTee.TeEngine, Vcl.ExtCtrls, VCLTee.TeeProcs, VCLTee.Chart, VCLTee.TeeTools,
  Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    chtMain: TChart;
    chkAnno: TCheckBox;
    procedure FormCreate(Sender: TObject);
    procedure chtMainMouseMove(Sender: TObject; Shift: TShiftState; X,
      Y: Integer);
  private
    FCursor : TCursorTool;
    FAnno   : TAnnotationTool;
  public
    { Public-Deklarationen }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

uses
  VCLTee.Series,
  System.DateUtils;

const
  ARR_MAXS : array[0..3] of Double = (12.5, 25.8, 2.8, 56.7);

procedure TForm1.chtMainMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);

  function GetXValueIndex(const ASerie: TChartSeries; const AX: Double): Integer;
  var
    index: Integer;
  begin
    for index := 0 to ASerie.XValues.Count - 1 do
    begin
      if ASerie.XValue[index] >= AX then
        Break;
    end;
    //
    Result := index - 1;
  end;

var
  Idx, I    : Integer;
  CursorX,
  CursorY,
  Value     : Double;
  Serie     : TChartSeries;
  LegendTxt : string;
  AnnoLst   : TStrings;
  HasData   : Boolean;
  ShownDate : TDateTime;
begin
  //
  if not Self.chkAnno.Checked then
  begin
    //
    FAnno.Text := Format('Position:'#13#10'  X: %d'#13#10'  Y: %d', [X, Y]);
  end
  else
  begin
    //
    if (Self.chtMain.SeriesCount < 1) then
    begin
      //
      if Assigned(Self.FAnno) then
        Self.FAnno.Active := False;
      Exit;
    end;
    //
    Self.chtMain.Series[0].GetCursorValues(CursorX, CursorY);
    //
    AnnoLst := TStringList.Create;
    try
      //
      ShownDate := 0;
      HasData   := False;
      for I := 0 to Self.chtMain.SeriesCount - 1 do
      begin
        //
        Serie := Self.chtMain.Series[I];
        //
        Idx := GetXValueIndex(Serie, CursorX);

        if Serie.XValue[Idx] > ShownDate then
        begin
          //
          LegendTxt := DateTimeToStr(Serie.XValue[Idx]);
          if (AnnoLst.Count > 0) and
            (ShownDate > 0) then
            AnnoLst[0] := LegendTxt
          else if AnnoLst.Count > 0 then
            AnnoLst.Insert(0, LegendTxt)
          else
            AnnoLst.Add(LegendTxt);
          HasData   := True;
          ShownDate := Serie.XValue[Idx];
        end;
        //
        LegendTxt := Format('Serie: %d', [I]);
        if Length(LegendTxt) <= 25 then
          LegendTxt := Format('%-25s', [LegendTxt])
        else
          LegendTxt := Format('%s...', [LegendTxt.Substring(0, 22)]);
        //
        Value     := Serie.YValue[Idx] * Abs(ARR_MAXS[I]);
        LegendTxt := Format('%s: %3.3f %s', [LegendTxt, Value, 'none']);
        AnnoLst.Add(LegendTxt);
      end;

      FAnno.Text := AnnoLst.Text;
    finally
      FreeAndNil(AnnoLst);
    end;
  end;

  if (X < Self.chtMain.Width - Self.FAnno.Width - 5) then
    Self.FAnno.Shape.Left := X + 10
  else
    Self.FAnno.Shape.Left := X - Self.FAnno.Width - 15;
  //
  if (Y < Self.chtMain.Height - Self.FAnno.Height - 5) then
    Self.FAnno.Shape.Top := Y + 10
  else
    Self.FAnno.Shape.Top := Y - Self.FAnno.Height - 15;
  //
  if (X >= Self.chtMain.BottomAxis.IStartPos) and
    (X <= Self.chtMain.BottomAxis.IEndPos) and
    (Y >= Self.chtMain.LeftAxis.IStartPos) and
    (Y <= Self.chtMain.LeftAxis.IEndPos) then
    Self.FAnno.Active := True
  else
    Self.FAnno.Active := False;
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  Idx, J : Integer;
  Serie  : TFastLineSeries;
  Start  : TDateTime;
  Value  : Double;
begin
  //
  Self.chtMain.View3D                    := False;
  Self.chtMain.Align                     := alClient;
  Self.chtMain.BackColor                 := clWhite;
  Self.chtMain.Color                     := clWhite;
  Self.chtMain.Gradient.Visible          := False;
  Self.chtMain.Legend.LegendStyle        := lsSeries;
  Self.chtMain.Zoom.Allow                := False; 
  Self.chtMain.AllowPanning              := pmNone;  
  Self.chtMain.BackWall.Color            := clWhite;
  Self.chtMain.BackWall.Gradient.Visible := False;

  Self.chtMain.LeftAxis.Automatic        := False;
  Self.chtMain.LeftAxis.Minimum          := 0;
  Self.chtMain.LeftAxis.Maximum          := 2;
  Self.chtMain.LeftAxis.Increment        := 0.1;
  Self.chtMain.LeftAxis.Visible          := True;
  Self.chtMain.LeftAxis.AxisValuesFormat := '#,##0.## LV';
  //
  Self.chtMain.BottomAxis.DateTimeFormat   := 'dd.mm.yyyy hh:mm:ss';
  Self.chtMain.BottomAxis.Increment        := 1 / 6; 
  Self.chtMain.BottomAxis.Automatic        := True;
  Self.chtMain.BottomAxis.LabelsSize       := 32;
  Self.chtMain.BottomAxis.LabelsMultiLine  := True;
  Self.chtMain.MarginBottom                := 6;
  Self.chtMain.BottomAxis.Title.Caption    := 'Date';
  Self.chtMain.BottomAxis.Visible          := False;


  FAnno := Self.chtMain.Tools.Add(TAnnotationTool) as TAnnotationTool;
  FAnno.Active := False;
  FAnno.Shape.CustomPosition := True;

  FCursor := Self.chtMain.Tools.Add(TCursorTool) as TCursorTool;
  FCursor.FollowMouse := True;
  FCursor.Style := cssVertical;

  Randomize;
  Start := Now;
  for Idx := 0 to 3 do
  begin
    //
    Serie := Self.chtMain.AddSeries(TFastLineSeries) as TFastLineSeries;
    Serie.FastPen := True;
    Serie.ShowInLegend := False;
    Serie.XValues.DateTime := True;
    Serie.VertAxis := aLeftAxis;
    Serie.ParentChart := Self.chtMain;

    for J := 1 to 1000 do
    begin
      //
      Value := Random * ARR_MAXS[Idx] * 1.8;
      Serie.AddXY(IncSecond(Start, J), Value / ARR_MAXS[Idx]);
    end;
  end;
end;

end.
  1. Press [F9]

I do not observe any difference, whether you use the position annotation code or the other one.


Solution

  • The TCursorTool has a FullRepaint property (false by default) to draw it using XOR so the full chart doesn't need to be repainted everytime it updates its position. And this is fast.

    However, the TAnnotationTool doesn't include this possibility so, when you update your FAnnot text or position, you are forcing a chart repaint, and having many points makes the process slower.

    You could use a TLabel component instead of using a TAnnotationTool to draw the text.