YOUSEFUL / DELPHI / SAMPLE PROJECTS

Interactive plot

VCL application Product: Atlas Plot · Components: TAtlasPlot, TAtlasPlotSeries, TAtlasPlotAxis, TAtlasPlotSeriesList

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

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

Download all 34 projects

The ZIP contains every sample project with its README. See getting started for the search path to add.

Source

InteractivePlot.dpr

pascal
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

pascal
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.