# @chartbuddy.io/embed — LLM / Claude usage guide
Package: @chartbuddy.io/embed
Public API: Insight (new Insight()), getInsights, snapshotInsights,
clearPersistedChart, version
Purpose: Mount a ChartBuddy chart in HTML. Default is view-only (hover ball:
Download PNG / Drag to slide / Edit). Pass editable:true for the full
interactive editor (canvas + formatting toolbar).
NOT Chart.js. Spreadsheet (Handsontable) is NOT included.
No ChartBuddy account / chart ID required — pass data via options.chartData.
Stable docs URLs (bookmark / fetch these):
- https://unpkg.com/@chartbuddy.io/embed/llms.txt
- https://cb-acc.xyz/docs/
- https://unpkg.com/@chartbuddy.io/embed/README.md
Every JS artifact (package root stub, .single.mjs, multi loader) opens with a
comment header naming the exports and a minimal Insight example.
## Claude Visualizer / inline artifacts (REQUIRED path)
Import the PACKAGE ROOT (readable API stub → self-contained single-file build):
```js
import { Insight } from 'https://unpkg.com/@chartbuddy.io/embed';
```
Equivalents (also fine — each file self-describes in its first lines):
- https://unpkg.com/@chartbuddy.io/embed/chartbuddy-embed.api.mjs
- https://unpkg.com/@chartbuddy.io/embed/chartbuddy-embed.single.mjs
- https://cdn.jsdelivr.net/npm/@chartbuddy.io/embed
- https://cb-acc.xyz/api/embed/chartbuddy-embed.single.mjs
Rules:
- Create with **new Insight()** (not createChart / renderChart / mountEditor / mountChart).
- Do NOT pass assetBase on this path.
- Do NOT import chartbuddy-embed.mjs (multi-file) inside Claude Visualizer.
- Do NOT expect a spreadsheet UI.
- Default is view-only; pass editable: true for editor chrome / formatting UI.
- Full chartData schema is below; also in README.md on the same package.
### Copy-paste HTML artifact
```html
ChartBuddy
```
### API
```js
import {
Insight,
getInsights,
snapshotInsights,
clearPersistedChart,
version,
} from 'https://unpkg.com/@chartbuddy.io/embed';
const insight = new Insight(target, options?);
// target: CSS selector string OR HTMLElement
// options.chartData?: ChartBuddy cd object (schema below)
// options.instanceId?: string // STABLE id for agents + persist (recommended)
// options.editable?: boolean (default false → view mode)
// options.toolbar?: boolean (edit mode only; default false)
// options.persist?: boolean (default false; needs stable instanceId to reload)
// options.assetBase?: IGNORE for single-file
await insight.ready;
insight.instanceId
insight.mode // 'view' | 'edit'
insight.chart
insight.setChartData(cd) // partial OK — chartType optional
insight.setData(seriesData) // data-only refresh
insight.update(patch?) // partial patch; no arg = redraw
insight.getChartData() // full cd snapshot — best source for a complete schema example
insight.on('ready' | 'mode' | 'change', handler) // returns unsubscribe
insight.off(event, handler)
insight.isDirty()
insight.getRevision()
insight.toPngBase64() // PNG base64 (NO download); default background #ffffff
insight.toPngBase64({ background: '#0f172a' }) // custom opaque underlay
insight.toPngBase64({ background: null }) // transparent (Slides-style)
insight.toPngBlob() // PNG as Blob (NO download); same background defaults
insight.downloadPng() // human Save dialog; default transparent
insight.downloadPng({ background: '#ffffff' }) // opaque human download
insight.exportConfig() // human JSON download
insight.enterEditMode() // view → full editor
insight.exitEditMode() // edit → view (also ball → Done); checkpoints if persist
insight.focus()
insight.destroy()
getInsights() // { [instanceId]: Insight }
await snapshotInsights() // config-only for all mounts
await snapshotInsights({ png: true }) // + pngBase64 (default white bg)
await snapshotInsights({ png: true, background: '#111827' })
clearPersistedChart(instanceId?)
version
validateChartData(cd, options?) // → { valid, issues, errors, warnings }; NEVER throws
assertValidChartData(cd, context?, options?) // throws ChartDataValidationError
formatValidationIssues(issues, context?) // human-readable string
// Also: window.__CHARTBUDDY_INSIGHTS__ === getInsights()
```
Bundler: `import { Insight } from '@chartbuddy.io/embed';`
React: `import { InsightChart, useInsight } from '@chartbuddy.io/embed/react';`
Vue 3: `import { InsightChart, useInsight } from '@chartbuddy.io/embed/vue';`
Angular / Svelte / Solid / plain HTML — custom element, no peer dependency:
`import '@chartbuddy.io/embed/element';` → ``
set `.chartData` as a PROPERTY; events are CustomEvents (ready/change/mode/error)
All bindings mount once, patch on chartData change, and destroy on unmount.
NO BUNDLER? The react/vue subpaths import `react` / `vue` as bare specifiers, so a
plain HTML page needs an import map mapping BOTH the framework AND the subpath
(subpaths need the explicit file, e.g. `.../@chartbuddy.io/embed/react.mjs`):
```html
```
Then use React.createElement (no JSX — there is no build step).
For a standalone artifact, prefer plain `new Insight()` or the custom element:
neither needs an import map or a framework.
## Agent observation (round-trip / visual QA)
Browsers cannot rewrite the HTML file. Agents (Cursor, Claude, CI) can —
ChartBuddy returns structured data; the host agent writes files.
### Recommended loop
1. Author HTML with **stable `instanceId`** per mount.
2. Human edits in-browser (ball → Edit → Done).
3. Agent observes:
```js
// CDP / Runtime.evaluate — after page has mounted Insights:
const insights = window.__CHARTBUDDY_INSIGHTS__;
const insight = insights['revenue'];
const cd = insight.getChartData();
// Default opaque white underlay — pass background for other colors:
const png = await insight.toPngBase64(); // raw base64, no data: prefix
// const png = await insight.toPngBase64({ background: '#0f172a' });
```
4. Agent writes `deck.charts.json` / `exports/revenue.png` into the workspace.
5. LLM diffs JSON vs prior seed; updates narrative or re-seeds HTML.
### PNG background note
Export strips the live SVG `#chartBackground` so Slides overlays stay transparent.
`chartData.backgroundColor` alone does **not** bake into PNGs. Use
`toPngBase64({ background })` / `toPngBlob({ background })` instead.
Agent helpers default to `#ffffff`. Pass any CSS color, or `null`/`'transparent'`
for a transparent PNG.
### Rules for agents
- Prefer **`toPngBase64` / `getChartData`** over screenshots or `downloadPng`.
- Use **`background`** on PNG helpers for visual QA (do not rely on `cd.backgroundColor`).
- Always pass **`instanceId`** on multi-chart pages (and for `persist: true`).
- Snapshot **config-only** by default; request PNG one chart at a time.
- Do **not** expect HTML to rewrite itself after edits.
- Do **not** expect `downloadPng` / `exportConfig` downloads to reach the agent.
### Optional sidecar convention (agent-written, not ChartBuddy)
```text
deck.html
deck.charts.json // all chartData from getInsights / snapshotInsights
exports/
revenue.png
```
## chartData (`cd`) schema
Partial `chartData` is merged over defaults. Nested keys `title`, `subtitle`,
`legend`, `axes`, `canvas`, `footnote` are deep-merged.
### Required
- `chartType` (string)
- `seriesData` (2D array)
### Recommended
- `isDataTransposed: true`
- `title: { visible: true, text: '…' }`
- `subtitle: { visible: false, text: '' }` // hide default placeholder subtitle
- `legend: { visible: true, colors?: string[] }` // palette is legend.colors
- `backgroundColor`, `orientation` (`"vertical"` | `"horizontal"`)
### chartType values
CHARTBUDDY:GENERATED-CHART-TYPES:BEGIN
`area` | `area100` | `barMekko` | `clusteredBar` | `combo` | `line`
`mekko` | `pie` | `scatter` | `stackedBar` | `stackedBar100` | `waterfall`
Aliases: `bubble`→`scatter`, `donut`→`pie`.
CHARTBUDDY:GENERATED-CHART-TYPES:END
Aliases are input-only and SEED DEFAULTS — they are not just renames:
chartType: 'donut' → pie with pie.innerRadiusRatio 0.5 (a real hole)
chartType: 'pie' → pie with pie.innerRadiusRatio 0 (a full pie)
chartType: 'bubble' → scatter with a larger point diameter
For a donut just pass `chartType: 'donut'`; do NOT also set innerRadiusRatio
unless you want a different hole size. getChartData() reports the CANONICAL type
(a donut round-trips as `pie` + innerRadiusRatio 0.5 — that is correct, not loss).
### Validation (read this before generating a config)
`new Insight({ chartData })` and `setChartData()` / `setData()` / `update()`
validate input and THROW on invalid data, listing the path of every problem.
Checked: chartType (with a did-you-mean hint), field types, enums, numeric
ranges, the seriesData grid and its per-type row/column minimums, and
option-bag/chartType agreement — `bar` options on a `pie` chart are rejected.
Ragged seriesData (unequal row lengths) WARNS but does not throw. Duplicate
instanceId on the same page THROWS. Unknown keys are never an error.
Partial updates are first-class: `setData(seriesData)`, `update(patch)`, and
`setChartData(partial)` all keep the current chart type when chartType is omitted.
Events: `on('ready'|'mode'|'change', handler)`, `isDirty()`, `getRevision()`.
Example failure:
`pie.innerRadiusRatio: 5 is above the maximum of 1`
BEST PRACTICE — check before you mount, instead of using a blank chart as the
error signal. `validateChartData(cd)` never throws and returns every problem:
```js
const { valid, errors, warnings } = validateChartData(cd);
// each issue: { path, code, message, severity, expected?, received?, allowed?, suggestion? }
```
Branch on `code`, NEVER on `message` (messages may be reworded in any release):
unknown-chart-type → use issue.suggestion, or pick from issue.allowed
not-in-enum → use issue.suggestion, or pick from issue.allowed
wrong-type → coerce to issue.expected (also catches NaN / Infinity)
out-of-range → clamp to the bound in issue.expected
series-data-shape → reshape; expected names the row/column minimum
ragged-series-data → WARNING; pad the short rows
foreign-option-bag → move options into the bag named in issue.suggestion
empty-patch → include chartType and/or seriesData
not-an-object → pass an object
Repair loop (no second model call needed for typos):
```js
let cd = generated;
for (const issue of validateChartData(cd).errors) {
if (issue.suggestion) cd = setAtPath(cd, issue.path, issue.suggestion);
}
```
If you mount without checking, the throw is a `ChartDataValidationError` carrying
the same objects: `err.errors`, `err.warnings`, `err.issues`, `JSON.stringify(err)`.
The machine-readable form of these rules is chart-schema.json, which can be used
to constrain structured output directly.
### seriesData layouts
Bar / line / area / stacked (rows = series):
```js
[
['', 'Q1', 'Q2', 'Q3'],
['Revenue', 100, 112, 125],
['Costs', 60, 66, 70],
]
```
Pie:
```js
[
['Category', 'Value'],
['North', 45],
['South', 30],
]
```
Scatter (no transpose):
```js
[
['', 'Metric X', 'Metric Y', 'Size', 'Group'],
['Point 1', 10, 15, 8, 'A'],
]
```
### Waterfall (DO NOT compute your own totals)
Total columns are computed. Mark the column and leave its cell empty — a value
typed into an `isTotal` column is ignored, and a closing figure typed WITHOUT
`isTotal` becomes another contribution bar (the bridge then ends at ~2x).
```js
{
chartType: 'waterfall',
isDataTransposed: true,
seriesData: [
['', 'Start', 'Price', 'Volume', 'Mix', 'End'],
// ^0 ^1 ^2 ^3 ^4 <- waterfall.columns keys
['Bridge', 100, 18, -8, 5, null], // End carries NO value
],
waterfall: { columns: { 4: { isTotal: true } } }, // -> closing bar = 115
}
```
- `waterfall.columns` keys are 0-based **data-column** indices: index into the
header row AFTER the row-label cell. In the row above `'End'` is header
position 5 but data column 4. Getting this wrong fails silently.
- `isTotal: true` — show the running total, ignore this column's own value.
- `startBar: true` — reset to zero and drop the connector (new sequence).
A mid-chart opening bar needs BOTH `isTotal: true` and `startBar: true`;
`startBar` alone still renders as a floating contribution.
- `showSegments: true` — draw a total as per-series segments instead of a solid bar.
- Column 0 is always forced to `isTotal` + `startBar`, and unlike other totals it
DOES use its own value (the opening balance).
### Full cd
Prefer `insight.getChartData()` / `insight.exportConfig()`, or author in the editor then snapshot.
Advanced fields on full exports: `canvas`, `axes`, `annotations`, `multilines`,
type-specific blocks (`bar`, `line`, `pie`, …), `seriesLabels`, `chartPositionPercentages`.
## Multi-mount dashboard
Construct `new Insight()` once per container in the same document. Each instance is isolated.
Pass a stable `instanceId` per mount so agents can address charts by name.
```js
new Insight('#a', { instanceId: 'revenue', chartData: … });
new Insight('#b', { instanceId: 'bridge', chartData: … });
await snapshotInsights(); // { charts: { revenue: { chartData }, bridge: { … } } }
```
## Fallbacks
| Situation | Action |
|-----------|--------|
| Claude Visualizer / strict script-src | single-file unpkg/jsDelivr |
| Host blocks large ESM | iframe https://cb-acc.xyz/embed/ |
| You control page + siblings OK | multi-file chartbuddy-embed.mjs |
## Do not
- Pass assetBase with single-file
- Import sibling d3 / webapp-entry / wasm / worker yourself
- Expect Handsontable / spreadsheet windows
- Expect `downloadPng` to feed agents (use `toPngBase64`)
- Expect the browser to rewrite HTML / sidecar files
- Use for production client-facing apps under the current evaluation LICENSE
## Verification checklist
- `document.querySelectorAll('svg[id^="chart-svg-"]').length >= 1`
- `document.querySelectorAll('.spreadsheet-window').length === 0`
- Default view: no `.webapp-toolbar`; hover → `[data-cb-embed-chrome]`
- Network: one ChartBuddy JS request (`.single.mjs`) plus maybe fonts
- Observation: `Object.keys(window.__CHARTBUDDY_INSIGHTS__ || {}).length >= 1`
- Observation: `typeof insight.toPngBase64 === 'function'` and `getChartData()` returns `chartType`