Appendix B — The JavaScript Infrastructure
The tutorial modules treat the JavaScript layer as a black box — you install it once and never touch it again. This appendix explains how it works for those who want to understand or extend it.
There are three files:
| File | Purpose |
|---|---|
app/javascript/controllers/chart_controller.js |
Stimulus controller — owns the ECharts instance |
app/javascript/charts/chart_formatters.js |
Formatter registry — named tooltip and axis formatters |
app/javascript/charts/chart_palettes.js |
Palette registry — named colour arrays |
Together they form a thin runtime that bridges Ruby-generated ECharts options with the ECharts JavaScript library. The design goal is that all chart configuration lives in Ruby — the JavaScript layer is infrastructure, not logic.
B.1 — chart_controller.js
Overview
chart_controller.js is a Stimulus controller. Stimulus connects it to any DOM
element that carries data-controller="chart". The controller:
- Initialises an ECharts instance on the mount target
- Resolves palette names and formatter names in the options
- Calls
setOptionto render the chart - Watches for option changes via a Stimulus value observer
- Handles resize automatically via
ResizeObserver - Optionally subscribes to an ActionCable stream for real-time updates
- Optionally joins an ECharts group for linked chart interactions
- Disposes the ECharts instance cleanly on disconnect
Stimulus values
|
|
options carries the full ECharts configuration generated by the Ruby DSL.
streamName and group are opt-in features — absent means the chart is static
and standalone.
The connect/disconnect lifecycle
|
|
connect() fires when the controller’s element enters the DOM — on page load,
after a Turbo navigation, or when Turbo Streams adds the element. disconnect()
fires when the element leaves — on navigation away or when Turbo Streams removes
it. ECharts instances are disposed on disconnect to prevent memory leaks.
optionsValueChanged
Stimulus calls this automatically whenever data-chart-options-value changes:
|
|
This is the mechanism that makes real-time updates work without custom JavaScript.
When the broadcaster sends new options, the controller sets the data attribute,
Stimulus detects the change, and optionsValueChanged fires — calling setOption
on the existing ECharts instance.
#initChart
|
|
echarts.init creates the ECharts instance on the mount target element.
renderer: "svg" uses SVG rather than Canvas — better for accessibility, print,
and server-side rendering. The ResizeObserver calls chart.resize() whenever
the container changes size — making charts responsive without any extra code.
#applyOptions
|
|
#resolveOptions runs before setOption — it replaces palette names with colour
arrays and formatter names with functions. notMerge: true replaces the full
option state rather than merging — correct for most chart updates.
For real-time partial updates (where only series data changes) the broadcaster
uses notMerge: false — merging the new data with the existing configuration.
#joinGroup
|
|
chart.group assigns this instance to a named group. echarts.connect(group)
tells ECharts to synchronise tooltip and zoom state across all instances in the
group. All charts with the same group name will show coordinated tooltips when
any one is hovered.
#subscribe
|
|
Creates an ActionCable subscription to the named stream. Incoming data is merged with the current options using the spread operator — the broadcaster can send a complete options object (full replace) or just the changed keys (partial merge).
Setting dataset.chartOptionsValue triggers optionsValueChanged automatically.
No explicit call needed — Stimulus watches the attribute.
The complete file
|
|
B.2 — The Formatter Registry
How formatters work
ECharts formatter options accept either a string template or a JavaScript
function. The Ruby DSL passes formatter names as strings — "currency",
"rate", "thousands". The #resolveFormatters method in chart_controller.js
walks the options tree and replaces these strings with the corresponding functions
from the registry.
Trigger qualification
The same formatter name resolves differently depending on context:
|
|
The resolver reads the sibling trigger key to qualify the lookup. This means
the same name works correctly in all three contexts — you never need to remember
qualified names when writing Ruby.
Resolver logic
|
|
The resolver walks every object in the options tree recursively. When it finds a
formatter key with a string value, it attempts three lookups in order:
"axis:currency"— trigger-qualified (most specific)"currency"— unqualified (axis label context)- The original string — passed through unchanged (ECharts string templates)
ECharts string templates like "{value}%" or "{b}: {c}" pass through step 3
unchanged — they are not in the registry and ECharts handles them natively.
Adding custom formatters
Add to custom_chart_formatters.js. Custom formatters override base formatters
when names clash:
|
|
Then reference from Ruby:
|
|
Base formatters reference
| Name | Axis label | Axis tooltip | Item tooltip |
|---|---|---|---|
integer |
✓ | ✓ | — |
percent |
✓ | ✓ | ✓ |
rate |
✓ | ✓ | ✓ |
billions |
✓ | ✓ | ✓ |
millions |
✓ | ✓ | — |
thousands |
✓ | ✓ | ✓ |
currency |
✓ | ✓ | ✓ |
default |
— | ✓ | ✓ |
B.3 — The Palette Registry
How palettes work
ECharts color accepts an array of colour strings. The Ruby DSL passes palette
names as strings — color: "cool", color: "tableau". The #resolvePalette
method replaces the name with the corresponding array before setOption is called.
|
|
If color is already an array (inline colours) it passes through unchanged.
If color is a string not found in the registry (e.g. a CSS colour "#ef4444")
it also passes through — ECharts treats a single colour string as a fixed colour
for all series.
Colour correspondence
Charts on the same page with the same palette assign colours in series order.
If two charts both use "tableau" and both have series in the same order, they
will use the same colours — series 1 gets tableau colour 1 on both charts.
This is guaranteed when the service returns data in a consistent order (e.g. alphabetically by state). The palette correspondence principle from Module 03 applies: same palette + same series order = same colour mapping.
Adding custom palettes
|
|
Then from Ruby:
|
|
Available palettes
| Name | Character |
|---|---|
default |
ECharts built-in — balanced and familiar |
warm |
Reds, oranges, yellows — energy and urgency |
cool |
Blues, greens, purples — professional and calm |
earth |
Browns, tans, sage — grounded and natural |
pastel |
Soft muted tones — gentle and accessible |
vivid |
High saturation — bold and attention-grabbing |
monochrome |
Single blue hue — print-friendly |
accessible |
Okabe-Ito — colour-blind safe |
tableau |
Tableau classic — widely recognised, perceptually distinct |
B.4 — Extending the Infrastructure
Adding a new Stimulus value
To add a new opt-in capability (e.g. a theme value for ECharts themes):
|
|
Then add theme: to Components::Chart base class and pass it as
chart_theme_value. Every chart gains the capability with no further changes.
Adding a new formatter
Add to custom_chart_formatters.js. The name is available immediately in Ruby
via formatter: "myName".
Adding a new palette
Add to custom_chart_palettes.js. The name is available immediately in Ruby
via color: "myName".
No changes to chart_controller.js are needed for new formatters or palettes —
they are registered at import time and merged automatically.