enarratio

中文文档
0.7.2发布于 2026/11/03MITTypeScriptES Module零依赖构建来源已验证

Accessible, server-rendered charts and scientific figures for the web

原始使用文档
文档目录10 节
  1. Statement of need
  2. Install
  3. Five-minute start
  4. Charts
  5. Themes
  6. Your own tokens in a theme
  7. Accessibility
  8. Development
  9. Citing
  10. License

Enarratio

Accessible, server-rendered charts and scientific figures for the web.

Status: in active development. Enarratio is an alpha. The API may change before 1.0, and every change is recorded in the changelog.

Enarratio draws charts on the server, into the HTML. A chart is complete before any JavaScript runs: readers see it at once, screen readers get a written description and an equivalent data table, search engines and language models read the same numbers people do, and the page downloads no charting library. An optional enhancement layer adds hover details, keyboard navigation, click-to-filter events and range brushing on top.

It covers the charts a website needs (bars, lines, areas, scatter plots, heatmaps, networks, with forecast bands and time axes), the small primitives a dashboard needs (sparklines, progress rings, uptime and heat strips, event timelines) and the figures a laboratory needs, starting with antibody titer plots and genome tracks. Charts are built on Observable Plot and d3-force (optional peer dependencies, installed only for the charts that use them), and return plain HTML strings, so Enarratio works with any server framework. It is tested on Node.js 24 and 26 and runs on Cloudflare Workers (the gallery's first site renders charts in a Worker); Deno and Bun should work, since it uses only standard modules, but are not yet tested.

Gallery: every chart, in two themes, light and dark: enarratio.dustinedwards.info (built by npm run gallery).

Astro and EmDash: enarratio-astro draws Enarratio charts in Astro pages and Markdown, and enarratio-emdash adds them to the EmDash editor as a Chart block. They are separate packages, in DrDustinEdwards/astro; see enarratio.dustinedwards.info/astro.

Statement of need

Most web charting libraries draw in the browser. A chart drawn in the browser is absent until its script downloads and runs, is invisible to anything that reads HTML without running scripts, and is usually opaque to screen readers. Server-side alternatives exist for static images, but an image cannot follow the site's dark mode, cannot be read by assistive technology beyond its alt text, and cannot be made interactive.

Scientific figures on the web have the same problem, and a second one: the figures researchers publish (titer plots, genome maps, dose-response curves, phylogenies, surveillance maps) are not in general-purpose charting libraries, so each lab site redraws them by hand, inconsistently, and usually without text alternatives.

Enarratio is for developers of research, teaching and small-organization websites who want charts that:

  • render on the server as inline SVG inside semantic HTML, with no client code required;
  • are accessible by construction: a written description (generated from the data when none is given), an equivalent data table, keyboard access and screen reader announcements for every interaction, and no reliance on color alone;
  • follow a site's theme in light and dark mode from one render, with palettes checked for contrast and for color vision deficiency;
  • include scientific chart types designed alongside the general ones, sharing their theming, accessibility and interaction.

Install

npm install enarratio

That is the core: the theme, the stylesheet, the color checks, and the small primitives (sparkline, progress ring, uptime strip, and the rest), with no runtime dependencies. The charts are separate entry points, each with the packages it needs as optional peer dependencies, so a site installs only what it draws:

ImportHoldsAlso install
enarratioCore, primitives and tableChartNothing
enarratio/plotlineChart, areaChart, barChart, scatterPlot, heatmap and the charts below@observablehq/plot linkedom
enarratio/sciencegenomeTrack, titerPlot, doseResponse, survivalPlot, forestPlot, phylogeneticTree, networkChart, and their statistics@observablehq/plot d3-force linkedom
enarratio/mapdotMap and choroplethMap, drawn from your own GeoJSONNothing
enarratio/litebarChart, lineChart, areaChart without Observable Plot, for the browser; also enarratio/lite/bar, /line, /areaNothing
enarratio/enhanceBrowser interactionNothing
enarratio/redrawRedraws a server's chart to its frame's width; enarratio/redraw/narrow holds narrowChart, the swap to a server's narrow drawing aloneNothing
enarratio/themesdefaultThemeNothing
enarratio/base.cssThe theme-free stylesheetNothing

(linkedom is dropped from browser bundles, which use the browser's own document.) To draw bar, line and area charts in the browser, import them from enarratio/lite: the same options (error bars and annotations included) give the same SVG, with no Observable Plot, linkedom or DOM, in 15 to 16 kB per chart bundled, minified and gzipped. Enarratio is ESM only and needs Node.js 22 or later (or any runtime with standard ES modules). TypeScript types are included.

Five-minute start

1. Render a chart on the server. Every chart takes its data, the fields to plot, and alt: a sentence saying what the chart shows.

import { stylesheet } from "enarratio";
import { barChart } from "enarratio/plot";
import { defaultTheme } from "enarratio/themes";

const entries = [
  { year: 2022, type: "Publications" },
  { year: 2022, type: "Talks" },
  { year: 2023, type: "Publications" },
  { year: 2024, type: "Publications" },
  { year: 2024, type: "Grants" },
];

const chart = barChart({
  id: "entries",
  data: entries,
  x: "year",
  series: "type", // stacks by type; omit y to count rows
  title: "Entries per year",
  alt: "Entries per year by type, 2022 to 2024: one or two a year, mostly publications.",
});

2. Put it in a page with the theme's stylesheet. The stylesheet is included once per page, whatever the number of charts.

const html = `<!doctype html>
<html lang="en">
<head>
<meta name="color-scheme" content="light dark">
<style>${stylesheet(defaultTheme)}</style>
</head>
<body>${chart}</body>
</html>`;

That page is complete: the chart, its legend and a data table render with scripts off, and the color-scheme meta tag makes them follow the reader's light or dark preference.

3. Optionally, enhance it in the browser.

import { enhance } from "enarratio/enhance";

enhance(); // every Enarratio chart on the page
document.addEventListener("enarratio:select", (event) => {
  const { chartId, field, value } = event.detail; // e.g. "entries", "type", "Talks"
  console.log(chartId, field, value); // value is null when the filter is cleared
});

Now bars show details on hover and focus, arrow keys move between them (along a series and between series), Enter or a click filters by the bar's series (and fires enarratio:select), legend entries become toggle buttons, a drag, Shift with the arrow keys or a from-and-to form picks a range on time axes (enarratio:brush), and the data table sorts and can replace the chart. Escape clears. Everything is announced to screen readers and motion stops under prefers-reduced-motion. { expand: true, url: true } adds an expanded view and keeps the filter in the address; strings translates the layer.

To have a chart drawn on the server for a wide page fit a phone, write its spec beside it with redrawAttribute and call redrawCharts() (or redrawChart for one host) from enarratio/redraw instead: each chart is drawn again at its frame's width as that width changes, then enhanced.

Without a bundler, serve node_modules/enarratio/dist/enhance/index.js as a file, with sound.js and image.js from the same folder beside it (index.js imports the first only when a reader presses "Play as sound", the second on the first download), and load it with <script type="module">import { enhance } from "/enhance.js"; enhance();</script>, or from a CDN such as https://cdn.jsdelivr.net/npm/[email protected]/dist/enhance/index.js.

To redraw a chart in place, pass new server markup to update() on the chart enhance() returns. That markup should come from Enarratio: it is sanitized before use, but it is not a way to insert arbitrary HTML.

The examples directory holds a runnable file for every chart in the gallery. Each exports the chart's HTML, and npm run gallery renders them all into site/dist.

Charts

FunctionImportDraws
barChartenarratio/plotBars, stacked or grouped, vertical or horizontal, on a category or a time axis; counts rows when no value is given; error bars
divergingBarChartenarratio/plotSigned values about zero, each bar labeled with its sign
waterfallChartenarratio/plotA bridge from one total to another through increases and decreases
lineChartenarratio/plotLines over time or any number, with gaps, reference lines, event markers, direct labels, log y, and a forecast band with the line dashed after a chosen x
areaChartenarratio/plotStacked areas over time, or stacked to 100%
smallMultiplesenarratio/plotOne bar or line chart per group, on shared scales
scatterPlotenarratio/plotPoints on linear or log axes, symbols per series, optional regression with 95% band, bubbles with a size legend, error bars
histogram, densityPlotenarratio/plotDistributions: binned counts, and kernel density curves
boxPlot, stripPlotenarratio/plotBox plots, and every value as a beeswarm
dotPlot, rangePlot, dumbbellPlotenarratio/plotOne row per item: dots (with intervals), low-to-high ranges, and two values joined
slopeChartenarratio/plotEach item's value at two times, labeled at both ends
pieChartenarratio/plotA pie or donut, reported as an issue past six slices
heatmapenarratio/plotA grid on the theme's sequential ramp, with printed values and labeled bins
tableChartenarratioOne sortable table with inline bars, heat cells and sparklines
networkChartenarratio/scienceA force-directed network laid out on the server, deterministic
titerPlotenarratio/scienceTiters on a dilution axis with geometric mean, 95% CI and limit of detection
genomeTrackenarratio/scienceFeatures to scale on a nucleotide axis, arrows by strand, overlaps in lanes
doseResponseenarratio/scienceA four-parameter logistic curve, fitted or given, with the EC50 marked
survivalPlotenarratio/scienceKaplan-Meier curves with censor marks, confidence bands and the number at risk
forestPlotenarratio/scienceStudy estimates with intervals, and a pooled estimate (fixed or random effects)
phylogeneticTreeenarratio/scienceA tree from Newick, rectangular, tips colored by group, support at the nodes
dotMapenarratio/mapPoints sized by value over your streets or coastlines, landmarks as squares, one highlighted
choroplethMapenarratio/mapRegions in five labeled steps of the sequential ramp, values printed where they fit, no data outlined
sparklineenarratioA word-sized trend line
progressRingenarratioProgress toward a total
uptimeStripenarratioUp, degraded, down or unmeasured, one tick per period
heatStripenarratioA one-row strip on the sequential ramp, one cell per value
timelineenarratioLanes for sites or agents, spans and point events over a time window, status by color and shape

Forecasts are computed by you and only drawn by Enarratio: give lineChart the bounds as fields and a boundary, and it shades the band, dashes the line after the boundary, draws forecast points hollow, and marks forecast rows in the data table. A time axis on barChart needs no pre-formatted labels:

import { barChart, lineChart } from "enarratio/plot";

lineChart({
  data: days, // { day, visits, lower, upper }, the last rows computed by your model
  x: "day",
  y: "visits",
  band: { lower: "lower", upper: "upper", label: "95% interval" },
  forecastFrom: "2026-02-23",
  alt: "Daily visits, with a one-week forecast and its 95% interval from 24 February.",
});

barChart({
  data: runs, // { at: "2026-06-09T08:00Z", site }, one row per run
  x: "at",
  xType: "time", // one bar per day, quiet days included
  series: "site",
  alt: "Analysis runs per day at three sites.",
});

Every primitive can also state its numbers: sparklineTable, uptimeStripTable, progressRingTable, timelineTable and heatStripTable return a table and a summary sentence computed from the same values as the drawing, for pages that want them beside it.

Every function is documented in the API reference and in its TypeScript declarations. The chart types still to come, and the order they come in, are in the scientific chart plan.

Themes

A theme is data: fonts, gridlines, and a light and a dark color scheme, each with eight series colors, a five-step sequential ramp and status colors. stylesheet(theme) turns it into CSS custom properties; charts refer only to those properties, so one server render serves every theme and both schemes.

import { checkTheme, defineTheme, stylesheet } from "enarratio";
import { defaultTheme, highContrastTheme } from "enarratio/themes";

const theme = defineTheme({
  ...defaultTheme,
  name: "lab",
  fonts: { body: "Inter, system-ui, sans-serif" },
  light: { ...defaultTheme.light, background: "#fbfaf7" },
});
const report = checkTheme(theme);
// report.issues: text below 4.5:1, marks below 3:1 (WCAG 2.2),
// series names below 4.5:1 on the background, ramp steps under 3:1 by fill
// and edge, and series colors closer than 10 CIEDE2000 units under simulated
// protanopia, deuteranopia or tritanopia. stylesheet() throws on any of them.
const css = stylesheet(theme, { highContrast: highContrastTheme });

Series names written on the background (direct labels, survival curve labels) are drawn in a shade of each series color that reaches 4.5:1, and ramp steps too pale to see on the background (a heatmap's lowest values) get an edge at 3:1. stylesheet() derives both from the theme, or takes a theme's own seriesLabels and sequentialEdges.

Your own tokens in a theme

Any theme color can be one of the site's own custom properties, with its value as a fallback: var(--brand-ink, #1c1727). The stylesheet writes it as given, so charts follow the token wherever the site changes it (a family, a density, a dark mode of its own), and nothing is copied in hex. checkTheme() measures the fallback, so keep it equal to the token's value.

const theme = defineTheme({
  ...defaultTheme,
  name: "site",
  light: {
    ...defaultTheme.light,
    background: "var(--surface, #fdfdfe)",
    text: "var(--text, #1c1727)",
    mutedText: "var(--muted, #686471)",
  },
});

When the series or the ramp are tokens, give seriesLabels and sequentialEdges as tokens too; otherwise their shades are derived from the fallbacks and stay fixed.

A site whose colors and type already live in its own design tokens can skip the Theme object: link enarratio/base.css (or call baseStylesheet()), define the color and font properties yourself, and tune any size, weight, radius or animation timing the same way, for example --enarratio-font-size-mark or --enarratio-duration-grow. Every property has today's value as its fallback, so setting none changes nothing. The full list is in the API reference.

Any chart can override a series color with colors: { Talks: "#8a4a1b" } or a light and dark pair. The chart carries it in a data attribute, never a style attribute, so a strict Content-Security-Policy needs no 'unsafe-inline': add colorStyles(html) to the page's CSS. Two themes ship with the package in enarratio/themes: defaultTheme, and highContrastTheme, which the example above uses for readers who ask for more contrast. The gallery's second page uses a neutral, heavily commented example theme, examples/themes/example.ts, written to be copied and edited.

Accessibility

  • Every chart is a figure with a title, a visible one-sentence finding (its alt), a screen reader overview (type, axes and ranges, series and points), a summary computed from the data (trend, extremes, outliers, intervals), notes, a source line, a "Download CSV" link and the data table, all in the HTML with scripts off. Primitives are figures too, unless inline.
  • alt names the chart's SVG (role="img"), and belongs to the SVG rather than the figure so that the caption and table stay readable. A chart without one names itself from its data and reports an issue; strict: true turns every issue (no alt or title, an unlabeled axis, too many series or points, a CSV too large to inline) into an error that fails the build.
  • Numbers, dates and every generated string follow locale and strings.
  • Every chart carries an equivalent data table, in a disclosure or visually hidden.
  • Every mark has hover details that work without script, through SVG <title>.
  • Series are distinguished by more than hue: textures on filled bars and areas, legends and direct labels (moved apart so they never overlap), symbols in scatter plots, hollow points below a limit of detection, heights in uptime strips, dashed outlines for missing values.
  • Text in the drawing follows the browser's font size and never renders under 12px at default zoom, down to a 320px screen.
  • Charts follow forced colors (Windows High Contrast), prefers-contrast: more, prefers-reduced-motion and print, and carry no style attribute.
  • A chart's drawing is one named image (role="img"), enhanced or not; the enhancement layer's points sit beside it, in their own role="application" layer (record 0027).
  • Enhanced charts add "Download SVG", "Download PNG", "Download tactile SVG" (black on white on an 11 by 11.5 inch page, for a braille embosser or swell paper) and, for a figure with an id, "Copy embed" (a link to the chart) beside "Download CSV", and "Play as sound", which plays each series as rising and falling notes.
  • Maps take no boundaries from Enarratio: you pass GeoJSON, and each map has a summary, a data table and a keyboard walk from north to south, and never shows a value by color alone.
  • Enhanced charts are keyboard operable with one tab stop, through native buttons over the marks and a described key hint; they announce each point's place, filters, ranges, sorts and updates through a live region, and respect prefers-reduced-motion.
  • The test suite runs axe-core over the gallery in both themes, light and dark, with and without scripts.

Development

npm ci
npm run typecheck && npm run lint && npm test     # unit and rendering tests
npm run build && npm run gallery                  # site/dist
npm run test:browser                              # accessibility, interaction, visual

Development needs Node.js 24 or later, which runs the TypeScript scripts directly; .nvmrc pins 26.

The gallery is hosted at https://enarratio.dustinedwards.info as an assets-only Cloudflare Worker (wrangler.jsonc), with a strict Content-Security-Policy and the portfolio's shared security headers, from the gallery's entry in site-runtime's site registry (worker/index.ts, and _headers from site-runtime-headers). npm run deploy:gallery builds and deploys it; the "Deploy gallery" workflow does the same on every push to main once the repository has the CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID secrets. See docs/design/0008-gallery-hosting.md. See CONTRIBUTING.md for the workflow and docs/design for why Enarratio is built the way it is.

Citing

If you use Enarratio in research, please cite it using the metadata in CITATION.cff.

License

MIT. Copyright (c) 2026 Dustin Edwards.

版本与发布记录

enarratio 共发布 13 个版本,首次发布于 2026/09/29;过去 12 个月发布了 13 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.7.2latest2026/11/03今天
0.7.12026/11/03今天
0.7.02026/11/022 天前
0.6.12026/11/012 天前
0.6.02026/11/012 天前
0.5.0next2026/10/313 天前

关于 enarratio 的常见问题

enarratio 是什么?

可访问的,服务器渲染的图表和科学数字的网络

如何安装 enarratio?

在项目目录运行 npm install enarratio,或使用 pnpm add / yarn add / bun add。

enarratio 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

enarratio 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

enarratio 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

enarratio 可以商用吗?

enarratio 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

enarratio 还在维护吗?

最新版本 0.7.2 发布于 2026/11/03(今天);至今共发布 13 个版本,其中过去 12 个月发布了 13 个;首次发布于 2026/09/29。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。