autumn-plugin-d3
D3.js charts for Autumn
apps (autumn-web 0.10). Write charts in Rust. Get SVG charts that work
with htmx, with the default CSP, and without JavaScript.
- No npm, no bundler, no inline script. The crate serves D3 7.9.0 with SRI.
- Typed builder: bar, line, area, scatter, pie/donut, custom kinds.
- Data table in every chart: it shows without JavaScript and stays for screen readers.
- Tooltips by pointer and keyboard. Legend. Light and dark palette.
- Live data: same-origin JSON with optional refresh.
Quickstart
[]
= "0.3"
use ;
use *;
async
async
Chart kinds
| Builder | Data | Kind options |
|---|---|---|
Chart::bar(data) |
(label, value) or Datum |
horizontal() |
Chart::pie(data) |
(label, value) or Datum |
donut(inner) |
Chart::line(series) |
Series of (x, y) |
curve, dots, x_scale, y_scale, x_format |
Chart::area(series) |
Series of (x, y) |
curve, dots, x_scale, y_scale, x_format |
Chart::scatter(series) |
Series of (x, y) |
radius, x_scale, y_scale, x_format |
Chart::custom(name)? |
json(&value)? |
curve, dots, x_scale, y_scale, x_format |
Kind options compile only on their kinds. For example, Chart::bar(..).donut(0.5)
does not compile. y_min and y_max apply to all kinds except pie.
legend applies to line, area, scatter, and pie.
A non-finite value (NaN, ±∞) is a missing value. Bars skip it. Lines
show a gap. With Scale::Time, x is Unix time in milliseconds. Repeated
labels or series names draw as separate marks.
Common options
| Method | Attribute | Effect |
|---|---|---|
label(text) |
data-d3-label |
Accessible name. Also the table caption. |
caption(text) |
— | Visible <figcaption>. Also the name when there is no label. |
x_label(text), y_label(text) |
data-d3-x-label, data-d3-y-label |
Axis titles and table headers. |
format(Format) |
data-d3-format |
Value format (ticks, tooltips). |
colors([Color]) |
data-d3-colors |
Series colors, in order. |
aspect(ratio) |
data-d3-aspect |
Plot width / height. Default 16 / 9. |
duration(Duration) |
data-d3-duration |
Transition time. Default 400 ms. |
animate_reduced_motion() |
data-d3-reduced="animate" |
Animate also for reduced-motion users. |
legend(bool) |
data-d3-legend |
Default: on for two or more series or slices. |
src(url) / Chart::<K>::from_src(url) |
data-d3-src |
Load JSON from a same-origin URL. |
refresh(Duration) |
data-d3-refresh |
Load src again at this interval. |
table(false) |
— | Leave out the data table. |
id, class |
— | Element id and extra classes. |
Format maps to d3-format: integer(), decimal(n), percent(n),
si(n), currency(n), or d3("spec"). The runtime ignores a bad
specifier or a width above 64.
Hand-written markup
The builder only writes attributes. This markup works too:
An empty div.d3-table is optional. The runtime fills it with the data
table, and updates it when the data changes.
Data shapes: [{"label": "a", "value": 1}] or [["a", 1]] for bar and
pie; [{"name": "s", "points": [[x, y]]}] for line, area, and scatter. An
x value can also be a numeric string or an ISO 8601 date string (no zone
means UTC). null is a missing value.
htmx and live data
init.js starts charts after htmx swaps and frees charts that leave the
page. A change to data-d3-data animates the chart to the new data. A
change to another data-d3-* attribute rebuilds the chart.
// The handler returns Vec<Datum> or Vec<Series> as JSON.
async
// The chart loads it, then loads it again every 5 s.
Custom kinds and JavaScript
Load your script after d3_script(), with defer. Do not inline it.
script src= defer
// ...
// static/js/charts.js
;
The runtime clears the SVG before each draw of a custom kind. Use the
style() method for styles (CSSOM). Do not set style attributes. Data
can come from untrusted markup: write text with .text(), never .html().
| API | Use |
|---|---|
d3:ready event |
First draw. event.detail is the handle. |
d3:render event |
Each draw. |
d3:error event |
event.detail.error. The table stays. |
el.autumnD3 |
Handle: el, kind, d3, svg, data, options, duration, destroyed, update(data), render(), destroy(). |
AutumnD3 |
register(kind, draw), scan(root), get(el), version. |
data-d3-state |
loading, pending (custom kind not registered), ready, error. |
Theme
Set these custom properties on the chart element, for example
.sales { --d3-color-1: #0a7; }. The defaults use :where(), so your
rules win.
| Property | Default |
|---|---|
--d3-color-1 … --d3-color-8 |
Validated palette (light and dark steps). |
--d3-surface |
Canvas (#1a1a19 with data-theme="dark"). Ring and tooltip background. |
--d3-muted, --d3-grid |
Mixes of currentColor. |
--d3-bar-max |
24 (largest bar thickness in px). |
--d3-area-opacity |
0.1. |
Add class d3-show-table to keep the data table visible.
Accessibility
- Each chart has a data table. It shows without JavaScript and on error. When the chart is ready, it stays for screen readers only.
- The SVG has
role="img"and a name. When it has values, it is a tab stop: arrow keys, Home, and End move a tooltip, and a live region reads the value. Escape closes the tooltip. - A legend shows for two or more series. The palette has 8 colors.
- Reduced motion turns transitions off. Forced colors keep series colors.
Gotchas
- CSP. Do not inline
<script>or<style>. Put page scripts instatic/and load them withdeferafterd3_script(). - One D3. Use
window.d3. Do not load a second copy. - htmx in nonce mode. htmx adds an inline
<style>. Set<meta name="htmx-config" content='{"includeIndicatorStyles":false}'>. - Big data. Large inline data makes large HTML. Use
src. - User content. Remove
data-d3*attributes andid="AutumnD3"from user HTML. Else user markup can start charts and load same-origin URLs. - Refresh. After a failure, the wait doubles (up to 5 min). While the tab is hidden, refresh stops.
Demo
# then open http://127.0.0.1:3000
Development
See CLAUDE.md for commands, and docs/plan.md and docs/adr/ for the design.
License
Apache-2.0 for the plugin. D3 is ISC (assets/D3-LICENSE).