Skip to content

React

bash
npm install @chartbuddy.io/embed react

react 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 react and @chartbuddy.io/embed/react. The subpath needs the explicit .mjs file — bare-specifier subpath resolution does not work in browsers.
  • Use React.createElement (aliased to h above) 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 ready resolution, so you get one chart, not two.
  • Data changes patch instead of remount. A new chartData calls update() 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 changeEffect
chartDataupdate() on the live instance
instanceId, editable, toolbar, persist, assetBase, hostOverridesFull remount (destroy + construct)
onChange, onReady, onModeChange, onErrorNothing — handlers are swapped in place
className, styleNormal 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.

Developer & LLM documentation · Not the end-user Help Center · Help Center