@chartbuddy.io/embed
v1.8.13
Published
Embed ChartBuddy Insights via new Insight() — default view-only with hover ball (Download/Drag to slide/Edit; Done exits edit); allowEdit:false for display-only exports; editable:true for the full editor; editSession:'locked' for always-on edit without mo
Maintainers
Readme
ChartBuddy Embed API (@chartbuddy.io/embed)
Create ChartBuddy Insights in any web page. Default is view-only (headless
draw): hover the top-left ChartBuddy ball for Download (PNG), Export
config (JSON), and Edit (full interactive editor). Pass editable: true
to open the editor immediately. Spreadsheet is not included. No account or
chart ID required: pass data via chartData.
Public API: Insight (via new Insight()), getInsights, snapshotInsights,
version. Global registry: window.__CHARTBUDDY_INSIGHTS__.
Every JS entry (package root,
.single.mjs, multi loader) opens with a comment header naming the exports and a minimal example. Agents: start atllms.txt, then fetch linked docs.mdpages as needed. Also: https://unpkg.com/@chartbuddy.io/embed/llms.txt · https://chartbuddy.io/embed/docs/llms.txt. Package root on unpkg resolves to a readable stub that re-exports the single-file build.
Claude / strict CSP (recommended)
import { Insight } from 'https://unpkg.com/@chartbuddy.io/embed';
// equivalent:
// import { Insight } from 'https://unpkg.com/@chartbuddy.io/embed/chartbuddy-embed.single.mjs';<div id="chart" style="width:960px;height:560px"></div>
<script type="module">
import { Insight } from 'https://unpkg.com/@chartbuddy.io/embed';
// Default: view-only + ChartBuddy hover ball
new Insight('#chart', {
instanceId: 'revenue', // stable id for multi-mount
chartData: {
chartType: 'clusteredBar',
isDataTransposed: true,
seriesData: [
['', 'Q1', 'Q2', 'Q3', 'Q4'],
['Revenue', 100, 112, 125, 140],
['Costs', 60, 66, 70, 78],
],
title: { visible: true, text: 'Revenue vs Costs' },
subtitle: { visible: false, text: '' },
},
});
// Full editor: new Insight('#chart', { editable: true, chartData: … })
</script>Do not pass assetBase on this path. Bundler: import { Insight } from '@chartbuddy.io/embed'.
Install / multi-file (normal websites)
npm install @chartbuddy.io/embedCache-friendly multi-file loader (siblings OK when CSP allows):
https://unpkg.com/@chartbuddy.io/embed/chartbuddy-embed.mjsor import { Insight } from '@chartbuddy.io/embed/multi'.
With React
Use the bundled bindings — they handle StrictMode's double mount, patch the live chart on data changes instead of remounting, and destroy on unmount.
import { InsightChart } from '@chartbuddy.io/embed/react';
export function ChartEmbed({ chartData }) {
return <InsightChart chartData={chartData} style={{ width: '100%', height: 560 }} />;
}react is an optional peer dependency (React 18 or 19). For imperative access to
the instance:
import { useInsight } from '@chartbuddy.io/embed/react';
const { ref, insight, ready, error } = useInsight({ chartData, editable: true });
// <div ref={ref} style={{ height: 400 }} />chartData is compared by identity — memoize it if you build it inline.
Without a bundler, a plain HTML page needs an import map mapping both react
and the subpath (which needs the explicit .mjs file):
<script type="importmap">{"imports":{
"react": "https://esm.sh/react@19",
"react-dom/client": "https://esm.sh/react-dom@19/client",
"@chartbuddy.io/embed/react": "https://unpkg.com/@chartbuddy.io/embed/react.mjs"
}}</script>Use React.createElement there — no build step means no JSX. For a standalone
artifact, plain new Insight() or the custom element is simpler than either.
With Vue 3
<script setup>
import { InsightChart } from '@chartbuddy.io/embed/vue';
</script>
<template>
<InsightChart :chart-data="chartData" style="height: 400px" @change="onChange" />
</template>vue is an optional peer dependency. useInsight() is also exported for
imperative access. Assign a new object to chartData to patch the chart —
mutating in place is not detected.
With Angular (and Svelte / Solid / plain HTML)
Angular uses the <chartbuddy-insight> custom element rather than a compiled
Angular library, so ChartBuddy upgrades are not tied to your Angular major. No
peer dependency.
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import '@chartbuddy.io/embed/element';
// template:
// <chartbuddy-insight [chartData]="cd" instance-id="revenue"
// style="display:block; height:400px"
// (change)="onChange($event)"></chartbuddy-insight>chartData must be set as a property (it is an object). Events are
CustomEvents — read event.detail. Full guide:
https://chartbuddy.io/embed/docs/guides/angular
Usage (script tag / global)
<script src="https://unpkg.com/@chartbuddy.io/embed/chartbuddy-embed.global.js"></script>
<script>new ChartBuddyEmbed.Insight('#chart');</script>Single-file IIFE: chartbuddy-embed.single.global.js.
API
new Insight(target: string | HTMLElement, options?: {
chartData?: object; // ChartBuddy chart-data (cd) — see below
assetBase?: string; // multi-file only; ignored by single-file
editable?: boolean; // default false → view mode; true → full editor
allowEdit?: boolean; // default true; false = Download/Drag only (no Edit)
editSession?: 'toggle' | 'locked'; // locked = always edit, no mode chrome
host?: { // optional chrome hooks
getToolbarPlacement?: () => object; // widget (default) | float | dock
getToolbarContainer?: () => string | Element | null;
positionToolbar?: (toolbar, target) => void; // float only
getPopupContainer?: () => string | Element | null; // menus; default body
startDragging?: (event) => void;
};
}): {
container: HTMLElement;
assetBase: string;
instanceId: string;
chartContainerId: string;
mode: 'view' | 'edit';
editSession: 'toggle' | 'locked';
allowEdit: boolean;
ready: Promise<void>;
chart: any | null;
focus(): void;
setChartData(cd: object): void; // partial OK — chartType optional
setData(seriesData: any[][]): void;
update(patch?: object): void; // no arg = redraw only
getChartData(): object | null;
on(event: 'ready' | 'mode' | 'change', handler: Function): () => void;
off(event: 'ready' | 'mode' | 'change', handler: Function): void;
isDirty(): boolean;
getRevision(): number;
toPngBlob(options?: { scale?: number; background?: string | null }): Promise<Blob>;
toPngBase64(options?: { scale?: number; background?: string | null }): Promise<string>;
// PNG helpers default background to #ffffff; downloadPng defaults transparent
downloadPng(options?: { scale?: number; background?: string | null }): Promise<void>;
dragExport(): Promise<void>;
exportConfig(): void;
enterEditMode(): Promise<void>; // view → edit (no-op if allowEdit: false)
exitEditMode(): Promise<void>; // edit → view (also ball → Done)
destroy(): void;
};
getInsights(): Record<string, Insight>;
snapshotInsights(options?: {
png?: boolean;
scale?: number;
background?: string | null;
}): Promise<{ schemaVersion: 1; version: string; charts: Record<string, any> }>;
version: string;View mode (default)
- Headless chart draw (no formatting toolbar / selection chrome).
- On hover: ChartBuddy ball (top-left) → Download PNG, Drag to slide, Edit.
enterEditMode()/ ball Edit remounts the full editor with the currentchartData.
Display-only (allowEdit: false)
- Same view ball and exports, but Edit is omitted.
enterEditMode()is a no-op.
Edit mode (editable: true)
- Interactive ChartBuddy editor with formatting UI.
- Default toolbar placement is widget (modules in the morph rail). Override with
host.getToolbarPlacement. - Leave via ball Done or
exitEditMode()(returns to view with current config).
Locked edit (editSession: 'locked')
- Always-on editor: no Done / morph.
enterEditMode()/exitEditMode()are no-ops.- Use when the host page owns surrounding chrome (demos, dialogs).
chartData (cd) schema
options.chartData is a ChartBuddy chart-data object. Partial objects are
merged over defaults (nested title / subtitle / legend / axes /
canvas / footnote are deep-merged).
Required
| Field | Type | Notes |
|-------|------|--------|
| chartType | string | See allowed values below |
| seriesData | array[] | 2D grid (see layout) |
Recommended
| Field | Type | Notes |
|-------|------|--------|
| isDataTransposed | boolean | Prefer true with the grid layout below |
| title | text block | { visible, text } — text may be plain or HTML |
| subtitle | text block | Same shape; set visible: false to hide the default subtitle |
| footnote | text block | Same shape |
| legend | object | { visible, colors?: string[], … } — series colors live in legend.colors |
| backgroundColor | string | Default "transparent" |
| orientation | "vertical" \| "horizontal" | Default "vertical" |
Allowed chartType values
CHARTBUDDY:GENERATED-CHART-TYPES:BEGIN
barMekko · clusteredBar · combo · line · mekko · pie · scatter
stackedArea · stackedArea100 · stackedBar · stackedBar100
waterfall
Also accepted: bubble → scatter, donut → pie.
CHARTBUDDY:GENERATED-CHART-TYPES:END
Aliases are shortcuts that seed defaults — they are not just renames:
| You pass | Resolves to | Seeded |
|---|---|---|
| 'donut' | pie | pie.innerRadiusRatio: 0.5 (a real hole) |
| 'pie' | pie | pie.innerRadiusRatio: 0 (a full pie) |
| 'bubble' | scatter | a larger point diameter |
For a donut, just pass chartType: 'donut' — setting innerRadiusRatio yourself
is only needed to override the hole size. getChartData() returns pie with
innerRadiusRatio: 0.5, which round-trips correctly.
Validation
Chart data is validated at the API boundary — both new Insight({ chartData })
and setChartData() / setData() / update(). Invalid input throws with
the path of every problem, so you get one actionable error instead of a blank
chart:
insight.setChartData({ chartType: 'pie', pie: { innerRadiusRatio: 5 } });
// Error: [chartbuddy/embed] setChartData: invalid chart data — 1 problem:
// • pie.innerRadiusRatio: 5 is above the maximum of 1What is checked: chartType (with a did-you-mean hint), field types, enums,
numeric ranges, the seriesData grid (including the per-type row/column minimums
listed above), and whether an option bag matches the chart type — bar options on
a pie chart are rejected. Ragged seriesData (unequal row lengths) warns
but does not throw. Duplicate instanceId values on the same page throw.
Unknown keys are never an error, so a config written for a newer version still
loads. chart-schema.json is the machine-readable form of the same rules, usable
for your own validation or to constrain generated or structured chartData.
Checking without mounting
The same checks are exported. validateChartData() never throws and reports
everything it found, which is what you want when a model authored the config:
import { validateChartData } from '@chartbuddy.io/embed';
const { valid, errors, warnings } = validateChartData(candidate);
// errors[0] → {
// path: 'chartType',
// code: 'unknown-chart-type',
// severity: 'error',
// message: '"clusterdBar" is not a known chart type. Did you mean "clusteredBar"?',
// expected: 'a ChartType or alias',
// received: '"clusterdBar"',
// allowed: ['barMekko', 'clusteredBar', 'stackedArea', …],
// suggestion: 'clusteredBar',
// }code and path are the stable contract — branch on code, repair the value at
path, and never parse message. Codes: not-an-object, unknown-chart-type,
empty-patch, wrong-type, out-of-range, not-in-enum, series-data-shape,
ragged-series-data (warning), foreign-option-bag.
Throws carry the same objects:
import { ChartDataValidationError } from '@chartbuddy.io/embed';
try {
insight.setChartData(candidate);
} catch (err) {
if (err instanceof ChartDataValidationError) console.log(err.errors);
}Partial updates
await insight.ready;
insight.setData([['', 'Q1'], ['Revenue', 120]]); // data only
insight.update({ title: { text: 'FY26' } }); // any partial patch
insight.update(); // redraw onlysetChartData also accepts partials — chartType is not required.
Events
insight.on('ready', () => { … });
insight.on('mode', (mode) => { … }); // 'view' | 'edit'
insight.on('change', (cd) => { … }); // after meaningful edits
insight.isDirty(); // true since boot / last Done
insight.getRevision(); // monotonic edit counterseriesData layout (recommended)
With isDataTransposed: true, use chart-native rows-as-series:
[
['', 'Q1', 'Q2', 'Q3'], // row 0: categories ( [0][0] usually "" )
['Revenue', 100, 112, 125], // each later row = one series
['Costs', 60, 66, 70],
]Pie (category / value pairs):
[
['Category', 'Value'],
['North', 45],
['South', 30],
['East', 25],
]Scatter (transpose not supported):
[
['', 'Metric X', 'Metric Y', 'Size', 'Group'],
['Point 1', 10, 15, 8, 'A'],
['Point 2', 20, 12, 5, 'B'],
]Minimal example
{
chartType: 'clusteredBar',
isDataTransposed: true,
seriesData: [
['', 'Q1', 'Q2', 'Q3'],
['Revenue', 100, 112, 125],
],
title: { visible: true, text: 'Revenue' },
subtitle: { visible: false, text: '' },
}Full config
The richest, safest cd is a snapshot from a live editor via
insight.getChartData() (or insight.exportConfig()) after ready. Pass that JSON back as chartData.
Optional advanced fields (present on full exports): canvas, axes,
chartPositionPercentages, annotations, multilines, type-specific blocks
(bar, line, area, pie, waterfall, mekko, combo), seriesLabels.
How it works
Single-file: d3 + DOMPurify + engine + CSS in one module — use for Claude.
Multi-file: small loader pulls siblings (webapp-entry.js, vendors, CSS,
worker). Use on sites you control.
Notes
- Multi-mount supported. Construct
new Insight()multiple times in one document. - No spreadsheet in this package.
- Default view mode has no formatting toolbar; use Edit or
editable: true. - Single-file uses main-thread label placement (no worker/wasm fetch).
- Requires a browser (
document).
License
Proprietary evaluation license — © ChartBuddy. See the bundled LICENSE file.
