@dnax/plot
v0.0.21
Published
Readme
@dnax/plot
A lightweight, declarative charting library with a mark-based unified API across rendering engines — inspired by Observable Plot. Powered by Apache ECharts and Observable Plot.
Features
- 21 mark types —
barX,barY,line,area,pie,radar,scatter,dot,image,tree,treemap,sunburst,network,sankey,heatmap,box,candlestick,histogram,funnel,gauge,text - Server-side rendering —
@dnax/plot/serverexports charts to SVG/PNG without a browser (ECharts SSR mode) - Multi-mark composition — Layer as many marks as you want on the same chart
- Multi-driver —
echarts(default) orobservable. Same API, swap the driver - Axis config —
label,grid,domain,tickRotate,type,nice,zero - Auto-color + Legend — Each driver auto-assigns colors;
legend: trueto display - Responsive — Auto-resize with proper cleanup (
dispose()) - TypeScript First — Full type definitions
Installation
bun installQuick Start
import { plot, barY } from "@dnax/plot";
plot({
target: document.getElementById("chart")!,
title: "Sales by Product",
marks: [
barY(
[
{ product: "Laptop", sales: 120 },
{ product: "Phone", sales: 200 },
{ product: "Tablet", sales: 90 },
],
{ x: "product", y: "sales" },
),
],
});
driverdefaults to"echarts". Usedriver: "observable"to switch.
Namespace usage
import { Plot } from "@dnax/plot";
Plot.plot({
target: container,
marks: [Plot.barY(data, { x: "product", y: "sales" })],
});API Reference
Mark factories
barX(data, options): Mark // horizontal bars
barY(data, options): Mark // vertical bars
line(data, options): Mark // line chart
area(data, options): Mark // filled line / range band (y1/y2)
pie(data, options): Mark // pie / donut (innerRadius) — ECharts only
radar(data, options): Mark // radar / spider chart
scatter(data, options): Mark // scatter plot
box(data, options): Mark // box plot (statistical distribution)
candlestick(data, options): Mark // OHLC — ECharts only
histogram(data, options): Mark // binned distribution
funnel(data, options): Mark // conversion stages — ECharts only
gauge(data, options): Mark // single KPI — ECharts only
dot(data, options): Mark // alias for scatter (Observable naming)
image(data, options): Mark // images at data positions
tree(data, options): Mark // hierarchy {name, children}
treemap(data, options): Mark // hierarchy as nested rectangles — ECharts only
sunburst(data, options): Mark // radial hierarchy — ECharts only
network(data, options): Mark // graph (nodes + edges)
sankey(data, options): Mark // flow diagram — ECharts only
heatmap(data, options): Mark // heatmap (x × y grid)
text(data, options): Mark // text labelsCommon Mark options
| Option | Type | Marks | Description |
|--------|------|-------|-------------|
| x | string | all† | X-axis / category / name field |
| y | string | all† | Y-axis / value field |
| fill | string | all | Interior color. Auto if unset |
| stroke | string | all | Outline / line color. Auto if unset |
| r | number | string | all | Border radius (px) — or data field name for bubble size on scatter/dot |
| name | string | all | Legend/series name |
| tip | boolean | all | Enable tooltip (default: true) |
| title | string | all | Tooltip text field — shows this column on hover |
† tree/network use their own data format (see below).
Mark-specific options
| Option | Marks | Description |
|--------|-------|-------------|
| smooth | line, area | Smooth curve interpolation |
| showSymbol | line | Show point markers |
| showBackground | barX/barY | Background grid |
| stack | barX/barY/line/area | Stack series — true (group from fill/name field) or a field name |
| innerRadius | pie | Donut hole — number (px) or string ("50%") |
| bins | histogram | Number of bins (default ~10) |
| y1 / y2 | area | Range band: lower / upper bound fields |
| open/close/low/high | candlestick | OHLC field names |
| min / max | gauge | Scale bounds (default 0/100) |
| src | image | Image URL field |
| width | image | Image width (px) |
| height | image | Image height (px) |
| links | network, sankey | [{source, target, value?}] edges |
| value | heatmap | Intensity field for color |
| colorScheme | heatmap | Palette name ("warm", "viridis"…) |
| text | text | Text field to display |
| dx | text | Horizontal offset (px) |
| dy | text | Vertical offset (px) |
| textAnchor | text | "start", "middle", "end" |
| lineAnchor | text | "top", "bottom", "middle" |
| fontSize | text | Font size (px) |
| fontFamily | text | Font family |
| fontWeight | text | "normal", "bold", 600… |
| fontStyle | text | "normal", "italic" |
Data-driven colors:
fill/strokecan be a CSS color ("#e74c3c","red") for a constant, or a field name ("temperature","category") to map colors from data values.
plot(options)
interface PlotOptions {
driver?: "echarts" | "observable"; // default: "echarts"
target: HTMLElement;
marks: Mark[];
theme?: "dark" | "default"; // ECharts only
title?: string;
width?: string;
height?: string;
legend?: boolean; // Show legend
color?: string[] | { // Global color scale
scheme?: string; // Palette name (Observable: "turbo", "warm"…)
type?: string; // "categorical", "linear"… (Observable)
legend?: boolean; // Show color legend (Observable)
};
x?: AxisConfig;
y?: AxisConfig;
}
interface AxisConfig {
label?: string;
grid?: boolean;
domain?: [number, number];
tickRotate?: number;
type?: "linear" | "log" | "band";
nice?: boolean;
zero?: boolean;
tickFormat?: string;
}Examples
Bar + Line + Text on same chart
import { plot, barY, line, text } from "@dnax/plot";
const data = [
{ month: "Jan", sales: 120, profit: 40 },
{ month: "Feb", sales: 200, profit: 80 },
{ month: "Mar", sales: 90, profit: 30 },
];
plot({
target: container,
title: "Sales & Profit",
marks: [
barY(data, { x: "month", y: "sales", name: "Sales", fill: "#5470c6" }),
line(data, { x: "month", y: "profit", name: "Profit", stroke: "#91cc75", smooth: true }),
text(data, { x: "month", y: "sales", text: "sales", dy: -10, textAnchor: "middle", fontSize: 12 }),
],
});Legend
plot({
target: container,
legend: true,
marks: [
barY(data, { x: "month", y: "sales", name: "Sales", fill: "#5470c6" }),
line(data, { x: "month", y: "profit", name: "Profit", stroke: "#91cc75" }),
],
});Data-driven colors
// "Anomaly" n'est pas une couleur → traité comme un champ de données
plot({
target: container,
color: { scheme: "rdbu", legend: true },
marks: [
dot(data, { x: "Date", y: "Anomaly", fill: "Anomaly" }),
],
});Tooltip control
plot({
target: container,
marks: [
barY(data, { x: "month", y: "sales", tip: false }), // sans tooltip
line(data, { x: "month", y: "profit", title: "profit" }), // tooltip custom
],
});Global color palette
plot({
target: container,
color: ["#e74c3c", "#2ecc71", "#3498db"],
legend: true,
marks: [
barY(data, { x: "month", y: "sales", name: "Sales" }),
line(data, { x: "month", y: "profit", name: "Profit" }),
],
});Line with smooth curves and symbols
plot({
target: container,
marks: [
line(data, { x: "month", y: "revenue", smooth: true, showSymbol: true }),
],
});Pie chart
plot({
target: container,
marks: [pie(data, { x: "browser", y: "share" })],
});Observable Plot does not support pie. Use
driver: "echarts"(default).
Radar chart
Tidy data format: one row per (series, indicator) pair — x = indicator field, y = value field. If name matches a column in the data, rows are grouped per series; otherwise it's a single series labeled by name.
const stats = [
{ team: "Alpha", metric: "Speed", value: 80 },
{ team: "Alpha", metric: "Quality", value: 70 },
{ team: "Alpha", metric: "Stability",value: 60 },
{ team: "Beta", metric: "Speed", value: 65 },
{ team: "Beta", metric: "Quality", value: 85 },
{ team: "Beta", metric: "Stability",value: 75 },
];
plot({
target: container,
legend: true,
marks: [
radar(stats, { x: "metric", y: "value", name: "team", fill: "#5470c6" }),
],
});Radar charts work on both drivers: native
radarseries onecharts, and a polar-coordinate composition (spokes + closed polygons + dots + labels) onobservable. Use the same tidy data format on both.
Area chart
plot({
target: container,
marks: [
area(data, { x: "month", y: "revenue", fill: "#5470c6", smooth: true }),
],
});Area range (band)
plot({
target: container,
marks: [
area(data, { x: "month", y1: "low", y2: "high", fill: "#91cc75" }),
],
});Donut chart
plot({
target: container,
marks: [
pie(data, { x: "browser", y: "share", innerRadius: "45%" }),
],
});Stacked bars / areas
// `name` matches a data column → series grouped and stacked
plot({
target: container,
marks: [
barY(data, { x: "month", y: "value", name: "category", stack: true }),
],
});
// Stacked area (ECharts: grouped series + areaStyle; Observable: stackY transform)
plot({
target: container,
marks: [
area(data, { x: "month", y: "value", name: "category", stack: true }),
],
});Bubble chart (r as a data field)
plot({
target: container,
marks: [
dot(data, { x: "height", y: "weight", r: "population", fill: "#e74c3c" }),
],
});Box plot
const scores = [
{ team: "A", score: 58 }, { team: "A", score: 62 }, { team: "A", score: 95 },
{ team: "B", score: 40 }, { team: "B", score: 70 }, { team: "B", score: 85 },
];
plot({
target: container,
marks: [box(scores, { x: "team", y: "score" })],
});Histogram
plot({
target: container,
marks: [histogram(data, { x: "height", bins: 15, fill: "#5470c6" })],
});Treemap / Sunburst (ECharts only)
const hierarchy = [
{
name: "Root",
children: [
{ name: "A", value: 40 },
{ name: "B", value: 60, children: [{ name: "B1", value: 20 }] },
],
},
];
plot({ target: container, marks: [treemap(hierarchy)] });
plot({ target: container, marks: [sunburst(hierarchy)] });Funnel (ECharts only)
plot({
target: container,
marks: [funnel(conversion, { x: "stage", y: "users" })],
});Gauge (ECharts only)
plot({
target: container,
marks: [gauge([{ k: "CPU", v: 62 }], { x: "k", y: "v", min: 0, max: 100 })],
});Sankey (ECharts only)
plot({
target: container,
marks: [
sankey(
[{ name: "In" }, { name: "A" }, { name: "Out" }],
{
links: [
{ source: "In", target: "A", value: 10 },
{ source: "A", target: "Out", value: 7 },
],
},
),
],
});Candlestick (ECharts only)
const ohlc = [
{ day: "Mon", open: 10, close: 12, low: 9, high: 13 },
{ day: "Tue", open: 12, close: 11, low: 10, high: 14 },
];
plot({
target: container,
marks: [candlestick(ohlc, { x: "day" })],
});Scatter / Dot
plot({
target: container,
marks: [dot(data, { x: "height", y: "weight", fill: "#e74c3c", r: 4 })],
});Image mark
const flags = [
{ country: "France", x: 2, y: 48, src: "https://flagcdn.com/fr.svg" },
{ country: "Germany", x: 10, y: 51, src: "https://flagcdn.com/de.svg" },
];
plot({
target: container,
x: { label: "Longitude" },
y: { label: "Latitude" },
marks: [
image(flags, { x: "x", y: "y", src: "src", width: 30, height: 20, r: 4 }),
],
});Tree (hierarchy)
const orgChart = [
{
name: "CEO",
children: [
{ name: "CTO", children: [{ name: "Dev A" }, { name: "Dev B" }] },
{ name: "CFO", children: [{ name: "Accountant" }] },
],
},
];
plot({
target: container,
marks: [tree(orgChart, { fill: "#5470c6" })],
});Network (graph)
const nodes = [
{ name: "Alice" }, { name: "Bob" }, { name: "Charlie" },
];
const links = [
{ source: "Alice", target: "Bob" },
{ source: "Bob", target: "Charlie" },
];
plot({
target: container,
marks: [network(nodes, { links, fill: "#5470c6", stroke: "#999" })],
});Heatmap
const temps = [
{ day: "Mon", hour: "08h", value: 12 },
{ day: "Mon", hour: "12h", value: 22 },
{ day: "Tue", hour: "08h", value: 10 },
{ day: "Tue", hour: "12h", value: 24 },
];
plot({
target: container,
x: { label: "Day" },
y: { label: "Hour" },
marks: [heatmap(temps, { x: "day", y: "hour", value: "value", colorScheme: "warm" })],
});Dark theme + axis config
plot({
target: container,
theme: "dark",
x: { label: "Month", grid: true, tickRotate: 45 },
y: { label: "Revenue ($)", grid: true, domain: [0, 1000] },
marks: [barY(data, { x: "month", y: "revenue" })],
});Observable Plot driver
plot({
driver: "observable",
target: container,
marks: [
barY(data, { x: "product", y: "sales" }),
line(data, { x: "product", y: "profit", smooth: true }),
],
});Driver compatibility
| Mark | ECharts | Observable |
|------|---------|------------|
| barX / barY | ✅ | ✅ |
| line | ✅ | ✅ |
| area | ✅ | ✅ |
| pie | ✅ | ❌ |
| radar | ✅ | ✅ (polar composition) |
| scatter / dot | ✅ | ✅ |
| box | ✅ | ✅ |
| histogram | ✅ | ✅ |
| image | ✅ | ✅ |
| tree | ✅ | ✅ |
| treemap | ✅ | ❌ |
| sunburst | ✅ | ❌ |
| network | ✅ | ✅ (circular layout) |
| sankey | ✅ | ❌ |
| heatmap | ✅ | ✅ |
| candlestick | ✅ | ❌ |
| funnel | ✅ | ❌ |
| gauge | ✅ | ❌ |
| text | ✅ | ✅ |
Advanced: Direct Provider Access
// Raw ECharts
import { Plot, Interaction } from "@dnax/plot/echarts";
const p = new Plot(container, { theme: "dark" });
p.plot({ /* full ECharts PlotOptionConfig */ });
// Raw Observable Plot
import { Plot } from "@dnax/plot/observable";Server-Side Rendering (SSR)
Export charts to SVG or PNG without a browser, from a backend (Bun / Node):
import { renderSVG, renderPNG, saveImage } from "@dnax/plot/server";
import { barY } from "@dnax/plot";
// SVG string (no DOM needed — ECharts SSR mode)
const svg = renderSVG({ marks: [barY(data, { x: "product", y: "sales" })], width: 800, height: 600 });
// PNG buffer (rasterized with @resvg/resvg-js)
const png = await renderPNG({ marks: [barY(data, { x: "product", y: "sales" })] });
// Write a file — format from extension (`.png`) or `options.format`
await saveImage({ marks: [barY(data, { x: "product", y: "sales" })] }, "./chart.png");width/heightare pixels (defaults: 800×600).- A white background (
background: "#ffffff") is applied by default so saved images are never transparent; override withbackground: "transparent"or any CSS color. driver: "observable"works too, but requires a DOMdocument(e.g. from happy-dom) passed inoptions.document; without one,renderSVGthrows a clear error. Prefer the defaultechartsdriver for SSR.
Package Structure
packages/plot/
├── index.ts # Mark factories + plot() + Plot namespace
├── server.ts # SSR: renderSVG / renderPNG / saveImage
├── types.ts # Mark, MarkOptions, PlotOptions, AxisConfig
├── drivers/
│ ├── types.ts # PlotDriver interface
│ ├── echarts.ts # EChartsDriver
│ └── observable.ts # ObservableDriver
├── echarts/ # Raw ECharts provider (advanced)
│ ├── index.ts
│ ├── Plot.ts
│ └── types.ts
├── observable/ # Raw Observable Plot provider
│ └── index.ts
├── package.json
├── tsconfig.json
└── README.mdEntry Points
| Import | Description | Status |
|--------|-------------|--------|
| @dnax/plot | Mark-based unified API | ✅ Ready |
| @dnax/plot/echarts | Raw ECharts provider | ✅ Ready |
| @dnax/plot/observable | Raw Observable Plot provider | ✅ Ready |
| @dnax/plot/server | Server-side rendering (SVG/PNG, no DOM) | ✅ Ready |
Dependencies
| Dependency | Purpose |
|------------|---------|
| echarts | Apache ECharts engine |
| @observablehq/plot | Observable Plot engine |
| @resvg/resvg-js | SVG → PNG rasterization (server-side) |
| @fontsource/mona-sans | Default font (ECharts only) |
License
MIT — Part of the Anteros project.
