Template component wrapper
@chart-on/vue wraps the engine and the shell as components and composables for apps built with a template-based component framework (version 3.5 or later), including apps that render every page on the server first. It follows the wrapper contract: the chart is created when the component mounts and destroyed when it unmounts, and changing a prop updates the chart in place instead of creating a new one.
| Export | From | What it is |
|---|---|---|
<Chart> | @chart-on/vue | The engine alone, in a div. |
useChart() | @chart-on/vue | A ref holding the Chart of the nearest <Chart> or <ChartShell>. |
<ChartShell> | @chart-on/vue/shell | The widget: toolbar, drawing rail, bottom bar and the chart. |
useChartShell() | @chart-on/vue/shell | A ref holding the ChartShell of the nearest <ChartShell>. |
Install
pnpm add @chart-on/vue @chart-on/core
# Only for <ChartShell>:
pnpm add @chart-on/uiThe framework (version 3.5 or later), @chart-on/core and @chart-on/ui are peer dependencies: the app installs them once and the wrapper uses the app's copies. @chart-on/ui is optional, because <ChartShell> lives in its own entry, @chart-on/vue/shell. An app that only imports @chart-on/vue never loads it.
The components are plain component objects, so they need no compiler plugin. Import them where you use them, or register them globally with app.component('Chart', Chart).
The engine
Give the component a height; the chart fills its container. class, style and other attributes go to the container div.
<script setup lang="ts">
import { createMockDataSource, dataSource as dataSourcePlugin } from '@chart-on/core';
import { Chart } from '@chart-on/vue';
defineProps<{ symbol: string }>();
// Created once per component instance: a new source reloads the data.
const dataSource = createMockDataSource();
const options = { barSpacing: 8, plugins: [dataSourcePlugin()] };
</script>
<template>
<Chart style="height: 480px" :options="options" :data-source="dataSource" :symbol="symbol" interval="1h" />
</template>| Prop | Applied with | Notes |
|---|---|---|
options | createChart at mount, then chart.applyOptions | Watched deeply. Plain data in it is compared by value, so an object literal in the template does not apply again on every render, and a change inside a reactive object applies. Functions, dates and class instances inside it are compared by identity: a formatter written inline in the template is new on every render and applies the options again each time, so define callbacks once in the script. The chart gets a plain copy, never a reactive proxy. Leaving out a key does not reset it; pass the value you want. |
dataSource | chart.setDataSource | Changing it attaches the new source and loads symbol again. Keep it in a plain variable, a shallowRef or markRaw: the wrapper unwraps a reactive one, but a deeply reactive source makes its own work slower. Removing the prop keeps the current source; pass null to detach it. The chart needs the dataSource() plugin in options.plugins (or builtins()); without it the source is not attached and development builds warn once (data-source-missing). |
symbol, interval | chart.setSymbol | Loads once both are set and a dataSource is given; loads again when either changes. A failed load emits data:error. |
on | chart.on | Event callbacks, see Events. |
A prop left out, or set to undefined after having a value, leaves the chart as it is: the wrapper stops controlling it but does not reset it. To detach the data source, pass null.
A few options are fixed when the chart is created, for example gl. Changing them in options later has no effect; remount the component on purpose with a new key instead.
One of them is plugins, which gives the chart opt-in features such as annotations, drawings and heatmaps: put plugins: chartPlugins in options, with const chartPlugins = builtins() at module scope. Create the list once, since the functions in it are compared by identity.
Reaching the chart
The component gives the app the Chart it created, with the whole imperative API. It exists only while the component is mounted: it is null during server rendering and before the first mount. The wrapper keeps it out of deep reactivity (shallowRef and markRaw), so calling into it never goes through a proxy. Do the same if you store it yourself: shallowRef, not ref or reactive.
With a template ref, which reaches the component's exposed chart:
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { Chart, type ChartExposed } from '@chart-on/vue';
const view = useTemplateRef<ChartExposed>('view');
</script>
<template>
<button @click="view?.chart?.fitContent()">Fit</button>
<Chart ref="view" style="height: 400px" />
</template>With useChart() in a component inside the default slot. It returns a ref that holds null until the chart exists, so a watch on it runs at the right time, and its clean-up runs before the chart is destroyed:
<!-- Candles.vue -->
<script setup lang="ts">
import { onWatcherCleanup, watch } from 'vue';
import type { Bar } from '@chart-on/core';
import { useChart } from '@chart-on/vue';
const props = defineProps<{ bars: Bar[] }>();
const chart = useChart();
watch(
[chart, () => props.bars],
([current, bars]) => {
if (!current) return;
const series = current.addCandles();
series.setData(bars);
onWatcherCleanup(() => current.removeSeries(series));
},
{ immediate: true },
);
</script><template>
<Chart style="height: 400px">
<Candles :bars="bars" />
</Chart>
</template>For live ticks, keep the series handle and call series.update(bar): data never travels through a remount.
Events
The on prop takes one callback per event, keyed by the chart's own event names. Only the events listed are subscribed, so leave out the busy ones (crosshair:move, frame:render) unless you use them. The payload is passed on unchanged.
<Chart
style="height: 400px"
:on="{
'crosshair:move': (e) => (hover = e?.time ?? null),
'data:error': (e) => console.warn(e.symbol, e.error),
}"
/>A new callback for the same event is used straight away without subscribing again, so inline functions are fine. Removing a key unsubscribes it, and unmounting unsubscribes everything.
Events come through a prop rather than @event listeners because the chart and the shell share some event names (theme:change): on and shellOn say which one you mean.
The widget
<ChartShell> mounts the full shell. Import its stylesheet once in the app.
<script setup lang="ts">
import { ChartShell } from '@chart-on/vue/shell';
import '@chart-on/ui/style.css';
defineProps<{ dark: boolean }>();
</script>
<template>
<ChartShell
style="height: 600px"
:theme="dark ? 'dark' : 'light'"
:features="{ tooltip: true }"
:shell-options="{ navigator: true }"
:shell-on="{ 'theme:change': (e) => console.log(e.resolved) }"
/>
</template>It takes the same options, dataSource, symbol, interval and on props and container attributes as <Chart>, plus:
| Prop | Applied with | Notes |
|---|---|---|
theme | shell.setTheme | dark, light or system. |
palette | shell.setPalette | |
features | shell.applyFeatures | Compared by value. |
shellOptions | createChartShell at mount | Toolbar, rail, dialogs and the rest of the set-up. Read once; remount with a new key to change them. |
shellOn | shell.on | Shell event callbacks, like on. |
As with <Chart>, setting theme, palette or features to undefined leaves the shell as it is, including a theme the user picked in the shell.
A template ref reaches shell and its chart (type ChartShellExposed). Inside the shell, useChart() holds that chart and useChartShell() the shell.
Kept-alive components
Inside <KeepAlive>, a hidden component is not unmounted: the chart, its data and its view stay. While hidden the chart has no size, so it draws nothing; when shown again it measures its container and draws at the right size straight away.
Server rendering
Both entries import on a server without touching a browser global, and both components render on a server as their container div with the slot content inside. The chart is created in the mount hook, which runs only in the browser, after the page has hydrated. Nothing is needed to opt in: meta-frameworks that render every page on the server and then hydrate it can use the components like any other, and useChart() holds null until the browser mounts the chart.
To skip server rendering for a page section altogether, for example a section that only makes sense with a chart, wrap it in the framework's client-only component (often <ClientOnly>). The wrapper does not need it.
Tests
Tests mount each component, change every prop and check that the same instance received the matching method call, check that events the first load emits while mounting reach the callbacks, hide and show a kept-alive chart, unmount it and check that no WebGL context or animation frame is left (also over 100 mount and unmount cycles), render both components on a server, and hydrate server HTML without mismatches.