Appearance
React
bash
npm install @chartbuddy.io/embed reactreact is an optional peer dependency — install it only if you use these bindings. React 18 or 19.
jsx
import { InsightChart } from '@chartbuddy.io/embed/react';
export function Revenue({ grid }) {
return (
<InsightChart
chartData={{ chartType: 'clusteredBar', isDataTransposed: true, seriesData: grid }}
style={{ height: 400 }}
/>
);
}The chart fills its container, so give the container a height. A container with no height renders nothing visible.
Without a bundler (CDN / single HTML file)
The bindings import react as a bare specifier, so a plain HTML page needs an import map to tell the browser where React lives. This is the whole setup — no build step:
html
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@19",
"react-dom": "https://esm.sh/react-dom@19",
"react-dom/client": "https://esm.sh/react-dom@19/client",
"@chartbuddy.io/embed": "https://unpkg.com/@chartbuddy.io/embed",
"@chartbuddy.io/embed/react": "https://unpkg.com/@chartbuddy.io/embed/react.mjs"
}
}
</script>
<div id="root"></div>
<script type="module">
import { createElement as h } from 'react';
import { createRoot } from 'react-dom/client';
import { InsightChart } from '@chartbuddy.io/embed/react';
const chartData = {
chartType: 'clusteredBar',
isDataTransposed: true,
seriesData: [['', 'Q1', 'Q2'], ['Revenue', 100, 112]],
};
createRoot(document.getElementById('root')).render(
h(InsightChart, { chartData, style: { height: '400px' } }),
);
</script>Two things to get right:
- The import map must map both
reactand@chartbuddy.io/embed/react. The subpath needs the explicit.mjsfile — bare-specifier subpath resolution does not work in browsers. - Use
React.createElement(aliased tohabove) rather than JSX, since there is no build step to compile JSX.
Import maps need a modern browser. If you are generating a standalone artifact for an AI host and JSX or import maps are awkward, skip React entirely and use the plain Insight API or the custom element — both work with a single <script type="module"> and no mapping.
Why use the wrapper
new Insight() owns a DOM node and lives across renders, which is the shape React is worst at. The wrapper handles four things you would otherwise write yourself:
- StrictMode double-mount. React mounts, unmounts, and remounts every effect in development. The wrapper tears the throwaway instance down and ignores its late
readyresolution, so you get one chart, not two. - Data changes patch instead of remount. A new
chartDatacallsupdate()on the live instance. Rebuilding the chart on every keystroke is the most common way to make an embedded editor feel broken. - Inline handlers don't remount.
onChange={() => …}is a new function every render. The wrapper reads handlers through a ref, so the chart never sees a changed dependency. - Unmount destroys. Listeners and the instance are cleaned up, so navigating away does not leak.
Editable charts with controlled data
jsx
import { useState, useMemo } from 'react';
import { InsightChart } from '@chartbuddy.io/embed/react';
export function Editor({ initial }) {
const [chartData, setChartData] = useState(initial);
return (
<>
<InsightChart
chartData={chartData}
editable
onChange={setChartData}
style={{ height: 480 }}
/>
<button onClick={() => save(chartData)}>Save</button>
</>
);
}onChange fires after live edits in edit mode, after programmatic updates, and on Done. Feeding it straight back into chartData is safe: the wrapper compares by identity, and the object it hands you is the one it already applied.
Memoize inline chartData
chartData is compared by identity, not deeply. An object literal built in the render body is a new object every render, so it fires an update() every render:
jsx
// Fires update() on every render
<InsightChart chartData={{ chartType: 'line', seriesData: grid }} />
// Fires update() only when `grid` changes
const chartData = useMemo(() => ({ chartType: 'line', seriesData: grid }), [grid]);
<InsightChart chartData={chartData} />Redundant updates are not incorrect — the chart redraws to the same result — but on a dashboard of many charts you will feel it.
useInsight for imperative access
When you need the instance itself — PNG export, edit mode, revision tracking — use the hook and place the container yourself.
jsx
import { useInsight } from '@chartbuddy.io/embed/react';
function Panel({ chartData }) {
const { ref, insight, ready, error } = useInsight({
chartData,
instanceId: 'revenue',
onReady: () => console.log('booted'),
});
return (
<>
<div ref={ref} style={{ height: 400 }} />
<button disabled={!ready} onClick={() => insight.downloadPng()}>
Download PNG
</button>
<button disabled={!ready} onClick={() => insight.enterEditMode()}>
Edit
</button>
{error && <p role="alert">{error.message}</p>}
</>
);
}insight is null until the mount resolves — guard imperative calls on ready or on insight itself.
What remounts, what patches
| Prop change | Effect |
|---|---|
chartData | update() on the live instance |
instanceId, editable, toolbar, persist, assetBase, hostOverrides | Full remount (destroy + construct) |
onChange, onReady, onModeChange, onError | Nothing — handlers are swapped in place |
className, style | Normal React DOM update |
Toggling editable remounting is deliberate: view mode and the full editor are different mounts. If you toggle it often, prefer one mount plus insight.enterEditMode() / exitEditMode().
Handling invalid data
Chart data is validated at the API boundary, so bad data throws during mount. The wrapper catches it and gives you a choice:
jsx
import { ChartDataValidationError } from '@chartbuddy.io/embed';
<InsightChart
chartData={fromLLM}
onError={(err) => {
if (err instanceof ChartDataValidationError) {
setIssues(err.errors); // [{ path, code, expected, suggestion }, …]
}
}}
/>Without an onError, the error is re-thrown during render so a React error boundary catches it — a chart that fails validation should never fail silently. See Validation.
Dashboards
One <InsightChart> per chart, each with a stable instanceId. Duplicate ids on a page throw, so derive them from your data rather than the array index if the list can reorder.
jsx
{panels.map((panel) => (
<InsightChart
key={panel.id}
instanceId={panel.id}
chartData={panel.chartData}
persist={false}
style={{ height: 300 }}
/>
))}Each chart loads the engine once per page, not once per chart — but do read Installation before putting a lot of charts on one screen.
Server-side rendering
The bindings are browser-only: the chart engine needs a real DOM. Under Next.js or Remix the mount happens in an effect, so nothing runs during SSR, but the module itself should not be imported on the server:
jsx
const InsightChart = dynamic(
() => import('@chartbuddy.io/embed/react').then((m) => m.InsightChart),
{ ssr: false },
);There is no headless/Node render path today, so charts cannot be pre-rendered to PNG on a server.