YOUSEFUL / DELPHI

Atlas Plot: FAQ, examples and tutorial

Learning center · Component reference

Atlas Plot is a chart control for VCL applications on Windows (Win32 and Win64). One component, TAtlasPlot, draws line, step, area, bar, point and pie or donut series against an argument axis and a value axis, with a title, a footnote and a legend. Users zoom, pan and click points with the mouse; code exports the same picture to PNG, BMP, EMF or SVG, prints it, or exchanges the data as CSV, with or without a window. Add Atlas.Plot (the control, stacking and legend enumerations) and Atlas.PlotSeries (series, points, pkBar and the other kinds, marks) to uses; add Atlas.PlotAxis when you catch EAtlasPlotError or use an axis on its own, and Atlas.PlotSurface only to draw onto your own surface. Atlas Plot depends on nothing but the RTL and the VCL: its runtime package requires rtl, vcl and vclimg, and Atlas VCL is not needed. There is no FMX version.

FAQ

What does Atlas Plot contain? One control, TAtlasPlot, on the Atlas Plot palette page, from the design package AtlasPlotDesign370.bpl (runtime package AtlasPlotRuntime370.bpl). The supporting types are the series (TAtlasPlotSeries in a TAtlasPlotSeriesList, each point a TAtlasPlotPoint), the axis (TAtlasPlotAxis), the drawing surfaces (TAtlasPlotSurface and its canvas and SVG implementations) and the exception EAtlasPlotError. The design package compiles, but installing it in the IDE has not been tested; the sample projects link the units statically instead.

Which series kinds are there, and can I mix them? Six, chosen with Kind (TAtlasPlotKind): pkLine, pkStep, pkArea, pkBar, pkPoint and pkPie. Line, step, area, bar and point series mix freely on the same axes. A pie cannot be mixed: the first visible series decides the form of the chart. If it is a pie, the chart shows that pie (a donut when DonutPercent is above 0) and nothing else; otherwise every pie series is ignored. Use a second TAtlasPlot for a pie beside a line chart.

How do I draw horizontal bars? Set Rotated := True. The argument axis then runs vertically, bottom to top, and the value axis horizontally, so bar series become horizontal bars. Set ArgumentAxis.Inverted := True as well to list the categories from the top down.

How does stacking work? Set Stacking to psStacked or psStacked100 (TAtlasPlotStacking). Bars stack only with bars and areas only with areas; lines, steps and points are never stacked. Points stack when their arguments are exactly equal, so build the series the same way (for example AddY in each, which numbers the arguments 0, 1, 2, ...). Positive and negative values build separate stacks from zero. psStacked100 scales each stack to 100 % of the sum of absolute values; value marks still show the original numbers.

Why does my manual axis range have no effect? A manual range is used only while Automatic is False, and setting Minimum or Maximum does not switch it off. Call SetRange(Low, High), which sets both and switches Automatic off in one step. Minimum must also be below Maximum, otherwise the data range is used. While the chart is zoomed, the zoom window replaces both ranges; ResetZoom (or a double-click) returns to them.

How do logarithmic and date/time axes behave? Logarithmic puts ticks on powers of ten; an automatic logarithmic value axis rounds out to whole decades. Points with a zero or negative coordinate on that axis are skipped, not rejected, and bars and areas grow from the bottom of the view. A manual range on a logarithmic axis needs a positive Minimum. DateTime treats values as TDateTime and chooses calendar ticks: the first of a month for long ranges, second to week steps for short ones. LabelFormat takes a FormatDateTime mask then, and a FormatFloat mask otherwise. An axis cannot be both logarithmic and date/time; setting the second raises EAtlasPlotError.

Where do the category labels come from? From the Text of the points of the first visible series. When any of its points has a text, those texts replace the numeric ticks of the argument axis, placed at the points' arguments and thinned out when they would collide. AddY(42, 'North') is the usual way to build them. Date/time and logarithmic argument axes never show categories.

Which output formats are there, and do they need a window? SaveToFile writes .png, .bmp, .emf and .svg at any size from 1 to 16384 pixels, and .csv with the data. CreateBitmap returns a bitmap, ToSVG the SVG text, CopyToClipboard puts a bitmap on the clipboard, and RenderTo draws on any canvas, including Printer.Canvas. None of them needs a window: a chart created with TAtlasPlot.Create(nil) exports from a console program. Files are written to <name>.part and renamed, so a failed export keeps the previous file. Exporting a visible chart at another size does not disturb its mouse handling.

How do I respond to clicks and read values under the mouse? OnPointClick (TAtlasPlotPointEvent) receives the series and point index when a bar, vertex, dot or slice is clicked. For anything else use the pixel functions, which describe the most recent render: HitTest finds the point at a pixel, PixelToValues converts a pixel to data values, and ValuesToPixel does the reverse. Left-drag zooms, right-drag pans, the wheel zooms about the cursor and a double-click resets; AllowZoom and AllowPan switch the mouse parts off, and OnZoom reports every change.

What happens with invalid values? Values must be finite and at most 1E100 in magnitude, and pie values cannot be negative. Every call that would store something else raises EAtlasPlotError and leaves the chart unchanged; the same goes for empty or inverted ranges, bad zoom windows, image sizes outside 1 to 16384, unknown export extensions and malformed CSV. CheckPlotValue in Atlas.PlotAxis applies the value rule so you can check input first.

Are trend lines and moving averages updated when the data changes? No. AddTrendLine and AddMovingAverage add ordinary line series computed once. After changing the source, delete the computed series (Plot.Series.Delete, or free it) and add it again.

What is not implemented? Data-aware series bound to a TDataSet (fill series in a loop inside BeginUpdate / EndUpdate, or load CSV), secondary axes and multiple panes, 3D, gradients, animation, further series kinds such as bubble, candle, gauge or polar, a print dialog or preview, keyboard navigation, and FMX or any platform other than Windows. For candles, indicators and panes see Atlas Fin; statistical series are in Atlas Plot Pro.

Examples

A bar series with category labels, value marks and a least-squares trend line. Point texts on the first series become the category ticks.

pascal
uses
  Atlas.Plot, Atlas.PlotSeries;

procedure BuildSales(Plot: TAtlasPlot);
var
  Sales: TAtlasPlotSeries;
begin
  Plot.Title := 'Sales';
  Sales := Plot.AddSeries('2026', pkBar);
  Sales.AddY(42, 'North');
  Sales.AddY(35, 'South');
  Sales.AddY(51, 'East');
  Sales.Marks := pmValue;
  Plot.AddTrendLine(Sales);
end;

Three blue bars labelled North, South and East, each with its value above it, and an orange straight line across them. The legend on the right lists "2026" and "2026 trend".

Horizontal bars stacked to 100 %, with the first category at the top.

pascal
uses
  Atlas.Plot, Atlas.PlotSeries;

procedure BuildShares(Plot: TAtlasPlot);
const
  Teams: array[0..2] of string = ('Platform', 'Apps', 'Data');
  DoneCounts: array[0..2] of Double = (24, 31, 12);
  OpenCounts: array[0..2] of Double = (9, 14, 10);
var
  I: Integer;
  Done, Open: TAtlasPlotSeries;
begin
  Plot.Rotated := True;
  Plot.ArgumentAxis.Inverted := True;
  Plot.Stacking := psStacked100;
  Plot.ValueAxis.LabelFormat := '0"%"';
  Plot.LegendPosition := lpBottom;
  Done := Plot.AddSeries('Done', pkBar);
  Open := Plot.AddSeries('Open', pkBar);
  for I := 0 to High(Teams) do
  begin
    Done.AddY(DoneCounts[I], Teams[I]);
    Open.AddY(OpenCounts[I], Teams[I]);
  end;
end;

One full-width bar per team, Platform at the top, split into a Done and an Open share. The value axis runs from 0% to 100% and the legend sits below the plot.

A date/time argument axis with a missing day and a seven-day moving average.

pascal
uses
  System.SysUtils, Atlas.Plot, Atlas.PlotSeries;

procedure BuildDaily(Plot: TAtlasPlot);
var
  Daily: TAtlasPlotSeries;
  Day: Integer;
begin
  Plot.ArgumentAxis.DateTime := True;
  Daily := Plot.AddSeries('Orders');
  for Day := 0 to 59 do
    if Day = 20 then
      Daily.AddNull(EncodeDate(2026, 3, 1) + Day) // no data that day
    else
      Daily.Add(EncodeDate(2026, 3, 1) + Day, 100 + Day + 10 * Sin(Day));
  Plot.AddMovingAverage(Daily, 7).LineWidth := 3;
end;

A thin line from 1 March to 29 April 2026 with a break at 21 March, and a thicker "Orders avg(7)" line that starts on 7 March, when the first window of seven values is complete. The average does not break at the gap. The bottom axis shows dates.

A logarithmic value axis for data that grows by a constant factor.

pascal
uses
  Atlas.Plot, Atlas.PlotSeries;

procedure BuildGrowth(Plot: TAtlasPlot);
var
  Users: TAtlasPlotSeries;
  Year: Integer;
  Count: Double;
begin
  Plot.ValueAxis.Logarithmic := True;
  Plot.ValueAxis.Title := 'Users';
  Users := Plot.AddSeries('Users', pkPoint);
  Count := 3;
  for Year := 2016 to 2026 do
  begin
    Users.Add(Year, Count);
    Count := Count * 2.5;
  end;
end;

Eleven dots rising in a straight line, with value ticks at 1, 10, 100, 1000, 10000 and 100000. A point with a value of 0 would simply not be drawn.

A donut whose slices are labelled with their share, one of them in a colour of its own.

pascal
uses
  Vcl.Graphics, Atlas.Plot, Atlas.PlotSeries;

procedure BuildChannels(Plot: TAtlasPlot);
var
  Tickets: TAtlasPlotSeries;
begin
  Plot.Title := 'Tickets by channel';
  Plot.DonutPercent := 45;
  Tickets := Plot.AddSeries('Tickets', pkPie);
  Tickets.Marks := pmPercent;
  Tickets.AddY(412, 'Email');
  Tickets.AddY(268, 'Chat');
  Tickets.AddY(131, 'Phone');
  Tickets.AddY(19, 'Post', clSilver);
end;

A ring starting at twelve o'clock and running clockwise: Email 49.6%, Chat 32.3%, Phone 15.8% and a silver Post slice of 2.3%. The legend lists the four channels.

A live read-out: the label shows the point under the mouse, or the data position of the cursor.

pascal
uses
  System.Classes, System.SysUtils, Vcl.Forms, Vcl.StdCtrls, Atlas.Plot;

type
  TReadoutForm = class(TForm)
  private
    FPlot: TAtlasPlot;
    FReadout: TLabel;
    procedure PlotMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
  public
    procedure Attach(Plot: TAtlasPlot; Readout: TLabel);
  end;

procedure TReadoutForm.Attach(Plot: TAtlasPlot; Readout: TLabel);
begin
  FPlot := Plot;
  FReadout := Readout;
  FPlot.OnMouseMove := PlotMouseMove;
end;

procedure TReadoutForm.PlotMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
var
  Arg, Val: Double;
  SeriesIndex, PointIndex: Integer;
begin
  if FPlot.HitTest(X, Y, SeriesIndex, PointIndex) then
    FReadout.Caption := Format('%s: %g', [FPlot.Series[SeriesIndex].Title,
      FPlot.Series[SeriesIndex][PointIndex].Y])
  else if FPlot.PixelToValues(X, Y, Arg, Val) then
    FReadout.Caption := Format('x = %.2f, y = %.2f', [Arg, Val])
  else
    FReadout.Caption := '';
end;

Over a bar, dot or slice the label reads, for example, "Sales: 42"; elsewhere in the chart it shows the cursor position in data units. Over a pie it is empty between slices, because a pie has no axes.

Exporting a chart and reading its data back from CSV.

pascal
uses
  System.SysUtils, System.IOUtils, Atlas.Plot;

procedure ArchiveChart(Plot: TAtlasPlot; const Folder: string);
var
  Restored: TAtlasPlot;
begin
  Plot.SaveToFile(TPath.Combine(Folder, 'chart.png'), 1200, 700);
  Plot.SaveToFile(TPath.Combine(Folder, 'chart.emf'), 1200, 700);
  Plot.SaveToFile(TPath.Combine(Folder, 'chart.csv'));
  Restored := TAtlasPlot.Create(nil);
  try
    Restored.DataFromCSV(TFile.ReadAllText(TPath.Combine(Folder, 'chart.csv'), TEncoding.UTF8));
    Assert(Restored.DataToCSV = Plot.DataToCSV);
  finally
    Restored.Free;
  end;
end;

A 1200 x 700 PNG and metafile of the chart, and a CSV file starting with the header series,kind,x,y,text. The restored chart has the same series titles, kinds and points; colours, marks and axis settings are not part of the CSV.

Tutorial

Sample project: Plot to PNG.

Goal: build a bar and line chart in code, export it as PNG, SVG and CSV without showing a window, and check every file.

  1. Create a console application and save it as PlotToPng.dpr. Add the Atlas Plot src folder to the project's search path, or install the runtime package. Replace the generated code with this header: the units, the data, and a Check routine that stops the program on the first failed expectation.

    pascal
    program PlotToPng;
    
    // Atlas Plot quick start: build a chart in code, export it as PNG, SVG and CSV
    // without showing a window, then verify the files.
    
    {$APPTYPE CONSOLE}
    
    uses
      System.SysUtils, System.Classes, System.IOUtils, Vcl.Graphics, Vcl.Imaging.pngimage,
      Atlas.Plot, Atlas.PlotSeries, Atlas.PlotAxis;
    
    const
      Months: array[0..11] of string = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
        'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec');
      OrderCounts: array[0..11] of Double = (42, 38, 51, 47, 55, 61, 58, 64, 70, 66, 73, 81);
      Revenue: array[0..11] of Double = (12.5, 11.2, 15.8, 14.1, 17.3, 19.0, 18.2, 20.6, 22.9, 21.4, 24.0, 27.5);
    
    procedure Check(Condition: Boolean; const What: string);
    begin
      if not Condition then raise Exception.Create('Check failed: ' + What);
      Writeln('  ok: ', What);
    end;
  2. Build the chart. AddY numbers the arguments 0 to 11, and the month names on the first series become the category labels. The revenue line gets dots, value marks and a thicker pen; AddTrendLine adds a third series with the least-squares fit. BeginUpdate / EndUpdate make the whole build one change.

    pascal
    procedure BuildChart(Plot: TAtlasPlot);
    var
      Orders, Sales: TAtlasPlotSeries;
      I: Integer;
    begin
      Plot.BeginUpdate;
      try
        Plot.Title := 'Orders and revenue, 2026';
        Plot.Footnote := 'Revenue in thousands';
        Plot.LegendPosition := lpBottom;
        Plot.ArgumentAxis.Title := 'Month';
        Plot.ValueAxis.Title := 'Count / thousands';
        Plot.ValueAxis.Grid := True;
        Orders := Plot.AddSeries('Orders', pkBar);
        Sales := Plot.AddSeries('Revenue', pkLine, clMaroon);
        Sales.LineWidth := 3;
        Sales.ShowPoints := True;
        Sales.Marks := pmValue;
        for I := 0 to High(Months) do
        begin
          // AddY numbers the argument 0, 1, 2, ... and the text becomes a category tick.
          Orders.AddY(OrderCounts[I], Months[I]);
          Sales.AddY(Revenue[I]);
        end;
        // A least-squares fit of the revenue; a snapshot that does not follow later edits.
        Plot.AddTrendLine(Sales).LineWidth := 1;
      finally
        Plot.EndUpdate;
      end;
    end;
  3. Add the image checks. TPngImage reads the PNG back to confirm its size; the SVG is text, so its title, category labels and plot clipping can be checked with Pos.

    pascal
    procedure VerifyPng(const FileName: string);
    var Png: TPngImage;
    begin
      Check(TFile.GetSize(FileName) > 1000, 'PNG file has content');
      Png := TPngImage.Create;
      try
        Png.LoadFromFile(FileName);
        Check((Png.Width = 800) and (Png.Height = 500), 'PNG is 800 x 500');
      finally
        Png.Free;
      end;
    end;
    
    procedure VerifySvg(const FileName: string);
    var Svg: string;
    begin
      Svg := TFile.ReadAllText(FileName, TEncoding.UTF8);
      Check(Pos('<svg xmlns="http://www.w3.org/2000/svg" width="800" height="500"', Svg) > 0, 'SVG root has the requested size');
      Check(Pos('>Orders and revenue, 2026<', Svg) > 0, 'SVG contains the title');
      Check((Pos('>Jan<', Svg) > 0) and (Pos('>Dec<', Svg) > 0), 'SVG shows category ticks from the point text');
      Check(Pos('<clipPath', Svg) > 0, 'SVG clips the series to the plot area');
    end;
  4. Add the CSV check. Each point is one row under the header series,kind,x,y,text, numbers use a decimal point in every locale, and importing the file into a second chart with DataFromCSV gives the same data back.

    pascal
    procedure VerifyCsv(const FileName: string; Plot: TAtlasPlot);
    var
      Lines: TArray<string>;
      Copy: TAtlasPlot;
    begin
      Lines := TFile.ReadAllLines(FileName, TEncoding.UTF8);
      Check(Lines[0] = 'series,kind,x,y,text', 'CSV header is series,kind,x,y,text');
      // 12 order rows, 12 revenue rows and the two-point trend line.
      Check(Length(Lines) = 1 + 12 + 12 + 2, 'CSV has one row per point');
      Check(Lines[1] = 'Orders,bar,0,42,Jan', 'first row is the first order bar');
      Check(Lines[13] = 'Revenue,line,0,12.5,', 'numbers use a decimal point regardless of locale');
      Check(Pos('Revenue trend,line,', Lines[25]) = 1, 'the trend line is exported as a series');
      Copy := TAtlasPlot.Create(nil);
      try
        Copy.DataFromCSV(TFile.ReadAllText(FileName, TEncoding.UTF8));
        Check(Copy.Series.Count = 3, 'CSV import recreates all three series');
        Check(Copy.Series.Find('orders').Kind = pkBar, 'series kinds survive the round trip');
        Check(Copy.DataToCSV = Plot.DataToCSV, 'import and export are symmetric');
      finally
        Copy.Free;
      end;
    end;
  5. Add the main routine. It creates the chart with no owner and no window, exports the three files with SaveToFile (the extension picks the format), runs the checks, and shows that an invalid value raises EAtlasPlotError without changing the series.

    pascal
    procedure Run;
    var
      Plot: TAtlasPlot;
      Folder: string;
    begin
      Folder := TPath.Combine(GetCurrentDir, 'plot-output');
      TDirectory.CreateDirectory(Folder);
      Plot := TAtlasPlot.Create(nil);
      try
        BuildChart(Plot);
        Check(Plot.Series.Count = 3, 'chart has two data series and one trend line');
        Check(Plot.Series[2].Count = 2, 'a trend line has exactly two points');
        Writeln('Exporting to ', Folder);
        // The extension selects the format; width and height are in pixels.
        Plot.SaveToFile(TPath.Combine(Folder, 'orders.png'), 800, 500);
        Plot.SaveToFile(TPath.Combine(Folder, 'orders.svg'), 800, 500);
        Plot.SaveToFile(TPath.Combine(Folder, 'orders.csv'));
        VerifyPng(TPath.Combine(Folder, 'orders.png'));
        VerifySvg(TPath.Combine(Folder, 'orders.svg'));
        VerifyCsv(TPath.Combine(Folder, 'orders.csv'), Plot);
        // Invalid input raises EAtlasPlotError and leaves the chart unchanged.
        try
          Plot.Series[0].Add(12, 1E101);
          Check(False, 'a value beyond 1E100 must be rejected');
        except
          on EAtlasPlotError do Check(Plot.Series[0].Count = 12, 'a value beyond 1E100 is rejected and nothing changes');
        end;
        Check(Length(TDirectory.GetFiles(Folder, '*.part')) = 0, 'no partial files are left behind');
      finally
        Plot.Free;
      end;
    end;
    
    begin
      try
        Run;
        Writeln('PlotToPng: all checks passed');
      except
        on E: Exception do
        begin
          Writeln(E.ClassName, ': ', E.Message);
          ExitCode := 1;
        end;
      end;
    end.
  6. Build and run the program: in the IDE, press F9. The program prints one line per check and exits with code 0:

    Example
      ok: chart has two data series and one trend line
      ok: a trend line has exactly two points
    Exporting to ...\plot-output
      ok: PNG file has content
      ok: PNG is 800 x 500
      ok: SVG root has the requested size
      ok: SVG contains the title
      ok: SVG shows category ticks from the point text
      ok: SVG clips the series to the plot area
      ok: CSV header is series,kind,x,y,text
      ok: CSV has one row per point
      ok: first row is the first order bar
      ok: numbers use a decimal point regardless of locale
      ok: the trend line is exported as a series
      ok: CSV import recreates all three series
      ok: series kinds survive the round trip
      ok: import and export are symmetric
      ok: a value beyond 1E100 is rejected and nothing changes
    PlotToPng: all checks passed
  7. Open plot-output/orders.png. It shows twelve blue bars labelled Jan to Dec, a maroon revenue line with dots and values, a thin green trend line, the axis titles, and the legend and footnote at the bottom. To adapt the program, replace the constants with your own data, or load a CSV file with DataFromCSV instead of calling AddY. For zooming, panning and click handling in a window, continue with the Interactive plot sample.

Troubleshooting

Undeclared identifier: 'pkBar' (or pmValue, TAtlasPlotSeries). The series kinds, marks and the series class are declared in Atlas.PlotSeries, not in Atlas.Plot. Add both units to uses. EAtlasPlotError and CheckPlotValue need Atlas.PlotAxis; clDefault and the other colours need Vcl.Graphics.

Hint H2443: inline function TCollection.GetCount has not been expanded. Plot.Series.Count is inherited from TCollection, whose inline getter lives in System.Classes. Add System.Classes to the uses clause of the unit that calls it.

EAtlasPlotError when switching an axis between date/time and logarithmic. An axis cannot be both at any moment, so clear one before setting the other: Axis.Logarithmic := False; Axis.DateTime := True;. The same applies when a manual logarithmic range has a Minimum of zero or less: set a positive minimum, or Automatic := True, before switching Logarithmic on.

A pie series is not drawn, or the other series disappear. The first visible series decides the form. A pie that is not first is ignored; a pie that is first hides everything else. Reorder the series, hide the pie with Visible := False, or use two charts.

Bars or areas do not stack. Check that Stacking is set, that the series have the same kind (bars stack with bars, areas with areas), and that their arguments are exactly equal. An argument computed at run time, such as 0.1 added three times, differs from 0.3 in the last bit: such bars do not stack, and because bar width follows the closest spacing between bar arguments, they also shrink to one pixel. Use whole-number arguments (AddY) or round them. Lines, steps and points never stack.

Category labels are missing or numeric ticks appear instead. Only the texts of the first visible series are used. Put the texts on that series, and check that the argument axis is neither date/time nor logarithmic. Labels that would collide are thinned out; a wider chart or a smaller font shows more of them.

HitTest, ValuesToPixel or PixelToValues return False or the wrong point. They describe the most recent render. Before the chart has been painted there is nothing to test; for a pie, the value functions return False because a pie has no axes. After drawing the chart onto your own surface with Render, call Repaint so the on-screen layout is restored. RenderTo, CreateBitmap, ToSVG and SaveToFile restore it themselves.

Access violation after DataFromCSV or Clear. Both free the existing series. A TAtlasPlotSeries variable kept from before points to freed memory; look the series up again with Plot.Series.Find(Title) or by index.

A line zigzags back and forth. Lines join points in the order they were added, not by argument. Call SortByX after adding points out of order.

The mouse wheel does not zoom. Wheel messages go to the focused control in many setups. Set TabStop := True so a click on the chart focuses it, keep the cursor over the plot area, and check AllowZoom.

SaveToFile raises EFCreateError or EOSError. The folder does not exist, or the target file is locked. The export writes <name>.part first, so an existing file is kept and no partial file is left behind. EAtlasPlotError instead means an unsupported extension or an image size outside 1 to 16384.