Placement of Series.Marks (TPointSeries)

TeeChart VCL for Borland/CodeGear/Embarcadero RAD Studio, Delphi and C++ Builder.
Post Reply
Softdrill NL
Newbie
Newbie
Posts: 17
Joined: Thu Dec 28, 2017 12:00 am

Placement of Series.Marks (TPointSeries)

Post by Softdrill NL » Thu May 08, 2025 4:53 pm

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?

Yeray
Site Admin
Site Admin
Posts: 9674
Joined: Tue Dec 05, 2006 12:00 am
Location: Girona, Catalonia
Contact:

Re: Placement of Series.Marks (TPointSeries)

Post by Yeray » Fri May 09, 2025 1:42 pm

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.
Best Regards,
ImageYeray Alonso
Development & Support
Steema Software
Av. Montilivi 33, 17003 Girona, Catalonia (SP)
Image Image Image Image Image Image Please read our Bug Fixing Policy

Softdrill NL
Newbie
Newbie
Posts: 17
Joined: Thu Dec 28, 2017 12:00 am

Re: Placement of Series.Marks (TPointSeries)

Post by Softdrill NL » Sun May 11, 2025 6:31 pm

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)?

Yeray
Site Admin
Site Admin
Posts: 9674
Joined: Tue Dec 05, 2006 12:00 am
Location: Girona, Catalonia
Contact:

Re: Placement of Series.Marks (TPointSeries)

Post by Yeray » Mon May 12, 2025 6:39 am

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;
Best Regards,
ImageYeray Alonso
Development & Support
Steema Software
Av. Montilivi 33, 17003 Girona, Catalonia (SP)
Image Image Image Image Image Image Please read our Bug Fixing Policy

Softdrill NL
Newbie
Newbie
Posts: 17
Joined: Thu Dec 28, 2017 12:00 am

Re: Placement of Series.Marks (TPointSeries)

Post by Softdrill NL » Mon May 12, 2025 8:18 am

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?

Yeray
Site Admin
Site Admin
Posts: 9674
Joined: Tue Dec 05, 2006 12:00 am
Location: Girona, Catalonia
Contact:

Re: Placement of Series.Marks (TPointSeries)

Post by Yeray » Mon May 12, 2025 10:12 am

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.
Best Regards,
ImageYeray Alonso
Development & Support
Steema Software
Av. Montilivi 33, 17003 Girona, Catalonia (SP)
Image Image Image Image Image Image Please read our Bug Fixing Policy

Post Reply