Skip to content

Module 02 — Formatters and Colour Palettes

What We’re Building

The GDP chart from Module 01 renders correctly but the numbers are raw — axis labels show 351.882 instead of $352B, tooltips show a plain list of values with no units. This module introduces the formatter and palette systems that make charts readable.

By the end of this module you will have:

  • Named formatters applied to axis labels and tooltips
  • A colour palette applied to the chart
  • The same chart looking significantly more polished

2.1 — The Problem with JavaScript Formatters

ECharts formatters are JavaScript functions. A formatter function like:

1
params => `$${params.value.toFixed(1)}B`

cannot be serialised to JSON — functions are not valid JSON. We cannot pass them from Ruby to the browser via a data attribute.

The solution is a named formatter registry. Ruby passes a string name; chart_controller.js resolves it to a function before calling setOption:

1
2
# Ruby — pass a name
y_axis: { axisLabel: { formatter: "billions" } }
1
2
// chart_controller.js resolves "billions" → function
v => `$${parse(v).toLocaleString()}B`

The base formatter registry in chart_formatters.js covers the most common cases. You add application-specific formatters to custom_chart_formatters.js.


2.2 — Axis Label Formatters

Axis label formatters receive a single value — the tick value on the axis. Reference them by name on axisLabel.formatter:

1
2
3
4
y_axis: {
  type:      "value",
  axisLabel: { formatter: "billions" }
}

Available axis label formatters

Name Example output Use for
"integer" 3,842 Whole numbers with thousands separator
"percent" 62% Whole number percentages
"rate" 3.8% One decimal place percentages
"billions" $42B Dollar values in billions
"millions" $1,234M Dollar values in millions
"thousands" 3,842k Values in thousands
"currency" $42.3B / $500M Auto-scaling dollar values

ECharts string templates also work and pass through unchanged:

1
2
axisLabel: { formatter: "{value}%" }    # ECharts template — passes through
axisLabel: { formatter: "rate" }        # Named formatter — resolved to function

Use named formatters when you need locale-aware number formatting or currency symbols. Use string templates for simple suffix/prefix additions.


2.3 — Tooltip Formatters

Tooltip formatters receive either a params array (axis trigger) or a single params object (item trigger). The trigger key on the tooltip option provides context — the resolver qualifies the lookup automatically:

1
2
3
4
5
tooltip: { trigger: "axis", formatter: "billions" }
# → resolves "axis:billions"

tooltip: { trigger: "item", formatter: "percent" }
# → resolves "item:percent"

You never need to write "axis:billions" in Ruby — the trigger qualifies it for you.

Available tooltip formatters

Name Trigger Output
"default" axis / item Plain value with thousands separator
"billions" axis / item $42.3B
"millions" axis / item $1,234M
"thousands" axis / item 3,842k
"currency" axis / item Auto-scaling $42.3B / $500M
"rate" axis / item 3.8%
"percent" axis / item 62%

2.4 — Applying Formatters

Update the GDP chart from Module 01:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def chart_options
  ::Chart::Options.new(
    color:   "cool",
    tooltip: { trigger: "axis", formatter: "billions" },  # ← add formatter
    legend:  { type: "scroll", bottom: 5 },
    x_axis:  { type: "category", data: quarters },
    y_axis:  {
      type:      "value",
      axisLabel: { formatter: "billions" }                # ← add formatter
    },
    grid:    { left: 8, right: 8, bottom: 40, containLabel: true },
    series:  build_series
  )
end

The axis labels now show $352B instead of 351.882. The tooltip shows a formatted table with each industry’s value in billions.


2.5 — Colour Palettes

ECharts assigns colours to series in order from the color array. The palette registry maps names to arrays:

1
2
3
4
::Chart::Options.new(
  color: "cool",   # ← palette name
  ...
)

chart_controller.js resolves "cool" to its colour array before calling setOption. If you pass a colour array directly it passes through unchanged — the registry is only consulted for string values.

Available palettes

Name Character
"default" ECharts built-in — balanced and familiar
"warm" Reds, oranges, yellows
"cool" Blues, greens, purples — professional and calm
"earth" Browns, tans, sage — grounded and natural
"pastel" Soft muted tones
"vivid" High saturation — bold
"monochrome" Single blue hue — print-friendly
"accessible" Okabe-Ito — colour-blind safe
"tableau" Tableau classic — widely recognised

Colour correspondence

Charts on the same page using the same palette assign colours in series order. If two charts both use "tableau" and return series in the same order, NSW will always be the same colour on both. This is why services sort consistently — alphabetically by state or industry.


2.6 — Adding Custom Formatters

Add to custom_chart_formatters.js. Custom formatters override base formatters when names clash:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// app/javascript/charts/custom_chart_formatters.js
export default {
  // Axis label — receives a single value
  "myUnit": v => `${v} units`,

  // Axis tooltip — receives params array
  "axis:myUnit": params => {
    const header = params[0]?.axisValueLabel ?? ""
    const rows   = params.map(p =>
      `<tr>
        <td>${p.marker}${p.seriesName}</td>
        <td><strong>${p.value} units</strong></td>
      </tr>`
    ).join("")
    return `${header}<br/><table>${rows}</table>`
  }
}

Then from Ruby:

1
2
tooltip:  { trigger: "axis", formatter: "myUnit" }
y_axis:   { axisLabel: { formatter: "myUnit" } }

2.7 — Adding Custom Palettes

1
2
3
4
5
6
7
// app/javascript/charts/custom_chart_palettes.js
export default {
  brand: [
    "#005f73", "#0a9396", "#94d2bd", "#e9d8a6",
    "#ee9b00", "#ca6702", "#bb3e03", "#ae2012"
  ]
}

Then from Ruby:

1
::Chart::Options.new(color: "brand", ...)

2.8 — The Updated GDP Chart

With formatters and palette applied, the chart is significantly more readable. The full component:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# app/views/components/charts/gdp_by_industry.rb
module Components
  module Charts
    class GdpByIndustry < Components::Chart
      prop :data, _Any, default: -> { {} }

      private

      def chart_options
        ::Chart::Options.new(
          color:   "cool",
          toolbox: { feature: { saveAsImage: {}, restore: {} } },
          tooltip: { trigger: "axis", formatter: "billions" },
          legend:  { type: "scroll", bottom: 5 },
          x_axis:  {
            type:      "category",
            data:      quarters,
            axisLabel: { interval: 7, rotate: 30 }
          },
          y_axis:  {
            type:      "value",
            axisLabel: { formatter: "billions" }
          },
          grid:    { left: 8, right: 8, bottom: 60, containLabel: true },
          series:  build_series
        )
      end

      def build_series
        @data.map do |industry, rows|
          ::Chart::Series::Line.new(
            name:   industry,
            data:   rows.map { |r| r[:value] },
            smooth: true,
            symbol: "none"
          )
        end
      end

      def quarters
        @data.values.first&.map { |r| "#{r[:year]} Q#{r[:quarter]}" } || []
      end
    end
  end
end

axisLabel: { interval: 7, rotate: 30 } — shows every 8th label (one per year for quarterly data) rotated 30 degrees to prevent overlap.


2.9 — Gallery

Add a gallery index to showcase charts as you build them. Create a simple index view:

1
2
3
4
# app/controllers/charts_controller.rb
def index
  # gallery of all charts
end
<%# app/views/charts/index.html.erb %>
<div class="max-w-5xl mx-auto px-4 py-8">
  <h1 class="text-3xl font-bold mb-8">Chart Gallery</h1>
  <div class="grid grid-cols-2 gap-4">
    <%= render "charts/gallery_card",
      title:       "GDP by Industry",
      description: "Chain volume measures by industry, 2000–2024.",
      path:        charts_gdp_by_industry_path %>
  </div>
</div>
<%# app/views/charts/_gallery_card.html.erb %>
<div class="border border-neutral-200 rounded-lg p-4 hover:border-neutral-400
            transition-colors">
  <%= link_to path, class: "block" do %>
    <h2 class="font-semibold mb-1"><%= title %></h2>
    <p class="text-sm text-neutral-500"><%= description %></p>
  <% end %>
</div>

Each module adds cards to the gallery as new charts are built.


2.10 — Module Summary

Patterns introduced:

  • Named formatter registry — Ruby string, JavaScript function
  • Trigger qualification — formatter: "billions" resolves to "axis:billions" or "item:billions" automatically
  • Named palette registry — color: "cool" resolves to colour array
  • Colour correspondence — same palette + same series order = same colour mapping
  • Custom formatters in custom_chart_formatters.js
  • Custom palettes in custom_chart_palettes.js
  • Gallery index — adding cards as charts are built

The formatter principle:

Ruby passes names. JavaScript resolves them. You never write a JavaScript function to format a chart value — you name it once and reference it anywhere.