@j-t-mcc/vue3-chartjs

Vue3 wrapper for Chart.js
207,152 119
Install
npm install @j-t-mcc/vue3-chartjs
Latest Version:3.0.0
Vue:3
License:MIT
Last Updated:Sep 3, 2026
Links: GitHub  ·  npm
Maintainer: j-t-mcc

For the v2 API, see v2.1.0

For ChartJS 3.1, see v1.3.0

For ChartJS 2, see v0.3.0

Requirements

  • Vue 3
  • ChartJS 4

Installation

pnpm add chart.js @j-t-mcc/vue3-chartjs

npm install chart.js @j-t-mcc/vue3-chartjs

yarn add chart.js @j-t-mcc/vue3-chartjs

Configuration

Props use the same structure as ChartJS arguments and are passed to the ChartJS instance as-is.

Prop Type Required Default Notes
type ChartType yes — Any ChartJS chart type, e.g. 'doughnut'
data ChartData yes — Passed straight to ChartJS
options ChartOptions no {} Passed straight to ChartJS
plugins Plugin[] no [] Inline plugins, read once when the chart is created
autoUpdate boolean no true Set false to stop watching props and drive the chart yourself
height number no — Needs responsive: false; applied when the chart is built
width number no — Needs responsive: false; applied when the chart is built

ChartJS charts are responsive by default. For a fixed size, set height and width and turn responsive off in options.

Any attribute that is not a prop falls through to the underlying <canvas>, so id, class, style and aria-* work as you would expect.

The chart tracks its props. Mutate or replace data and options and it updates itself; change type, height or width and it rebuilds. See Updating a chart.

Every prop is reactive. Change one and the chart follows — there is nothing to call:

chart.data.datasets[0].data = [1, 2, 3]   // updates
chart.options.plugins.title = { ... }     // updates
chart.type = 'bar'                        // rebuilds

data and options are re-read by an update. type, height and width are fixed when the ChartJS instance is built, so changing those rebuilds it. Both are debounced, so a burst of changes costs one update, and changing height and width together rebuilds once.

A data change reassigns only the data, so state that plugins keep on options survives it — a chart zoomed with chartjs-plugin-zoom holds its range while its data updates. Changing options, or calling update() yourself, replaces the options object and resets that state.

update() and the other methods below remain available for driving the chart explicitly.

Opting out of automatic updates

Set autoUpdate to false and the chart changes only when you drive it:

<template>
  <div style="height:600px;width:600px;">
    <vue3-chart-js ref="chartRef" :auto-update="false" v-bind="chart" />
    <button @click="redraw">Redraw</button>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue'
import Vue3ChartJs from '@j-t-mcc/vue3-chartjs'

const chartRef = ref(null)
const chart = reactive({ /* type, data, options */ })

const redraw = () => {
  // batch as many changes as you like, then apply them once
  chart.data.datasets[0].data = [1, 2, 3]
  chartRef.value.update()
}
</script>

The watchers are removed rather than left in place behind a condition, so the deep walk over data stops costing anything. That matters at size: ten mutations of a fifty-thousand-point dataset take around 476ms of watching, and none at all with autoUpdate off.

It can be toggled at runtime, and update(), render(), resize() and destroy() keep working either way.

Playground

j-t-mcc.github.io/vue3-chartjs

An interactive playground, published from this repository and running the same build that npm installs. Presets cover all eight ChartJS chart types, every prop is editable, and the chart follows as you type. autoUpdate can be switched off there to watch the difference. It will also hand the whole chart back as a config object or as a ready-to-paste component.

View the ChartJS Docs for more chart examples.

Events

A default event hook plugin is injected into each chart object and emits the ChartJS plugin events.

Event listeners are converted to camelcase in Vue 3, so the beforeRender hook is listened to as @before-render.

Events marked as "cancellable" in the ChartJS documentation can be canceled by calling preventDefault() on the event parameter. isDefaultPrevented() reports whether that has happened, returning true once preventDefault() has been called.

Cancellation applies to a single emission — the handler is consulted again on the next one, so a conditional guard resumes normally once its condition clears:

const onBeforeUpdate = (event) => {
  // updates are skipped while loading, and resume by themselves afterwards
  if (isLoading.value) {
    event.preventDefault()
  }
}

To suppress a hook for the lifetime of the chart, call preventDefault() unconditionally.

The chart is destroyed automatically when the component unmounts, so beforeDestroy and afterDestroy fire then as well.

Methods

A few ChartJS methods are exposed for common interactions, available by reference:

chartRef.value.render()
chartRef.value.update(mode = 'default')
chartRef.value.resize()
chartRef.value.destroy()

render() creates the chart, or updates it if one already exists. It runs on mount, so you only need it to rebuild a chart you destroyed yourself.

update() re-reads the data and options props and applies them. It takes a ChartJS transition mode: 'default', 'none', 'reset', 'resize', 'show', 'hide' or 'active'. Pass 'none' to apply an update without animating it. To change animation duration, set it in options:

options: {
  animation: {
    duration: 750
  }
}

For anything else, reach the ChartJS instance directly through chartJSState:

const base64Image = chartRef.value.chartJSState.chart.toBase64Image()

See the ChartJS Docs for more.

Examples

Simple chart

<template>
  <div style="height:600px;width:600px;">
    <vue3-chart-js v-bind="doughnutChart" @before-render="beforeRenderLogic" />
  </div>
</template>

<script setup>
import Vue3ChartJs from '@j-t-mcc/vue3-chartjs'

const doughnutChart = {
  type: 'doughnut',
  data: {
    labels: ['VueJs', 'EmberJs', 'ReactJs', 'AngularJs'],
    datasets: [
      {
        backgroundColor: ['#41B883', '#E46651', '#00D8FF', '#DD1B16'],
        data: [40, 20, 80, 10]
      }
    ]
  }
}

const beforeRenderLogic = (event) => {
  // if (a === b) {
  //   event.preventDefault()
  // }
}
</script>

Updating a chart

The chart watches its props, so changing them is enough. Both styles work: mutate the existing object, or replace it entirely. update() is shown here for the cases where you want to drive it explicitly, such as choosing a transition mode.

<template>
  <div style="height:600px;width:600px;">
    <vue3-chart-js ref="chartRef" v-bind="doughnutChart" />
    <button @click="updateChart">Update Chart</button>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue'
import Vue3ChartJs from '@j-t-mcc/vue3-chartjs'

const chartRef = ref(null)

const doughnutChart = reactive({
  type: 'doughnut',
  data: {
    labels: ['VueJs', 'EmberJs', 'ReactJs', 'AngularJs'],
    datasets: [
      {
        backgroundColor: ['#41B883', '#E46651', '#00D8FF', '#DD1B16'],
        data: [40, 20, 80, 10]
      }
    ]
  },
  options: {
    plugins: {}
  }
})

const updateChart = () => {
  doughnutChart.options.plugins.title = {
    text: 'Updated Chart',
    display: true
  }

  // replacing the whole data object works as well as mutating it
  doughnutChart.data = {
    labels: ['Cats', 'Dogs', 'Hamsters', 'Dragons'],
    datasets: [
      {
        backgroundColor: ['#333333', '#E46651', '#00D8FF', '#DD1B16'],
        data: [100, 20, 800, 20]
      }
    ]
  }

  chartRef.value.update()
}
</script>

See the ChartJS docs for more on updating charts.

Exporting a chart as PNG

<template>
  <div style="height:600px;width:600px;">
    <button type="submit" @click="exportChart">Export Chart as PNG</button>
    <vue3-chart-js ref="chartRef" v-bind="doughnutChart" />
  </div>
</template>

<script setup>
import { ref } from 'vue'
import Vue3ChartJs from '@j-t-mcc/vue3-chartjs'

const chartRef = ref(null)

const doughnutChart = {
  type: 'doughnut',
  data: {
    labels: ['VueJs', 'EmberJs', 'ReactJs', 'AngularJs'],
    datasets: [
      {
        backgroundColor: ['#41B883', '#E46651', '#00D8FF', '#DD1B16'],
        data: [40, 20, 80, 10]
      }
    ]
  }
}

const exportChart = () => {
  const a = document.createElement('a')
  a.href = chartRef.value.chartJSState.chart.toBase64Image()
  a.download = 'image-export.png'
  a.click()
}
</script>

TypeScript

EventObject and UpdateMode are exported for annotating handlers and update calls:

<template>
  <div style="height:600px;width:600px;">
    <vue3-chart-js ref="chartRef" v-bind="doughnutChart" @before-draw="onBeforeDraw" />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import type { ChartData, ChartOptions, ChartType } from 'chart.js'
import Vue3ChartJs from '@j-t-mcc/vue3-chartjs'
import type { EventObject, UpdateMode } from '@j-t-mcc/vue3-chartjs'

const chartRef = ref<InstanceType<typeof Vue3ChartJs> | null>(null)

const doughnutChart: { type: ChartType; data: ChartData; options: ChartOptions } = {
  type: 'doughnut',
  data: {
    labels: ['VueJs', 'EmberJs', 'ReactJs', 'AngularJs'],
    datasets: [
      {
        backgroundColor: ['#41B883', '#E46651', '#00D8FF', '#DD1B16'],
        data: [40, 20, 80, 10]
      }
    ]
  },
  options: { responsive: true }
}

const onBeforeDraw = (event: EventObject) => {
  if (document.hidden) {
    event.preventDefault()
  }
}

const refreshWithoutAnimating = () => {
  const mode: UpdateMode = 'none'
  chartRef.value?.update(mode)
}
</script>

Plugins

ChartJS has two kinds of plugins: inline and global.

Inline plugins

Passed through the plugins prop and scoped to that one chart. The prop is read once, when the chart is created, so changing it later has no effect.

<template>
  <div style="height:600px;width:600px;">
    <vue3-chart-js v-bind="doughnutChart" :plugins="[whiteBackground]" />
  </div>
</template>

<script setup>
import Vue3ChartJs from '@j-t-mcc/vue3-chartjs'

// paints the canvas so exported PNGs are not transparent
const whiteBackground = {
  id: 'whiteBackground',
  beforeDraw (chart) {
    const { ctx } = chart
    ctx.save()
    ctx.globalCompositeOperation = 'destination-over'
    ctx.fillStyle = '#ffffff'
    ctx.fillRect(0, 0, chart.width, chart.height)
    ctx.restore()
  }
}

const doughnutChart = {
  type: 'doughnut',
  data: {
    labels: ['VueJs', 'EmberJs', 'ReactJs', 'AngularJs'],
    datasets: [
      {
        backgroundColor: ['#41B883', '#E46651', '#00D8FF', '#DD1B16'],
        data: [40, 20, 80, 10]
      }
    ]
  }
}
</script>

Global plugins

Registered with ChartJS and available to every chart. Some plugins require registration. Here is chartjs-plugin-zoom, registered either way:

Via component install
import { createApp } from 'vue'
import Vue3ChartJs from '@j-t-mcc/vue3-chartjs'
import zoomPlugin from 'chartjs-plugin-zoom'
import App from './App.vue'

const app = createApp(App)

app.use(Vue3ChartJs, {
  plugins: [
    zoomPlugin
  ]
})

app.mount('#app')
Via helper function
import Vue3ChartJs from '@j-t-mcc/vue3-chartjs'
import zoomPlugin from 'chartjs-plugin-zoom'

Vue3ChartJs.registerGlobalPlugins([zoomPlugin])

Used with a locally imported component:

<template>
  <div style="height:600px;width:600px;">
    <vue3-chart-js v-bind="lineChart" />
  </div>
</template>

<script setup>
import Vue3ChartJs from '@j-t-mcc/vue3-chartjs'
import zoomPlugin from 'chartjs-plugin-zoom'

Vue3ChartJs.registerGlobalPlugins([zoomPlugin])

const lineChart = {
  type: 'line',
  data: {
    labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
    datasets: [{
      label: '# of Votes',
      data: [50, 19, 3, 5, 2, 3],
      borderColor: 'rgba(75, 192, 192, 1)',
      borderWidth: 1
    }]
  },
  options: {
    plugins: {
      zoom: {
        zoom: {
          wheel: { enabled: true },
          pinch: { enabled: true },
          mode: 'y'
        }
      }
    }
  }
}
</script>

Migrating from v2

The chart tracks its props

In v2 the component read its props once and nothing changed the chart until you called update() yourself. Every prop is now watched, so the calls that used to be required are not:

// v2
doughnutChart.data.datasets[0].data = [1, 2, 3]
chartRef.value.update()

// v3
doughnutChart.data.datasets[0].data = [1, 2, 3]

data and options changes are applied by an update; type, height and width are fixed when the ChartJS instance is built, so changing those rebuilds it. Both are debounced, so a burst of changes costs one update.

Existing update() calls are harmless and still useful for choosing a transition mode, so nothing has to be removed to upgrade — and :auto-update="false" restores the v2 behaviour outright if you would rather keep driving the chart yourself. Two things are worth knowing:

  • data is watched deeply. Vue walks the whole structure on each change, which is around 1ms for a thousand points and closer to 45ms for fifty thousand. If you render very large datasets and were batching updates deliberately, measure before assuming the automatic path is equivalent — or set :auto-update="false", which removes the watchers and the walk with them.
  • A data change reassigns only the data, so plugin state kept on options survives it. Changing options, or calling update() yourself, replaces that object and resets it — a zoomed chart returns to its full range.

preventDefault() applies to one emission

Previously a single preventDefault() call canceled that hook for the lifetime of the chart, because the event object was created once and its flag was never reset. A conditional guard therefore froze the chart permanently the first time its condition was true.

It now applies only to the emission it was called on, and the handler is consulted again next time.

// v2: ran once, then the hook was canceled forever
const onBeforeDraw = (event) => {
  if (isFirstRender) {
    event.preventDefault()
    isFirstRender = false
  }
}

// v3: to suppress a hook permanently, cancel unconditionally
const onBeforeDraw = (event) => {
  event.preventDefault()
}

Charts are destroyed on unmount

The component now destroys its chart in onBeforeUnmount. Calling destroy() yourself is no longer required, and stays safe if you do. beforeDestroy, afterDestroy, uninstall and stop now fire when the component unmounts.

Replacing a whole prop object works

Assigning a new data or options object is picked up. In v2 the component held a snapshot taken at setup, so only in-place mutation of the original object had any effect.

update() takes a transition mode

Changed in 2.1.0 and repeated here for anyone coming from 2.0: update() takes a ChartJS transition mode rather than an animation duration in milliseconds. update(750) is silently ignored — set options.animation.duration instead.

Types resolve on modern module resolution

The package now declares a types condition in its exports map and references ChartJS types through the public entry point. Consumers on moduleResolution: "bundler", "node16" or "nodenext" previously got no types at all, or silently degraded to any under skipLibCheck. No code change is required.

Demo

For a demo, Clone this repository and run:

pnpm install

pnpm dev

License

MIT

Related Packages

vue-chart.js

Chart.js for Vue

10,566 4
charts.vue

Simple Vue3 wrapper for Chart.js

495 1
vue-chart-js

Chart.js for VueJs.

10,491 33
hchs-vue-charts

Base on Vue2.0 wrapper for ChartJs.

70,593 353

Version History

Version Released License
3.0.0 MIT
2.1.0 MIT
2.0.0 MIT
1.3.0 MIT
1.2.0 MIT