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, 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={chartData}
  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}
    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 documentation for Chartbuddy Embed · Not the end-user Help Center · Help Center