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

PieceUse it for
.soma-chart (240px)The standard dashboard chart panel inside a widget.
-small / -largeCompact trend strips (160px) vs a featured full-width panel (320px).
.soma-legendThe series key under a chart; add -vertical to stack it beside one.
Stat tile insteadA single number with a delta — no plot needed.

Examples

Container + legend

The standard dashboard panel:

  • Requests per hour

The legend is a plain <ul> under the box, muted and wrapping.

Without a legend

A single obvious series needs no key:

When the widget title already names the one series, skip the legend; the accessible name lives on the svg (role="img" + aria-label).

Multi-series, vertical legend

soma-legend-vertical stacks the items:

  • Requests
  • Cache hits

Legend item colors

One --soma-legend-color per item:

  • Primary
  • Success
  • Warning
  • Danger
  • Default (primary)

The dot takes whatever color the item passes — usually your engine's series palette; the custom property defaults to primary when omitted. There are no built-in on/off states: dimming a toggled-off series is app logic.

Small

soma-chart-small · 160px:

Compact trend strip.

Default

soma-chart · 240px:

The standard panel height.

Large

soma-chart-large · 320px:

All three boxes hold the same svg: height is fixed by the class, width follows the column, and preserveAspectRatio="none" lets the drawing stretch with the box.

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

ClassEffect
.soma-chart240px-high box; a direct child svg/canvas/img is absolutely positioned to fill it.
.soma-chart-small / .soma-chart-large160px / 320px heights.
.soma-legendHorizontal wrapping series key (a plain <ul>).
.soma-legend-itemOne series; set --soma-legend-color inline to color its dot.
.soma-legend-dotThe 8px color swatch (defaults to primary).
.soma-legend-verticalStacks 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.