Skip to main content

@bndynet/icharts

A lightweight charting library with a Web Component (<i-chart>) and a simple createChart() API.

Installation

npm install @bndynet/icharts

Quick Start

Option 1 — Web Component (HTML)

<script type="module">
import '@bndynet/icharts';
</script>

<i-chart id="myChart" type="line" style="width: 600px; height: 400px;"></i-chart>

<script type="module">
const chart = document.getElementById('myChart');
chart.data = {
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
series: [
{ name: 'Revenue', data: [120, 200, 150, 80, 270] },
{ name: 'Cost', data: [90, 170, 130, 60, 210] },
],
};
chart.options = { title: 'Monthly Overview' };
</script>

Option 2 — JavaScript / TypeScript

import { createChart } from '@bndynet/icharts';

const chart = createChart(
document.getElementById('app'),
'bar',
{
categories: ['Q1', 'Q2', 'Q3', 'Q4'],
series: [{ name: 'Sales', data: [300, 450, 280, 600] }],
},
{ title: 'Quarterly Sales' }
);

// Update / resize later:
chart.update(newData, newOptions);
chart.resize();

// Disposal is automatic — the chart releases itself the moment its
// container leaves the DOM (e.g. when a Vue/React component unmounts).
// Calling `chart.dispose()` explicitly is still supported and idempotent
// for cases where you want to free resources eagerly.
chart.dispose();

Option 3 — CDN (no bundler)

<script src="./node_modules/@bndynet/icharts/dist/index.global.js"></script>

<div id="chart" style="width: 600px; height: 400px;"></div>
<script>
iCharts.createChart(document.getElementById('chart'), 'pie', [
{ name: 'Chrome', value: 65 },
{ name: 'Firefox', value: 15 },
{ name: 'Safari', value: 12 },
{ name: 'Edge', value: 8 },
]);
</script>

Option 4 — SSR / Server-Side Rendering (Next.js, Nuxt, Astro, SvelteKit, Vite SSR…)

Import from the SSR-safe subpath @bndynet/icharts/core so the module graph never touches the <i-chart> web component, Lit, or the @echarts-x plugin installers. There are two distinct SSR scenarios this entry supports:

4a. Client-side chart, SSR-safe import (the common case)

The page is server-rendered to HTML, the chart is instantiated on the client inside a lifecycle hook (onMounted, useEffect, 'use client' boundary, …). The server import only needs to be safe to evaluate.

// SSR-safe: this import does not access `window` / `document` /
// `customElements` and is safe to evaluate on the server.
import { createChart } from '@bndynet/icharts/core';

// In client-only lifecycle code:
onMounted(() => {
createChart(el.value, 'line', {
categories: ['Jan', 'Feb', 'Mar'],
series: [{ name: 'Sales', data: [10, 20, 30] }],
});
});

4b. Server-rendered chart image (no browser needed)

Generate a complete <svg>...</svg> document directly on the server — inline it into HTML, save it to disk, attach it to an email, or pipe it through sharp / @resvg/resvg-js to produce PNG. No DOM, no canvas, no headless browser required. Powered by ECharts 6's built-in SSR mode.

import { renderChartToSVGString } from '@bndynet/icharts/core';
import { writeFileSync } from 'node:fs';

const svg = renderChartToSVGString(
'line', // chart type
{ // data
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
series: [
{ name: 'Revenue', data: [120, 200, 150, 80, 270] },
{ name: 'Cost', data: [90, 170, 130, 60, 210] },
],
},
{ width: 800, height: 400 }, // SSR options
{ title: 'Quarterly Revenue', theme: 'default' }, // chart options
);

writeFileSync('chart.svg', svg);

RenderChartToSVGStringOptions controls the rendered viewport:

FieldRequiredDescription
widthyesViewport width in px (used as the SVG coordinate system).
heightyesViewport height in px.
localenoECharts locale (default 'EN').
useViewBoxnotrue (default) → root <svg> carries width + height + viewBox. false → no viewBox (strictly fixed-size).

The underlying ECharts instance is created and disposed entirely inside the function, so calling it in a hot request loop never leaks engine state.

To convert the SVG to PNG (for emails, social cards, server-side rasterization), use a standalone library — icharts deliberately stays SVG-only so SSR consumers don't pay for a native canvas dependency they may not need:

import { renderChartToSVGString } from '@bndynet/icharts/core';
import { Resvg } from '@resvg/resvg-js';

const svg = renderChartToSVGString(
'pie',
[{ name: 'Apple', value: 30 }, { name: 'Banana', value: 50 }, { name: 'Cherry', value: 20 }],
{ width: 800, height: 500 },
{ title: 'Fruit Mix', legend: { show: true, position: 'bottom' } },
);

const png = new Resvg(svg).render().asPng(); // Buffer
// or with sharp:
// const png = await sharp(Buffer.from(svg)).png().toBuffer();

SSR-mode fallbacks — a few canvas-based features degrade gracefully when no DOM / canvas is available:

  • Label-width measurement falls back to a chars × 7px estimate, so race-chart grid.right headroom and tree label widths can drift by 1–2 px from browser output.
  • ResizeObserver is unavailable, so the pie adapter's resize-driven pixel-perfect center / radius recompute is replaced by the static percent fallback (still legible, slightly less centered).
  • ECharts' SVG renderer approximates text widths internally, so very long labels can wrap differently than in the browser. Pad your widths slightly if you depend on exact parity.

These trade-offs are intrinsic to the SSR contract; the library documents them but does not paper over them with hidden polyfills.

Entry-point cheat sheet

EntryAuto-loads <i-chart>Auto-loads wordcloud + liquid-progressSafe to import on the serverrenderChartToSVGString
@bndynet/icharts (default)yes (no-op when customElements is absent)yesno — the @echarts-x plugins reference window / document at module loadyes (re-exported from /core)
@bndynet/icharts/core (SSR)nonoyesyes

If you import the default entry from a server bundle, keep the import inside a client-only branch (Next.js 'use client' files, Nuxt <client-only> islands, dynamic await import() inside onMounted, …).

Plugin-backed chart types under /core (liquidprogress, wordcloud)

The SSR-safe entry deliberately does NOT auto-register the @echarts-x/* plugins on import. Opt in per chart type:

  • liquidprogress (server + client): call the SSR-safe installer once at boot. It hides the echarts and @echarts-x/* packages — you stay on @bndynet/icharts/core-only imports.

    import { installLiquidProgress, renderChartToSVGString } from '@bndynet/icharts/core';

    installLiquidProgress(); // idempotent — call once at boot or per request

    const svg = renderChartToSVGString(
    'liquidprogress',
    { value: 0.65 },
    { width: 400, height: 400 },
    { title: 'CPU' },
    );

    For the client path under /core, the same installLiquidProgress() call works from a client-only branch (onMounted, useEffect, …). Or simply pull in the main entry via dynamic import — that registers liquidprogress + wordcloud both:

    await import('@bndynet/icharts'); // both plugins
    await import('@bndynet/icharts/dist/installers/index.js'); // narrower, same effect
  • wordcloud (browser-only): the @echarts-x/custom-word-cloud package requires a real window at module-load and a real <canvas> with addEventListener at render-time — both fundamental browser dependencies that renderChartToSVGString cannot fake. There is intentionally no installWordCloud() helper. Render wordcloud only on the client, via createChart('wordcloud', …) from the main @bndynet/icharts entry (or from a client-only branch when using /core).

    If you genuinely need wordcloud as a server-rendered image, run the chart in a real browser (Playwright / Puppeteer) and screenshot it — there is no pure-Node path today.


Chart Types

Typetype valueVariants
Linelinedefault, spark, race
Barbardefault, horizontal, spark, race
Areaareadefault, spark
Mapmapdefault
Piepiedefault, doughnut, half-doughnut, nightingale
Gaugegaugedefault, percentage
Liquid Progressliquidprogressdefault
Sankeysankeydefault, vertical
Chordchorddefault
Radarradardefault, circle
Networknetworkdefault, circular
Treetreedefault (use direction for layout orientation)
Treemaptreemapdefault
Word Cloudwordclouddefault, diamond, poster

Map resources (type: 'map')

map charts require a pre-registered map resource. Register your GeoJSON (or SVG map) once with registerMap, then reference it via options.mapName:

import { registerMap, createChart } from '@bndynet/icharts';

// `chinaGeoJson` can come from your own local file or a remote fetch.
registerMap('china', chinaGeoJson);

createChart(el, 'map', [
{ name: '北京市', value: 92 },
{ name: '上海市', value: 88 },
{ name: '广东省', value: 97 },
{ name: '浙江省', value: 85 },
], {
title: '中国区域评分',
mapName: 'china',
visualMap: { min: 60, max: 100 },
});

Data Formats

Each chart type expects a specific data shape. Full schemas, field notes, and chart-specific options live in docs/ (one file per family).

ChartData typeReference
Line / Bar / AreaXYData (LineData / BarData / AreaData)./chart-xy.md
MapMapData./chart-map.md
PiePieData./chart-pie.md
Word CloudWordCloudData./chart-wordcloud.md
GaugeGaugeData./chart-gauge.md
Liquid ProgressLiquidProgressData./chart-liquidprogress.md
SankeySankeyData./chart-sankey.md
ChordChordData./chart-chord.md
RadarRadarData./chart-radar.md
NetworkNetworkData./chart-network.md
TreeTreeData./chart-tree.md
TreemapTreemapData./chart-treemap.md

Shared options (theme, title, colors, tooltip, …): ./chart-options-common.md.

Runnable demos for every variant live in the demo site (npm start).


Options Reference

All options fields are optional. Each chart type extends the base ChartOptions; line / bar / area also share XYChartOptions.

Chart typeOptions interfaceExtendsReference
lineLineChartOptionsXYChartOptions./chart-xy.md
barBarChartOptionsXYChartOptions./chart-xy.md
areaAreaChartOptionsXYChartOptions./chart-xy.md
mapMapChartOptionsChartOptions./chart-map.md
piePieChartOptionsChartOptions./chart-pie.md
gaugeGaugeChartOptionsChartOptions./chart-gauge.md
liquidprogressLiquidProgressChartOptionsChartOptions./chart-liquidprogress.md
sankeySankeyChartOptionsChartOptions./chart-sankey.md
chordChordChartOptionsChartOptions./chart-chord.md
radarRadarChartOptionsChartOptions./chart-radar.md
networkNetworkChartOptionsChartOptions./chart-network.md
treeTreeChartOptionsChartOptions./chart-tree.md
treemapTreemapChartOptionsChartOptions./chart-treemap.md
wordcloudWordCloudChartOptionsChartOptions./chart-wordcloud.md

createChart accepts AnyChartOptions — a chart-specific literal type-checks without importing the subtype. For stricter validation, import the matching XxxChartOptions and annotate explicitly.

Cross-cutting fields (theme, title, padding, colors, colorMap, labelFontSize, tooltip, echarts): ./chart-options-common.md.

Internal design notes: ./COLORS.md (color pipeline), ./LAYOUT.md (title + legend layout).


Theming

Two built-in themes: light (default) and dark.

Custom Theme

import { registerTheme } from '@bndynet/icharts';

registerTheme({
name: 'ocean',
colorMode: 'dark', // inherit unspecified tokens from 'dark'
colors: {
background: '#0c1a2e',
textPrimary: '#ccd6f6',
},
palette: ['#64ffda', '#00b4d8', '#48cae4'],
});

createChart(el, 'bar', data, { theme: 'ocean' });

Call registerTheme once at application start, before any charts are created.

Global Default Theme via configure

You can also set a library-level default theme through configure. When a chart does not specify options.theme, this configured theme will be used.

import { configure } from '@bndynet/icharts';

configure({
theme: {
name: 'ocean',
colorMode: 'dark',
colors: {
background: '#0c1a2e',
textPrimary: '#ccd6f6',
},
palette: ['#64ffda', '#00b4d8', '#48cae4'],
},
});

options.theme still takes precedence over the global config for that specific chart.

Consistent Colors Across Charts

When building dashboards with multiple charts, enable consistentColors so that the same name always gets the same color — regardless of which chart it appears in or how many series that chart has.

import { configure } from '@bndynet/icharts';

configure({ consistentColors: true });

With this enabled, if "Revenue" is palette color #1 in a line chart, it will also be #1 in a pie chart, a bar chart, or any other chart on the page.

Pre-register specific name → color mappings (sticky pins):

import { setColorMap } from '@bndynet/icharts';

// Apply to all themes
setColorMap({
'Revenue': '#ff6384',
'Expenses': '#36a2eb',
'Profit': '#4bc0c0',
});

// Apply only to the dark theme
setColorMap({ 'Revenue': '#ff8fab' }, 'dark');

Pins set via setColorMap are sticky: they survive switchTheme() and resetColorMap(), so a single call at app startup is enough. The auto- assigned palette slots get wiped by SPA navigation, but your pinned colors do not.

Between-page state — usually automatic:

switchTheme(name) now clears the target theme's auto-assigned color slots as part of the switch, so most SPA pages do not need to call resetColorMap() directly — mounting a page that calls switchTheme(currentTheme) automatically restarts palette consumption at index 0, while preserving any setColorMap pins.

import { resetColorMap } from '@bndynet/icharts';

// Use these only if a page doesn't call switchTheme on mount, or to wipe
// state mid-page without changing theme:
resetColorMap(); // every theme — auto entries cleared, pins kept
resetColorMap('dark'); // single theme — auto entries cleared, pins kept

Per-chart colors and colorMap options always take highest priority regardless of the global setting.


Global API

FunctionDescription
configure(opts)Set global options (e.g. { consistentColors: true } or { theme: { name, colors, palette } })
switchTheme(name)Switch all charts to a registered theme
registerTheme(config)Register a custom theme
setColorMap(map, themeName?)Pre-register name → color mappings (all themes or one)
resetColorMap(themeName?)Clear accumulated color assignments (all themes or one)
getSeriesColor(name)Get the assigned color (with hover/active/disabled states) for a name
getCurrentTheme()Get the active theme object
getThemeColors()Get the active theme's UI color tokens
registerAdapter(type, adapter)Register a custom chart type adapter (warns when shadowing a built-in type)
getAdapter(type)Look up the registered adapter for a type (or undefined)
hasAdapter(type)Whether an adapter is registered for type
listAdapters()Type strings of every registered adapter (built-in + custom)
unregisterAdapter(type)Remove a registered adapter; returns whether one existed

Instance API

createChart() returns an instance with these methods:

MethodDescription
update(data?, options?)Re-render with new data / options
setTheme(name)Switch to a registered theme without re-creating the instance
resize()Trigger resize (e.g. after container size change)
dispose()Destroy the chart and free memory. Called automatically when the container leaves the DOM (idempotent — safe to call again).
getEChartsInstance()Access the underlying ECharts instance

Events

Listen for clicks and hovers with typed options.events handlers — no need to reach into the raw ECharts instance. Each handler receives a normalized ChartEventContext whose data reuses the same item/edge shape that tooltip.customHtml gets:

createChart(el, 'pie', data, {
events: {
onClick: (ctx) => {
// ctx.type === 'click'
if (ctx.data?.kind === 'item') {
console.log('clicked slice', ctx.data.name, ctx.data.value, ctx.data.color);
}
},
onMouseOver: (ctx) => {
/* ctx.data?.kind: 'item' (slice / node / word) | 'edge' (link) | undefined */
},
},
});

ChartEventContext:

interface ChartEventContext {
type: 'click' | 'dblclick' | 'mouseover' | 'mouseout';
data?: TooltipContextItem | TooltipContextEdge; // narrow with data.kind
componentType?: string; // 'series' | 'markPoint' | 'title' | …
seriesType?: string; // 'line' | 'pie' | 'sankey' | …
seriesIndex?: number;
raw: unknown; // raw ECharts params — escape hatch
}
  • A click on a pie slice / sankey node / word-cloud word → data.kind === 'item'; on a sankey / chord / network link → data.kind === 'edge'. There is no 'axis' event kind (ECharts clicks always hit a single data item).
  • data is undefined when the hit wasn't on a series item (legend, title, empty canvas) — fall back to componentType / raw.
  • Handlers stay in sync across update({ events }) (no stacked listeners) and are detached on dispose(). A throwing handler is swallowed so it can't break ECharts' event dispatch.
  • For events not covered (legendselectchanged, datazoom, …) use getEChartsInstance().on(...) directly.

Type-safe by chart type

createChart infers data and options from the type argument, so a mismatch is a compile-time error (not a runtime throw) and editors offer accurate completions for the chart you picked:

// ✅ data is narrowed to PieData, options to PieChartOptions
createChart(el, 'pie', [{ name: 'A', value: 1 }], { variant: 'doughnut' });

// ❌ compile error — XYData is not assignable to PieData
createChart(el, 'pie', { categories: [], series: [] });

Passing a dynamic string type still works and falls back to the broad ChartData / AnyChartOptions unions. The same inference applies to chart.update(data, options).


Extensibility

Custom chart types can be registered via registerAdapter. Each adapter implements a validate guard and a resolve function that returns a full ECharts option object.

import { registerAdapter, createChart, type ChartAdapter } from '@bndynet/icharts';

interface ScatterPoint { x: number; y: number }

const scatterAdapter: ChartAdapter = {
validate(data) {
return Array.isArray(data) && data.every((d) => 'x' in d && 'y' in d);
},
resolve(data) {
const points = data as ScatterPoint[];
return {
option: {
xAxis: { type: 'value' },
yAxis: { type: 'value' },
series: [{ type: 'scatter', data: points.map((d) => [d.x, d.y]) }],
},
};
},
};

registerAdapter('scatter', scatterAdapter);

createChart(el, 'scatter', [{ x: 1, y: 2 }, { x: 3, y: 5 }]);

Making a custom type first-class (no as casts)

Fold your type into ChartTypeRegistry via declaration merging and the custom type gets the same inference as a built-in — createChart('scatter', …) will type-check data as your shape with full editor completions:

declare module '@bndynet/icharts' {
interface ChartTypeRegistry {
scatter: { data: ScatterPoint[]; options: ChartOptions };
}
}

// data is now inferred as ScatterPoint[] — no cast in the adapter or call site
createChart(el, 'scatter', [{ x: 1, y: 2 }]);

Adapter capabilities

The object returned by resolve and the adapter itself support a few optional hooks:

FieldOnPurpose
onInit(instance)resolve resultRuns after the first setOption — attach event listeners, read canvas dimensions, etc.
notMergeresolve resultForwarded to ECharts setOption(option, notMerge). Default true (full replace); set false to let ECharts animate transitions across successive update() calls.
mergeData(prev, next)adapterFold the next update() data into the previous frame instead of replacing it — lets a live chart accept a partial patch (e.g. gauge update({ value }) carries max / label forward). The engine only calls it when both frames pass validate.
clearOnThemeChangeadapterWhen true, the engine clears the instance before repainting on setTheme() — needed by custom-series renderers (e.g. wordcloud) that leave stale marks during diff/merge.

onInit cleanup. If onInit wires a ResizeObserver, event listener, or timer, return a teardown function. The engine runs it before the next render's onInit and once more on dispose() — so you never leak observers and never have to poll isDisposed():

const adapter: ChartAdapter = {
validate: (d) => Array.isArray(d),
resolve: (data) => ({
option: buildOption(data),
onInit: (chart) => {
const ro = new ResizeObserver(() => chart.resize());
ro.observe(chart.getDom());
return () => ro.disconnect(); // engine calls this on re-render + dispose
},
}),
};

Registry introspection

getAdapter(type) / hasAdapter(type) / listAdapters() / unregisterAdapter(type) let you inspect and manage the registry — branch before calling createChart, build a type picker, or swap an adapter during hot-reload. Re-registering a built-in type logs a console.warn (the override still applies); use a distinct type string for custom charts to avoid the warning.

For the full runtime model — the _apply render loop, the onInit teardown lifecycle, the engine-vs-adapter boundary, and how to add per-type behavior without touching the engine — see the design guide in docs/LIFECYCLE.md.


Development

npm install
npm run dev # Watch mode
npm run build # Build all formats
npm run typecheck # Type check

License

MIT