Chart.js Best Practices
Overview
Following best practices helps create effective, performant, and accessible charts with Chart.js.
Performance Optimization
Data Management
javascript
// Limit data points for better performance
const MAX_DATA_POINTS = 1000;
function optimizeData(data) {
if (data.length > MAX_DATA_POINTS) {
// Sample data points
const step = Math.ceil(data.length / MAX_DATA_POINTS);
return data.filter((_, index) => index % step === 0);
}
return data;
}
// Use optimized data
const optimizedData = optimizeData(rawData);
const chart = new Chart(ctx, {
type: 'line',
data: {
labels: optimizedData.labels,
datasets: [{
data: optimizedData.values,
// Disable animations for large datasets
animation: {
duration: optimizedData.length > 500 ? 0 : 1000
}
}]
}
});Chart Rendering Optimization
javascript
const optimizedChart = new Chart(ctx, {
type: 'line',
data: {
// ... data
},
options: {
// Disable animations for performance
animation: false,
// Disable hover effects for large datasets
interaction: {
intersect: false,
mode: 'index'
},
// Optimize rendering
elements: {
point: {
radius: 0, // Hide points for large datasets
hoverRadius: 5
},
line: {
tension: 0.1, // Reduce curve complexity
borderWidth: 1
}
},
// Responsive settings
responsive: true,
maintainAspectRatio: false,
// Performance plugins
plugins: {
decimation: {
enabled: true,
algorithm: 'lttb', // Largest Triangle Three Buckets
samples: 1000,
threshold: 1000
}
}
}
});Accessibility
ARIA Labels and Descriptions
javascript
const accessibleChart = new Chart(ctx, {
type: 'bar',
data: {
labels: ['Product A', 'Product B', 'Product C'],
datasets: [{
label: 'Sales Data',
data: [120, 190, 80],
backgroundColor: [
'rgba(255, 99, 132, 0.8)',
'rgba(54, 162, 235, 0.8)',
'rgba(255, 205, 86, 0.8)'
]
}]
},
options: {
plugins: {
// Custom tooltip for accessibility
tooltip: {
callbacks: {
label: function(context) {
const label = context.dataset.label || '';
const value = context.parsed.y;
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = ((value / total) * 100).toFixed(1);
return `${label}: ${value} (${percentage}%)`;
}
}
},
// Accessibility plugin
legend: {
display: true,
position: 'top',
labels: {
generateLabels: function(chart) {
const data = chart.data;
return data.labels.map((label, i) => {
const value = data.datasets[0].data[i];
return {
text: `${label}: ${value}`,
fillStyle: data.datasets[0].backgroundColor[i],
hidden: false,
index: i
};
});
}
}
}
}
}
});
// Add ARIA attributes
document.getElementById('chartCanvas').setAttribute('role', 'img');
document.getElementById('chartCanvas').setAttribute('aria-label', 'Sales data bar chart showing product performance');Responsive Design
Mobile-First Approach
javascript
const responsiveChart = new Chart(ctx, {
type: 'line',
data: {
// ... data
},
options: {
responsive: true,
maintainAspectRatio: false,
// Breakpoint-specific configurations
plugins: {
legend: {
display: true,
position: window.innerWidth < 768 ? 'bottom' : 'top',
labels: {
boxWidth: window.innerWidth < 768 ? 10 : 15,
padding: window.innerWidth < 768 ? 10 : 20,
font: {
size: window.innerWidth < 768 ? 10 : 12
}
}
}
},
scales: {
x: {
ticks: {
maxTicksLimit: window.innerWidth < 768 ? 6 : 12,
maxRotation: window.innerWidth < 768 ? 45 : 0,
minRotation: window.innerWidth < 768 ? 45 : 0,
font: {
size: window.innerWidth < 768 ? 10 : 12
}
}
},
y: {
ticks: {
maxTicksLimit: window.innerWidth < 768 ? 6 : 8,
font: {
size: window.innerWidth < 768 ? 10 : 12
}
}
}
}
}
});
// Handle window resize
window.addEventListener('resize', () => {
responsiveChart.options.plugins.legend.position = window.innerWidth < 768 ? 'bottom' : 'top';
responsiveChart.update();
});Data Visualization Best Practices
Choose the Right Chart Type
javascript
// Data-driven chart type selection
function selectChartType(data, purpose) {
const dataPoints = data.labels.length;
const dataSeries = data.datasets.length;
switch (purpose) {
case 'trend':
return 'line';
case 'comparison':
return dataPoints <= 10 ? 'bar' : 'line';
case 'composition':
return dataSeries === 1 ? 'pie' : 'doughnut';
case 'distribution':
return 'scatter';
case 'relationship':
return 'bubble';
default:
return 'bar';
}
}
// Usage
const chartType = selectChartType(chartData, 'comparison');Color Schemes
javascript
// Accessible color palettes
const colorPalettes = {
// Colorblind-friendly palette
accessible: [
'#1f77b4', '#ff7f0e', '#2ca02c', '#d62728',
'#9467bd', '#8c564b', '#e377c2', '#7f7f7f'
],
// Sequential palette for single-series data
sequential: [
'#f7fbff', '#deebf7', '#c6dbef', '#9ecae1',
'#6baed6', '#4292c6', '#2171b5', '#08519c'
],
// Diverging palette
diverging: [
'#67001f', '#b2182b', '#d6604d', '#f4a582',
'#fddbc7', '#f7f7f7', '#d1e5f0', '#92c5de',
'#4393c3', '#2166ac', '#053061'
]
};
function getColors(count, palette = 'accessible') {
const colors = colorPalettes[palette];
return Array.from({ length: count }, (_, i) => colors[i % colors.length]);
}Error Handling
Robust Chart Creation
javascript
function createChartSafely(canvasId, config) {
try {
const canvas = document.getElementById(canvasId);
if (!canvas) {
throw new Error(`Canvas element with id '${canvasId}' not found`);
}
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Unable to get 2D context from canvas');
}
// Validate data
if (!config.data || !config.data.labels || !config.data.datasets) {
throw new Error('Invalid chart data configuration');
}
// Validate datasets
config.data.datasets.forEach((dataset, index) => {
if (!dataset.data || !Array.isArray(dataset.data)) {
throw new Error(`Dataset ${index} has invalid data`);
}
});
return new Chart(ctx, config);
} catch (error) {
console.error('Chart creation failed:', error);
// Show error message to user
const errorElement = document.getElementById(canvasId);
if (errorElement) {
errorElement.innerHTML = `
<div style="text-align: center; padding: 20px; color: #666;">
<p>Unable to display chart: ${error.message}</p>
<button onclick="location.reload()">Retry</button>
</div>
`;
}
return null;
}
}
// Usage
const chart = createChartSafely('myChart', chartConfig);Testing
Chart Testing Utilities
javascript
// Test utilities for Chart.js
class ChartTestUtils {
static createMockCanvas() {
const canvas = document.createElement('canvas');
canvas.width = 400;
canvas.height = 300;
return canvas;
}
static createMockData(labels = ['A', 'B', 'C'], values = [10, 20, 30]) {
return {
labels: labels,
datasets: [{
label: 'Test Data',
data: values,
backgroundColor: 'rgba(54, 162, 235, 0.8)'
}]
};
}
static async waitForChartUpdate(chart, timeout = 1000) {
return new Promise((resolve) => {
const originalUpdate = chart.update;
chart.update = function(...args) {
const result = originalUpdate.apply(this, args);
setTimeout(resolve, 100); // Wait for animation
return result;
};
setTimeout(resolve, timeout); // Fallback timeout
});
}
static getChartImage(chart) {
return chart.toBase64Image();
}
static compareCharts(chart1, chart2) {
const image1 = this.getChartImage(chart1);
const image2 = this.getChartImage(chart2);
return image1 === image2;
}
}
// Example test
async function testChartCreation() {
const canvas = ChartTestUtils.createMockCanvas();
const ctx = canvas.getContext('2d');
const mockData = ChartTestUtils.createMockData();
const chart = new Chart(ctx, {
type: 'bar',
data: mockData
});
await ChartTestUtils.waitForChartUpdate(chart);
console.log('Chart created successfully');
return chart;
}