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.