Module 05 — Pie, Donut, and Rose Charts: Industry Sales Composition
What We’re Building
Pie and donut charts show composition — how a whole divides into parts. They work best with a small number of meaningful slices (ideally 5–10) where the proportional differences matter. With too many slices they become unreadable; with too few they are trivial.
This module uses quarterly business sales by industry from the ABS Business Indicators dataset. The composition of Australian business sales by industry is genuinely surprising — and the differences between industries are large enough to make the proportions visually clear.
By the end of this module you will have:
- A basic pie chart with a rich custom tooltip
- A donut chart with a centre label showing the total
- A rose chart showing variation in a different dimension
- A half-donut dashboard gauge variant
- A data story page showcasing all four variants with explanatory prose
- Custom tooltip formatters for item-trigger charts
5.1 — The Data
The business_indicator_readings table has quarterly sales figures by industry from
2005–2024. For composition charts we want a single point in time — the most recent
complete quarter. The service selects the latest quarter and returns industries with
their sales values sorted descending.
|
|
The share field — percentage of total — is computed in the service. ECharts
calculates its own percent value for tooltips, but having it in the data means
we can use it in custom formatters and test it independently.
5.2 — Custom Tooltip Formatter
Pie and donut charts use trigger: "item" — the tooltip fires on a single slice,
not on a cross-axis position. The params object contains the slice name, value,
and ECharts’ calculated percentage.
Add a rich formatter to custom_chart_formatters.js:
|
|
Reference from Ruby:
|
|
Because the trigger is "item", the resolver qualifies this as "item:industrySlice".
Add the qualified key to custom_chart_formatters.js:
|
|
5.3 — The Service Call Helper
All four components call the same service. To avoid repeating the call in each
component, add a private helper to Components::Chart — or simply call it in each
component. The latter is simpler and keeps components independent:
|
|
The service result is an array of hashes — not grouped by a key — so it maps
directly to ECharts data format:
|
|
5.4 — Chart 1: Basic Pie Chart
|
|
label: { formatter: "{b}\n{d}%" } uses ECharts string template syntax:
{b}— the slice name{d}— the percentage (calculated by ECharts){c}— the value{a}— the series name
These are ECharts’ own template variables and pass through the resolver unchanged.
5.5 — Chart 2: Donut with Centre Label
A donut chart is a pie chart with an inner radius. The hole creates space for a centre label showing the total — a common dashboard pattern.
ECharts does not have a native “centre label” feature. The label is rendered using
the graphic option — ECharts’ drawing API for arbitrary SVG elements overlaid on
the chart.
|
|
radius: ["40%", "68%"] sets inner radius to 40% and outer to 68% — the gap
creates the donut hole. label: { show: false } hides slice labels on the donut
itself; emphasis.label shows a label only when a slice is hovered.
The graphic option positions arbitrary text over the chart. left: "center" and
top: "middle" centre it in the chart container — which aligns with the donut hole
because center: ["50%", "45%"] positions the donut centrally.
5.6 — Chart 3: Rose Chart
A rose chart (also called a nightingale chart) uses varying radius rather than varying angle to encode value — each slice spans an equal angle but its radius varies with its value. This makes small differences between slices more visible than a standard pie chart.
|
|
roseType: "radius" switches from standard pie to rose mode. itemStyle: { borderRadius: 6 } rounds the corners of each slice — a subtle refinement that improves readability when slices are narrow.
radius: ["15%", "70%"] gives the rose chart an inner radius — combining donut
and rose modes — which prevents the smallest slices from collapsing to a point at
the centre.
5.7 — Chart 4: Half Donut
A half donut uses startAngle and endAngle to render only the top half of the
chart — a compact format suited to dashboards where vertical space is limited.
|
|
center: ["50%", "72%"] shifts the centre point down so the flat edge of the
half-donut sits near the bottom of the chart area, using the space efficiently.
startAngle: 180 and endAngle: 360 render only the top semicircle.
Grouping smaller industries into “Other” is important — a half donut with 15 slices is unreadable. Six meaningful slices plus “Other” is the practical limit.
5.8 — Controller and View
|
|
|
|
<%# app/views/charts/industry_composition.html.erb %>
<div class="max-w-5xl mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-2">Australian Business Sales by Industry</h1>
<p class="text-neutral-500 text-sm mb-8">
Latest quarter, seasonally adjusted.
Source: <a href="https://www.abs.gov.au" class="underline">Australian Bureau of Statistics</a>,
Business Indicators (ABS cat. 5676.0), CC BY 4.0.
</p>
<%# ── Pie + Donut side by side ───────────────────────────────── %>
<div class="grid grid-cols-2 gap-6 mb-8">
<div>
<h2 class="text-lg font-semibold mb-1">Pie Chart</h2>
<p class="text-neutral-500 text-sm mb-3">
Area encodes share of total. Hover a slice for details.
</p>
<%= render Components::Charts::IndustryPie.new(
readings: @readings,
height: "360px"
) %>
</div>
<div>
<h2 class="text-lg font-semibold mb-1">Donut Chart</h2>
<p class="text-neutral-500 text-sm mb-3">
The hollow centre carries a summary value — a common dashboard pattern.
</p>
<%= render Components::Charts::IndustryDonut.new(
readings: @readings,
height: "360px"
) %>
</div>
</div>
<%# ── Rose + Half Donut side by side ─────────────────────────── %>
<div class="grid grid-cols-2 gap-6 mb-8">
<div>
<h2 class="text-lg font-semibold mb-1">Rose Chart</h2>
<p class="text-neutral-500 text-sm mb-3">
Radius encodes value rather than angle — small differences between
industries are more visible than in a standard pie chart.
</p>
<%= render Components::Charts::IndustryRose.new(
readings: @readings,
height: "360px"
) %>
</div>
<div>
<h2 class="text-lg font-semibold mb-1">Half Donut</h2>
<p class="text-neutral-500 text-sm mb-3">
A compact format for dashboards. Smaller industries are grouped into
"Other" to keep the chart readable.
</p>
<%= render Components::Charts::IndustryHalfDonut.new(
readings: @readings,
height: "360px"
) %>
</div>
</div>
<%# ── Guidance ────────────────────────────────────────────────── %>
<div class="bg-neutral-50 rounded-lg p-6 mb-8">
<h2 class="text-lg font-semibold mb-3">Choosing Between Variants</h2>
<div class="grid grid-cols-2 gap-6 text-sm text-neutral-600">
<div>
<p class="font-medium text-neutral-800 mb-1">Use a pie chart when:</p>
<ul class="list-disc list-inside space-y-1">
<li>You have 5–8 categories</li>
<li>Proportions are the primary message</li>
<li>One slice is clearly dominant</li>
</ul>
</div>
<div>
<p class="font-medium text-neutral-800 mb-1">Use a donut chart when:</p>
<ul class="list-disc list-inside space-y-1">
<li>You want a summary value in the centre</li>
<li>The chart sits in a dashboard layout</li>
<li>You want to reduce visual weight</li>
</ul>
</div>
<div>
<p class="font-medium text-neutral-800 mb-1">Use a rose chart when:</p>
<ul class="list-disc list-inside space-y-1">
<li>Values are similar in magnitude</li>
<li>You want to emphasise relative differences</li>
<li>Visual impact matters more than precision</li>
</ul>
</div>
<div>
<p class="font-medium text-neutral-800 mb-1">Use a half donut when:</p>
<ul class="list-disc list-inside space-y-1">
<li>Vertical space is constrained</li>
<li>You are building a dashboard widget</li>
<li>You have 6 or fewer categories</li>
</ul>
</div>
</div>
</div>
<div class="border-t border-neutral-200 pt-6">
<p class="text-neutral-400 text-xs">
Data: Australian Bureau of Statistics, Business Indicators, Australia
(ABS cat. 5676.0). Accessed via ABS Data API. Licensed under
<a href="https://www.abs.gov.au/website-privacy-copyright-and-disclaimer"
class="underline">CC BY 4.0</a>.
</p>
</div>
</div>5.9 — Chart::Series::Pie Updates
The IndustryRose and IndustryHalfDonut components pass roseType:,
startAngle:, endAngle:, and itemStyle: options. These are not modelled
in Chart::Series::Pie — they pass through via **extra in Chart::Series::Base.
No DSL changes needed. The escape hatch handles them:
|
|
This is the intended use of **extra — ECharts has hundreds of series options,
and the DSL models only the most common ones.
5.10 — Gallery
Add to app/views/charts/index.html.erb:
<%= render "charts/gallery_card",
title: "Industry Composition",
description: "Four pie/donut variants — pie, donut, rose, and half donut — "\
"showing Australian business sales by industry.",
path: charts_industry_composition_path %>5.11 — Module Summary
New files:
| File | Purpose |
|---|---|
app/services/stats/business_composition.rb |
Latest quarter sales by industry with shares |
app/views/components/charts/industry_pie.rb |
Standard pie chart |
app/views/components/charts/industry_donut.rb |
Donut with centre label |
app/views/components/charts/industry_rose.rb |
Rose/nightingale chart |
app/views/components/charts/industry_half_donut.rb |
Half donut dashboard variant |
app/views/charts/industry_composition.html.erb |
Four-chart showcase page |
Patterns introduced:
trigger: "item"tooltip for single-slice charts- Custom rich tooltip formatter for pie/donut slices
radius: ["inner%", "outer%"]for donut chartsgraphicoption for centre label text overlayroseType: "radius"for nightingale/rose chartsstartAngle/endAnglefor half donutemphasis.labelfor hover-activated slice labels- ECharts label template variables —
{b},{c},{d},{a} - Grouping small slices into “Other” for readability
- Side-by-side chart layout with Tailwind grid
Label template variables:
| Variable | Value |
|---|---|
{a} |
Series name |
{b} |
Data item name (slice label) |
{c} |
Data value |
{d} |
Percentage (calculated by ECharts) |