Chart
Design examples
Section titled “Design examples”A line and its summary share a quiet frame; values are fictional.
<section class="specimen">
<ds-row class="line" ><ds-text literal strong>Daily visitors</ds-text></ds-row><ds-chart kind="line" rows="8" aria-label="Daily visitors, from 32 to 84" data-showcase-props='{"data":[{"time":0,"value":32},{"time":86400,"value":48},{"time":172800,"value":43},{"time":259200,"value":67},{"time":345600,"value":84},{"time":432000,"value":76},{"time":518400,"value":52}]}'></ds-chart><ds-row class="line" ><ds-text literal tone="muted" size="small">Busiest day: Friday, 84 visitors</ds-text></ds-row>
</section>
<style>
.specimen { width: 100%; min-width: 0; --cell: calc(20px * var(--sf, 1)); }
.specimen .line { width: 100%; }
.specimen .grow { flex: 1; min-width: 0; }
.specimen .muted { color: var(--ds-text-color-muted); }
.specimen .mono { --ds-typeface-regular: var(--ds-typeface-mono); font-variant-numeric: tabular-nums; }
.specimen .value { width: calc(4 * var(--cell)); text-align: right; }
.specimen .mark { display: inline-flex; align-items: flex-end; width: var(--cell); height: var(--ds-control-height); flex: 0 0 var(--cell); }
.specimen .columns { display: flex; flex-wrap: wrap; gap: var(--cell); }
.specimen .columns > * { min-width: 0; }
.specimen fieldset { border: 0; padding: 0; margin: 0; }
.specimen legend { padding: 0; }
.specimen label { display: flex; align-items: flex-end; gap: calc(var(--cell) / 2); min-height: var(--cell); }
.specimen ds-panel { height: calc(10 * var(--cell)); }
</style> (function mountShowcase(root) {
root.querySelectorAll("[data-showcase-props]").forEach((element) => {
const props = JSON.parse(element.dataset.showcaseProps);
Object.assign(element, props);
if (element.localName === "ds-sidebar") {
let items = props.items;
const render = () => {
let collapsedDepth;
const visible = items.filter((item) => {
const depth = item.depth ?? 0;
if (collapsedDepth !== void 0 && depth > collapsedDepth) return false;
collapsedDepth = item.expanded === false ? depth : void 0;
return true;
});
Object.assign(element, { items: visible });
};
element.addEventListener("sidebar-activate", (event) => {
const id = event.detail.id;
items = items.map((item) => ({ ...item, current: item.id === id }));
render();
});
element.addEventListener("sidebar-toggle", (event) => {
const { id, expanded } = event.detail;
items = items.map((item) => item.id === id ? { ...item, expanded } : item);
render();
});
}
});
root.querySelectorAll("ds-token-field").forEach((field) => {
field.addEventListener("ds-token-commit", (event) => {
const { text } = event.detail;
const input = field.querySelector("input");
if (!input || !text.trim()) return;
const token = document.createElement("ds-token");
token.setAttribute("value", text.trim());
token.setAttribute("label", text.trim());
token.setAttribute("remove-label", `Remove ${text.trim()}`);
field.insertBefore(token, input);
input.value = "";
input.dispatchEvent(new Event("input", { bubbles: true }));
});
field.addEventListener("ds-token-remove", (event) => {
const token = event.target;
if (token instanceof Element && token.localName === "ds-token") token.remove();
});
});
window.addEventListener("message", (event) => {
if (event.origin !== location.origin || event.source !== parent || event.data?.type !== "ds1-example-theme") return;
requestAnimationFrame(() => root.querySelectorAll("ds-chart, ds-spark, ds-bars").forEach((chart) => {
chart.refreshTheme?.();
}));
});
})(document); Short bars compare quantities, with labels on the same grid.
<section class="specimen">
<ds-bars rows="6" aria-label="Paper 48, cloth 26, board 34" data-showcase-props='{"items":[{"label":"Paper","value":48},{"label":"Cloth","value":26},{"label":"Board","value":34}]}'></ds-bars>
</section>
<style>
.specimen { width: 100%; min-width: 0; --cell: calc(20px * var(--sf, 1)); }
.specimen .line { width: 100%; }
.specimen .grow { flex: 1; min-width: 0; }
.specimen .muted { color: var(--ds-text-color-muted); }
.specimen .mono { --ds-typeface-regular: var(--ds-typeface-mono); font-variant-numeric: tabular-nums; }
.specimen .value { width: calc(4 * var(--cell)); text-align: right; }
.specimen .mark { display: inline-flex; align-items: flex-end; width: var(--cell); height: var(--ds-control-height); flex: 0 0 var(--cell); }
.specimen .columns { display: flex; flex-wrap: wrap; gap: var(--cell); }
.specimen .columns > * { min-width: 0; }
.specimen fieldset { border: 0; padding: 0; margin: 0; }
.specimen legend { padding: 0; }
.specimen label { display: flex; align-items: flex-end; gap: calc(var(--cell) / 2); min-height: var(--cell); }
.specimen ds-panel { height: calc(10 * var(--cell)); }
</style> (function mountShowcase(root) {
root.querySelectorAll("[data-showcase-props]").forEach((element) => {
const props = JSON.parse(element.dataset.showcaseProps);
Object.assign(element, props);
if (element.localName === "ds-sidebar") {
let items = props.items;
const render = () => {
let collapsedDepth;
const visible = items.filter((item) => {
const depth = item.depth ?? 0;
if (collapsedDepth !== void 0 && depth > collapsedDepth) return false;
collapsedDepth = item.expanded === false ? depth : void 0;
return true;
});
Object.assign(element, { items: visible });
};
element.addEventListener("sidebar-activate", (event) => {
const id = event.detail.id;
items = items.map((item) => ({ ...item, current: item.id === id }));
render();
});
element.addEventListener("sidebar-toggle", (event) => {
const { id, expanded } = event.detail;
items = items.map((item) => item.id === id ? { ...item, expanded } : item);
render();
});
}
});
root.querySelectorAll("ds-token-field").forEach((field) => {
field.addEventListener("ds-token-commit", (event) => {
const { text } = event.detail;
const input = field.querySelector("input");
if (!input || !text.trim()) return;
const token = document.createElement("ds-token");
token.setAttribute("value", text.trim());
token.setAttribute("label", text.trim());
token.setAttribute("remove-label", `Remove ${text.trim()}`);
field.insertBefore(token, input);
input.value = "";
input.dispatchEvent(new Event("input", { bubbles: true }));
});
field.addEventListener("ds-token-remove", (event) => {
const token = event.target;
if (token instanceof Element && token.localName === "ds-token") token.remove();
});
});
window.addEventListener("message", (event) => {
if (event.origin !== location.origin || event.source !== parent || event.data?.type !== "ds1-example-theme") return;
requestAnimationFrame(() => root.querySelectorAll("ds-chart, ds-spark, ds-bars").forEach((chart) => {
chart.refreshTheme?.();
}));
});
})(document); Try it
Section titled “Try it”One stage on the grid shows each chart type in turn: a ds-cycle steps through line, area, baseline, sparkline, bars, ratio and range by itself, and a click on it chooses one. Every chart sits on the 20px squares of ds-grid, with its own ruling off (grid="none"). Switch the preview theme, inspect a series with the pointer, or copy the HTML and JavaScript. The same example is available in the component catalogue and library preview. Canvas inspection is pointer-based; the sample values and labels provide a text alternative.
The cycle moves through the seven chart types by itself; click it to choose one. Sample values only; move over a series to inspect a point. No live data is loaded.
<style>
.chart-stage { --ds-grid-color: color-mix(in srgb, var(--text-color) 12%, transparent); position: relative; isolation: isolate; box-sizing: border-box; width: 100%; width: round(down, 100%, 20px); padding: 20px; box-shadow: inset -1px -1px 0 var(--ds-grid-color); }
.chart-stage > :not(ds-grid) { position: relative; }
.chart-head { display: flex; justify-content: space-between; align-items: center; height: 20px; }
.chart-views { height: 160px; margin-block: 20px; }
.chart-views > [hidden] { display: none; }
.chart-views > :not([hidden]) { animation: chart-in 240ms cubic-bezier(.2, 0, 0, 1); }
.chart-views .compact { width: min(100%, 240px); margin-top: 60px; }
.chart-stage output { display: block; height: 20px; font-size: 10px; color: var(--text-color); }
@keyframes chart-in { from { opacity: 0; transform: translateY(4px); } }
@media (prefers-reduced-motion: reduce) { .chart-views > :not([hidden]) { animation: none; } }
</style>
<section class="chart-stage" data-chart-stage aria-label="Chart types">
<ds-grid contained></ds-grid>
<div class="chart-head">
<ds-cycle values='["Line","Area","Baseline","Sparkline","Bars","Ratio","Range"]'></ds-cycle>
<span data-chart-count aria-hidden="true">1 of 7</span>
</div>
<div class="chart-views">
<div data-chart-view="Line"><ds-chart kind="line" rows="8" grid="none" aria-label="Line: sample values rising from 12 to 27"></ds-chart></div>
<div data-chart-view="Area" hidden><ds-chart kind="area" rows="8" grid="none" aria-label="Area: sample values rising from 12 to 27"></ds-chart></div>
<div data-chart-view="Baseline" hidden><ds-chart kind="baseline" rows="8" grid="none" aria-label="Baseline: sample values against a reference of 18"></ds-chart></div>
<div data-chart-view="Sparkline" hidden><ds-spark class="compact" cols="12" aria-label="Sparkline: a trend from 12 to 27"></ds-spark></div>
<div data-chart-view="Bars" hidden><ds-bars rows="6" span="2" aria-label="Bars: A 12 against 16 expected, B 20, C 15, D 18, E 24"></ds-bars></div>
<div data-chart-view="Ratio" hidden><ds-ratio class="compact" aria-label="Ratio: 65 percent to 35 percent"></ds-ratio></div>
<div data-chart-view="Range" hidden><ds-range class="compact" aria-label="Range: 20 to 80 out of 100, midpoint 50"></ds-range></div>
</div>
<output data-chart-status aria-live="polite"></output>
</section> (function mountChartExample(root) {
const values = [12, 15, 14, 18, 16, 21, 19, 17, 22, 20, 24, 23, 26, 24, 28, 27];
const data = values.map((value, index) => ({ time: index * 60, value }));
const status = root.querySelector("[data-chart-status]");
const say = (text) => {
if (status) status.textContent = text;
};
root.querySelectorAll("ds-chart").forEach((chart) => {
Object.assign(chart, { data, base: 18, markers: [{ time: 300, text: "A" }], format: (value) => String(Math.round(value)) });
chart.addEventListener("scrub", (event) => {
const point = event.detail;
say(point ? `Value ${point.value} at ${point.time} seconds` : chart.getAttribute("aria-label") ?? "");
});
});
const spark = root.querySelector("ds-spark");
if (spark) Object.assign(spark, { values });
const bars = root.querySelector("ds-bars");
if (bars) Object.assign(bars, { items: [{ label: "A", value: 12, compare: 16 }, { label: "B", value: 20, tone: "up" }, { label: "C", value: 15, tone: "down" }, { label: "D", value: 18 }, { label: "E", value: 24, tone: "up" }] });
const ratio = root.querySelector("ds-ratio");
if (ratio) Object.assign(ratio, { value: 0.65 });
const range = root.querySelector("ds-range");
if (range) Object.assign(range, { min: 0, max: 100, from: 20, to: 80, marks: [{ value: 50, kind: "dot" }] });
const stage = root.querySelector("[data-chart-stage]");
const cycle = root.querySelector("ds-cycle");
const count = root.querySelector("[data-chart-count]");
if (!stage || !cycle) return;
const views = [...stage.querySelectorAll("[data-chart-view]")];
const show = (name) => {
views.forEach((view, index) => {
const current = view.dataset.chartView === name;
view.hidden = !current;
if (!current) return;
if (count) count.textContent = `${index + 1} of ${views.length}`;
const chart = view.querySelector("ds-chart");
if (chart) requestAnimationFrame(() => {
chart.drawKey = String(Date.now());
});
say(view.firstElementChild?.getAttribute("aria-label") ?? "");
});
};
let taken = false;
let paused = false;
cycle.addEventListener("change", (event) => {
taken = true;
show(event.detail.value);
});
stage.addEventListener("pointerenter", () => {
paused = true;
});
stage.addEventListener("pointerleave", () => {
paused = stage.contains(document.activeElement);
});
stage.addEventListener("focusin", () => {
paused = true;
});
stage.addEventListener("focusout", () => {
paused = stage.matches(":hover");
});
show(cycle.value || cycle.values[0]);
if (matchMedia("(prefers-reduced-motion: reduce)").matches) return;
const timer = setInterval(() => {
if (!stage.isConnected) return clearInterval(timer);
if (taken || paused) return;
const next = cycle.values[(cycle.values.indexOf(cycle.value || cycle.values[0]) + 1) % cycle.values.length];
cycle.value = next;
show(next);
}, 3e3);
})(document); Charts require JavaScript. The HTML downloads load the same component runtime. Set each chart’s unit when its surface scale changes; widths respond to their container. Theme changes redraw through refreshTheme().
Charts belong to 2-core. They own their drawing and interaction; a chart composed with headings, summaries and range controls belongs to 3-unit.
Import DS1/2-core/ds-chart to register ds-chart, ds-spark, ds-bars, ds-ratio and ds-range. The same entry exports GridChart, createChart, drawSparkline and the data and option types. With Sync 1, select "components": ["chart"] in the components export.
import './design/2-core/ds-chart';import type { GridLineChart } from './design/2-core/ds-chart';
const chart = document.querySelector('ds-chart') as GridLineChart;chart.data = [{ time: 0, value: 12 }, { time: 60, value: 18 }];chart.kind = 'area'; // line, area or baselinechart.unit = 20; // CSS pixels per square; pass 20 × the surface scalechart.rows = 8;chart.grid = 'rows'; // full, rows or nonechart.addEventListener('scrub', event => { // CustomEvent.detail is the nearest DataPoint, or null.});Series charts
Section titled “Series charts”ds-chart accepts data, kind, base (the baseline value), color (a CSS colour or custom-property name), rows, markers, timeRange, extremes, labels, live, format and drawKey. Data and markers are JavaScript properties. Each marker has a Unix-seconds time and text. Assign a new array after editing data. Changing drawKey replays the draw-in; reduced motion skips it. The scrub event bubbles and is composed across shadow roots. Mouse hover or pointer dragging moves the crosshair.
For multiple series, use createChart(canvas, options), addSeries, setData, update, setMarkers, applyOptions, setTimeRange, resize and subscribeCrosshairMove. Unsubscribe listeners and call destroy when removing a chart. timeRange sets the horizontal viewport; vertical scale currently includes all supplied data. The components resize automatically and can be removed and reinserted.
Compact charts
Section titled “Compact charts”ds-spark:values, optionalbase, andcols(default four). One square high.ds-bars:itemswithlabel,value, optionalcompare,tone(up,down,ink) andcaption;rowsandspanin squares. Intended for nonnegative values.ds-ratio:valuebetween zero and one; out-of-range values clamp.ds-range:min,max,from,toandmarks(value, optionallabel,kindofdotortick). This is a display, not a range input.
One value against its range on one line, spent of a budget or saved of a goal, is a Meter: a bar drawn as the ratio is, or the range in squares, with an over state and role="meter". A row of such bars, one per category in a table, is a meter in each row rather than a chart.
Every component has independent unit and grid settings. There are no global setters. Set unit when the containing surface scale changes. Call refreshTheme() after changing application theme classes or tokens; system colour-scheme changes redraw automatically.
Appearance and accessible content
Section titled “Appearance and accessible content”Set scoped tokens on the chart host: --ds-chart-ink, --ds-chart-muted, --ds-chart-faint, --ds-chart-up, --ds-chart-down, --ds-chart-surface, --ds-chart-grid-minor, --ds-chart-grid-major, --ds-chart-live, --ds-chart-line, --ds-chart-fill, --ds-chart-font and --ds-chart-small. Defaults use the design palette. Fonts and client branding remain configurable. Geometry and internal styles live in shadow DOM; products own placement and outer margins.
Supply an accessible label and a textual summary or data table beside a canvas chart. The canvas crosshair is a pointer aid, not a keyboard-accessible data browser. These components do not own data fetching, currency, dates, account state or range controls.