TeeChart VCL for Borland/CodeGear/Embarcadero RAD Studio, Delphi and C++ Builder.
-
Softdrill NL
- Newbie

- Posts: 18
- Joined: Thu Dec 28, 2017 12:00 am
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

- Posts: 9674
- Joined: Tue Dec 05, 2006 12:00 am
- Location: Girona, Catalonia
-
Contact:
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.
-
Softdrill NL
- Newbie

- Posts: 18
- Joined: Thu Dec 28, 2017 12:00 am
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

- Posts: 9674
- Joined: Tue Dec 05, 2006 12:00 am
- Location: Girona, Catalonia
-
Contact:
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;
-
Softdrill NL
- Newbie

- Posts: 18
- Joined: Thu Dec 28, 2017 12:00 am
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

- Posts: 9674
- Joined: Tue Dec 05, 2006 12:00 am
- Location: Girona, Catalonia
-
Contact:
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.
-
Softdrill NL
- Newbie

- Posts: 18
- Joined: Thu Dec 28, 2017 12:00 am
Post
by Softdrill NL » Wed May 14, 2025 7:07 pm
Hi Yeray,
Thanks for your help and explanations so far. However, I have one last question:
In the same scenario, how can I resize the marks (TPositions) when the font size has changed? Series.Marks.ResetPositions just before Chart.Draw and Chart.DrawToMetaCanvas seems to have no effect and I have also tried something like below, but this gives my inconsistent results.
Code: Select all
procedure TSeriesAdaptor.SeriesGetMarkText(Sender: TChartSeries; ValueIndex: Integer; var MarkText: string);
var
XPos, YPos: Integer;
begin
inherited;
if assigned(Sender.Marks.Positions[ValueIndex]) then
begin
Sender.Marks.Positions[ValueIndex].Custom := true;
XPos := Sender.CalcXPos(ValueIndex);
YPos := Sender.CalcYPos(ValueIndex);
Sender.Marks.Positions[ValueIndex].LeftTop.X := XPos + TCustomSeries(Sender).Pointer.Size * 2;
Sender.Marks.Positions[ValueIndex].LeftTop.Y := YPos - TCustomSeries(Sender).Pointer.Size;
// ATTEMPT TO RESIZE/FIT TEXT HERE......
Sender.Marks.Positions[ValueIndex].Width := Sender.ParentChart.Canvas.TextWidth(MarkText);
Sender.Marks.Positions[ValueIndex].Height := Sender.ParentChart.Canvas.TextHeight(MarkText);
Sender.Marks.Positions[ValueIndex].ArrowFrom.X := XPos;
Sender.Marks.Positions[ValueIndex].ArrowFrom.Y := YPos;
Sender.Marks.Positions[ValueIndex].ArrowTo.X := Sender.Marks.Positions[ValueIndex].LeftTop.X;
Sender.Marks.Positions[ValueIndex].ArrowTo.Y := YPos;
end;
end;

- 2025-05-14_21-01-14.png (62.44 KiB) Viewed 323 times
Any ideas or suggestions?