Can I get the position and size of an axis title? lazarus / 55-TAChart ---------------------------------------------------------------------- CM630 · 2026-04-01 07:29 I am trying to get the positions (rather borders) of axis titles. The idea is to use multiple axises. I have aded a ChartToolset with a ChartToolset1AxisClickTool activated on left mouse click. When I click on an axis I get “0;4;4” in the label, which is not the result I am looking for. Is there a way to find the position and size of the axis title? Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i procedure T Form1.ChartToolset1AxisClickTool1Click(ASender:TChartTool;Axis:TChartA xis;AHitInfo:TChartAxisHitTests);begin  Label1.Caption := IntToStr(axis.Margin) + '; ' + IntToStr(axis.Title.Margins.Left) +  '; ' + IntToStr(axis.Title.Margins.Right);end; A snippet is attached. ---------------------------------------------------------------------- wp · 2026-04-01 12:52 You can query the vertices of the polygon around the title which would be painted when its Frame.Style is not clear and its Frame.Visible is true. This is also the polygon which is checked by the AxisTitleClickTool to decide whether the click is inside the title. Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i procedure TForm1.ChartToolset1AxisClickTool1Click(ASender:TChartTool;  Axis: TChartAxis; AHitInfo: TChartAxisHitTests);var  s, sp: String;  i: Integer;begin//  Label1.Caption := IntToStr(axis.Margin) + '; ' + IntToStr(axis.Title.Margins.Left) +  '; ' + IntToStr(axis.Title.Margins.Right);  s := '';  for i := 0 to High(Axis.TitlePolygon) do  begin    sp := Format('%d: (%d;%d)', [i, Axis.TitlePolygon[i].X, Axis.TitlePolygon[i].Y]);    if i > 0 then    s := s + '; ' + sp    else      s := sp;  end;  Label1.Caption := 'TitlePolygon = [' + s + ']';end; ---------------------------------------------------------------------- CM630 · 2026-04-03 07:50 Thanks! Meanwhile I found out that there is a property *Scale.Editable, but it is “Not implemented”. I added a Tmemo in the main form. And why trying it, it occurred that when the scale is more than a line long, is switches from centered to aligned left... sort of (see the image attached. Also, if there is an empty line in the caption, it is ignored: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i BottomAxis.Title.Caption = 'Line 1 ' + LineEnding  + LineEnding + 'Line 4';is displayed as: QuoteLine 1 Line 4 Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i unit Unit1; {$mode objfpc}{$H+} interface uses  Classes, SysUtils, Forms, Controls, Graphics, Dialogs,StdCtrls,TAGraph,TATools, TAChartAxis, TAChartAxisUtils, LCLType, math; type   { TForm1 }   TForm1 = class(TForm)    Chart1:TChart;    ChartToolset1:TChartToolset;    ChartToolset1AxisClickTool1:TAxisClickTool;    Label1:TLabel;    Memo1:TMemo;    procedure ChartToolset1AxisClickTool1Click(ASender:TCh artTool;Axis:TChartAxis;AHitInfo:TChartAxisHitTests);    procedure FormCreate(Sender:TObject);    procedure Memo1KeyDown(Sender:TObject;var Key:Word;Shift:TShiftState);  private     FClickedAxis : TChartAxis;  public   end; var  Form1: TForm1; implementation {$R *.lfm} { TForm1 } procedure TForm1.ChartToo lset1AxisClickTool1Click(ASender:TChartTool;Axis:TChartAxis;AHitInfo:T ChartAxisHitTests);var  s, sp: String;  i: Integer;begin  FClickedAxis := Axis;  Memo1.Top :=   Chart1.top +  Axis.TitlePolygon[0].Y;  Memo1.Height := Axis.TitlePolygon[1].Y - Axis.TitlePolygon[0].Y +1;  Memo1.left :=  Chart1.ClipRect.Left +   Chart1.Left;  Memo1.Width := Chart1.Width - Chart1.ClipRect.Left - Chart1.MarginsExternal.right;  Memo1.Font := Axis.Title.LabelFont;  memo1.Text :=  Axis.Title.Caption;  Memo1.Visible := true;  end; procedure TForm1.FormCreate(Sender:TObject);var  i: integer;begin  Chart1.Toolset := ChartToolset1;  chart1.AxisList.Add;  chart1.AxisList.Add;  chart1.AxisList.Add;  chart1.AxisList.Add;  for i:= 0 to   chart1.AxisList.Count -1 do  begin    chart1.AxisList[i].Title.Visible  := True;    chart1.AxisList[i].Title.Caption := 'Axis ' + IntToStr(i);    if i mod 2 = 0 then    begin      chart1.AxisList[i].Title.LabelFont.Orientation := 900;      chart1.AxisList[i].Alignment := calLeft;    end    else    begin      chart1.AxisList[i].Title.LabelFont.Orientation := 0;      chart1.AxisList[i].Alignment := calbottom;    end;  end;  chart1.AxisList[3].Title.Caption := chart1.AxisList[2].Title.Caption +  'abc' + LineEnding + '456';  Memo1.Alignment := taCenter;  memo1.WordWrap := False;end; function GetMemoLineHeight(AMemo: TMemo): Integer;var  Bitmap: TBitmap;begin  Bitmap := TBitmap.Create;  try    Bitmap.Canvas.Font.Assign(AMemo.Font);    // Measure a standard character    Result := Bitmap.Canvas.TextHeight('Wg');  finally    Bitmap.Free;  end;end; procedure TForm1.Memo1KeyDown(Sender:TObject;var Key:Word;Shift:TShiftState);var  FontHeight : integer = 14;begin  FontHeight := abs(GetFontData(Memo1.Font.Handle).Height);  if (Key = VK_ESCAPE) then  begin    Memo1.Visible := false;    Exit;  end;  if ((Key = VK_RETURN) and (ssCtrl in Shift)) then  begin    FClickedAxis.Title.Caption := Memo1.Text;     Memo1.Visible := false;    exit;  end;  if ((key = LCLtype.VK_RETURN) and (shift = [])) then  begin      Memo1.Height := min (FontHeight*3+8,Memo1.Height + FontHeight+4); //The first line starts 5 pixels below the top of the memo (in Windows)      Memo1.top := chart1.top+ FClickedAxis.TitlePolygon[0].Y - Memo1.Height + FontHeight+2;    end;end; end. ---------------------------------------------------------------------- wp · 2026-04-03 08:25 Quote from: CM630 on April 03, 2026, 09:50:23 amswitches from centered to aligned left... There's a property Alignment for the individual lines of the axis title (and Chart.Title and marks text, as well), you may want to switch it to taCenter. Another (newly introduced) property Anchor defines where the overall title is placed along the axis: ctaStart = where the axis begins, ctaEnd = where the axis ends, ctaCenter = center of the axis (default). Quote from: CM630 on April 03, 2026, 09:50:23 amAlso, if there is an empty line in the caption, it is ignored This is because an empty line is considered to have zero extent (x:0; y:0). If you want to include the empty line insert a space character between the two LineEndings: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i Chart1.BottomAxis.Title.Caption := 'Line 1' + LineEnding + ' ' + LineEnding + 'Line 2'; ---------------------------------------------------------------------- CM630 · 2026-04-03 08:50 Indeed. Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i   for i:= 0 to   chart1.AxisList.Count -1 do  begin    chart1.AxisList[i].Title.Visible  := True;    chart1.AxisList[i].Title.Caption := 'Axis ' + IntToStr(i);    chart1.AxisList[i].Title.Alignment := taCenter;   //Here  did the job. And can I get some position data for the numeric scale (the scale itself) of the axis? The scale position itself, or in the perfect case the position/size of the left/bottommost number and the right/topmost number? I checked in TChartAxis = class(TChartBasicAxis) ... public... but nothings sounds similar. ---------------------------------------------------------------------- wp · 2026-04-03 09:40 What do you understand as "numeric scale"? ---------------------------------------------------------------------- CM630 · 2026-04-03 11:34 I want to make the numeric scale editable (to change the extents of the scale) by the user. So currently I overlap a text box (named txtExtents) over the first/last number on the scale when the user clicks there (with green rectangles on the image attached). Currently (part of) the code is: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i  procedure *.ctMainAxisClickLeftOnClick(ASender: TChartTool; Axis: TChartAxis; AHit: TChartAxisHitTests);... txtExtents.Alignment := taLeftJustify;    if (ahtAxisStart in aHit) and XScale.Visible then        begin        try            txtExtents.Font := Axis.Marks.LabelFont;          finally          end;          txtExtents.Text := FloatToStr(GetExtentsReal(Chart).coords[ExtXmin]);          AutoSizeEditWidth(txtExtents,True);          txtExtents.Left := Chart.Margins.Left + 75;          txtExtents.Top := Chart.ClipRect.Bottom + Axis.TickLength + 1;          txtExtents.Visible := True;          ExtentPosition := epXmin;        end;... but I am implementing multiple scales and this solution will not work, because txtExtents.Left and txtExtents.Top will not be set properly (most likely Top, Left shall be quite okay). ---------------------------------------------------------------------- wp · 2026-04-04 23:34 Please test the following solution that I just committed to Lazarus trunk/main: * TChartAxis has a (public) property Value[index] which lists the numeric and text value of the axis labels ("scale" in your words) * TChartAxis.ValueCount is the length of this array. * I added another element, FPolygon (TPointArray), to the Value record which contains the polygon surrounding each label. A polygon is used rather than a TRect because the label frame can have a rather complex shape. By default, it has the shape of a rectangle, though. * Unfortunately the first and/or last axis labels are often outside the axis range so that you must search for the first/last VISIBLE label yourself. * In order to draw a green border around the first and last labels on the axis, I tested the following (quick-and-dirty) code: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i uses  Types, TAChartUtils, TADrawUtils, TACustomSource, TATransformations;  procedure HighlightFirstAndLastAxisLabel(Axis: TChartAxis);var  R: TRect;  ext: TDoubleRect;  v: Double;  i, idx: Integer;  t: TChartValueText;  transf: TChartAxisTransformations;  chart: TChart;begin  Axis.Drawer.SetPenParams(psSolid, clLime);  Axis.Drawer.SetBrushParams(bsClear, clNone);  chart := TChart(Axis.GetChart);  ext := chart.CurrentExtent;  transf := TransformByAxis(chart.AxisList, Axis.Index);   // Find first label which is on the axis  idx := -1;  for i := 0 to Axis.ValueCount - 1 do  begin    t := Axis.Value[i];    v := transf.AxisToGraph(t.FValue);    if Axis.IsVertical then    begin    if (v >= ext.a.y) then      begin        idx := i;        break;      end;    end else    begin      if (v >= ext.a.x) then      begin      idx := i;        break;      end;    end;  end;  if idx = -1 then    exit;  R.TopLeft := Axis.Value[idx].FPolygon[0];  R.BottomRight := Axis.Value[idx].FPolygon[2];  Types.InflateRect(R, 4, 2);  Axis.Drawer.Rectangle(R);   // Find last label which is on the axis;  idx := -1;  for i := Axis.ValueCount - 1 downto 0 do  begin    t := Axis.Value[i];    v := transf.AxisToGraph(t.FValue);    if Axis.IsVertical then    begin      if (v <= ext.b.y) then      begin      idx := i;        break;      end;    end else    begin      if (v <= ext.b.x) then      begin        idx := i;        break;      end;  end;  end;  if idx = -1 then    exit;  R.TopLeft := Axis.Value[idx].FPolygon[0];  R.BottomRight := Axis.Value[idx].FPolygon[2];  Types.InflateRect(R, 4, 2);  Axis.Drawer.Rectangle(R);end; ---------------------------------------------------------------------- wp · 2026-04-05 10:56 Now also added methods GetIndexOfFirst(Last)VisibleMark to TChartAxis. This way my previous sample code simplifies to Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i procedure HighlightFirstAndLastAxisLabels(Axis: TChartAxis);var  R: TRect;  idx: Integer;  chart: TChart;begin  Axis.Drawer.SetPenParams(psSolid, clLime);  Axis.Drawer.SetBrushParams(bsClear, clNone);   // Find first label which is on the axis  idx := Axis.GetIndexOfFirstVisibleMark;  if idx > -1 then  begin    R.TopLeft := Axis.Value[idx].FPolygon[0];  R.BottomRight := Axis.Value[idx].FPolygon[2];    Types.InflateRect(R, 4, 2);    Axis.Drawer.Rectangle(R);  end;   // Find last label which is on the axis;  idx := Axis.GetIndexOfLastVisibleMark;  if idx > -1 then begin    R.TopLeft := Axis.Value[idx].FPolygon[0];    R.BottomRight := Axis.Value[idx].FPolygon[2];    Types.InflateRect(R, 4, 2);    Axis.Drawer.Rectangle(R);  end;end; QuoteI want to make the numeric scale editable (to change the extents of the scale) by the user. So currently I overlap a text box (named txtExtents) over the first/last number on the scale when the user clicks there (with green rectangles on the image attached).I am not sure whether this will work. Because the first and last visible labels are not necessarily the values at which the axis starts and ends. You must provide extra code to make sure that these labels really are positioned at axis start and end. If you are interesting in how to provide a GUI for run-time editing of chart properties you may want to take a look at the "charteditor" in the demos folder of the TAChart installation. ---------------------------------------------------------------------- CM630 · 2026-04-06 09:42 Thanks! 1. When I try to use HighlightFirstAndLastAxisLabel* in Form.Create the app crashes Adding if (axis = nil) then exit; does not prevent the crash. 2. I tried to move it to .OnShow but still the app crashes.   Neither Chart1.Prepare;  nor   chart1.Invalidate; help.   I do not intend to use it this way, but maybe this indicates some problem. Pretty odd:   3. The procedure from Reply #7 worked from the first try (named HighlightFirstAndLastAxisLabel_1 in the attached snippet).   4. The procedure from Reply #8 did not work (named HighlightFirstAndLastAxisLabel_2 in the attached snippet) initially. After I removed a Chart that was not used from the form, it started working. I have done “clean up and build” before that with no result. It seemed to find some coordinates, but nothing was drawn. 5. A snippet is attached. Stll no care is taken for inverted axises (the smaller one is on the right, the higher is on the left). Quote from: wp on April 05, 2026, 12:56:41 pmQuoteI want to make the numeric scale editable (to change the extents of the scale) by the user. So currently I overlap a text box (named txtExtents) over the first/last number on the scale when the user clicks there (with green rectangles on the image attached).I am not sure whether this will work. Because the first and last visible labels are not necessarily the values at which the axis starts and ends. You must provide extra code to make sure that these labels really are positioned at axis start and end. Indeed, the user might click on a 5 and get text 8. I am not sure if I have taken to make sure that these labels really are positioned at axis start and end, I will find out. Quote from: wp on April 05, 2026, 12:56:41 pmIf you are interesting in how to provide a GUI for run-time editing of chart properties you may want to take a look at the "charteditor" in the demos folder of the TAChart installation. I will check that, but if there are questions or issues, I will use another tread. ---------------------------------------------------------------------- wp · 2026-04-06 15:26 Quote from: CM630 on April 06, 2026, 11:42:15 am1. When I try to use HighlightFirstAndLastAxisLabel* in Form.Create the app crashes Adding if (axis = nil) then exit; does not prevent the crash. 2. I tried to move it to .OnShow but still the app crashes.   Neither Chart1.Prepare;  nor   chart1.Invalidate; help.   I do not intend to use it this way, but maybe this indicates some problem. TAChart does all its sizing and layout adjustments during the Paint method. Therefore, when your calls are too early, before anything has been painted, bad things may happen... A good place to invoke TChartAxis.GetIndexOfFirst(Last)VisibleMarkIndex is the OnAfterPaint method of the chart; this happens when all painting is finished and everything is known. Quote from: CM630 on April 06, 2026, 11:42:15 am5. A snippet is attached. Stll no care is taken for inverted axises (the smaller one is on the right, the higher is on the left). I am attaching a test project for clicking on axis labels after having added a (read-only) property ClickedLabelIndex to the TAxisClickTool. Special code to care for inverted axes is not required. ---------------------------------------------------------------------- CM630 · 2026-04-07 11:30 I updated to the latest TChart and your example worked. What are the ways to have different values on the scales? For example if I have voltage and current on one waveform, I would need a scale for -350 V to + 350 V and another one for current, say from -10 A to + 10 A. One possible way is TchartAxisTransformations, but can I simply set the minimum and maximum values for the scale? Also, in your example I tried Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i procedure TForm1.FormCreate(Sender:TObject);begin  Chart1.AxisList.Add;  Chart1.AxisList[Chart1.AxisList.Count -1].Alignment := calTop;  Chart1.AxisList.Add;  Chart1.AxisList[Chart1.AxisList.Count -1].Alignment := calBottom;  Chart1.BottomAxis.Transformations := ChartAxisTransformations1;  TLinearAxisTransform(ChartAxisTransformations1.List[0]).Offset :=0;  TLinearAxisTransform(ChartAxisTransformations1.List[0]).Scale :=10;end; I expected that the BottomAxis (AxisList[1]) would be scaled (from -60 to 120), but actually it remains -6 to 12, and the other two horizontal axises are scaled 10 times. Is this the expected behaviour? Edit: I added a snippet. Things look ever more mysterious. ---------------------------------------------------------------------- wp · 2026-04-07 23:00 For having two (or more) independent vertical axes (or horizontal - just the same logic...), there is a tutorial: https://wiki.lazarus.fre epascal.org/TAChart_Tutorial:_Dual_y_axis,_Legend. In essence: - Add a TChartAxistransformations component per axis, link it to the Transformations property of the axis. - Add an AutoScaleAxistransform to the component (rather than a LinearAxisTransform). - Every series must set the AxisIndexX and AxisIndexY to the index of it x and y axis. For specific axis range: - Use the Range property of the axis (axis.Range.Min, .Max, .UseMax, .UseMin) to define the value at which the axis begins (Min) and ends (Max). But note, when data in the series assigned to this axis will run out of this range, the range will automatically expand. For user-entry of axis range: - My recommendation: Do not use your label-click idea because the first visible label is not necessarily at the axis start. Suppose you want the axis to run between 190 and 500. Then TAChart will very probably put the first label to the value 200. I'd add two edits (or SpinEdits) to the form close to the axis start/end to define the axis start/end and let TAChart decide which labels will be displayed. Inverted axis: - I don't know whether this is a bug or a feature, but when you have two vertical axes and set the left axis' Inverted to true, both axes will flip their direction. It's better to add an additional LinearAxisTransform ("Linear" is correct now!) and switch its Scale property to -1 in order to invert the axis direction - the other axis then is not affected. But note that the LinearAxisTransform must be listed in the ChartAxisTransformations before the AutoScaleTransform, otherwise the flipping process will not work. The transformations are executed in the listed order: at first you must flip the axis coordinates (TLinearAxisTransform with Scale=-1) and then you must map the axis coordinates to the interal graph coordinates (TAutoScaleTransform). See attached project. ---------------------------------------------------------------------- CM630 · 2026-04-17 13:36 But with inverted axises the mess seems to be even greater. In the attached example ahtAxisStart and ahtAxisEnd in AHitInfo are correct for axis 1, which is set to Inverted and is displayed as inverted. But for the other X axises (which are displayed as inverted, but their .Inverted property is false) ahtAxisStart and ahtAxisEnd are wrong. ---------------------------------------------------------------------- wp · 2026-04-17 22:23 The Inverted property cannot be applied to the case of multiple axes in horizontal or vertical directions. As I showed above you should add a TLinearAxisTransform with Scale parameter -1 for each individual axis to be inverted. ---------------------------------------------------------------------- CM630 · 2026-04-21 05:29 Indeed, inversion works with Transformations. But it occurs that GetIndexOfFirstVisibleMark returns a wrong result when a transformation with .Scale ≠ |1|. Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i ...procedure TForm1.Button1Click(Sender:TObject);begin  ChartAxisTransformations1LinearAxisTransform1.Scale := 1; //Returns 1, as it should  Chart1.Invalidate;  Application.ProcessMessages;  ShowMessage (IntToStr(  Chart1.BottomAxis.GetIndexOfFirstVisibleMark));end; procedure TForm1.Button2Click(Sender:TObject);begin  ChartAxisTransformations1LinearAxisTransform1.Scale := 0.1;  Chart1.Invalidate;  Application.ProcessMessages ;  ShowMessage (IntToStr(Chart1.BottomAxis.GetIndexOfFirstVisibleMark)); //Returns 6 instead of 1end; ... ---------------------------------------------------------------------- CM630 · 2026-05-11 13:31 Some changes in TAChartAxis seem to make things better: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i function TChartAxis.GetIndexOfFirstVisibleMark(aOffset: double = 0; aScale: double=1): Integer;var  coord: Integer;begin  for Result := 0 to High(FMarkValues) do begin    coord := FHelper.GraphToImage(FMarkValues[Result].FValue*aScale + aOffset);    if FHelper.IsInClipRange(coord) then      exit;  end;  Result := -1;end; function TChartAxis.GetIndexOfLastVisibleMark(aOffset: double = 0; aScale: double=1): Integer;var  coord: Integer;begin  for Result := High(FMarkValues) downto 0 do begin    coord := FHelper.GraphToImage(FMarkValues[Result].FValue*aScale + aOffset);    if FHelper.IsInClipRange(coord) then      exit;  end;  Result := -1;end;     Usage: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i   ShowMessage (IntToStr(Chart1.BottomAxis.GetIndexOfFirstVisibleMark(Cha rtAxisTransformations1LinearAxisTransform1.Offset,ChartAxisTransformat ions1LinearAxisTransform1.Scale))); ---------------------------------------------------------------------- CM630 · 2026-05-12 06:38 This should be even better: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i function TChartAxis.GetIndexOfFirstVisibleMark: Integer;var  coord: Integer;  mScale : double = 1;  mOffset : double = 0;  i: integer = 0;begin  if (FTransformations <> Nil) and (FTransformations.List.Count >0)  then  begin    for i:=0 to FTransformations.List.Count - 1 do      if ((TObject(Transformations.List.Items[i]) is TLinearAxisTransform)) and (TLinearAxisTransform(Transformations.List.Items[i]).Enabled = True) then      begin        mScale := mScale * TLinearAxisTransform(Transformations.List.Items[i]).Scale;        mOffset := mOffset * TLinearAxisTransform(Transformations.List.Items[i]).Scale + TLinearAxisTransform(Transformations.List.Items[i]).Offset;      end;  end;   for Result := 0 to High(FMarkValues) do begin    coord := FHelper.GraphToImage(FMarkValues[Result].FValue*mScale + mOffset);    if FHelper.IsInClipRange(coord) then      exit;  end;  Result := -1;end; function TChartAxis.GetIndexOfLastVisibleMark: Integer;var  coord: Integer;  mScale : double = 1;  mOffset : double = 0;  i: integer = 0;begin  if (FTransformations <> Nil) and (FTransformations.List.Count >0)  then  begin    for i:=0 to FTransformations.List.Count - 1 do      if ((TObject(Transformations.List.Items[i]) is TLinearAxisTransform)) and (TLinearAxisTransform(Transformations.List.Items[i]).Enabled = True) then      begin        mScale := mScale * TLinearAxisTransform(Transformations.List.Items[i]).Scale;        mOffset := mOffset * TLinearAxisTransform(Transformations.List.Items[i]).Scale + TLinearAxisTransform(Transformations.List.Items[i]).Offset;      end;  end;   for Result := High(FMarkValues) downto 0 do begin    coord := FHelper.GraphToImage(FMarkValues[Result].FValue * mScale + mOffset);  if FHelper.IsInClipRange(coord) then      exit;  end;  Result := -1;end; ---------------------------------------------------------------------- wp · 2026-05-12 22:05 Please check out the new version of TChartAxis.GetIndexOfFirst/LastVisibleAxisMark in the Laz/main repository. The issue was that the indices were searched by conversion of the label values from graph to image coordinates, but this is correct only when no axis transform is involved because label values always are in axis coordinates - the conversion from axis to graph coordinates was missing in my old code. The full combination of all involved axis transformations, in the correct order, can be found from the GetTransform method of the axis. In the attachment you can find a small project which combines a logarithmic and a linear (inverting) axis transformation (it works correctly only with the new TAChart revision). ---------------------------------------------------------------------- CM630 · 2026-05-13 05:59 Thanks! The snippet worked fine for me, except that something seemed to be wrong with the project so I opened the form manually. I expanded it a little, adding Cumulative something (I doubt that I will ever use that, maybe someone else will need it, since it is there), and there is an issue with it, as shown on the image attached. The extended snippet is also attached. ---------------------------------------------------------------------- wp · 2026-05-13 09:26 TCumulNormDistrAxisTransform is a special transformation for statistical probability data which expands values from the range 0...1 to the range -INF...+INF in a way to linearize a cumulative normal distribution. Due to the transformed negative numbers it cannot be combined with a logarithmic transform. Or, if the log transform would be executed first, the CumulNormDistrTransform would get values outside its domain again (e.g. log(0.001) = -3). In any case, these two transformations cannot be combined in general. To support the TCumulNormDistrAxisTransform I changed the plotted function to a sigmoid function y= 1/(1+exp(-x)) which ranges between 0 and 1 as required. This transformation results in a highly non-linear density of axis labels, and thus I had to adjust the Axis.Intervals parameters for it. To see the effect of an AutoScaleTransform I added a second y axis and assigned the function y=x to it. Unfortunately there were still some bugs in the index finder, for example when the suggested label was outside the range of the transformation. Fixed in new revision. ---------------------------------------------------------------------- CM630 · 2026-05-14 08:16 GetIndexOfLastVisibleMark returns the mark with the higher value, not the rightmost/topmost. So when the axis is inverted, it returns the leftmost value, not the rightmost. The same with GetIndexOfFirstVisibleMark. I am not sure if this is the intended behaviour, just to make sure. I could not understand what you have done in that example, but I cannot change the parameters of the transformations during runtime. So here is what I did (maybe rather funny than useful): Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i unit Unit1; {$mode objfpc}{$H+} interface uses  Classes, SysUtils, Forms, Controls, Graphics, Dialogs,StdCtrls,  Types, math,  TAGraph,TASeries,TATools,TATransformations, TAChartAxis, TACustomSource; type   { TForm1 }   TForm1 = class(TForm)    Button1:TButton;    Chart1:TChart;    Chart1LineSeries1:TLineSeries;  Chart1LineSeries2:TLineSeries;    ChartAxisTransformationsBottom:TChartAxisTransformations;    AutoBottom:TAutoScaleAxisTransform;    AutoLeft:TAutoScaleAxisTransform;    CumuBottom:TCumulNormDistrAxisTransform;    CumulLeft:TCumulNormDistrAxisTransform;    LogarithmBottom:TLogarithmAxisTransform;    LogarithmLeft:TLogarithmAxisTransform;    LinearBottom:TLinearAxisTransform;    ChartAxisTransformationsLeft:TChartAxisTransformations;    LinearLeft:TLinearAxisTransform;    procedure Button1Click(Sender:TObject);    procedure FormCreate(Sender:TObject);  private   public   end; var  Form1: TForm1; implementation {$R *.lfm} { TForm1 }  procedure HighlightFirstAndLastAxisLabels(Axis: TChartAxis);   procedure DrawLabelBorder(AIndex: Integer; AColor: TColor);  var    R: TRect;  vt: TChartValueText;  begin    if AIndex = -1 then      exit;    vt := Axis.Value[AIndex];    if Length(vt.FPolygon) = 0 then      exit;    R.TopLeft := vt.FPolygon[0];    R.BottomRight := vt.FPolygon[2];    Types.InflateRect(R, 4, 2);     Axis.Drawer.SetPenParams(psSolid, AColor);    Axis.Drawer.Rectangle(R);  end; begin  Axis.Drawer.SetBrushParams(bsClear, clNone);  // Find first label which is on the axis and mark it by a green border  DrawLabelBorder(Axis.GetIndexOfFirstVisibleMark, clLime);  // Find last label which is on the axis and mark it by a red border  DrawLabelBorder(Axis.GetIndexOfLastVisibleMark, clRed);end; procedure TForm1.FormCreate(Sender: TObject);var  i: Integer;  x, y: Double;begin  for i := -100 to 100 do  begin    x := i / 10;    y := 1.0 / (1.0 + exp(-x));    Chart1LineSeries1.AddXY(x, y);    Chart1LineSeries2.AddXY(x, x);  end;end; procedure TForm1.Button1Click(Sender:TObject);begin  Randomize;  LinearLeft.Enabled :=  RandomRange(0, 2) = 1;  LinearBottom.Enabled :=  RandomRange(0, 2) = 1;  LogarithmLeft.Enabled :=  RandomRange(0, 2) = 1;  LogarithmBottom.Enabled :=  RandomRange(0, 2) = 1;  AutoLeft.Enabled :=  RandomRange(0, 2) = 1;  AutoBottom.Enabled :=  RandomRange(0, 2) = 1;  if not LogarithmLeft.Enabled  then    CumulLeft.Enabled  :=  RandomRange(0, 2) = 1;  if not LogarithmBottom.Enabled then    CumuBottom.Enabled :=  RandomRange(0, 2) = 1;   LinearLeft.Scale := (Random(2) * 2 - 1) {plus or minus} * Random * (100 - 0.01) + 0.01;  LinearLeft.Offset := Random * (100 - 0.01) + 0.01;   LinearBottom.Scale := (Random(2) * 2 - 1) {plus or minus} * Random * (100 - 0.01) + 0.01;  LinearBottom.Offset := Random * (100 - 0.01) + 0.01;  Chart1.Invalidate;  Application.ProcessMessages;  HighlightFirstAndLastAxisLabels (chart1.AxisList[0]);  HighlightFirstAndLastAxisLabels (chart1.AxisList[1]);end; end.  As I did it gradually, I saw no issues, except the one I just mentioned. I will see what will happen when I get to multiple axises... Also, again, I had to open the .pas file manually, after adding the project (just FYI). ---------------------------------------------------------------------- wp · 2026-05-14 11:07 Quote from: CM630 on May 14, 2026, 10:16:48 amGetIndexOfLastVisibleMark returns the mark with the higher value, not the rightmost/topmost. So when the axis is inverted, it returns the leftmost value, not the rightmost. The same with GetIndexOfFirstVisibleMark. I am not sure if this is the intended behaviour, just to make sure. Wasn't this your intention in one of the previous posts? Quote from: CM630 on May 14, 2026, 10:16:48 amAlso, again, I had to open the .pas file manually, after adding the project (just FYI). This is because of a recent change in the Laz/main project file, I should save all projects with the option "Maximize compatibility of project files (lpi, lps)". Quote from: CM630 on May 14, 2026, 10:16:48 amCode: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i   if not LogarithmLeft.Enabled  then    CumulLeft.Enabled  :=  RandomRange(0, 2) = 1;  if not LogarithmBottom.Enabled then    CumuBottom.Enabled :=  RandomRange(0, 2) = 1; The cumulative normal distribution transform is very nitpicking on parameters. It only accepts input arguments in the range 0 ... 1. There normally combinations with any other transform in which this range is left can fail. Quote from: CM630 on May 14, 2026, 10:16:48 amCode: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i   LinearLeft.Scale := (Random(2) * 2 - 1) {plus or minus} * Random * (100 - 0.01) + 0.01;  LinearLeft.Offset := Random * (100 - 0.01) + 0.01;   LinearBottom.Scale := (Random(2) * 2 - 1) {plus or minus} * Random * (100 - 0.01) + 0.01;  LinearBottom.Offset := Random * (100 - 0.01) + 0.01; Random linear transformation parameters together with a log transform? This will not work in general, because the log transform can only operate on positive numbers I don't see what you want to prove with these experiments. ---------------------------------------------------------------------- CM630 · 2026-05-14 12:37 Quote from: wp on May 14, 2026, 01:07:10 pmQuote from: CM630 on May 14, 2026, 10:16:48 amGetIndexOfLastVisibleMark returns the mark with the higher value, not the rightmost/topmost. So when the axis is inverted, it returns the leftmost value, not the rightmost. The same with GetIndexOfFirstVisibleMark. I am not sure if this is the intended behaviour, just to make sure. Wasn't this your intention in one of the previous posts? We started from Inverted was not usable with more than one scale, so we went to using transformations, but GetIndexOfLast(First)VisibleMark were not working with transformations. At some place there was some mess and it if was not possible to tell if GetIndexOFirstVisibleMark is on the left or on the right and if it is linked to the minimum or the maximum value. So let's leave things as they are for now and see when the path will go to...? Quote from: wp on May 14, 2026, 01:07:10 pmI don't see what you want to prove with these experiments. When scale and offset vary, the number of marks varies too (sometimes there is one mark only). I just wanted to see if something goes wrong when the number of marks changes. Personally I will not use anything except linear and log scale. ---------------------------------------------------------------------- CM630 · 2026-05-22 13:10 Maybe some TRY is needed in TATransformations? ---------------------------------------------------------------------- wp · 2026-05-22 15:48 Can you try to provide a small sample project reproding the issue? Better than hide it by a silent try-except. ---------------------------------------------------------------------- CM630 · 2026-05-30 09:59 I will try do it next week. The entire project is public anyway, but it is also absolutely messy. ---------------------------------------------------------------------- CM630 · 2026-06-04 10:39 So this is the code that crashes when the app is closed: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i unit Unit1; {$mode objfpc}{$H+} interface uses  Classes, SysUtils, Forms, Controls, Graphics, Dialogs,StdCtrls,TAGraph,TATools,TATransformations; type   { TForm1 }   TForm1 = class(TForm)    procedure FormCreate(Sender:TObject);  private    Chart1 : TChart;    FAxisTransformations : array of TChartAxisTransformations;    FLinearAxisTransform : array of TLinearAxisTransform;    FLogarithmAxisTransform : array of TLogarithmAxisTransform;  public   end; var  Form1: TForm1; implementation {$R *.lfm} { TForm1 } procedure TForm1.FormCreate(Sender:TObject);var  i: integer = 0;begin  Chart1 := TChart.Create(Self);  Chart1.Parent := Self;   SetLength(FAxisTransformations,2); //A TAChart has 2 axises when created.  SetLength(FLogarithmAxisTransform,2);  SetLength(FLinearAxisTransform,2);   for i := 0 to high(FAxisTransformations) do  begin    FAxisTransformations[i] := TChartAxisTransformations.Create(self);      FLinearAxisTransform[i] := TLinearAxisTransform.Create(FAxisTransformations[i]);      FLinearAxisTransform[i].Enabled := True;    FLogarithmAxisTransform[i] := TLogarithmAxisTransform.Create(FAxisTransformations[i]);      FLogarithmAxisTransform[i].Enabled := False;     FAxisTransformations[i].List.Add(FLinearAxisTransform[i]);    FAxisTransformations[i].List.Add(FLogarithmAxisTransform[i]);  end;  //for i    FLinearAxisTransform[1].Scale := 0.01;end; end. But this code does not work anyway (FLinearAxisTransform[1].Scale := 0.01; is not applied). On the other hand this code seems to works and so far it does not crash: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i procedure TForm1.FormCreate(Sender:TObject);var  i: integer = 0;begin  Chart1 := TChart.Create(Self);  Chart1.Parent := Self;   SetLength(FAxisTransformations,2); //A TAChart has 2 axises when created.  SetLength(FLogarithmAxisTransform,2);  SetLength(FLinearAxisTransform,2);   for i:=0 to high (FAxisTransformations) do  begin    FAxisTransformations[i]     := TChartAxisTransformations.Create(Chart1);      FLinearAxisTransform[i]   := TLinearAxisTransform.Create(FAxisTransformations[i]);      FLinearAxisTransform[i].Transformations := FAxisTransformations[i];    FLinearAxisTransform[i].Enabled := True;       FLogarithmAxisTransform[i] := TLogarithmAxisTransform.Create(FAxisTransformations[i]);      FLogarithmAxisTransform[i].Transformations := FAxisTransformations[i];      FLogarithmAxisTransform[i].Enabled := false;       Chart1.AxisList[i].Transformations := FAxisTransformations[i];  end;   FLinearAxisTransform[1].Scale := 0.01;end; So maybe there is no issue here. EDIT: This code seems to work, but it crashes: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i   for i := 0 to high(FAxisTransformations) do  begin    FAxisTransformations[i] := TChartAxisTransformations.Create(self);      FLinearAxisTransform[i] := TLinearAxisTransform.Create(FAxisTransformations[i]);      FLinearAxisTransform[i].Enabled := True;    FLogarithmAxisTransform[i] := TLogarithmAxisTransform.Create(FAxisTransformations[i]);      FLogarithmAxisTransform[i].Enabled := False;     FAxisTransformations[i].List.Add(FLinearAxisTransform[i]);    FAxisTransformations[i].List.Add(FLogarithmAxisTransform[i]);     Chart1.AxisList[i].Transformations := FAxisTransformations[i];  end;  //for i ---------------------------------------------------------------------- wp · 2026-06-04 12:51 Yes, adding elements to collections or collection-like classes in TAChart is not always intuitive. I remember that I once wrote a wiki about it some years ago: https://wiki.lazarus.freepascal.org/TAChart_R untime_FAQ#How_to_add_an_axis_transformation? I think the problem with Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i     FAxisTransformations[i].List.Add(FLinearAxisTransform[i]); is that it does not set the Transformations element of the AxisTransform. This is done in the setter SetTransformations of TChartAxisTransform: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i procedure TAxisTransform.SetTransformations(AValue: TChartAxisTransformations);begin   if FTransformations = AValue then exit;  if FTransformations  <> nil then    FTransformations.List.Remove(Self);  FTransformations := AValue;  if FTransformations <> nil then    FTransformations.List.Add(Self);end; I really recommend to use the following code for adding elements to the ChartTransformations: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i     FLinearAxisTransform[i].Transformations := FAxisTransformations[i]; ---------------------------------------------------------------------- CM630 · 2026-06-10 09:00 I am trying to set the extents using transformations. Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i procedure SetAxisMinMax(aAxis: TChartAxis; aBottom_Left : Double; aTop_Right: Double);var  mExtents: TDoubleRect;  debdo : double = 0;begin  mExtents := TChart(TChartAxis(aAxis).GetChart).LogicalExtent;   if (aAxis.Alignment = calTop) or (aAxis.Alignment = calBottom) then  begin //X axis    mExtents.coords[ord(ExtXmin)] := min(aBottom_Left,aTop_Right);    mExtents.coords[ord(ExtXmax)] := max(aBottom_Left,aTop_Right);  end  else  begin //Y axis    mExtents.coords[ord(ExtYmin)] := min(aBottom_Left,aTop_Right);    mExtents.coords[ord(ExtYmax)] := max(aBottom_Left,aTop_Right); {    mExtents.coords[ord(ExtYmin)] := aBottom_Left;    mExtents.coords[ord(ExtYmax)] := aTop_Right;}  end;   TChart(TChartAxis(aAxis).GetChart).LogicalExtent := mExtents;   if (aAxis.Transformations = nil) or (aAxis.Transformations.List.Count = 0)    then exit;  if (aBottom_Left > aTop_Right) then  begin    debdo := TLinearAxisTransform(aAxis.Transformations.List[0]).Scale;    TLinearAxisTransform(aAxis.Transformations.List[0]).Scale := - abs(TLinearAxisTransform(aAxis.Transformations.List[0]).Scale);    debdo := TLinearAxisTransform(aAxis.Transformations.List[0]).Scale;  TLinearAxisTransform(aAxis.Transformations.List[0]).Enabled := true;  end;  TChart(TChartAxis(aAxis).GetChart).Invalidate;  TChart(TChartAxis(aAxis).GetChart).Update;  Application.ProcessMessages;end; The first time I execute  SetAxisMinMax(Chart1.LeftAxis,-1,-5); on the Y (left) scale I get Top= -1; Bottom = 1 (the scales are inverted, but the values are wrong). The second time I execute the same I get Top= + 1; Bottom = +5. I am doing something wrong?