Skip to content

Synchronized charts ​

Two charts share one X axis: zooming or panning one moves the other. A single tooltip and a single toolbox cover both.

ts
import {
  D3PAutoDomain, d3pAutoInteractionArea, D3PAxis, D3PAxisDomain, D3PBarRenderer, D3PChart, D3PCursorIndicator,
  d3pDomTooltip, D3PLineRenderer, D3PPanTool, D3PResetTool, D3PSeries, D3PToolbox, D3PTooltip,
  D3PWheelZoomTool, getGlobalBus,
} from 'd3pict'
// your series data: replace these generators with your own values
import { indexed, rng, walk } from './data'

const bus = getGlobalBus()

const row = document.getElementById('app')! // a flex container with a size: the charts share it

const n = 80
const r = rng(11)
const price = new D3PSeries({ bus, name: 'price', data: indexed(walk(3, n, 50, 8, 10, 90)) })
const volume = new D3PSeries({ bus, name: 'volume', data: indexed(Array.from({ length: n }, () => Math.round(r() * 100))) })

const dx = new D3PAxisDomain({ bus, domain: [0, n - 1] })
const dPrice = new D3PAxisDomain({ bus, domain: [0, 100] })
const dVolume = new D3PAxisDomain({ bus, domain: [0, 100] })
const auto = new D3PAutoDomain({ bus })
auto.linkSeries(price, [dx, dPrice])
auto.linkSeries(volume, [dx, dVolume])

const x = new D3PAxis({ bus, name: 'x', domain: dx, props: { grid: { enabled: true } } })
const yPrice = new D3PAxis({ bus, name: 'price', domain: dPrice, position: 'left', props: { grid: { enabled: true } } })
const yVolume = new D3PAxis({ bus, name: 'volume', domain: dVolume, position: 'left' })
const line = new D3PLineRenderer({ bus, series: price, axes: ['x', 'price'], props: { style: { color: '#b07aa1' } } })
const bars = new D3PBarRenderer({ bus, series: volume, axes: ['x', 'volume'], props: { style: { color: '#76b7b2', barWidth: 6 } } })

const cursor = new D3PCursorIndicator({ bus })
const tooltip = new D3PTooltip({ bus })
const toolbox = new D3PToolbox({ bus })
toolbox.addTool(new D3PPanTool({ bus, axes: ['x'] }))
toolbox.addTool(new D3PWheelZoomTool({ bus, axes: ['x'] }))
toolbox.addTool(new D3PResetTool({ bus }))
for (const name of ['pan', 'wheel-zoom', 'reset']) toolbox.setToolActive(name)

// the cursor, the tooltip and the tools are attached to both charts, and so is the axis `x`
const shared = [cursor, tooltip, toolbox]
const margin = { left: 70 }
const charts = [
  { objects: [x, yPrice, line, ...shared], margin },
  { objects: [x, yVolume, bars, ...shared], margin },
]

const stops = charts.map(({ objects, margin }) => {
  const container = row.appendChild(document.createElement('div'))
  container.style.cssText = 'flex: 1; min-width: 0; min-height: 0' // the charts share the space
  const chart = new D3PChart({ container, bus, margin })
  const stopArea = d3pAutoInteractionArea(chart) // before the objects: the tools ask for an interaction area
  objects.forEach((o) => o.addToChart(chart))
  return () => (stopArea(), chart.destroy())
})
const stopTooltip = d3pDomTooltip(tooltip)