Appearance
Vue
bash
npm install @chartbuddy.io/embed vuevue 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 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
chartDatacallsupdate()on the live instance. Rebuilding the chart on every change makes an embedded editor feel broken. - Only construction options remount. Changing
editableorinstanceIdrebuilds 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, toolbar, persist, 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="fromLLM" @error="onError" />
</template>See 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"
:persist="false"
style="height: 300px"
/>The engine loads once per page, not once per chart — but read Installation 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.