Line chart
A line chart over a time or numeric x axis, drawn on canvas by uPlot, with series declared as children.
One line per series over a time or numeric x axis. Data is rows plus c2-chart-series children — the same
authoring shape as c2-table’s rows and columns — and presentation is ordinary attributes: curve,
points, legend, zoom, the axis bounds.
c2-line-chart is the base of the family. Area chart extends it
with a fill, and bar,
sparkline and pie share the
same data contract and the same --c2-chart__* theme namespace, so an app styles every chart at once.
One engine, loaded on demand. Lines are drawn by uPlot, an optional
peer dependency reached through a dynamic import() on first paint — nothing is bundled into the component,
and a page that shows no chart downloads no engine.
Installation
npm install @c2n/chart uplotUsage
All three demos use a France extract from the public Our World in Data Energy dataset: annual electricity generation in terawatt-hours, one row per year. The source publishes a codebook and traces these electricity fields to Ember and the Energy Institute. Keeping the same rows across the chart family makes the examples comparable rather than decorative.
Data and series
data is an array of row objects. x-field names the key holding the x value (the row index is used when
it is omitted) and x-type says how to read it — linear by default, time for epoch milliseconds,
category for discrete slots. Each c2-chart-series child names a field — a dotted path such as
stats.cpu works — and optionally a label, a color and a line-width. With no children at all, one
line is drawn per numeric key of the first row.
The children are definitions, not rendered elements: they are read the way c2-table reads its columns, so
adding, hiding or re-colouring one is an ordinary attribute change.
Realtime
data is compared by identity, and a change to it alone is pushed straight into the engine without
re-rendering anything. For a live feed call appendPoint() instead, which writes into the frame’s spare
capacity and redraws with no reallocation and no Lit update. max-points keeps the window bounded, dropping
the oldest points off the front.
uPlot redraws immediately rather than tweening between samples. That is useful for honest live telemetry,
but a slow feed will look stepped. The realtime demos interpolate each annual sample with
requestAnimationFrame() and append those intermediate points; this keeps the engine’s inexpensive data
path while making the moving window visually smooth. ECharts supports animation="auto", but it is better
left off for a high-frequency feed so animations do not overlap.
const chart = document.querySelector('c2-line-chart')
chart.maxPoints = 6
for (const row of incomingYears) {
chart.appendPoint(row.year, [row.nuclear, row.wind, row.solar])
}Independent legend and tooltip
The built-in legend and tooltip are convenient defaults. For full layout control, place
c2-chart-legend and c2-chart-tooltip anywhere in the same document or shadow root. Their for attribute
targets the chart’s id, like a label targets a form control, and automatically replaces the corresponding
built-in display. The tooltip defaults to a viewport-safe floating
position; set position="inline" to keep it exactly where its HTML is authored. The renderLegend,
renderLegendItem and renderTooltip properties accept Lit render functions for application-specific content.
The custom demo maps each source to a Phosphor icon (atom, drop, wind, sun, or factory) and computes
each value’s share of the five tracked generation sources.
import { html } from 'lit'
legend.renderLegendItem = (item) => html`
${energyIcon(item.label, item.color)}
<strong>${item.label}</strong>
`
tooltip.renderTooltip = (context) => {
const total = trackedTotalFor(context.formattedX)
return html`
<strong>${context.formattedX}</strong>
${context.entries.map((entry) => html`
<span>${energyIcon(entry.series.label, entry.color)} ${entry.formatted} TWh · ${formatPercent(entry.value / total)}</span>
`)}
`
}Sizing
A canvas has no intrinsic size, so a chart is sized by its host rather than by its content:
--c2-chart--height (320px by default) plus whatever width the layout gives it. A ResizeObserver keeps the
canvas in step, and the plot box is contain: strict so a resize can never feed back into itself. Use
lazy-render to defer loading the engine until the chart first scrolls into view.
Theming
Every chart in the package reads the same variables, so setting them once themes all of them. The series
palette is --c2-chart__series-1--color through --c2-chart__series-8--color, wired by @c2n/theme to the
--c2-theme--chart-series-* tokens, which carry light and dark values. Because the plot is a canvas, these
are read with getComputedStyle and handed to the engine — so they behave like any other CSS variable,
including under a dark theme, but cannot be reached with ::part().