Page 1 of 1

Placement of Series.Marks (TPointSeries)

Posted: Thu May 08, 2025 4:53 pm
by 16582633
I am trying to place the Marks for a TPointSeries at a consistent offset from the points. As such I have assigned the following procedure to the OnGetMarkText event of my series (note this is an in-memory chart drawn to a metafile but I think that doesn't make a difference):

Code: Select all

Series.OnGetMarkText := SeriesGetMarkText;
The procedure looks like this (thanks to several samples on the forum):

Code: Select all

procedure TMyForm.SeriesGetMarkText(Sender: TChartSeries; ValueIndex: Integer; var MarkText: string);
var
  LPosition: TSeriesMarkPosition;
  XPos, YPos: Integer;
begin
  inherited;
  LPosition := Sender.Marks.Positions[ValueIndex];
  if LPosition = nil then
  begin
    LPosition := TSeriesMarkPosition.Create;
    LPosition.Custom := true;
  end;
  XPos := Sender.CalcXPos(ValueIndex);
  YPos := Sender.CalcYPos(ValueIndex);
  LPosition.LeftTop.X := XPos + LPosition.Width + 50; // Arbitrary shift for demo
  LPosition.LeftTop.Y := YPos - LPosition.Height - 10;
  Sender.Marks.Positions[ValueIndex] := LPosition;
end;
I have several issues with this example:
  • At first pass, LPosition is never (!) assigned, so it must be created (if there is a non-empty label).
  • When creating the TSeriesMarkPosition, its Width and Height are always initiated as 0 (i.e. not sized to MarkText)
  • The code above leaks. ReportMemoryLeaksOnShutdown always reports exactly the number of TSeriesMarkPositions created (i.e. the number of points where label is a non-empty string).
How can I properly implement this? The documentation (Help) is not exactly clear. For example, when are the Positions created and what's the effect of Marks.AutoPosition and TSeriesMarkPosition.Custom on this? Are the created TSeriesMarkPositions not owned by something (e.g. Marks, Series)? If TSeriesMarks are created, where to destroy them and when?

Re: Placement of Series.Marks (TPointSeries)

Posted: Fri May 09, 2025 1:42 pm
by yeray
Hello,

During the drawing routine, at the DrawMarks function the string to be drawn is calculated (which fires the OnGetMarkText event). Next, the TSeriesMarks.InternalDraw function is called. In this function we create an array of TSeriesMarkPosition if it doesn't exist and initialise it, setting Custom property to False. This allows the users to automatically initialise the marks positions at a first draw, and modify them relative to that initial position if they want.
The Marks.AutoPosition property is used to enable our anti-overlap algorithm to automatically move the marks to fit.

Re: Placement of Series.Marks (TPointSeries)

Posted: Sun May 11, 2025 6:31 pm
by 16582633
Thanks for the explanation but I still can't succeed in getting it to work.

I have fixed the memory leaks by simply not creating the TPosition. Only if assigned, I set properties.

Code: Select all

procedure TMyForm.SeriesGetMarkText(Sender: TChartSeries; ValueIndex: Integer; var MarkText: string);
var
  LPosition: TSeriesMarkPosition;
  XPos, YPos: Integer;
begin
  inherited;
  LPosition := Sender.Marks.Positions[ValueIndex];
  if LPosition <> nil then
  begin
    LPosition.Custom := true;
  XPos := Sender.CalcXPos(ValueIndex);
  YPos := Sender.CalcYPos(ValueIndex);
  LPosition.LeftTop.X := XPos + LPosition.Width + 50; // Arbitrary shift for demo
  LPosition.LeftTop.Y := YPos - LPosition.Height - 10;
end;
As mentioned, the graph is in memory only and I draw it with DrawToMetaCanvas. But whatever I do, whenever the Series is populated with new data the first call to DrawToMetaCanvas does not trigger OnGetSeriesMarks. This only happens on the second and subsequent calls. How can I fix this (i.e. trigger the event on the first DrawToMetaCanvas)?

Re: Placement of Series.Marks (TPointSeries)

Posted: Mon May 12, 2025 6:39 am
by yeray
Hello,

You may need to force a chart draw before exporting the chart to a metafile.
I've tried it in this example and it seems to work:

Code: Select all

uses Chart, Series, TeEngine, ExtCtrls;

procedure TForm1.FormCreate(Sender: TObject);
var
  Chart1: TChart;
  metafile: TMetafile;
  image: TImage;
begin
  // Create in-memory chart
  Chart1:=TChart.Create(Self);

  with Chart1 do
  begin
    //Parent:=Self;
    Align:=alClient;
    Color:=clWhite;
    Gradient.Visible:=False;
    Walls.Back.Color:=clWhite;
    Walls.Back.Gradient.Visible:=False;
    Legend.Hide;
    View3D:=False;

    with AddSeries(TBarSeries) do
    begin
      FillSampleValues;
      OnGetMarkText:=SeriesGetMarkText;
    end;
  end;

  // Force a chart draw
  Chart1.Draw;

  // Export to metafile
  metafile := Chart1.TeeCreateMetafile(True, Rect(0, 0, 400, 300));
  if (Assigned(metafile)) then
  begin
    image := TImage.Create(Self);
    image.Parent:=Self;
    image.Align:=alClient;
    image.Picture.Assign(Metafile);
  end;

  metafile.Free;
end;

procedure TForm1.SeriesGetMarkText(Sender: TChartSeries; ValueIndex: Integer; var MarkText: string);
var
  LPosition: TSeriesMarkPosition;
  XPos, YPos: Integer;
begin
  inherited;
  LPosition := Sender.Marks.Positions[ValueIndex];
  if LPosition <> nil then
  begin
    LPosition.Custom := true;
    XPos := Sender.CalcXPos(ValueIndex);
    YPos := Sender.CalcYPos(ValueIndex);
    LPosition.LeftTop.X := XPos + LPosition.Width + 50; // Arbitrary shift for demo
    LPosition.LeftTop.Y := YPos - LPosition.Height - 10;
  end;
end;

Re: Placement of Series.Marks (TPointSeries)

Posted: Mon May 12, 2025 8:18 am
by 16582633
I had figured that something like that was required. However, Invalidate and Repaint did NOT work.

Code: Select all

  // FChart.Invalidate;
  // FChart.Repaint;
  FChart.Draw;

  FChart.DrawToMetaCanvas(FCanvas, FDrawingRect);
Thanks for providing the solution. Any thoughts why the other two methods didn't work and Draw did?

Re: Placement of Series.Marks (TPointSeries)

Posted: Mon May 12, 2025 10:12 am
by yeray
Hello,

Invalidate sets the chart "dirty" but not immediately repainted.
Repaint calls Invalidate and Update, where the later immediately repaints any invalidated region if the WindowHandle is allocated, which isn't the case at the form FormCreate yet.