Skip to content

Step 7: several axes ​

Two quantities in different units can share a chart: give each its own axis. Temperature is on the left, humidity on the right.

ts
import {
  D3PAutoDomain, d3pAutoInteractionArea, D3PAxis, D3PAxisDomain, D3PAxisTitle, D3PChart, D3PCursorIndicator,
  d3pDomTooltip, D3PLegendPanel, D3PLineRenderer, D3PPanTool, D3PResetTool, D3PSeries, D3PTitle, D3PToolbox,
  D3PTooltip, D3PWheelZoomTool, getGlobalBus,
} from 'd3pict'

const bus = getGlobalBus()

const container = document.getElementById('app')! // any element with a size: the chart fills it

const temperature = new D3PSeries({ bus, name: 'temperature', data: [[0, 12], [4, 9], [8, 14], [12, 21], [16, 24], [20, 17], [24, 13]] })
const feelsLike = new D3PSeries({ bus, name: 'feels like', data: [[0, 10], [4, 6], [8, 12], [12, 22], [16, 25], [20, 15], [24, 11]] })
const humidity = new D3PSeries({ bus, name: 'humidity', data: [[0, 80], [4, 85], [8, 70], [12, 45], [16, 40], [20, 60], [24, 75]] })

const hours = new D3PAxisDomain({ bus, domain: [0, 24] })
const degrees = new D3PAxisDomain({ bus, domain: [0, 30] })
const percent = new D3PAxisDomain({ bus, domain: [0, 100] })
const auto = new D3PAutoDomain({ bus })
auto.linkSeries(temperature, [hours, degrees])
auto.linkSeries(feelsLike, [hours, degrees])

const x = new D3PAxis({ bus, name: 'hour', domain: hours, props: { tickCount: 6, tickFormat: (v) => `${Math.round(v)} h`, grid: { enabled: true } } })
const y = new D3PAxis({ bus, name: 'temperature', domain: degrees, position: 'left', props: { tickFormat: (v) => `${Math.round(v * 10) / 10} °C`, grid: { enabled: true } } })
// a second Y axis, on the right, for a quantity in other units
const yRight = new D3PAxis({ bus, name: 'humidity', domain: percent, position: 'right', props: { tickFormat: (v) => `${Math.round(v)} %` } })
// one renderer per series; its style is a prop
const line = new D3PLineRenderer({ bus, series: temperature, props: { style: { color: '#e15759', curve: 'monotone', strokeWidth: 2.5 } } })
const humidityLine = new D3PLineRenderer({ bus, series: humidity, axes: ['hour', 'humidity'], props: { style: { color: '#59a14f', curve: 'monotone' } } })
const feelsLine = new D3PLineRenderer({ bus, series: feelsLike, props: { style: { color: '#4e79a7', curve: 'monotone' } } })

// what the pointer shows: a crosshair and a tooltip
const cursor = new D3PCursorIndicator({ bus })
const tooltip = new D3PTooltip({ bus })

// tools act on the domains: drag to pan, wheel to zoom
const tools = new D3PToolbox({ bus })
tools.addTool(new D3PPanTool({ bus }))
tools.addTool(new D3PWheelZoomTool({ bus }))
tools.addTool(new D3PResetTool({ bus }))
for (const name of ['pan', 'wheel-zoom', 'reset']) tools.setToolActive(name)

// titles and a legend take their room from the plot
const title = new D3PTitle({ bus, props: { text: 'Temperature', subtitle: 'on 12 June' } })
const xTitle = new D3PAxisTitle({ bus, axis: x, props: { text: 'hour of the day' } })
const yTitle = new D3PAxisTitle({ bus, axis: y, props: { text: 'temperature' } })
const yRightTitle = new D3PAxisTitle({ bus, axis: yRight, props: { text: 'humidity' } })
const legendPanel = new D3PLegendPanel({ bus, placement: 'bottom' })

const objects = [x, y, line, feelsLine, cursor, tooltip, tools, title, xTitle, yTitle, legendPanel]
const objects = [x, y, yRight, line, feelsLine, humidityLine, cursor, tooltip, tools, title, xTitle, yTitle, yRightTitle, legendPanel]

const chart = new D3PChart({ container, bus })
const stopArea = d3pAutoInteractionArea(chart) // before the objects: the cursor, the tooltip and the tools ask for an interaction area
objects.forEach((o) => o.addToChart(chart))
const stopTooltip = d3pDomTooltip(tooltip)

What changed:

  • A second y axis. yRight is on the right side, and has a domain of its own (percent, in %).
  • A renderer names its axes. A renderer takes the first x axis and the first y axis of the chart. humidityLine has to say which ones it draws against: axes: ['hour', 'humidity'], by name. The renderers of the temperature use the defaults, since the temperature axis was attached first.
  • A title for the new axis, against its own labels (D3PAxisTitle with axis: yRight).

More than one axis on a side ​

An axis is at bottom, top, left or right. To put two on the same side, move the second one away from the plot with offset (in pixels). Leave room for the title of the first one between them: the labels, a gap and the title take about 80 px.

ts
const cpu = new D3PAxis({ bus, name: 'cpu', domain: dCpu, position: 'left', offset: 84 })

Give the titles, and the axes, the color of their series (props.style.color of the title; the postProcess prop of the axis receives its SVG group) so that each one is clearly the axis of its series.

Tools, cursor and tooltip ​

  • The cursor shows one label per axis, on the axis, at the value under the pointer.
  • The tooltip lists the series, each in the units of its own axis.
  • A tool acts on every axis unless told otherwise (axes: ['hour']): zoom the time without touching the units of the y axes.

See the several axes example for four y axes, one of them logarithmic, and two x axes on one domain.