Skip to content

Step 4: cursor, tooltip and legend ​

Three types of object react to the pointer: a cursor that gives the value under it on each axis, a tooltip that lists the values of the series, and a legend that shows and hides series.

ts
import {
  D3PAutoDomain, D3PAxis, D3PAxisDomain, D3PChart, D3PLineRenderer, D3PSeries, getGlobalBus,
  D3PAutoDomain, d3pAutoInteractionArea, D3PAxis, D3PAxisDomain, D3PChart, D3PCursorIndicator, d3pDomLegend,
  d3pDomTooltip, D3PLegendBundle, D3PLineRenderer, D3PSeries, D3PTooltip, 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 hours = new D3PAxisDomain({ bus, domain: [0, 24] })
const degrees = new D3PAxisDomain({ bus, domain: [0, 30] })
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 } } })
// 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 feelsLine = new D3PLineRenderer({ bus, series: feelsLike, props: { style: { color: '#4e79a7', curve: 'monotone' } } })

const objects = [x, y, line, feelsLine]
// what the pointer shows: a crosshair, a tooltip, a legend
const cursor = new D3PCursorIndicator({ bus })
const tooltip = new D3PTooltip({ bus })
const legend = new D3PLegendBundle({ bus })

const objects = [x, y, line, feelsLine, cursor, tooltip, legend]

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)
const stopLegend = d3pDomLegend(legend)

What changed:

  • D3PCursorIndicator draws a crosshair that follows the pointer, with the value of each axis on the axis.
  • D3PTooltip lists the value of each series at the pointer.
  • D3PLegendBundle lists the series; a click on an entry hides its line.
  • Core only: an interaction area. Pointer events come from an interaction area, a transparent rectangle over the plot. In plain TypeScript, d3pAutoInteractionArea(chart) creates it when something asks for it, and must be called before the objects are attached. The Vue and React charts do it for you.
  • The HTML is yours. The tooltip and the legend compute what to show and give an element to put it in. d3pDomTooltip and d3pDomLegend fill that element with a simple default; the Vue and React adapters have components for it, which take your own content too (see Vue and React).

The labels of the cursor ​

The cursor writes the value under the pointer with the tickFormat of the axis. That value is not a tick: it has many decimals, so a format like (v) => `${Math.round(v)} %` keeps the label short. A label is never wider than the room its axis leaves it: a longer text wraps onto several lines, and ends with an ellipsis where even that does not fit. props.axisLabelStyle.maxWidth (160 px) lowers the limit; valueFormatter writes the labels yourself.

What is under the pointer ​

On every move, each series finds the point nearest to the pointer, and its marker marks it: a dot on a line, an outline around a bar. Markers belong to renderers: give another with marker, or none with marker: false.

ts
new D3PLineRenderer({ bus, series, marker: false })

The tooltip, the cursor and the markers all read the same pointer event, and the work is done once.