YOUSEFUL / DELPHI / SAMPLE PROJECTS

Grouped report to HTML

Console program Self-checking Product: Atlas Analytics · Components: TAtlasReportDefinition, TAtlasAnalytics, TAtlasMeasure, TAtlasFilter

The Atlas Analytics quick start and the project of the tutorial. A console program builds a small sales table, defines a grouped TAtlasReportDefinition, runs it, writes the HTML export next to the executable, round-trips the definition through JSON and prepares a paginated print document. No window is shown.

What it demonstrates

Build and run

Open GroupedReport.dpr in Delphi 13; the IDE creates the project file. Add the Atlas Analytics src folder and the Atlas VCL src folder to the project's search path, then build and run for Win32 or Win64. No packages need to be installed. The program needs no input. It checks its own results and stops with an error if any of them differs from the output below.

Expected output

Example
Grouped report: 3 rows, 5 columns
    East  revenue  24400  units  20  average  12200  orders 2
    West  revenue  32100  units  27  average  16050  orders 2
   North  revenue  16800  units  14  average   8400  orders 2
Wrote ...\GroupedReport\sales-by-region.html (1453 characters)
Definition: {"version":1,"title":"Sales by region","subtitle":"Revenue, units and average sale per region","fields":[],"dimensions":["Region"],"measures":[{"field":"Revenue","caption":"Revenue","kind":1},{"field":"Units","caption":"Units","kind":1},{"field":"Revenue","caption":"Average sale","kind":4},{"field":"","caption":"Orders","kind":0}],"filters":[]}
Print document: 1 page, title "Sales by region"
Detail report: 3 Desktop rows
PASS

Open sales-by-region.html in a browser to see the report. The exit code is 0; any mismatch prints Check failed: ... and exits with 1.

Adapting it

Change SalesTable to load your own data (ImportCSV accepts a CSV string whose header matches the columns), then change the dimension and measures in GroupBy. Every measure except amCount needs an ackNumber column, and every result column (dimensions and measure captions) needs a distinct name.

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

GroupedReport.dpr

pascal
program GroupedReport;

// Atlas Analytics quick start: define a grouped report, run it over a
// TAtlasTable, export it to HTML and prepare a print document, all without
// a window. The program checks its own results and exits with code 1 on any
// mismatch.

{$APPTYPE CONSOLE}

uses
  System.SysUtils, System.IOUtils,
  Atlas.Data, Atlas.Analytics, Atlas.Reports, Atlas.Printing;

function SalesTable: TAtlasTable;
begin
  Result := TAtlasTable.Create(nil);
  try
    Result.AddColumn('Region');
    Result.AddColumn('Product');
    Result.AddColumn('Revenue', ackNumber);
    Result.AddColumn('Units', ackNumber);
    Result.AddColumn('Booked', ackDate);
    Result.ImportCSV(
      'Region,Product,Revenue,Units,Booked' + sLineBreak +
      'East,Desktop,18000,12,2026-09-01' + sLineBreak +
      'West,Desktop,22500,15,2026-09-02' + sLineBreak +
      'East,Service,6400,8,2026-09-03' + sLineBreak +
      'North,Desktop,12000,8,2026-09-04' + sLineBreak +
      'West,Service,9600,12,2026-09-05' + sLineBreak +
      'North,Service,4800,6,2026-09-06');
  except
    Result.Free;
    raise;
  end;
end;

procedure Check(Condition: Boolean; const Message: string);
begin
  if not Condition then
    raise Exception.Create('Check failed: ' + Message);
end;

procedure Expect(const Expected, Actual, What: string);
begin
  Check(Expected = Actual, What + ': expected "' + Expected + '", got "' + Actual + '"');
end;

var
  Source, Summary, Detail: TAtlasTable;
  Report: TAtlasReportDefinition;
  Document: TAtlasPrintDocument;
  HTML, FileName, JSON: string;
  I: Integer;
begin
  try
    Source := SalesTable;
    Report := TAtlasReportDefinition.Create(nil);
    try
      Report.Title := 'Sales by region';
      Report.Subtitle := 'Revenue, units and average sale per region';
      Report.GroupBy(['Region'], [
        TAtlasMeasure.Create('Revenue', 'Revenue', amSum),
        TAtlasMeasure.Create('Units', 'Units', amSum),
        TAtlasMeasure.Create('Revenue', 'Average sale', amAverage),
        TAtlasMeasure.Create('', 'Orders', amCount)]);

      // 1. Execute returns a new table that the caller owns.
      Summary := Report.Execute(Source);
      try
        Writeln('Grouped report: ', Summary.RowCount, ' rows, ', Summary.ColumnCount, ' columns');
        for I := 0 to Summary.RowCount - 1 do
          Writeln('  ', Summary.Cell(0, I):6, '  revenue ', Summary.Cell(1, I):6,
            '  units ', Summary.Cell(2, I):3, '  average ', Summary.Cell(3, I):6,
            '  orders ', Summary.Cell(4, I));
        Check(Summary.RowCount = 3, 'three regions');
        Expect('East', Summary.Cell(0, 0), 'groups keep first-seen order');
        Expect('24400', Summary.Cell(1, 0), 'East revenue');
        Expect('20', Summary.Cell(2, 0), 'East units');
        Expect('12200', Summary.Cell(3, 0), 'East average sale');
        Expect('2', Summary.Cell(4, 0), 'East orders');
        Expect('32100', Summary.Cell(1, 1), 'West revenue');
        Expect('16800', Summary.Cell(1, 2), 'North revenue');
      finally
        Summary.Free;
      end;

      // 2. Filters narrow the rows before they are grouped; the source is untouched.
      Report.SetFilters([TAtlasFilter.Create('Revenue', '5000', afGreaterEqual)]);
      Summary := Report.Execute(Source);
      try
        Expect('12000', Summary.Cell(1, 2), 'North revenue with filter');
        Expect('1', Summary.Cell(4, 2), 'North orders with filter');
      finally
        Summary.Free;
      end;
      Check(Source.RowCount = 6, 'source table unchanged');

      // 3. RenderHTML writes a complete, escaped, printable page.
      Report.SetFilters([]);
      HTML := Report.RenderHTML(Source);
      FileName := TPath.Combine(TPath.GetDirectoryName(ParamStr(0)), 'sales-by-region.html');
      TFile.WriteAllText(FileName, HTML, TEncoding.UTF8);
      Check(Pos('<title>Sales by region</title>', HTML) > 0, 'HTML title');
      Check(Pos('<th scope="col">Average sale</th>', HTML) > 0, 'HTML column header');
      Check(Pos('<td class="number">24400</td>', HTML) > 0, 'HTML numeric cell');
      Check(Pos('<p>3 result rows</p>', HTML) > 0, 'HTML row count');
      Writeln('Wrote ', FileName, ' (', Length(HTML), ' characters)');

      // 4. The definition round-trips through JSON.
      JSON := Report.ToJSON;
      Report.LoadJSON(JSON);
      Expect(JSON, Report.ToJSON, 'JSON round trip');
      Writeln('Definition: ', JSON);

      // 5. PreparePrint returns a paginated document the caller owns.
      Document := Report.PreparePrint(Source);
      try
        Check(Document.PageCount = 1, 'one printed page');
        Writeln('Print document: ', Document.PageCount, ' page, title "', Document.Title, '"');
      finally
        Document.Free;
      end;

      // 6. SelectFields turns the same definition into a detail report.
      Report.SelectFields(['Region', 'Product', 'Revenue']);
      Report.SetFilters([TAtlasFilter.Create('Product', 'Desktop')]);
      Detail := Report.Execute(Source);
      try
        Check(Detail.RowCount = 3, 'three Desktop rows');
        Check(Detail.ColumnCount = 3, 'three selected columns');
        Writeln('Detail report: ', Detail.RowCount, ' Desktop rows');
      finally
        Detail.Free;
      end;
    finally
      Report.Free;
      Source.Free;
    end;
    Writeln('PASS');
  except
    on E: Exception do
    begin
      Writeln(E.ClassName, ': ', E.Message);
      ExitCode := 1;
    end;
  end;
end.