1
votes

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.

1
I don't think the code you have shown is the reason for the delay. The two if and related setting of the annotation position should not take any significant time (but why are you referring to FY instead of Y in the last one?). Same for the last if. But getting the values from the underlying data to the AnnoLst, might take time, but you did not show that, so impossible to evaluate.Tom Brunberg
So, please show the code where you populate AnnoLst.Tom Brunberg
The FY was a mistake I changed it. I timed everything in the mouse move event and the Data -> AnnoLst part took a not measurable time. I only observed a differerence when I leave out this code above. Maybe you are right it not the code above do not cause the lag, but something in the backend of the TChart component. I will put some example in the question that you can evaluate the behaviour.FlorianSchunke
Drawing on the chart requires chart repainting that might take time for long series. Would you like to output info outside the plot region (i.e. legend or special panel)?MBo
That will speed up everything, but that is not a solution for me, unfortunately. The requirement is to show the values beside the cursor.FlorianSchunke

1 Answers

1
votes

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.