npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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 typesbarX, 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/server exports 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-driverecharts (default) or observable. Same API, swap the driver
  • Axis configlabel, grid, domain, tickRotate, type, nice, zero
  • Auto-color + Legend — Each driver auto-assigns colors; legend: true to display
  • Responsive — Auto-resize with proper cleanup (dispose())
  • TypeScript First — Full type definitions

Installation

bun install

Quick 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" },
    ),
  ],
});

driver defaults to "echarts". Use driver: "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 labels

Common 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/stroke can 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 radar series on echarts, and a polar-coordinate composition (spokes + closed polygons + dots + labels) on observable. 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/height are pixels (defaults: 800×600).
  • A white background (background: "#ffffff") is applied by default so saved images are never transparent; override with background: "transparent" or any CSS color.
  • driver: "observable" works too, but requires a DOM document (e.g. from happy-dom) passed in options.document; without one, renderSVG throws a clear error. Prefer the default echarts driver 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.md

Entry 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.