Introduction
Data visualization transforms raw numbers into insights your users can actually understand. Whether you're building a financial dashboard, tracking analytics, or displaying real-time metrics, charts in Vue: what every Vue & Nuxt developer should know becomes essential knowledge as soon as you move beyond static lists and tables. We'll walk through the libraries, patterns, and gotchas that matter most when adding charts to production Vue and Nuxt applications.
Vue-echarts for Rich Charts
Vue-echarts wraps Apache ECharts, giving you access to an incredibly mature charting engine with support for dozens of chart types: line, bar, scatter, heatmap, gauge, tree, sankey, and more. If you need sophisticated interactivity, animations, and cross-filtering across multiple chart instances, ECharts is your strongest choice.
Installation is straightforward:
npm install echarts vue-echartsBasic setup in a Vue component:
<template>
<div class="chart-container">
<v-chart :option="chartOption" autoresize />
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import VChart from 'vue-echarts'
import * as echarts from 'echarts'
const salesData = ref([120, 200, 150, 80, 70, 110, 130])
const chartOption = computed(() => ({
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [
{
data: salesData.value,
type: 'line',
smooth: true,
itemStyle: { color: '#2563eb' },
areaStyle: { color: 'rgba(37, 99, 235, 0.1)' }
}
],
tooltip: { trigger: 'axis' },
grid: { containLabel: true }
}))
</script>
<style scoped>
.chart-container {
width: 100%;
height: 400px;
}
</style>The autoresize prop is crucial in real applications—it ensures the chart reflows when the viewport changes. ECharts' strength shines when you need advanced features like dynamic axis switching, visual mapping, or custom tool tips with rich HTML content. We've found it invaluable in trading dashboards and analytics platforms where users expect near-native application interactivity.
Chart.js with Vue-chartjs
If you need simpler, lighter-weight charts, Chart.js combined with vue-chartjs is a pragmatic alternative. Chart.js focuses on the essentials—line, bar, pie, doughnut, radar—with a smaller bundle footprint. For most e-commerce dashboards, SaaS metrics pages, and basic reporting, this covers 80% of use cases.
Installation:
npm install chart.js vue-chartjsA reusable Chart.js component:
<template>
<div class="chart-wrapper">
<Bar :data="chartData" :options="chartOptions" />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { Bar } from 'vue-chartjs'
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend
} from 'chart.js'
ChartJS.register(
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend
)
interface Props {
labels: string[]
values: number[]
title?: string
}
const props = withDefaults(defineProps<Props>(), {
title: 'Chart'
})
const chartData = computed(() => ({
labels: props.labels,
datasets: [
{
label: props.title,
data: props.values,
backgroundColor: 'rgba(37, 99, 235, 0.7)',
borderColor: 'rgba(37, 99, 235, 1)',
borderWidth: 1
}
]
}))
const chartOptions = {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
display: true,
position: 'top'
}
},
scales: {
y: {
beginAtZero: true
}
}
}
</script>
<style scoped>
.chart-wrapper {
position: relative;
width: 100%;
height: 300px;
}
</style>Chart.js is ideal when you're shipping quickly and your design system is already defined. We've used it extensively in CRM and SaaS dashboards because developers get up and running in minutes, and the learning curve is shallow.
Update Charts Reactively
The real challenge isn't drawing a chart once—it's keeping it in sync with your data. Charts in Vue require you to think about reactivity patterns carefully, especially when data updates arrive from WebSocket connections, polling, or user interactions.
Here's a production pattern we've used for real-time metric dashboards:
<template>
<div class="realtime-chart">
<div class="controls">
<button @click="toggleAutoRefresh" :class="{ active: autoRefresh }">
{{ autoRefresh ? 'Pause' : 'Resume' }} Updates
</button>
<span class="last-update">Last: {{ lastUpdateTime }}</span>
</div>
<v-chart :option="chartOption" autoresize />
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import VChart from 'vue-echarts'
interface DataPoint {
timestamp: number
value: number
}
const dataPoints = ref<DataPoint[]>([])
const autoRefresh = ref(true)
const lastUpdateTime = ref('')
const maxDataPoints = 60
onMounted(() => {
// Simulate WebSocket or polling connection
const interval = setInterval(() => {
if (!autoRefresh.value) return
const newPoint: DataPoint = {
timestamp: Date.now(),
value: Math.floor(Math.random() * 100) + 20
}
dataPoints.value.push(newPoint)
if (dataPoints.value.length > maxDataPoints) {
dataPoints.value.shift()
}
lastUpdateTime.value = new Date().toLocaleTimeString()
}, 1000)
return () => clearInterval(interval)
})
const chartOption = computed(() => ({
xAxis: {
type: 'category',
data: dataPoints.value.map(p =>
new Date(p.timestamp).toLocaleTimeString()
)
},
yAxis: { type: 'value' },
series: [
{
data: dataPoints.value.map(p => p.value),
type: 'line',
smooth: true,
symbolSize: 0,
lineStyle: { width: 2, color: '#10b981' },
areaStyle: { color: 'rgba(16, 185, 129, 0.1)' }
}
],
tooltip: {
trigger: 'axis',
formatter: (params: any) => {
if (!params.length) return ''
const { value, axisValue } = params[0]
return `${axisValue}<br/>Value: ${value}`
}
},
animation: true,
animationDuration: 300,
grid: { containLabel: true }
}))
const toggleAutoRefresh = () => {
autoRefresh.value = !autoRefresh.value
}
</script>
<style scoped>
.realtime-chart {
width: 100%;
}
.controls {
margin-bottom: 1rem;
display: flex;
gap: 1rem;
align-items: center;
}
button {
padding: 0.5rem 1rem;
background: #e5e7eb;
border: none;
border-radius: 0.375rem;
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s;
}
button.active {
background: #10b981;
color: white;
}
.last-update {
font-size: 0.875rem;
color: #6b7280;
}
</style>Key reactive patterns to remember: avoid direct mutation of array elements within charts—use Vue's reactivity helpers. Set reasonable limits on historical data (we typically keep 60–100 points in memory), and always clean up intervals and WebSocket listeners in unmount hooks. Use computed for chart options to ensure updates propagate correctly.
Keep Accessibility in Mind
Charts are inherently visual, but we have a responsibility to make them accessible. Many of your users may rely on screen readers or keyboard navigation, and some may have low vision or color blindness.
Accessibility best practices for charts:
- Use ARIA labels: Add
role="img",aria-label, and a descriptive title to each chart container. - Provide text alternatives: Include a summary table alongside visual charts, especially for critical data.
- Avoid color-only encoding: Use patterns, shapes, or labels in addition to color to distinguish data series.
- Ensure sufficient contrast: Verify that text and chart lines meet WCAG AA standards (4.5:1 for normal text).
- Support keyboard navigation: If charts have interactive elements, ensure they're reachable via Tab and usable via Enter/Space.
Here's a pattern for an accessible chart wrapper:
<template>
<figure
role="img"
:aria-label="accessibleLabel"
class="accessible-chart"
>
<figcaption class="sr-only">{{ tableCaption }}</figcaption>
<v-chart :option="chartOption" autoresize />
<details class="chart-data">
<summary>View data as table</summary>
<table>
<thead>
<tr>
<th>Label</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr v-for="(value, label) in data" :key="label">
<td>{{ label }}</td>
<td>{{ value }}</td>
</tr>
</tbody>
</table>
</details>
</figure>
</template>
<script setup lang="ts">
import { computed } from 'vue'
interface Props {
data: Record<string, number>
title: string
description?: string
}
const props = defineProps<Props>()
const accessibleLabel = computed(() =>
props.description || `Chart showing ${props.title}`
)
const tableCaption = computed(() =>
`Data table for ${props.title}`
)
// Chart option setup...
const chartOption = computed(() => ({}))
</script>
<style scoped>
.accessible-chart {
width: 100%;
margin: 0;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.chart-data {
margin-top: 1.5rem;
padding: 1rem;
background: #f9fafb;
border-radius: 0.375rem;
}
.chart-data summary {
cursor: pointer;
font-weight: 500;
user-select: none;
}
.chart-data table {
width: 100%;
margin-top: 1rem;
border-collapse: collapse;
}
.chart-data th,
.chart-data td {
padding: 0.5rem;
text-align: left;
border-bottom: 1px solid #d1d5db;
}
.chart-data th {
background: #f3f4f6;
font-weight: 600;
}
</style>Common Mistakes
Not debouncing frequent updates: If your chart receives updates every millisecond from a WebSocket, the rendering will choke. Use throttle or debounce from a utility library to batch updates.
Mutating chart data directly: Assign a new array reference instead of mutating in place. Vue won't always detect mutations to nested properties.
Ignoring memory leaks: Always clean up timers, intervals, and event listeners in onUnmounted. We've seen memory usage climb steadily in long-running dashboards because cleanup was missing.
Hardcoding colors and styling: Use CSS custom properties or your design system tokens so colors adapt to light/dark mode and brand changes.
Skipping responsive sizing: Charts that don't resize properly on mobile trash the user experience. Use autoresize props and ensure your container has explicit dimensions.
Missing error states: If data fails to load, the chart sits silent. Always render a meaningful error message or skeleton loader.
Key Takeaways
- Vue-echarts is your go-to for complex, interactive, feature-rich charts; Chart.js with vue-chartjs covers simpler use cases with a smaller footprint.
- Use computed properties for chart options and reactive refs for data to ensure updates propagate correctly.
- Implement sensible data limits (typically 60–100 points) for real-time charts to avoid memory bloat.
- Clean up intervals, timers, and listeners in unmounted hooks to prevent memory leaks.
- Pair visual charts with text alternatives, ARIA labels, and accessible data tables for users relying on assistive technology.
- Avoid color-only encoding; use patterns and labels alongside color to support color-blind users.
- Test your charts on mobile devices and ensure they resize responsively.






Comments
Loading comments…