Two charting libraries are available out of the box: Apache ECharts for high-level declarative charts, and D3 for fully custom visualisations. Both are loaded on demand — only when [data-viz] or [data-d3] elements appear in the post.
ECharts
Use data-viz="echarts" with a data-options JSON attribute to embed any ECharts chart. The full ECharts option schema is supported.
Bar chart
Line chart
How it works
The renderEChart adapter:
- Registers the
loomtheme (palette + text styles matching the site’s CSS variables) - Calls
echarts.init(el, 'loom') - Attaches a
ResizeObserverso charts resize with their container - Returns the chart instance for potential scrolly updates
The full ECharts options object is passed directly to chart.setOption(), so anything from the ECharts docs works without any wrapper configuration.
D3
Use data-d3="type" with a data-options JSON attribute containing a data array.
Built-in: bar chart
Built-in: line chart
Built-in: force graph
The force graph above shows the dependency structure of the theme’s visualization system.
Adding custom chart types
Register a new D3 chart type from a post HTML block or a separate JS file. The factory receives the container element, the data array, and the remaining options, and must return an object with an update(newData) method.
import { registerD3Chart } from '/assets/js/viz/d3.js';
registerD3Chart('scatter', (el, data, options) => {
const d3 = window.d3;
const { width, height } = el.getBoundingClientRect();
const svg = d3.select(el)
.append('svg')
.attr('width', width).attr('height', height);
// … draw circles …
return {
update(newData) {
// re-render with newData
}
};
});
Then use it in the post:
<div data-d3="scatter" style="height:320px"
data-options='{"data":[{"x":1,"y":2},{"x":3,"y":4}]}'></div>
The factory pattern means each chart type is entirely self-contained — it owns its own scales, axes, and update logic. The registry just dispatches to the right factory at render time, and stores the returned instance for scrolly updates.