unit VirtualPlotForm; // Two pages. "One million rows": a TAtlasVirtualData that computes its cells on // demand and stores only the cells the user edits, shown with bands, two frozen // columns, validated editing and conditional colours. "Chart companion": a // TAtlasPlot shown and edited as a table through TAtlasPlotData. interface uses System.SysUtils, System.Classes, System.Math, System.Diagnostics, System.UITypes, System.Generics.Collections, Vcl.Forms, Vcl.Controls, Vcl.StdCtrls, Vcl.ExtCtrls, Vcl.ComCtrls, Vcl.Graphics, Atlas.Plot, Atlas.PlotSeries, Atlas.VGridData, Atlas.VGridPlot, Atlas.VGrid; const RowTotal = 1000000; // Data columns of the on-demand source. ColRow = 0; ColSensor = 1; ColReading = 2; ColTaken = 3; ColChecked = 4; ColNote = 5; type TVirtualPlotForm = class(TForm) private FPages: TPageControl; FStatus: TLabel; FFilter: TEdit; FReadings: TAtlasVirtualData; // Only the cells the user changed are stored; everything else is computed. FEdits: TDictionary; FGrid: TAtlasVirtualGrid; FChart: TAtlasPlot; FChartData: TAtlasPlotData; FChartGrid: TAtlasVirtualGrid; function AddPage(const Caption: string): TTabSheet; procedure AddButton(Parent: TWinControl; X: Integer; const Caption: string; Handler: TNotifyEvent); procedure BuildReadings(Page: TTabSheet); procedure BuildChart(Page: TTabSheet); procedure GetReading(Sender: TObject; Column, Row: Integer; var Cell: TAtlasCell); procedure SetReading(Sender: TObject; Column, Row: Integer; const Cell: TAtlasCell); procedure StyleReading(Sender: TObject; Column: TAtlasGridColumn; DataRow: Integer; const Cell: TAtlasCell; var Background, TextColor: TColor); procedure EditRejected(Sender: TObject; const Message: string); procedure ApplyFilter(Sender: TObject); procedure GoToLast(Sender: TObject); procedure ChartRowChanged(Sender: TObject); procedure PointClicked(Sender: TObject; Series: TAtlasPlotSeries; PointIndex: Integer); public constructor Create(AOwner: TComponent); override; destructor Destroy; override; end; implementation function CellKey(Column, Row: Integer): Int64; begin Result := Int64(Row) * 8 + Column; end; constructor TVirtualPlotForm.Create(AOwner: TComponent); begin inherited CreateNew(AOwner); Caption := 'Atlas Virtual Grid: on-demand rows and a chart companion'; Width := 1000; Height := 640; Position := poScreenCenter; FEdits := TDictionary.Create; FStatus := TLabel.Create(Self); FStatus.Parent := Self; FStatus.Align := alBottom; FStatus.AlignWithMargins := True; FStatus.Caption := 'F2, Enter, double-click or type to edit; Esc cancels. Click a header to sort.'; FPages := TPageControl.Create(Self); FPages.Parent := Self; FPages.Align := alClient; BuildReadings(AddPage('One million on-demand rows')); BuildChart(AddPage('Chart companion')); end; destructor TVirtualPlotForm.Destroy; begin // The grids and sources are owned by the form and go first; the edits they read go last. inherited; FEdits.Free; end; function TVirtualPlotForm.AddPage(const Caption: string): TTabSheet; begin Result := TTabSheet.Create(Self); Result.PageControl := FPages; Result.Caption := Caption; end; procedure TVirtualPlotForm.AddButton(Parent: TWinControl; X: Integer; const Caption: string; Handler: TNotifyEvent); var Button: TButton; begin Button := TButton.Create(Self); Button.Parent := Parent; Button.SetBounds(X, 5, 88, 26); Button.Caption := Caption; Button.OnClick := Handler; end; procedure TVirtualPlotForm.BuildReadings(Page: TTabSheet); var Bar: TPanel; begin FReadings := TAtlasVirtualData.Create(Self); FReadings.AddColumn('Row', ckNumber); FReadings.AddColumn('Sensor'); FReadings.AddColumn('Reading', ckNumber); FReadings.AddColumn('Taken', ckDateTime); FReadings.AddColumn('Checked', ckBoolean); FReadings.AddColumn('Note'); FReadings.OnGetCell := GetReading; FReadings.OnSetCell := SetReading; // without it every column is read-only FReadings.RowCount := RowTotal; Bar := TPanel.Create(Self); Bar.Parent := Page; Bar.Align := alTop; Bar.Height := 36; Bar.BevelOuter := bvNone; FFilter := TEdit.Create(Self); FFilter.Parent := Bar; FFilter.SetBounds(8, 6, 200, 24); FFilter.TextHint := 'Text to look for'; AddButton(Bar, 216, 'Filter', ApplyFilter); AddButton(Bar, 312, 'Last row', GoToLast); FGrid := TAtlasVirtualGrid.Create(Self); FGrid.Parent := Page; FGrid.Align := alClient; FGrid.Data := FReadings; // one grid column per data column FGrid.FrozenColumns := 2; // Row and Sensor stay put when scrolling sideways FGrid.Columns[ColRow].Width := 80; FGrid.Columns[ColRow].Format := '#,##0'; FGrid.Columns[ColRow].ReadOnly := True; FGrid.Columns[ColSensor].ReadOnly := True; FGrid.Columns[ColReading].Format := '0.00'; FGrid.Columns[ColTaken].Width := 140; FGrid.Columns[ColTaken].Format := 'yyyy-mm-dd hh:nn'; FGrid.Columns[ColTaken].ReadOnly := True; FGrid.Columns[ColNote].Width := 640; // wider than the window, so the grid scrolls sideways FGrid.Columns[ColRow].Band := 'Source'; FGrid.Columns[ColSensor].Band := 'Source'; FGrid.Columns[ColReading].Band := 'Measurement'; FGrid.Columns[ColTaken].Band := 'Measurement'; FGrid.Columns[ColChecked].Band := 'Review'; FGrid.Columns[ColNote].Band := 'Review'; FGrid.OnCellStyle := StyleReading; FGrid.OnEditError := EditRejected; end; procedure TVirtualPlotForm.GetReading(Sender: TObject; Column, Row: Integer; var Cell: TAtlasCell); begin // Cell arrives as a null of the column's kind. An edited value wins over the computed one. if FEdits.TryGetValue(CellKey(Column, Row), Cell) then Exit; case Column of ColRow: Cell := TAtlasCell.OfNumber(Row + 1); ColSensor: Cell := TAtlasCell.OfText('Sensor ' + Chr(Ord('A') + Row mod 8)); ColReading: Cell := TAtlasCell.OfNumber(RoundTo(50 + 60 * Sin(Row / 40) + (Row mod 7) * 3, -2)); ColTaken: Cell := TAtlasCell.OfDateTime(EncodeDate(2026, 1, 1) + Row / 1440); ColChecked: Cell := TAtlasCell.OfBoolean(Row mod 5 = 0); end; // ColNote stays null until edited end; procedure TVirtualPlotForm.SetReading(Sender: TObject; Column, Row: Integer; const Cell: TAtlasCell); begin // The grid has already parsed the text for the column's kind. Raising // EAtlasGridError rejects the value: the editor stays open and OnEditError says why. case Column of ColReading: if Cell.IsNull then raise EAtlasGridError.Create('A reading cannot be blank') else if (Cell.Number < -50) or (Cell.Number > 150) then raise EAtlasGridError.Create('A reading must lie between -50 and 150'); ColNote: if Length(Cell.Text) > 40 then raise EAtlasGridError.Create('A note holds at most 40 characters'); end; FEdits.AddOrSetValue(CellKey(Column, Row), Cell); FStatus.Caption := Format('Row %d changed; %d edited cells held in memory', [Row + 1, FEdits.Count]); end; procedure TVirtualPlotForm.StyleReading(Sender: TObject; Column: TAtlasGridColumn; DataRow: Integer; const Cell: TAtlasCell; var Background, TextColor: TColor); begin // Selection is painted over these backgrounds, so the current row stays visible. if FEdits.ContainsKey(CellKey(Column.DataIndex, DataRow)) then Background := $00C8F0FF else if (Column.DataIndex = ColReading) and not Cell.IsNull and (Cell.Number > 100) then Background := $00D8D8FF; if (Column.DataIndex = ColReading) and not Cell.IsNull and (Cell.Number < 0) then TextColor := clBlue; end; procedure TVirtualPlotForm.EditRejected(Sender: TObject; const Message: string); begin FStatus.Caption := 'Not accepted: ' + Message; end; procedure TVirtualPlotForm.ApplyFilter(Sender: TObject); var Watch: TStopwatch; begin // A filter reads every row once; on a million computed rows that takes a moment. Screen.Cursor := crHourGlass; try Watch := TStopwatch.StartNew; FGrid.FilterText := FFilter.Text; FStatus.Caption := Format('%s of %s rows match "%s" (%d ms)', [FormatFloat('#,##0', FGrid.RowCount), FormatFloat('#,##0', FReadings.RowCount), FGrid.FilterText, Watch.ElapsedMilliseconds]); finally Screen.Cursor := crDefault; end; end; procedure TVirtualPlotForm.GoToLast(Sender: TObject); begin if FGrid.RowCount = 0 then Exit; FGrid.SelectCell(ColReading, FGrid.RowCount - 1); // scrolls the row into view if FGrid.CanFocus then FGrid.SetFocus; end; procedure TVirtualPlotForm.BuildChart(Page: TTabSheet); const Quarters: array[0..3] of string = ('Q1', 'Q2', 'Q3', 'Q4'); var Plan, Actual: TAtlasPlotSeries; I: Integer; begin FChart := TAtlasPlot.Create(Self); FChart.Parent := Page; FChart.Align := alClient; FChart.Title := 'Edit a value in the table and the bar follows; click a bar to find its cell'; FChart.OnPointClick := PointClicked; Plan := FChart.AddSeries('Plan', pkBar); Actual := FChart.AddSeries('Actual', pkBar); for I := 0 to High(Quarters) do begin Plan.AddY(100 + I * 20, Quarters[I]); Actual.AddY(90 + I * 25 - (I mod 2) * 15, Quarters[I]); end; FChartData := TAtlasPlotData.Create(Self); FChartData.Plot := FChart; // column 0: the point labels; then one column per series FChartGrid := TAtlasVirtualGrid.Create(Self); FChartGrid.Parent := Page; FChartGrid.Align := alBottom; FChartGrid.Height := 170; FChartGrid.Data := FChartData; FChartGrid.Columns[0].Title := 'Quarter'; FChartGrid.Columns[1].Format := '#,##0.0'; FChartGrid.Columns[1].Total := tkSum; FChartGrid.Columns[2].Format := '#,##0.0'; FChartGrid.Columns[2].Total := tkSum; FChartGrid.OnEditError := EditRejected; FChartGrid.OnSelectionChange := ChartRowChanged; end; procedure TVirtualPlotForm.ChartRowChanged(Sender: TObject); var DataRow: Integer; begin DataRow := FChartGrid.DataRowOf(FChartGrid.Row); if DataRow >= 0 then FStatus.Caption := Format('%s sits at argument %g on the chart', [FChartGrid.CellText(0, FChartGrid.Row), FChartData.ArgumentOfRow(DataRow)]); end; procedure TVirtualPlotForm.PointClicked(Sender: TObject; Series: TAtlasPlotSeries; PointIndex: Integer); var Row: Integer; begin // The adapter gives the data row; the grid turns it into the row as shown (it may be sorted). Row := FChartGrid.RowOfDataRow(FChartData.RowOfPoint(Series, PointIndex)); if Row >= 0 then FChartGrid.SelectCell(Series.Index + 1, Row); end; end.