English
Cartesian and polar
The same series, the same line and area renderers, the same tooltip, cursor, legend and tools, drawn on cartesian axes or on polar ones. Only the axes change: switch with the buttons. In the code below, axes holds the two sets of axes and the chart takes one of them, plus everything else.
In polar, dragging turns the angle, the wheel zooms the radius, and the radius axis follows the pointer (D3PCursorRadiusAxisRenderer). The data covers three turns, and what is outside of the angle domain is not drawn: that is why the pattern keeps filling the circle while it turns.
ts
import {
D3PAngleAxis, D3PAreaRenderer, d3pAutoInteractionArea, D3PAxis, D3PAxisDomain, D3PChart, D3PCursorIndicator,
D3PCursorRadiusAxisRenderer, d3pDomLegend, d3pDomTooltip, D3PLegendBundle, D3PLineRenderer, d3pMergeArray,
D3PPanTool, D3PRadiusAxis, D3PResetTool, D3PSeries, D3PToolbox, D3PTooltip, D3PWheelZoomTool, getGlobalBus,
} from 'd3pict'
// your series data: replace these generators with your own values
import { pattern } from './data'
const bus = getGlobalBus()
const container = document.getElementById('app')! // any element with a size: the chart fills it
const angle = new D3PAxisDomain({ bus, domain: [0, 360] })
const radius = new D3PAxisDomain({ bus, domain: [0, 100] })
const lobes = new D3PSeries({ bus, name: 'lobes', data: pattern(35, 55, 4) })
const swell = new D3PSeries({ bus, name: 'swell', data: pattern(15, 40, 2, 1) })
const axisProps = { grid: { enabled: true } }
const axes = {
cartesian: [
new D3PAxis({ bus, name: 'angle', domain: angle, position: 'bottom', props: { ...axisProps, tickFormat: (v: number) => `${Math.round(v)}°` } }),
new D3PAxis({ bus, name: 'radius', domain: radius, position: 'left', props: axisProps }),
],
polar: [
new D3PAngleAxis({ bus, name: 'angle', domain: angle, props: { ...axisProps, tickCount: 12, tickFormat: (v: number) => `${Math.round(v)}°` } }),
// the spoke of the radius axis turns to follow the pointer
new D3PRadiusAxis({ bus, name: 'radius', domain: radius, props: axisProps, renderer: new D3PCursorRadiusAxisRenderer() }),
],
}
const area = new D3PAreaRenderer({ bus, series: lobes, name: 'lobes area', marker: false, props: { style: { color: '#4e79a7', opacity: 0.2 } } })
const lobesLine = new D3PLineRenderer({ bus, series: lobes, props: { style: { color: '#4e79a7' } } })
const swellLine = new D3PLineRenderer({ bus, series: swell, props: { style: { color: '#e15759' } } })
const cursor = new D3PCursorIndicator({ bus })
const tooltip = new D3PTooltip({ bus, series: [lobesLine, swellLine] })
const legend = new D3PLegendBundle({ bus, series: [lobesLine, swellLine] })
// the angle turns (drag), the radius zooms (wheel): in cartesian, X pans and Y zooms
const toolbox = new D3PToolbox({ bus })
toolbox.addTool(new D3PPanTool({ bus, axes: ['angle'] }))
toolbox.addTool(new D3PWheelZoomTool({ bus, axes: ['radius'] }))
toolbox.addTool(new D3PResetTool({ bus }))
for (const name of ['pan', 'wheel-zoom', 'reset']) toolbox.setToolActive(name)
// the axes are the only thing that depends on the geometry: the chart takes one set of them and `shared`
const shared = [area, lobesLine, swellLine, cursor, tooltip, legend, toolbox]
const bar = Object.assign(document.createElement('div'), { className: 'toolbar' })
container.before(bar)
const chart = new D3PChart({ container, bus })
const stopArea = d3pAutoInteractionArea(chart)
const stopTooltip = d3pDomTooltip(tooltip)
const stopLegend = d3pDomLegend(legend)
// the axes are the only thing that depends on the geometry: swap them, keep the rest attached
let attached: { addToChart(c: D3PChart): void; removeFromChart(c: D3PChart): void }[] = []
const show = (geometry: 'cartesian' | 'polar') => {
attached = d3pMergeArray(attached, [...axes[geometry], ...shared])
.exit((o) => o.removeFromChart(chart))
.enter((o) => o.addToChart(chart))
.value()
}
for (const geometry of ['cartesian', 'polar'] as const) {
bar.append(Object.assign(document.createElement('button'), { textContent: geometry, onclick: () => show(geometry) }))
}
show('polar')How the switch works
An axis has a position. bottom, top, left and right put it on a side of a cartesian chart; angle and radius make it the first and second dimension of a polar system (D3PAngleAxis and D3PRadiusAxis are shortcuts). The renderers read the axes of the chart and ask their coordinate system to project the points, so they do not know which geometry they draw in.
ts
// cartesian
new D3PAxis({ name: 'angle', domain: angle, position: 'bottom' })
new D3PAxis({ name: 'radius', domain: radius, position: 'left' })
// polar: same names, same domains
new D3PAxis({ name: 'angle', domain: angle, position: 'angle' })
new D3PAxis({ name: 'radius', domain: radius, position: 'radius' })Each axis is drawn by an axis renderer, chosen after the position. An axis refuses a renderer that does not support its position, with an explicit message.