ECharts

Apache ECharts driven by server-sent options, in the page's theme and language.

zweiundeins <sb-echarts> chart charts echarts graph data visualization Since 2026-09-23 306.2 kB0Open in playground Edit on GitHub

Playground

Draws an Apache ECharts chart from an option your server sends as JSON. The element does the part every page would otherwise write again: it colours the chart from the page's --sb-* tokens and follows theme changes, writes numbers, months and weekdays in the reader's language, keeps a wrapping legend clear of the axes, and animates each new option into place. ECharts itself is vendored with the component and loaded only when a chart comes near the screen.

Size it with CSS (block-size, height); it defaults to 18rem.

Examples

From the server

Send any ECharts option. A string that is exactly var(--token) becomes that colour, so the server can colour a series with the page's own palette.

<sb-echarts option='{
  "tooltip": {"trigger": "axis"},
  "legend": {"data": ["Launches", "Landings"], "top": 0},
  "xAxis": {"type": "category", "data": ["2022", "2023", "2024", "2025", "2026"]},
  "yAxis": {"type": "value"},
  "series": [
    {"name": "Launches", "type": "bar", "data": [61, 96, 134, 165, 190]},
    {"name": "Landings", "type": "line", "smooth": true, "data": [55, 90, 127, 159, 186], "itemStyle": {"color": "var(--sb-ok)"}}
  ]
}'></sb-echarts>

Live

When the option attribute changes (a server morph, or a signal as here), the chart moves to the new state instead of redrawing.

Refuel
<div data-signals="{_fuel: [82, 64, 91, 48, 73]}">
  <sb-echarts style="block-size: 14rem" data-preserve-attr="option"
    data-attr:option="JSON.stringify({xAxis: {type: 'category', data: ['Tank A', 'Tank B', 'Tank C', 'Tank D', 'Tank E']}, yAxis: {type: 'value', max: 100, name: '%'}, series: [{type: 'bar', data: $_fuel}]})"></sb-echarts>
  <sb-button data-on:click="$_fuel = $_fuel.map(() => Math.round(20 + Math.random() * 80))">Refuel</sb-button>
</div>

Beyond axes

Pies, gauges and the like get no phantom grid.

<sb-echarts style="block-size: 16rem" option='{
  "tooltip": {"trigger": "item"},
  "series": [{"type": "pie", "radius": ["45%", "70%"], "data": [
    {"name": "Hydrogen", "value": 74}, {"name": "Helium", "value": 24}, {"name": "Everything else", "value": 2}
  ]}]
}'></sb-echarts>

A fallback table

Put the same data inside the element. It shows until the chart has drawn (and without JavaScript), and afterwards it stays for screen readers, who get the numbers rather than a picture of them.

Signals received per quarter
Q1Q2Q3Q4
12191523
<sb-echarts style="block-size: 12rem" option='{"xAxis":{"type":"category","data":["Q1","Q2","Q3","Q4"]},"yAxis":{"type":"value"},"series":[{"type":"line","data":[12,19,15,23]}]}'>
  <table>
    <caption>Signals received per quarter</caption>
    <tr><th>Q1</th><th>Q2</th><th>Q3</th><th>Q4</th></tr>
    <tr><td>12</td><td>19</td><td>15</td><td>23</td></tr>
  </table>
</sb-echarts>

Clicks

A click on a data item fires sb-chart-click.

<div data-signals="{_picked: ''}">
  <sb-echarts style="block-size: 12rem" data-on:sb-chart-click="$_picked = evt.detail.name + ': ' + evt.detail.value"
    option='{"xAxis":{"type":"category","data":["Io","Europa","Ganymede","Callisto"]},"yAxis":{"type":"value","name":"km"},"series":[{"type":"bar","data":[3643,3122,5268,4821]}]}'></sb-echarts>
  <p data-text="$_picked || 'Click a bar.'"></p>
</div>

Charts whose shape is code

A custom series draws through a renderItem function, which no server can send as JSON. Define the chart once in the page's JavaScript and have the server name it:

import { defineChartKind } from '/c/echarts/echarts.js'

defineChartKind('timeline', (option, { echarts, color, css, formatNumber, lang, width, height }) => ({
  xAxis: { type: 'time' },
  yAxis: { type: 'category', data: option.rows },
  series: [{ type: 'custom', data: option.bars, renderItem: (params, api) => { /* … */ } }],
}))
<sb-echarts option='{"kind": "timeline", "rows": ["Apollo 11"], "bars": [[0, -14182940000, -13402140000]]}'></sb-echarts>

The builder gets the ECharts module (for echarts.graphic and friends), color() to resolve a token or CSS colour, css() to read a token such as a font, the element's number format, language and size. What it returns is themed like any other option.

Numbers

Value axes and axis tooltips format numbers with Intl.NumberFormat in the element's language, unless the option sets a formatter of its own. If your server formats numbers differently (another separator, say), set the same rule for every chart on the page so the two never disagree:

import { setNumberFormat } from '/c/echarts/echarts.js'
setNumberFormat((n, lang) => myFormat(n, lang))

Theming

Colours come from the --sb-* tokens: text, borders and the tooltip surface from the semantic ones, series from --sb-chart-1 to --sb-chart-8 (falling back to brand, accent, ok, warn, danger, info…). Set those on the element or an ancestor to give charts their own palette. A pick on sb-theme-switch, or the system flipping under "auto", redraws the chart in the new colours without starting it again.

Accessibility

With a fallback table in the element, screen readers get the table and the drawing is hidden from them. Without one, ECharts' own description is switched on, which names the chart type and series. With prefers-reduced-motion: reduce nothing animates.

Licence

ECharts is © The Apache Software Foundation, under the Apache License 2.0; it is vendored unmodified from the echarts@6.1.0 npm release (vendor.json), with its licence and notice in vendor/.

Installation

Add Datastar with Rocket and the Starbase autoloader once per page, then use the tag. The autoloader imports each component the first time its tag appears, including tags added later by a Datastar morph.

<!-- Once per page: Datastar with Rocket, and the Starbase autoloader.
     It loads every <sb-…> component the first time its tag appears. -->
<script type="importmap">
  { "imports": { "datastar": "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/autoloader.js"></script>
<!-- Optional, no flash of undefined elements: class="sb-cloak" on <html>, and -->
<style>.sb-cloak :not(:defined) { visibility: hidden }</style>

<sb-echarts style="block-size: 9rem" option='{"grid":{"top":8,"bottom":20,"left":30,"right":8},"xAxis":{"type":"category","data":["Mon","Tue","Wed","Thu","Fri","Sat","Sun"]},"yAxis":{"type":"value"},"series":[{"type":"bar","data":[120,200,150,80,70,110,130]}]}'></sb-echarts>

<!-- In production, pin today's catalog instead of the latest: the browser then
     refuses any file that changed. Add "integrity" to the import map above: the
     hashes from https://starbase.zweiundeins.gmbh/c/@722e189276ac/importmap.json and Datastar's, below. -->
<!--
<script type="importmap">
  { "imports": { "datastar": "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js" },
    "integrity": { "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js": "sha384-vUxZojLrF1Ar3de5h7VINqhJXBgjyZS4U49pHvGa87kum6j5Xn6JRziuARNw5ELG", "…": "…from importmap.json" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/@722e189276ac/autoloader.js" integrity="sha384-scFa7/4jztDOx1YZT9/1g1ULYm9LsDlQn/8hMTyyD4IkPnZV2pfhHytoNTWTJG6b"></script>
-->

<!-- Or load just this component, pinned to this version. The minified module
     is what the autoloader uses; the readable source is the same URL without .min. -->
<!-- <script type="module" src="https://starbase.zweiundeins.gmbh/c/echarts@c7ec5c49ba49/echarts.min.js" integrity="sha384-+0PdlcKv3ElWagbArdXBGRWw6wsQZAGXXfPU5iCn4dwPXkM2kL1XyB8zpndjsOrA"></script> -->

Size

Each file compressed on its own, the way it is served (gzip -9, brotli -11). The autoloader loads the minified files (esbuild), so that last column is what a page downloads; the readable source is always there too. Datastar and Rocket are shared by every component and not counted.

FileOriginalgzipbrotliminified
echarts.js 20.3 kB8.0 kB6.9 kB4.3 kB
vendor/echarts.esm.min.js 1121.7 kB367.3 kB298.8 kB298.8 kB
<sb-theme-switch> renders it11.5 kB4.5 kB3.8 kB3.1 kB
Total1153.5 kB379.8 kB309.5 kB306.2 kB

API reference

Props

AttributeTypeDefaultDescription
optioncustomThe ECharts option, as JSON. Strings that are exactly var(--token) become that colour. { "kind": "name", ... } is expanded by a builder the page defined with defineChartKind().
renderer"canvas" | "svg""canvas"Draw on a canvas, or as SVG (sharper when printed, slower with many points).
langstring""Locale for numbers, months and weekdays (default: the page's lang, then the browser's).

Slots

NameDescription
A fallback for readers without the chart: shown until ECharts has drawn, kept for screen readers after that. A table of the same data is ideal.

Events

NameDescription
sb-chart-clickA click on a data item. detail: { seriesName, seriesIndex, name, value, dataIndex }.