---
url: https://chartbuddy.io/embed/docs/guides/vue.md
---
# Vue

```bash
npm install @chartbuddy.io/embed vue
```

`vue` is an optional peer dependency — install it only if you use these bindings.
Vue 3.

```vue
<script setup>
import { InsightChart } from '@chartbuddy.io/embed/vue';

const chartData = {
  chartType: 'clusteredBar',
  isDataTransposed: true,
  seriesData: [['', 'Q1', 'Q2'], ['Revenue', 100, 112]],
};
</script>

<template>
  <InsightChart :chart-data="chartData" style="height: 400px" />
</template>
```

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 `vue` as a bare specifier, so a plain HTML page needs an
**import map**. Use the browser ESM build of Vue, and render without SFCs:

```html
<script type="importmap">
  {
    "imports": {
      "vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.prod.js",
      "@chartbuddy.io/embed": "https://unpkg.com/@chartbuddy.io/embed",
      "@chartbuddy.io/embed/vue": "https://unpkg.com/@chartbuddy.io/embed/vue.mjs"
    }
  }
</script>

<div id="app"></div>

<script type="module">
  import { createApp, h } from 'vue';
  import { InsightChart } from '@chartbuddy.io/embed/vue';

  const chartData = {
    chartType: 'clusteredBar',
    isDataTransposed: true,
    seriesData: [['', 'Q1', 'Q2'], ['Revenue', 100, 112]],
  };

  createApp({
    render: () => h(InsightChart, { chartData, style: 'height: 400px' }),
  }).mount('#app');
</script>
```

The import map must map **both** `vue` and `@chartbuddy.io/embed/vue`, and the
subpath needs the explicit `.mjs` file — bare-specifier subpath resolution does
not work in browsers.

For a standalone artifact with no framework at all, the
[custom element](/guides/angular#other-frameworks) needs no import map.

## Why use the wrapper

`new Insight()` owns a DOM node and lives across renders. The wrapper handles the
lifecycle so you don't:

* **Data changes patch instead of remount.** A new `chartData` calls `update()` on
  the live instance. Rebuilding the chart on every change makes an embedded
  editor feel broken.
* **Only construction options remount.** Changing `editable` or `instanceId`
  rebuilds the chart; everything else is applied in place.
* **Reactive proxies never reach the engine.** The wrapper passes `toRaw()`
  config, so Vue's proxy wrapper doesn't leak into chart internals.
* **Unmount destroys.** Listeners and the instance are cleaned up on
  `onBeforeUnmount`.

## Editable charts with two-way data

```vue
<script setup>
import { ref } from 'vue';
import { InsightChart } from '@chartbuddy.io/embed/vue';

const chartData = ref(initialConfig);

function onChange(cd) {
  chartData.value = cd;
}
</script>

<template>
  <InsightChart
    :chart-data="chartData"
    editable
    style="height: 480px"
    @change="onChange"
  />
  <button @click="save(chartData)">Save</button>
</template>
```

`change` 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.

## Assign, don't mutate

`chartData` is compared **by identity**, not deeply. Mutating a nested property in
place will not be detected:

```js
// Not picked up — same object identity
chartData.value.title.text = 'FY26';

// Picked up
chartData.value = { ...chartData.value, title: { text: 'FY26' } };
```

## Events

| Event | Payload |
|---|---|
| `ready` | The `Insight` instance |
| `change` | `ChartData \| null` |
| `mode` | `'view' \| 'edit'` |
| `error` | `Error` — a `ChartDataValidationError` for invalid config |

## `useInsight` for imperative access

When you need the instance itself — PNG export, edit mode, revision tracking —
use the composable and place the container yourself.

```vue
<script setup>
import { ref } from 'vue';
import { useInsight } from '@chartbuddy.io/embed/vue';

const chartData = ref(config);

const { containerRef, insight, ready, error } = useInsight({
  chartData,
  instanceId: 'revenue',
  onReady: () => console.log('booted'),
});
</script>

<template>
  <div ref="containerRef" style="height: 400px" />
  <button :disabled="!ready" @click="insight.downloadPng()">Download PNG</button>
  <button :disabled="!ready" @click="insight.enterEditMode()">Edit</button>
  <p v-if="error" role="alert">{{ error.message }}</p>
</template>
```

`chartData` accepts a plain object, a `ref`, or a getter. `insight` is a
`shallowRef` holding `null` until the mount resolves — guard imperative calls on
`ready`.

## What remounts, what patches

| Prop change | Effect |
|---|---|
| `chartData` | `update()` on the live instance |
| `instanceId`, `editable`, `assetBase`, `hostOverrides` | Full remount (destroy + construct) |
| Anything else in the parent | Nothing |

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 fails at mount. The
wrapper surfaces it rather than throwing through your render:

```vue
<script setup>
import { ChartDataValidationError } from '@chartbuddy.io/embed';

function onError(err) {
  if (err instanceof ChartDataValidationError) {
    issues.value = err.errors; // [{ path, code, expected, suggestion }, …]
  }
}
</script>

<template>
  <InsightChart :chart-data="chartData" @error="onError" />
</template>
```

See [Validation](/quality-assurance/validation).

## Dashboards

One `<InsightChart>` per chart, each with a stable `instance-id`. Duplicate ids on
a page throw, so derive them from your data rather than the loop index if the
list can reorder.

```vue
<InsightChart
  v-for="panel in panels"
  :key="panel.id"
  :instance-id="panel.id"
  :chart-data="panel.chartData"
  style="height: 300px"
/>
```

The engine loads once per page, not once per chart — but read
[Installation](/getting-started/installation#bundle-size-and-many-charts) before
putting a lot of charts on one screen.

## Nuxt / SSR

The bindings are browser-only: the chart engine needs a real DOM. The mount
happens in `onMounted`, so nothing runs during SSR, but wrap usage in
`<ClientOnly>` so Nuxt does not try to render it on the server:

```vue
<ClientOnly>
  <InsightChart :chart-data="chartData" style="height: 400px" />
</ClientOnly>
```

There is no headless/Node render path today, so charts cannot be pre-rendered to
PNG on a server.
