Chart overview
One architecture for data, series, interaction and presentation across every chart component.
The family contains line, area, bar, scatter, bubble,
candlestick, gauge, radar, pie, pyramid, butterfly,
overlap, heatmap, chord, depth, sankey, treemap, waterfall, radial bar, bullet, boxplot, sunburst and
sparkline. They are separate custom elements, but they inherit the same ChartBase and use the same
normalized frame, series definitions, theme tokens, presenters, state UI, events and update pipeline.
Every example on this page uses Apple fiscal-year data published in the public SEC Company Facts API or the product mix in Apple’s 2025 Form 10-K. Amounts are USD billions. It is a useful common dataset: trends suit Cartesian charts, product mix suits a pie, and annual values make realtime interpolation easy to see.
Architecture
The shared architecture keeps application code independent from the drawing engine:
data,revisionor adataSourceenters the data path.ChartFrameBuildernormalizes rows, arrays or columns into one typed frame.c2-chart-seriesadds labels, colors, axes and formatting without changing the data.- The concrete chart projects that frame through its uPlot or ECharts adapter.
ChartBaseowns resize, lazy loading, states, legend, tooltip and semantic events around the canvas.
Engines
The components lazy-load optional peer engines only when first rendered. Line, area, bar, sparkline and depth use
uPlot for a small, fast Cartesian path. Pie, gauge, scatter and candlestick use modular
Apache ECharts for specialized series, labels, visual maps and canvas/SVG renderers.
chart-ready reports the active engine; application code still sees the same chart API.
Charts animate their first draw by default and keep later data updates immediate. ECharts uses its native
shape animation, while uPlot charts use a lightweight plot reveal. Use animation="none" to disable the
entrance animation. Both paths respect the user’s reduced-motion preference.
Install only the engine your selected elements need:
npm install @c2n/components uplot # line, area, bar, sparkline, depth
npm install @c2n/components echarts # pie, gauge, scatter, candlestick
npm install @c2n/components uplot echarts # the full familyData and series
Every chart accepts row objects, number[], columnar [xs, ys…], or an already normalized ChartFrame.
Rows are the readable default; columnar data is the zero-conversion path for large numeric datasets. Declare
series as c2-chart-series children or assign the series property. Children win when both are present.
x-field, x-type, label-field and locale control interpretation and display. A series can add a label,
explicit color, second axis, gap behavior and a property-only format(value) function. With no definitions,
numeric fields are inferred from the first row.
Legend and tooltip
legend="top|bottom|start|end|none" and tooltip="axis|item|none" provide useful built-ins. For product UI,
link independent c2-chart-legend and c2-chart-tooltip elements with for="chart-id". They can live anywhere
in the same document or shadow root and automatically suppress the corresponding built-in presenter.
The linked elements expose renderLegend, renderLegendItem and renderTooltip properties for data-driven Lit
templates. This example uses recognizable finance/product icons, current values, growth, margin, mix and percentage
context rather than repeating the chart labels.
legend.renderLegendItem = (item) => html`
${metricIcon(item.label, item.color)}
<span><strong>${item.label}</strong><small>${latestValueAndGrowth(item)}</small></span>
`
tooltip.renderTooltip = (context) => html`
<strong>FY ${context.formattedX}</strong>
${context.entries.map((entry) => html`
<span>${metricIcon(entry.series.label, entry.color)} ${entry.formatted} · ${percentageOrYoY(entry)}</span>
`)}
`Slots
Every chart also takes its legend and tooltip as slots, like its empty, loading and error states. Markup in
slot="legend" replaces the generated entries in place, and markup in slot="tooltip" becomes the body of the chart’s
own tooltip bubble, which still follows the pointer. Update slotted content from the legend-change and
tooltip-change events; renderLegendItem and renderTooltip on the chart are the function form of the same thing.
<c2-line-chart id="revenue" x-field="month" data="…">
<c2-chart-series field="revenue" label="Revenue"></c2-chart-series>
<div slot="legend">Revenue, USD billions</div>
<div slot="tooltip"><strong class="month"></strong> <span class="value"></span></div>
</c2-line-chart>Hide or highlight from the legend
A legend click highlights its series by default: the series comes forward at full strength, drawn above the others
with a slightly heavier line (a deeper fill on an area chart, a shadow on a pie slice), and the others fade back to
--c2-chart__series__dimmed--opacity (0.25); a second click clears it. With legend-action="toggle" the click hides
or shows the series instead. Every chart supports both modes: pie charts highlight a slice, overlap charts a whole set. The chart fires series-highlight
with the key, and highlighted holds it: the series field, the slice label, or the set key. Setting highlighted
from the application does the same in either mode. A linked c2-chart-legend follows the chart’s mode.
Clicking a series in the plot always highlights it, whatever legend-action is: a line, a bar, a scatter point, a
pie slice, or the band of a stacked area under the pointer. Clicking it again clears the highlight. point-click fires first and is cancelable, so an application that
uses clicks for something else calls preventDefault() to keep the highlight as it is:
chart.addEventListener('point-click', (event) => {
event.preventDefault() // open a detail view instead of highlighting
openDetails(event.detail.series.field, event.detail.index)
})Realtime updates
The realtime path is shared and intentionally separate from presentation updates:
updateData(next)replaces a dataset immediately without waiting for Lit.appendPoint(x, values)writes into spare frame capacity and redraws once.- increment
revisionafter mutating an owned input buffer in place. dataSource.getWindow()pulls lazy ranges; optionalsubscribe()pushes points through the append path.max-pointsbounds a moving window.
uPlot redraws immediately; it does not invent values between samples. A slow annual feed therefore looks stepped.
The demo interpolates consecutive SEC observations with requestAnimationFrame() and calls updateData on each
frame. That smoothness comes from the feed policy, not a chart-engine limitation. For high-frequency telemetry,
append real samples directly and leave animation off so transitions never overlap.
const source = {
async getWindow({ xMin, xMax, maxPoints, signal }) {
return { data: await fetchWindow({ xMin, xMax, maxPoints, signal }) }
},
subscribe(push) {
return marketFeed.subscribe(({ time, revenue, income }) => push(time, [revenue, income]))
},
}
chart.dataSource = sourceEvents and interaction
Every chart reports chart-ready, chart-error, point-click, point-hover, tooltip-change,
legend-change, series-highlight and series-toggle. Cartesian charts also emit range-change after zooming or brushing.
Point and range events do not bubble, so attach listeners to the chart itself. Use the custom state
:state(ready) (c2-line-chart:state(ready), or chart.matches(':state(ready)')) as the stable test signal after
the first frame is drawn; :state(engine-uplot) and :state(engine-echarts) name the engine. The host itself never
gains an attribute, so server-rendered markup matches the hydrated chart. Earlier releases wrote data-chart-ready
and data-chart-engine attributes instead. A linked c2-chart-tooltip likewise shows itself through
:state(open) while a point is hovered rather than toggling hidden on its host; an authored hidden still hides it.
Hover a point, or click a series or a legend item below, to see the same semantic event model used by external presenters.
Responsive and lazy rendering
Canvas has no intrinsic size. The host gets width from layout and height from --c2-chart--height (320px by
default, 32px for a sparkline). A ResizeObserver keeps the engine canvas synchronized without resize loops.
lazy-render defers both engine import and first draw until the host enters the viewport.
Theming
All elements use the --c2-chart__* namespace. Set the palette, typography, axes, grid, crosshair, legend,
tooltip and state variables once on a container and every chart beneath it inherits them. Canvas marks cannot be
reached with ::part(), so the component resolves CSS variables with getComputedStyle and gives the result to
the engine. Theme changes redraw without replacing data.
States and slots
loading, error and empty data replace the plot with a consistent state layer. Each state has a named slot,
and empty-message changes the default copy. The shared actions slot places controls above the plot; legend
and tooltip slots replace their built-ins. Series definitions occupy the default slot.
Shared API
Use each component’s API tab for its additions: line curve/points, area fill, bar sizing, pie radii/labels, and sparkline type/tone. Cartesian charts also share axes, grid, cursor, zoom and y-domain controls through their uPlot base; pie instead adds ECharts renderer and polar-layout options.