YOUSEFUL / DELPHI / SAMPLE PROJECTS
Interactive plot
A VCL window with one TAtlasPlot and a row of buttons that rebuild it as four
different charts. The form is created in code (CreateNew); there is no .dfm.
What it demonstrates
- Series kinds: area, line, step, point and pie (donut)
Stacking := psStackedfor areas- A date/time argument axis with
AddNullgaps,AddMovingAverageandAddTrendLine - Logarithmic axes on both sides (log-log)
DonutPercent,Marks := pmPercentand a per-point colour- Zoom by dragging, pan by right-dragging, wheel zoom (
TabStopso a click focuses the chart), double-click reset,OnZoom OnPointClickfor exact hits;OnClickwithValuesToPixelto find the nearest point along the argument axis;OnMouseMovewithPixelToValuesandHitTestfor a live read-out- Resetting every axis and layout property inside
BeginUpdate/EndUpdate
Build and run
Open InteractivePlot.dpr in Delphi 13; the IDE creates the project file. Add the Atlas Plot src folder to the project's search path, then build and run for Win32 or Win64. No packages need to be installed.
Expected result
The window opens on the stacked area chart. Each button replaces the chart. Moving the mouse over the plot shows the cursor position in data units and, when the cursor is over a vertex, point or slice, the series title and point index. Dragging a rectangle zooms in; the status line says so; double-clicking returns to the full range. Clicking anywhere reports the nearest point along the argument axis; clicking on a point reports that point.
Adapting it
Copy one of the Build... procedures, give it a button with the next Tag and
add it to the case in ShowChart. ShowChart calls Reset first because
axis and layout settings persist on the control; the order inside Reset
matters, since a date/time axis cannot be logarithmic. Clear also ends any
zoom, as does changing Stacking.
Project files
The ZIP contains every sample project with its README. See getting started for the search path to add.
Source
InteractivePlot.dpr
program InteractivePlot;
// Atlas Plot: several series kinds, stacking, logarithmic and date axes,
// zoom, pan and hit testing in one window. The form is created in code.
uses
Vcl.Forms,
Interactive.Main in 'Interactive.Main.pas';
var
Form: TInteractiveForm;
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TInteractiveForm, Form);
Application.Run;
end.
Interactive.Main.pas
unit Interactive.Main;
// One TAtlasPlot that switches between four charts. Drag to zoom, right-drag
// to pan, use the wheel, double-click to reset; click to report the nearest
// point; move the mouse to see values and exact hits in the status line.
interface
uses
System.Classes, System.SysUtils, System.Types, Vcl.Controls, Vcl.Forms, Vcl.StdCtrls,
Vcl.ExtCtrls, Vcl.Graphics, Atlas.Plot, Atlas.PlotSeries;
type
TInteractiveForm = class(TForm)
private
FPlot: TAtlasPlot;
FStatus: TLabel;
FBar: TPanel;
procedure AddButton(const Caption: string; Tag: Integer; OnClick: TNotifyEvent);
procedure Reset;
function Describe(Series: TAtlasPlotSeries; Index: Integer): string;
procedure ShowChart(Sender: TObject);
procedure BuildStackedAreas;
procedure BuildDateLine;
procedure BuildLogPoints;
procedure BuildDonut;
procedure PlotClicked(Sender: TObject);
procedure PointClicked(Sender: TObject; Series: TAtlasPlotSeries; PointIndex: Integer);
procedure PlotMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
procedure ZoomChanged(Sender: TObject);
public
constructor Create(AOwner: TComponent); override;
end;
implementation
constructor TInteractiveForm.Create(AOwner: TComponent);
begin
inherited CreateNew(AOwner);
Caption := 'Atlas Plot: interactive';
SetBounds(0, 0, 900, 620);
Position := poScreenCenter;
FBar := TPanel.Create(Self);
FBar.Parent := Self;
FBar.Align := alTop;
FBar.Height := 34;
FBar.BevelOuter := bvNone;
FStatus := TLabel.Create(Self);
FStatus.Parent := Self;
FStatus.Align := alBottom;
FStatus.AlignWithMargins := True;
FStatus.Caption := 'Drag to zoom, right-drag to pan, wheel to zoom, double-click to reset. Click to find the nearest point.';
FPlot := TAtlasPlot.Create(Self);
FPlot.Parent := Self;
FPlot.Align := alClient;
// With TabStop a click focuses the chart, so it also receives wheel messages.
FPlot.TabStop := True;
FPlot.OnPointClick := PointClicked;
FPlot.OnClick := PlotClicked;
FPlot.OnMouseMove := PlotMouseMove;
FPlot.OnZoom := ZoomChanged;
AddButton('Stacked areas', 0, ShowChart);
AddButton('Dates + average', 1, ShowChart);
AddButton('Log-log points', 2, ShowChart);
AddButton('Donut', 3, ShowChart);
ShowChart(Self); // the form's Tag is 0: stacked areas
end;
procedure TInteractiveForm.AddButton(const Caption: string; Tag: Integer; OnClick: TNotifyEvent);
var Button: TButton;
begin
Button := TButton.Create(Self);
Button.Parent := FBar;
Button.Caption := Caption;
Button.Tag := Tag;
Button.Width := 130;
// ControlCount already includes this button.
Button.Left := 4 + (FBar.ControlCount - 1) * 134;
Button.Top := 4;
Button.OnClick := OnClick;
end;
procedure TInteractiveForm.Reset;
begin
// Every chart starts from the same settings; Clear also ends any zoom.
// Logarithmic is cleared before DateTime: a date/time axis cannot be logarithmic.
FPlot.Clear;
FPlot.ArgumentAxis.Logarithmic := False;
FPlot.ArgumentAxis.DateTime := False;
FPlot.ArgumentAxis.Title := '';
FPlot.ValueAxis.Logarithmic := False;
FPlot.ValueAxis.Title := '';
FPlot.Stacking := psNone;
FPlot.DonutPercent := 0;
FPlot.LegendPosition := lpRight;
FPlot.Footnote := '';
end;
procedure TInteractiveForm.ShowChart(Sender: TObject);
begin
// One batch: the chart repaints once, after the new chart is complete.
FPlot.BeginUpdate;
try
Reset;
case TComponent(Sender).Tag of
0: BuildStackedAreas;
1: BuildDateLine;
2: BuildLogPoints;
else
BuildDonut;
end;
finally
FPlot.EndUpdate;
end;
end;
procedure TInteractiveForm.BuildStackedAreas;
const Names: array[0..2] of string = ('Desktop', 'Mobile', 'Tablet');
var I, Month: Integer; S: TAtlasPlotSeries;
begin
FPlot.Title := 'Sessions by device (stacked areas)';
FPlot.Stacking := psStacked;
FPlot.LegendPosition := lpTop;
FPlot.ArgumentAxis.Title := 'Month';
FPlot.ValueAxis.Title := 'Sessions';
for I := 0 to High(Names) do
begin
S := FPlot.AddSeries(Names[I], pkArea);
for Month := 1 to 12 do
S.Add(Month, 30 + 10 * I + Month * (3 - I) + 8 * Sin(Month / 2 + I));
end;
end;
procedure TInteractiveForm.BuildDateLine;
var Daily: TAtlasPlotSeries; I: Integer; Start: TDateTime;
begin
FPlot.Title := 'Daily orders with a 7-day average and trend';
FPlot.ArgumentAxis.DateTime := True;
FPlot.ValueAxis.Title := 'Orders';
FPlot.LegendPosition := lpBottom;
Start := EncodeDate(2026, 3, 1);
Daily := FPlot.AddSeries('Orders');
Daily.LineWidth := 1;
for I := 0 to 89 do
if (I >= 40) and (I < 44) then Daily.AddNull(Start + I) // a gap: the line breaks here
else Daily.Add(Start + I, 120 + I * 0.9 + 25 * Sin(I / 3.1) + 12 * Cos(I * 1.7));
FPlot.AddMovingAverage(Daily, 7).LineWidth := 3;
FPlot.AddTrendLine(Daily);
end;
procedure TInteractiveForm.BuildLogPoints;
var Measured, Limit: TAtlasPlotSeries; I: Integer;
begin
FPlot.Title := 'Response time by payload (log-log)';
FPlot.ArgumentAxis.Logarithmic := True;
FPlot.ArgumentAxis.Title := 'Payload, KB';
FPlot.ValueAxis.Logarithmic := True;
FPlot.ValueAxis.Title := 'Milliseconds';
Measured := FPlot.AddSeries('Measured', pkPoint);
for I := 0 to 24 do
Measured.Add(Exp(Ln(10) * I / 6), 2 * Exp(Ln(10) * I / 8) * (1 + 0.3 * Sin(I * 2.3)));
Limit := FPlot.AddSeries('Tier limit', pkStep);
Limit.Add(1, 10); Limit.Add(100, 100); Limit.Add(10000, 1000);
end;
procedure TInteractiveForm.BuildDonut;
var S: TAtlasPlotSeries;
begin
FPlot.Title := 'Support tickets by channel';
FPlot.Footnote := 'Click a slice';
FPlot.DonutPercent := 45;
S := FPlot.AddSeries('Tickets', pkPie);
S.Marks := pmPercent;
S.AddY(412, 'Email'); S.AddY(268, 'Chat'); S.AddY(131, 'Phone'); S.AddY(54, 'Forum');
S.AddY(19, 'Post', clSilver); // a per-point colour
end;
function TInteractiveForm.Describe(Series: TAtlasPlotSeries; Index: Integer): string;
var P: TAtlasPlotPoint; Arg: string;
begin
P := Series[Index];
if FPlot.ArgumentAxis.DateTime then Arg := FormatDateTime('yyyy-mm-dd', P.X)
else Arg := FormatFloat('0.###', P.X);
Result := Format('%s [%d]: x = %s, y = %s', [Series.Title, Index, Arg, FormatFloat('0.###', P.Y)]);
if P.Text <> '' then Result := Result + ' (' + P.Text + ')';
end;
procedure TInteractiveForm.PlotClicked(Sender: TObject);
var
At, Pixel: TPoint;
I, J, BestSeries, BestPoint, Distance, Best: Integer;
S: TAtlasPlotSeries;
begin
// ValuesToPixel describes the latest render, so the nearest point can be
// found along the argument axis in screen space, whatever the stacking.
At := FPlot.ScreenToClient(Mouse.CursorPos);
Best := 0; BestSeries := -1; BestPoint := -1;
for I := 0 to FPlot.Series.Count - 1 do
begin
S := FPlot.Series[I];
if S.Visible then
for J := 0 to S.Count - 1 do
if not S[J].IsNull and FPlot.ValuesToPixel(S[J].X, S[J].Y, Pixel) then
begin
if FPlot.Rotated then Distance := Abs(Pixel.Y - At.Y) else Distance := Abs(Pixel.X - At.X);
if (BestSeries < 0) or (Distance < Best) then
begin
Best := Distance; BestSeries := I; BestPoint := J;
end;
end;
end;
if BestSeries >= 0 then
FStatus.Caption := Format('Nearest along the argument axis (%d px): %s',
[Best, Describe(FPlot.Series[BestSeries], BestPoint)]);
end;
procedure TInteractiveForm.PointClicked(Sender: TObject; Series: TAtlasPlotSeries; PointIndex: Integer);
begin
// Fires after OnClick, only when the click lands on a bar, dot, vertex or slice.
FStatus.Caption := 'Clicked ' + Describe(Series, PointIndex);
end;
procedure TInteractiveForm.PlotMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
var Arg, Val: Double; SeriesIndex, PointIndex: Integer; Text: string;
begin
if ssLeft in Shift then Exit; // keep the click or zoom message while dragging
Text := '';
if FPlot.PixelToValues(X, Y, Arg, Val) then
if FPlot.ArgumentAxis.DateTime then
Text := Format('Cursor: %s, %s', [FormatDateTime('yyyy-mm-dd', Arg), FormatFloat('0.###', Val)])
else
Text := Format('Cursor: x = %s, y = %s', [FormatFloat('0.###', Arg), FormatFloat('0.###', Val)]);
if FPlot.HitTest(X, Y, SeriesIndex, PointIndex) then
Text := Text + ' | over ' + Describe(FPlot.Series[SeriesIndex], PointIndex);
if FPlot.Zoomed then Text := Text + ' | zoomed (double-click to reset)';
if Text <> '' then FStatus.Caption := Text;
end;
procedure TInteractiveForm.ZoomChanged(Sender: TObject);
begin
if FPlot.Zoomed then FStatus.Caption := 'Zoomed in. Right-drag to pan, wheel to zoom further, double-click to reset.'
else FStatus.Caption := 'Back to the full data range.';
end;
end.