Data Visualisation with Phlex and ECharts
Series Overview
Prerequisites: Completion of the Phlex v2 on Rails series, or equivalent familiarity with Rails 8, Phlex 2.4.1, Literal 1.9.0, Tailwind CSS v4, Stimulus, and importmaps.
What This Series Builds
This series teaches you to integrate ECharts 5.x into a Rails 8 application using Phlex components and a Stimulus controller — arriving at a clean, testable architecture where each layer has a single, well-defined responsibility.
The central thesis: ECharts is a JavaScript runtime concern. Ruby’s job is to produce a well-typed, testable option hash. The Stimulus controller’s job is to own the ECharts instance lifecycle. Phlex bridges them via data attributes — nothing more.
By the end you will have:
- A
chart_controller.jsStimulus controller that manages ECharts initialisation, disposal, and resize correctly - A Ruby DSL in
app/lib/chart/for constructing ECharts option hashes — expressive, testable, and deliberately constrained to what Ruby does well - A named formatter and colour palette registry that keeps all JavaScript formatting logic out of Ruby
- A suite of Phlex chart components covering the most common chart types, all driven by the same architectural patterns
- A real-time update architecture using ActionCable
- Unit tests for chart configuration that run in plain Ruby, with no browser and no Rails
Dataset: Simulated Australian Economic Data
All examples use a curated slice of ABS open data covering Australia’s economy from 2000–2024 (see: ABS Data Fixtures)
| Data Series | Granularity | Used In |
|---|---|---|
| GDP by industry (chain volume measures) | Quarterly | Line, Area, Mixed |
| Labour force by state (employment, unemployment, participation rate) | Monthly | Bar, Scatter |
| CPI by expenditure category | Quarterly | Pie, Donut |
| Leading economic index | Monthly | Gauge, Real-time |
| Daily economic activity proxy | Daily | Calendar Heatmap |
Data is generated by a deterministic Ruby script using a seeded LCG random number generator — the same values are produced on every run, so seed data is reproducible without being stored in the repository.
Module Guide
Module 01 — Foundation: ECharts, Stimulus, and the Phlex Bridge
Establishes the core architecture. Pin ECharts via importmap, build chart_controller.js managing the full ECharts lifecycle (init, dispose, resize via ResizeObserver), and create the minimal Components::Chart Phlex base class.
No DSL yet — raw Ruby hashes serialised to JSON via a Stimulus value, exposing the seam cleanly before abstracting over it.
Key questions answered: Where does the ECharts instance live? What destroys it? What resizes it?
Module 02 — The Ruby DSL: Building ECharts Options in Ruby
Introduces the three-layer architecture and the DSL that underpins the rest of the series:
app/lib/chart/—Chart::Options,Chart::Series::*,Chart::Axis,Chart::Tooltip,Chart::Titleapp/services/stats/— data transformation services usingextend selfapp/views/components/charts/— chart-specific Phlex components
Covers the escape hatch: plain Ruby hashes pass through Chart::Options unchanged. The DSL wraps what benefits from Ruby; everything else stays as hashes.
Key questions answered: What should Ruby wrap? Where does data transformation live? How do you test chart configuration in plain Ruby?
Module 03 — Formatters and Colour Palettes
ECharts formatters are JavaScript functions — they cannot survive JSON serialisation. This module introduces the named formatter registry that solves this cleanly.
app/javascript/charts/chart_formatters.js— base formatter libraryapp/javascript/charts/custom_chart_formatters.js— application-specific formattersapp/javascript/charts/chart_palettes.js— 9 base colour palettesapp/javascript/charts/custom_chart_palettes.js— application-specific palettes
Ruby passes a string name; chart_controller.js resolves it to a function before calling setOption. The trigger key on tooltip options provides context automatically — formatter: "currency" resolves to "axis:currency" or "item:currency" depending on the trigger.
String templates ("{value}kg") pass through unchanged. Named formatters cover everything else.
Key questions answered: How do JavaScript functions cross the Ruby/JSON boundary? How do you ensure colour consistency across linked charts?
Module 04 — Bar and Stacked Bar: Labour Force by State
First multi-series chart. Builds Components::Charts::LabourForce using the Labour Force dataset. Covers grouped vs stacked bars using ECharts’ native magicType toolbox feature, horizontal bar orientation, and bucketing monthly data into annual averages.
Introduces groupdate conceptually — the Labour Force data uses integer year/month columns so we bucket manually, but the daily_activity_readings table in Module 07 uses a proper date column where groupdate earns its place fully.
Module 05 — Scatter: Labour Market Analysis
Introduces scatter charts and the data story pattern — multiple charts interspersed
with prose on a single page. Builds three components from one service call:
Components::Charts::ParticipationScatter (participation rate vs unemployment rate
by state), Components::Charts::EmploymentTrends (employment volume over time), and
Components::Charts::ParticipationTrends (participation rate trends with national
average reference line).
Covers visualMap for encoding a third dimension as colour, markLine for
statistical annotations, and the within-page colour correspondence principle — all
charts on the page share the same palette ensuring consistent colour mapping across
charts.
Reuses Stats::LabourForce from Module 04 — demonstrating that services are
chart-agnostic. Deepens the testing story — scatter chart option generation is
entirely testable in plain Ruby.
Module 06 — Pie and Donut: CPI Composition
Builds Components::Charts::CpiPie and a donut variant using the CPI dataset. Covers item-trigger tooltip formatters, label formatting, and the rose chart variant. Introduces trigger: "item" and the "item:percent" formatter pattern.
Module 07 — Calendar Heatmap: Business Activity
Builds Components::Charts::BusinessCalendar using the Business Indicators dataset
aggregated to a calendar view. ECharts’ calendar coordinate system is unusual —
this module covers explicit date range generation, sparse data handling, and
multi-year layouts. Introduces groupdate properly for bucketing quarterly data
into a calendar heatmap.
Module 08 — Gauge: National Accounts
Builds Components::Charts::NationalAccountsGauge using the National Accounts
dataset — GDP growth, household saving ratio, and terms of trade as gauge
indicators. Gauge charts have a deceptively complex option structure; this module
shows how the DSL handles nested configuration cleanly. Sets up the real-time
update story for Module 09.
Module 09 — Real-Time Updates via ActionCable
Builds EconomicIndicatorChannel and extends chart_controller.js to handle
incoming data via mergeOption — bypassing Turbo entirely for chart updates.
Covers when to use mergeOption (updating any option) vs full reinitialisation
(structural changes).
Includes a simulated broadcaster that replays National Accounts data at a configurable interval — no live data source required.
Module 10 — Advanced Label Formatting
ECharts supports multi-line, mixed-style labels via a rich object — named text styles referenced in formatter strings like "{bold|Mining}: {value|$42B}". This module covers rich text labels in depth: pie chart labels showing category, value, and percentage on separate lines; bar chart labels with multiple metrics; custom legend-style annotations.
Module 11 — Interactivity: Linked Charts, Drill-Down and dataZoom
The most complex interaction patterns:
- Click events wired via
chart.on('click')inchart_controller.js - Column chart → click → pie chart drill-down using Turbo Frames
dataZoom— slider and inside zoom, toolbox integration- Linked charts sharing colour palettes for visual consistency
Module 12 — Complex Multi-Series Mixed Chart
The capstone chart: a mixed line + bar visualisation on shared time axes with dual Y-axes. Pulls together the full DSL, formatter and palette systems, and the toolbox features introduced throughout the series.
Module 13 — Production Concerns
Rounds out the series:
- SVG and CSV export — extending beyond the built-in
saveAsImage - Server-side PDF generation from SVG strings
- Accessibility: ARIA labels and visually hidden data table fallback
- Performance: large datasets,
largemode, progressive rendering - Skeleton loading states
- Testing patterns summary
Architectural Decisions — Quick Reference
| Question | Decision |
|---|---|
| Where does the ECharts instance live? | Owned entirely by chart_controller.js |
| How does Ruby pass config to JavaScript? | JSON-serialised via data-chart-options-value Stimulus value |
| What does the Phlex component render? | A mount div with the correct data attributes — nothing more |
| Where does data transformation live? | Stats::* service modules in app/services/stats/ |
| How are JavaScript formatter functions handled? | Named formatter registry — Ruby passes a string, JavaScript resolves it |
| How are tooltip formatters contextualised? | The sibling trigger key qualifies the lookup automatically |
| How are colours kept consistent across charts? | Named palette registry — same palette name = same colour order |
| How are real-time updates delivered? | Direct to the ECharts instance via ActionCable |
| How is chart configuration tested? | Plain Ruby unit tests on DSL objects — no browser, no Rails required |
| What handles stack/line/bar/zoom toggles? | ECharts native toolbox with magicType — no custom JavaScript |
Key Files
app/
lib/
chart/
options.rb ← top-level builder
title.rb
tooltip.rb
axis.rb
series/
base.rb
line.rb
bar.rb
scatter.rb
pie.rb
services/
stats/ ← data transformation services
views/
components/
chart.rb ← base Phlex chart component
charts/ ← chart-specific components
app/javascript/
controllers/
chart_controller.js ← ECharts lifecycle + option resolution
charts/
chart_formatters.js ← base formatter registry
custom_chart_formatters.js
chart_palettes.js ← base palette registry
custom_chart_palettes.jsNext: Module 01 — Foundation: ECharts, Stimulus, and the Phlex Bridge