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.
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.
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.
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.
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.
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.
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.
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.
Create a console application and save it as
PlotToPng.dpr. Add the Atlas Plotsrcfolder to the project's search path, or install the runtime package. Replace the generated code with this header: the units, the data, and aCheckroutine that stops the program on the first failed expectation.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;Build the chart.
AddYnumbers 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;AddTrendLineadds a third series with the least-squares fit.BeginUpdate/EndUpdatemake the whole build one change.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;Add the image checks.
TPngImagereads the PNG back to confirm its size; the SVG is text, so its title, category labels and plot clipping can be checked withPos.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;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 withDataFromCSVgives the same data back.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;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 raisesEAtlasPlotErrorwithout changing the series.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.Build and run the program: in the IDE, press F9. The program prints one line per check and exits with code 0:
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 passedOpen
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 withDataFromCSVinstead of callingAddY. 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.