Charts
Summary
Soma deliberately ships no chart engine. Core
provides the surroundings: .soma-chart, a sized box
that whatever you mount inside (svg, canvas, img) stretches to
fill, and .soma-legend, the series key with dots
colored per item via --soma-legend-color. The
engine (rendering, axes, tooltips) is the consumer's choice;
a planned @nware/soma-charts adapter will wrap one
as a separate optional package.
Sizing: the box has a fixed height (240px by default,
-small 160, -large 320) and a fluid
width that fills its grid column, so the aspect ratio follows
the layout. A raw svg with a viewBox should use
preserveAspectRatio="none" to stretch with the box;
a real engine should redraw on resize instead.
When to use
| Piece | Use it for |
|---|---|
.soma-chart (240px) | The standard dashboard chart panel inside a widget. |
-small / -large | Compact trend strips (160px) vs a featured full-width panel (320px). |
.soma-legend | The series key under a chart; add -vertical to stack it beside one. |
| Stat tile instead | A single number with a delta — no plot needed. |
Examples
HTML
The container is a plain div; a direct-child svg, canvas or img is absolutely positioned to fill it:
<div class="soma-chart">
<!-- your engine's svg/canvas, or an img — it fills the box -->
<svg viewBox="0 0 600 240" preserveAspectRatio="none" role="img" aria-label="Requests per hour">…</svg>
</div>
Sizes are one class on the container; the child markup never changes:
<div class="soma-chart soma-chart-small">…</div> <!-- 160px trend strip -->
<div class="soma-chart">…</div> <!-- 240px standard -->
<div class="soma-chart soma-chart-large">…</div> <!-- 320px featured -->
The legend is a plain <ul> after the
container; each item colors its dot via the
--soma-legend-color custom property (defaults to
primary when omitted):
<ul class="soma-legend">
<li class="soma-legend-item" style="--soma-legend-color: var(--soma-color-primary)">
<span class="soma-legend-dot"></span>Requests per hour</li>
<li class="soma-legend-item" style="--soma-legend-color: var(--soma-color-success)">
<span class="soma-legend-dot"></span>Cache hits</li>
</ul>
Add -vertical to stack the items, for a legend
beside the chart rather than under it:
<ul class="soma-legend soma-legend-vertical">…</ul>
Give a meaningful chart role="img" and an
aria-label (or your engine's accessible
equivalent) — the box itself is a plain div.
CSS classes
| Class | Effect |
|---|---|
.soma-chart | 240px-high box; a direct child svg/canvas/img is absolutely positioned to fill it. |
.soma-chart-small / .soma-chart-large | 160px / 320px heights. |
.soma-legend | Horizontal wrapping series key (a plain <ul>). |
.soma-legend-item | One series; set --soma-legend-color inline to color its dot. |
.soma-legend-dot | The 8px color swatch (defaults to primary). |
.soma-legend-vertical | Stacks the items, for a legend beside the chart. |
The no-engine boundary
The boundary is a locked decision (charts resolved at M4):
Soma core styles the surroundings (box, legend, the
stat tile above the plot) and never
the plot itself. Rendering, axes, scales, tooltips, series
toggling and resize redraws belong to an engine the app brings
(or, later, the optional @nware/soma-charts
adapter, tracked outside core). In practice: nothing in
dist/soma.js knows charts exist, so picking or
swapping an engine never means waiting on a Soma release.
What core does give an engine: a stable-size mount
node, and design tokens. Read series colors from the
--soma-* palette so plots follow the active theme;
the legend example above passes the same tokens to its dots.
Wiring an engine
Server-render the container with a lightweight placeholder svg (or a skeleton block) so the panel is the right size before any JS loads — then let the engine replace it. No layout shift, and a graceful floor if JS never arrives:
<div class="soma-chart" id="requests-chart">
<!-- placeholder: a flat sparkline of cached data-points, rendered server-side -->
<svg viewBox="0 0 600 240" preserveAspectRatio="none" role="img"
aria-label="Requests per hour (loading live chart)">
<polyline fill="none" stroke="#2b7ac4" stroke-width="2"
points="0,180 100,170 200,175 300,150 400,155 500,130 600,135" />
</svg>
</div>
<ul class="soma-legend" id="requests-legend"></ul>
Mount the engine into the box, redraw on size changes, and build the legend from the same series list that feeds the plot — one palette, two consumers:
const box = document.getElementById('requests-chart');
const series = [
{ name: 'Requests', color: 'var(--soma-color-primary)', data: requests },
{ name: 'Cache hits', color: 'var(--soma-color-success)', data: cacheHits },
];
box.replaceChildren(); // drop the placeholder svg
const chart = engine.mount(box, { series }); // engine draws its own svg/canvas
// The box is fluid-width: redraw when the grid column resizes.
new ResizeObserver(() => chart.resize()).observe(box);
// Legend from the same series array — colors can be token strings.
const legend = document.getElementById('requests-legend');
legend.replaceChildren(...series.map((s) => {
const li = document.createElement('li');
li.className = 'soma-legend-item';
li.style.setProperty('--soma-legend-color', s.color);
const dot = document.createElement('span');
dot.className = 'soma-legend-dot';
li.append(dot, s.name);
return li;
}));
A static image is also a valid "engine"; a server-rendered png fills the box the same way:
<div class="soma-chart soma-chart-small">
<img src="/reports/latency-7d.png" alt="p95 latency, last 7 days: stable around 180 ms" />
</div>
JavaScript
None in Soma. Containers and legends are CSS-only, and everything dynamic on this page is engine or app code. The snippets above are the wiring pattern, not a Soma API.