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. points="always" draws a circle at every data point (auto, the
default, shows them only once points are far enough apart; none hides them), sized by --c2-chart__point--radius.
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.
Import
import '@c2n/components/chart/line-chart'This chart draws with uPlot, a peer dependency it loads on first draw: npm install uplot.
Usage
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])
}Annotations
annotations marks moments and periods on the plot: { x, label, tone } draws a dashed rule with a marker at
x, and adding x2 shades the range between the two. x is an x value — what x-field holds (epoch milliseconds
on a time chart, the year here), or the row index when there is no x-field — and tone (neutral, info, success, warning, danger)
picks the colour, each set by its own --c2-chart__annotation__<tone>--color. The markers are placed by the
engine’s own scale after every draw, so they stay on their x through a resize, new data and a drag-to-zoom, and
an annotation outside the visible window is hidden.
Each marker is a button: Tab reaches it, its accessible name is the label and where it sits (“Stress-corrosion
outages, 2022 to 2023”), and a click or Enter fires annotation-click with the entry and its index. The same
property works on the area, bar, sparkline, scatter, bubble and candlestick charts; a horizontal bar chart draws
the rule across the plot at its category. The other charts ignore it.
chart.annotations = [
{ x: Date.parse('2024-03-01T09:40Z'), label: 'Deploy 4.2', tone: 'info', id: 'deploy' },
{ x: Date.parse('2024-03-01T10:05Z'), x2: Date.parse('2024-03-01T10:40Z'), label: 'Error surge', tone: 'danger' },
]
chart.addEventListener('annotation-click', (event) => openIncident(event.detail.annotation.id))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.
Every chart in the package sizes the same way. Set the height through the variable and leave the host’s
display alone: the host is a flex column whose plot area grows into the height left after the legend, so a rule
such as display: block; height: 260px collapses the plot to nothing. The legend still shows, the canvas stays
empty, and the chart logs a warning once per page.
.balance-chart {
--c2-chart--height: 260px;
}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().