Case Study 2: Data Visualization Dashboard

Overview

This case study combines everything from the chapter into a comprehensive financial data visualization dashboard. We display multiple chart types — pie, bar, line, and a summary panel — on a single form, all driven by the same underlying dataset. This mirrors what the final PennyWise charts panel will look like.


The Dataset

We use a simple in-memory dataset representing monthly expenses by category:

type
  TMonthData = record
    Month: string;
    Food: Double;
    Transport: Double;
    Housing: Double;
    Utilities: Double;
    Entertainment: Double;
  end;

const
  SampleData: array[0..5] of TMonthData = (
    (Month: 'Jan'; Food: 450; Transport: 120; Housing: 800; Utilities: 95;  Entertainment: 180),
    (Month: 'Feb'; Food: 380; Transport: 135; Housing: 800; Utilities: 110; Entertainment: 150),
    (Month: 'Mar'; Food: 420; Transport: 100; Housing: 800; Utilities: 85;  Entertainment: 220),
    (Month: 'Apr'; Food: 490; Transport: 145; Housing: 800; Utilities: 75;  Entertainment: 160),
    (Month: 'May'; Food: 410; Transport: 130; Housing: 800; Utilities: 70;  Entertainment: 200),
    (Month: 'Jun'; Food: 460; Transport: 140; Housing: 800; Utilities: 65;  Entertainment: 190)
  );

Dashboard Layout

The dashboard uses a 2x2 grid of panels:

┌───────────────────────┬───────────────────────┐
│                       │                       │
│   Pie Chart           │   Bar Chart           │
│   (by category)       │   (by month, stacked) │
│                       │                       │
├───────────────────────┼───────────────────────┤
│                       │                       │
│   Line Chart          │   Summary Panel       │
│   (monthly trend)     │   (key metrics)       │
│                       │                       │
└───────────────────────┴───────────────────────┘

Layout Code

procedure TfrmDashboard.FormCreate(Sender: TObject);
begin
  Caption := 'Financial Dashboard';
  Width := 900;
  Height := 700;
  Position := poScreenCenter;

  { Top row }
  pnlTopRow.Align := alTop;
  pnlTopRow.Height := Height div 2;

  pbPie.Align := alLeft;
  pbPie.Width := pnlTopRow.Width div 2;
  pbPie.Anchors := [akTop, akLeft, akBottom];

  pbBar.Align := alClient;

  { Bottom row }
  pnlBottomRow.Align := alClient;

  pbLine.Align := alLeft;
  pbLine.Width := pnlBottomRow.Width div 2;
  pbLine.Anchors := [akTop, akLeft, akBottom];

  pbSummary.Align := alClient;

  InitializeColors;
end;

Chart: Category Pie

Aggregate all months to show total spending per category:

procedure TfrmDashboard.pbPiePaint(Sender: TObject);
var
  Values: array[0..4] of Double;
  Labels: array[0..4] of string;
  I: Integer;
begin
  Labels[0] := 'Food'; Labels[1] := 'Transport';
  Labels[2] := 'Housing'; Labels[3] := 'Utilities';
  Labels[4] := 'Entertainment';

  { Sum across all months }
  for I := 0 to 4 do Values[I] := 0;
  for I := 0 to High(SampleData) do
  begin
    Values[0] := Values[0] + SampleData[I].Food;
    Values[1] := Values[1] + SampleData[I].Transport;
    Values[2] := Values[2] + SampleData[I].Housing;
    Values[3] := Values[3] + SampleData[I].Utilities;
    Values[4] := Values[4] + SampleData[I].Entertainment;
  end;

  { Title }
  pbPie.Canvas.Brush.Color := clWhite;
  pbPie.Canvas.FillRect(0, 0, pbPie.Width, pbPie.Height);
  pbPie.Canvas.Font.Size := 11;
  pbPie.Canvas.Font.Style := [fsBold];
  pbPie.Canvas.Font.Color := clBlack;
  pbPie.Canvas.Brush.Style := bsClear;
  pbPie.Canvas.TextOut(
    (pbPie.Width - pbPie.Canvas.TextWidth('Spending by Category')) div 2,
    8, 'Spending by Category');
  pbPie.Canvas.Brush.Style := bsSolid;

  DrawPieChart(pbPie.Canvas,
    pbPie.Width div 2, pbPie.Height div 2 + 15,
    Min(pbPie.Width, pbPie.Height) div 2 - 40,
    Values, FCategoryColors, Labels);
end;

Chart: Monthly Bar

Show total monthly spending as grouped bars:

procedure TfrmDashboard.pbBarPaint(Sender: TObject);
var
  Months: array[0..5] of string;
  Totals: array[0..5] of Double;
  I: Integer;
begin
  for I := 0 to High(SampleData) do
  begin
    Months[I] := SampleData[I].Month;
    Totals[I] := SampleData[I].Food + SampleData[I].Transport +
                 SampleData[I].Housing + SampleData[I].Utilities +
                 SampleData[I].Entertainment;
  end;

  pbBar.Canvas.Brush.Color := clWhite;
  pbBar.Canvas.FillRect(0, 0, pbBar.Width, pbBar.Height);

  { Title }
  pbBar.Canvas.Font.Size := 11;
  pbBar.Canvas.Font.Style := [fsBold];
  pbBar.Canvas.Brush.Style := bsClear;
  pbBar.Canvas.TextOut(
    (pbBar.Width - pbBar.Canvas.TextWidth('Monthly Total Spending')) div 2,
    8, 'Monthly Total Spending');
  pbBar.Canvas.Brush.Style := bsSolid;

  DrawBarChart(pbBar.Canvas,
    Rect(20, 35, pbBar.Width - 20, pbBar.Height - 10),
    Totals, FCategoryColors, Months);
end;

Chart: Spending Trend Line

Show the monthly total as a line chart with data points:

procedure TfrmDashboard.pbLinePaint(Sender: TObject);
var
  Months: array[0..5] of string;
  Totals: array[0..5] of Double;
  I: Integer;
begin
  for I := 0 to High(SampleData) do
  begin
    Months[I] := SampleData[I].Month;
    Totals[I] := SampleData[I].Food + SampleData[I].Transport +
                 SampleData[I].Housing + SampleData[I].Utilities +
                 SampleData[I].Entertainment;
  end;

  pbLine.Canvas.Brush.Color := clWhite;
  pbLine.Canvas.FillRect(0, 0, pbLine.Width, pbLine.Height);

  pbLine.Canvas.Font.Size := 11;
  pbLine.Canvas.Font.Style := [fsBold];
  pbLine.Canvas.Brush.Style := bsClear;
  pbLine.Canvas.TextOut(
    (pbLine.Width - pbLine.Canvas.TextWidth('Spending Trend')) div 2,
    8, 'Spending Trend');
  pbLine.Canvas.Brush.Style := bsSolid;

  DrawLineChart(pbLine.Canvas,
    Rect(20, 35, pbLine.Width - 20, pbLine.Height - 10),
    Totals, RGBToColor(46, 134, 193), Months);
end;

Summary Panel

The summary panel uses canvas text rendering for a clean, custom display:

procedure TfrmDashboard.pbSummaryPaint(Sender: TObject);
var
  I: Integer;
  Total, MaxMonth, MinMonth, Avg: Double;
  MaxIdx, MinIdx: Integer;
begin
  Total := 0; MaxMonth := 0; MinMonth := 1e18;
  MaxIdx := 0; MinIdx := 0;

  for I := 0 to High(SampleData) do
  begin
    var MonthTotal := SampleData[I].Food + SampleData[I].Transport +
      SampleData[I].Housing + SampleData[I].Utilities +
      SampleData[I].Entertainment;
    Total := Total + MonthTotal;
    if MonthTotal > MaxMonth then begin MaxMonth := MonthTotal; MaxIdx := I; end;
    if MonthTotal < MinMonth then begin MinMonth := MonthTotal; MinIdx := I; end;
  end;
  Avg := Total / Length(SampleData);

  with pbSummary.Canvas do
  begin
    Brush.Color := clWhite;
    FillRect(0, 0, pbSummary.Width, pbSummary.Height);

    Font.Size := 11;
    Font.Style := [fsBold];
    Font.Color := clBlack;
    Brush.Style := bsClear;
    TextOut(20, 10, 'Summary');

    Font.Size := 10;
    Font.Style := [];
    var Y := 45;

    TextOut(20, Y, 'Total (6 months):');
    Font.Style := [fsBold];
    TextOut(200, Y, Format('$%.2f', [Total]));
    Font.Style := [];
    Inc(Y, 30);

    TextOut(20, Y, 'Monthly average:');
    Font.Style := [fsBold];
    TextOut(200, Y, Format('$%.2f', [Avg]));
    Font.Style := [];
    Inc(Y, 30);

    TextOut(20, Y, 'Highest month:');
    Font.Style := [fsBold];
    Font.Color := clRed;
    TextOut(200, Y, Format('%s ($%.2f)',
      [SampleData[MaxIdx].Month, MaxMonth]));
    Font.Style := [];
    Font.Color := clBlack;
    Inc(Y, 30);

    TextOut(20, Y, 'Lowest month:');
    Font.Style := [fsBold];
    Font.Color := clGreen;
    TextOut(200, Y, Format('%s ($%.2f)',
      [SampleData[MinIdx].Month, MinMonth]));
    Font.Style := [];
    Font.Color := clBlack;
    Inc(Y, 30);

    TextOut(20, Y, 'Top category:');
    Font.Style := [fsBold];
    TextOut(200, Y, 'Housing');
    Brush.Style := bsSolid;
  end;
end;

Key Design Observations

  1. Same data, multiple views. All four charts draw from the same dataset. This is the power of separating data from presentation — change the data once, and all charts update.

  2. Each chart is self-contained. The drawing logic for each chart type is in a reusable procedure. The dashboard just calls them with the right data and coordinates.

  3. Consistent color palette. All charts use the same color array for each category. Food is always the same shade of blue; Housing is always the same shade of green. Consistency helps the viewer make connections across charts.

  4. Titles and labels. Every chart has a title. The bar chart has axis labels. The summary uses bold and color for emphasis. These are not decorations — they are necessary for comprehension.


Lessons Learned

  1. Canvas drawing is flexible. Four different visualization types, all built from the same primitives: lines, rectangles, ellipses, and text.
  2. Reusable chart procedures enable dashboards. Write the chart once, call it from any paint handler with different data.
  3. Layout matters. The 2x2 grid uses Align and Anchors to resize gracefully.
  4. Performance is excellent. Even with four charts, the dashboard paints in under 1 millisecond on modern hardware. This is the native compilation advantage.
  5. Custom visualization gives complete control. No charting library to learn, no dependency to manage, no limitation on what you can draw.