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