Chart.js Advanced — Animation, Custom Plugins & Interactive Dashboards
Once you’ve mastered basic charts, Chart.js offers a powerful layer of advanced features: fine-tuned animations, custom plugins that extend the library, full control over tooltips and legends, and responsive configurations for production dashboards.
What You’ll Learn
By the end of this tutorial, you’ll configure animations with custom easing, duration, and staggering effects, control chart interactions — hover modes, click behavior, and crosshair-style tooltips, customize every visual element: tooltips, legends, titles, and subtitles, build custom plugins for annotations, backgrounds, and horizontal lines, and create a fully interactive animated dashboard with custom plugins and user controls.
Why Advanced Chart.js Matters
Basic charts show data. Advanced charts communicate data. A well-animated chart draws attention to changes. A custom tooltip shows precisely the information users need. A plugin adds annotations that highlight key thresholds.
Real-world use: Durga Antivirus Pro uses a real-time threat dashboard built with Chart.js advanced features. Custom plugins draw horizontal “threshold lines” showing acceptable risk levels. When a metric exceeds the threshold, a custom animation pulses the bar red. External tooltips show detailed threat metadata — file hash, category, and recommended action — without blocking the chart view.
Where This Fits in Your Learning Path
flowchart LR
A["Chart.js Basics"] --> B["Line, Bar & Mixed Charts"]
B --> C["Pie, Doughnut, Radar & Polar"]
C --> D["Scatter & Bubble Charts"]
D --> E["**Chart.js Advanced**"]
style E fill:#f97316,stroke:#c2410c,color:#fff
style A fill:#e5e7eb,stroke:#9ca3af,color:#374151
Animation
Chart.js animates every property change automatically. When you call chart.update(), it smoothly transitions bars growing taller, lines bending into new shapes, and pie slices rotating to their new angles. You don’t need to write any animation code — it just works.
But you can control how those animations behave.
Setting Duration and Easing
options: {
animation: {
duration: 1000, // milliseconds
easing: "easeInOutQuad"
}
}- Duration: How long the animation takes (in milliseconds). 1000ms = 1 second. Shorter values feel snappier; longer values feel smoother.
- Easing: The acceleration curve.
"linear"is constant speed."easeInOutQuad"starts slow, speeds up, then slows down at the end — the most natural-feeling option.
Available Easing Functions
Chart.js supports 28 easing functions. Here are the most useful ones:
| Easing | Feel | Use Case |
|---|---|---|
linear | Constant speed | Data updates, no flourish |
easeInOutQuad | Smooth start and end | General purpose |
easeOutBounce | Bouncy landing | Fun, playful charts |
easeInOutElastic | Overshoot with wobble | Attention-grabbing |
easeOutCubic | Fast start, slow end | Bars growing upward |
// The full list of available easings
easing: "linear"
easing: "easeInQuad" // slow start
easing: "easeOutQuad" // slow end
easing: "easeInOutQuad" // slow start and end
easing: "easeInCubic" // slower start
easing: "easeOutCubic" // slower end
easing: "easeInOutCubic" // slow start and end (stronger)
easing: "easeInSine" // sine-based slow start
easing: "easeOutSine" // sine-based slow end
easing: "easeInExpo" // very slow start
easing: "easeOutExpo" // very slow end
easing: "easeInOutExpo" // very slow both ends
easing: "easeInBounce" // bouncing start
easing: "easeOutBounce" // bouncing end
easing: "easeInOutBounce" // bouncing both ends
easing: "easeInElastic" // elastic start (overshoot)
easing: "easeOutElastic" // elastic end
easing: "easeInOutElastic"
easing: "easeInBack" // pulls back before starting
easing: "easeOutBack" // overshoots then returns
easing: "easeInOutBack"Staggering Animations (Per-Dataset)
You can animate x and y properties independently, and even delay each data point:
animation: {
x: {
type: "number",
easing: "easeOutQuad",
duration: 800,
from: NaN // Start from current position
},
y: {
type: "number",
easing: "easeOutQuad",
duration: 800,
from: function(ctx) {
return ctx.chart.scales.y.max; // Start from top
}
}
}The stagger trick: Apply a delay based on data index to make bars animate one after another:
animation: {
y: {
type: "number",
duration: 600,
delay: function(ctx) {
return ctx.dataIndex * 100; // Each bar delays 100ms more
}
}
}This creates a “wave” effect — bars pop up from left to right like dominoes.
Animation Events
Run custom code during or after animation:
options: {
animation: {
onProgress: function(animation) {
console.log("Progress:", animation.currentStep / animation.numSteps);
},
onComplete: function(animation) {
console.log("Animation complete");
}
}
}Disabling Animation
For large datasets, animation can cause performance issues. Disable it:
// Disable all animation
animation: false
// Disable specific property animations
animation: {
colors: false, // Don't animate color changes
numbers: {
properties: ["x", "y"],
type: "number"
}
}Interactions
Interaction controls what happens when users hover over or click on chart elements.
options: {
interaction: {
mode: "nearest", // Which elements to highlight
intersect: true, // Must hover directly over element
axis: "x" // Restrict to x-axis proximity
},
hover: {
mode: "nearest",
intersect: true,
animationDuration: 400
}
}Interaction Modes Explained
| Mode | What It Highlights | Best For |
|---|---|---|
"point" | Single point directly under mouse | Dense scatter plots |
"nearest" | Closest point(s) to mouse | General use |
"index" | All points with same index across datasets | Comparing series at a point in time |
"dataset" | All points in the hovered dataset | Highlighting one series |
"x" | All points near the mouse on x-axis | Line charts, crosshair feel |
"y" | All points near the mouse on y-axis | Finding values at a specific level |
Why intersect: false is useful: When set to false, the chart responds to mouse proximity rather than direct hits. This creates a crosshair-like experience — move your mouse across the chart and it shows values at every x position, even between data points.
Tooltip
Tooltips appear when users hover over data points. Chart.js gives you full control over their appearance and content.
Basic Tooltip Configuration
options: {
plugins: {
tooltip: {
enabled: true,
mode: "nearest",
intersect: true,
position: "average", // Position relative to data point
backgroundColor: "rgba(0,0,0,0.8)",
titleColor: "#fff",
bodyColor: "#fff",
titleFont: { size: 14, weight: "bold" },
bodyFont: { size: 13 },
padding: 10,
cornerRadius: 4,
caretSize: 6,
borderColor: "rgba(0,0,0,0.1)",
borderWidth: 1,
displayColors: true
}
}
}Tooltip Callbacks — Full Control Over Content
Callbacks let you format every part of the tooltip independently:
tooltip: {
callbacks: {
title: function(items) {
return items[0].label; // Chart label
},
beforeLabel: function(context) {
return ""; // Content before the value
},
label: function(context) {
var label = context.dataset.label || "";
if (label) label += ": ";
if (context.parsed.y !== undefined) label += context.parsed.y;
if (context.parsed.r !== undefined) label += "Size: " + context.parsed.r;
return label;
},
afterLabel: function(context) {
return ""; // Content after the value
},
labelColor: function(context) {
return {
borderColor: "transparent",
backgroundColor: context.dataset.borderColor
};
},
labelTextColor: function(context) {
return "#fff";
},
footer: function(items) {
return "Total: " + items.reduce(function(s, i) { return s + i.parsed.y; }, 0);
}
},
itemSort: function(a, b) {
return b.datasetIndex - a.datasetIndex; // Sort datasets in reverse
},
filter: function(item) {
return item.parsed.y !== null; // Hide null values
}
}External Tooltip (Fully Custom HTML)
For complete control over tooltip appearance (beyond what Chart.js config allows), use an external tooltip:
options: {
plugins: {
tooltip: {
enabled: false, // Hide the built-in tooltip
external: function(context) {
var tooltip = context.tooltip;
var tooltipEl = document.getElementById("customTooltip");
// Create tooltip element once
if (!tooltipEl) {
tooltipEl = document.createElement("div");
tooltipEl.id = "customTooltip";
tooltipEl.style.cssText =
"position:absolute;background:rgba(0,0,0,0.8);" +
"color:#fff;padding:8px 12px;border-radius:4px;" +
"pointer-events:none;font-size:13px;";
document.body.appendChild(tooltipEl);
}
// Hide when nothing is hovered
if (tooltip.opacity === 0) {
tooltipEl.style.opacity = 0;
return;
}
// Build custom HTML content
tooltipEl.innerHTML = tooltip.title[0] + ": " +
tooltip.dataPoints[0].formattedValue;
// Position at cursor
tooltipEl.style.opacity = 1;
tooltipEl.style.left = tooltip.caretX + "px";
tooltipEl.style.top = tooltip.caretY + "px";
}
}
}
}Why use external tooltips: The built-in tooltip is a Canvas-rendered element, limited in styling. External tooltips use real HTML elements — you can add images, links, formatted tables, or even mini sparkline charts inside the tooltip.
Legend
The legend identifies which color belongs to which dataset. You can control its position, styling, and behavior.
options: {
plugins: {
legend: {
display: true,
position: "top", // "top", "left", "bottom", "right", "chartArea"
align: "center", // "start", "center", "end"
reverse: false,
labels: {
boxWidth: 40,
boxHeight: 12,
color: "#666",
font: { size: 12 },
padding: 10,
pointStyle: "circle", // "circle", "rect", "line", "rectRounded"
usePointStyle: false,
borderRadius: 0,
filter: function(item, chart) { return true; },
sort: function(a, b) { return 0; }
},
onClick: function(event, legendItem, legend) {
// Toggle dataset visibility on click
var index = legendItem.datasetIndex;
var meta = legend.chart.getDatasetMeta(index);
meta.hidden = meta.hidden === null ?
!legend.chart.data.datasets[index].hidden : null;
legend.chart.update();
}
}
}
}The default click behavior toggles dataset visibility — click “Sales 2024” and that dataset hides. Click again to show it. This is useful for focusing on specific data series without removing them from the configuration.
Title and Subtitle
options: {
plugins: {
title: {
display: true,
text: "Chart Title", // String or array for multi-line
color: "#333",
font: { size: 18, weight: "bold" },
padding: { top: 10, bottom: 10 },
position: "top",
align: "center"
},
subtitle: {
display: true,
text: "Subtitle text here",
color: "#666",
font: { size: 14 },
padding: { bottom: 10 }
}
}
}Multi-line titles: Pass an array instead of a string:
text: ["First Line", "Second Line"]Each array element becomes a separate line.
Color Configuration
Chart.js has a built-in color palette of 7 colors, but you can override everything:
// Default chart colors
options: {
color: "#666", // Default text color
borderColor: "rgba(0,0,0,0.1)", // Default border
backgroundColor: "transparent" // Default background
}
// Per-dataset styling
datasets: [{
backgroundColor: "rgba(54, 162, 235, 0.5)",
borderColor: "rgb(54, 162, 235)",
pointBackgroundColor: "rgb(54, 162, 235)",
pointBorderColor: "#fff",
hoverBackgroundColor: "rgba(54, 162, 235, 0.8)",
hoverBorderColor: "rgb(54, 162, 235)"
}]Default Color Palette
Chart.js assigns these colors to datasets in order, cycling back to the start if you have more than 7:
var COLORS = [
"rgb(255, 99, 132)", // Red
"rgb(54, 162, 235)", // Blue
"rgb(255, 205, 86)", // Yellow
"rgb(75, 192, 192)", // Green
"rgb(153, 102, 255)", // Purple
"rgb(255, 159, 64)", // Orange
"rgb(201, 203, 207)" // Grey
];Custom Plugins
Plugins are the most powerful Chart.js feature. They let you hook into the chart lifecycle and add custom behavior or drawing.
The Plugin Lifecycle
A plugin is an object with an id and functions that run at specific points in the chart’s lifecycle:
var myPlugin = {
id: "myPlugin",
// Available hooks (executed in this order):
beforeInit: function(chart, args, options) {}, // Before chart initializes
afterInit: function(chart, args, options) {}, // After chart initializes
beforeUpdate: function(chart, args, options) {}, // Before data update
afterUpdate: function(chart, args, options) {}, // After data update
beforeLayout: function(chart, args, options) {},
afterLayout: function(chart, args, options) {},
beforeDatasetsUpdate: function(chart, args, options) {},
afterDatasetsUpdate: function(chart, args, options) {},
beforeRender: function(chart, args, options) {}, // Before render pass
afterRender: function(chart, args, options) {}, // After render pass
beforeDraw: function(chart, args, options) {}, // Before any drawing
afterDraw: function(chart, args, options) {}, // After all drawing
beforeDatasetsDraw: function(chart, args, options) {},
afterDatasetsDraw: function(chart, args, options) {},
beforeEvent: function(chart, args, options) {},
afterEvent: function(chart, args, options) {},
resize: function(chart, size, options) {},
destroy: function(chart) {}
};
// Register globally (available to all charts)
Chart.register(myPlugin);Why the hook ordering matters: If you want to draw something behind the chart, use beforeDraw. If you want to draw over the chart (annotations, labels), use afterDraw. Draw behind data but in front of grid: use beforeDatasetsDraw.
Plugin Example: Background Color
var backgroundColorPlugin = {
id: "backgroundColor",
beforeDraw: function(chart) {
var ctx = chart.ctx;
ctx.save();
ctx.fillStyle = chart.options.plugins.backgroundColor || "white";
ctx.fillRect(0, 0, chart.width, chart.height);
ctx.restore();
}
};
Chart.register(backgroundColorPlugin);
// Usage
options: {
plugins: {
backgroundColor: "#f8f9fa"
}
}Plugin Example: Horizontal Threshold Line
This is one of the most useful plugins — draw a line showing a target value:
var horizontalLinePlugin = {
id: "horizontalLine",
afterDraw: function(chart) {
var yScale = chart.scales.y;
var ctx = chart.ctx;
var lineY = yScale.getPixelForValue(
chart.options.plugins.horizontalLine.value
);
ctx.save();
ctx.beginPath();
ctx.strokeStyle = chart.options.plugins.horizontalLine.color || "red";
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
ctx.moveTo(chart.chartArea.left, lineY);
ctx.lineTo(chart.chartArea.right, lineY);
ctx.stroke();
ctx.restore();
}
};
Chart.register(horizontalLinePlugin);
// Usage in chart options:
options: {
plugins: {
horizontalLine: { value: 50, color: "#e91e63" }
}
}How it works:
getPixelForValue(50)converts the data value 50 to a pixel position on the y-axis- The plugin draws a dashed line across the chart area at that pixel position
- The
afterDrawhook ensures the line appears on top of the chart data
This pattern — compute a pixel position from a data value, then draw on the Canvas — is the foundation for all annotation plugins.
Responsive Configuration
Production charts must work at any screen size:
options: {
responsive: true, // Resize when container resizes
maintainAspectRatio: true, // Keep width/height ratio
aspectRatio: 2, // width = 2 * height
resizeDelay: 0, // Debounce resize handler (ms)
devicePixelRatio: window.devicePixelRatio // HiDPI support
}Container sizing best practice: Let the parent control dimensions via CSS, then set responsive: true:
<div style="width: 100%; max-width: 800px; height: 400px;">
<canvas id="myChart"></canvas>
</div>Manual Resize
chart.resize(); // Fit to current container
chart.resize(600, 300); // Set explicit pixel size
Layout Padding
Control the space around the chart area:
options: {
layout: {
padding: {
top: 10,
bottom: 10,
left: 10,
right: 10
}
// Shorthand:
// padding: 20 // All sides
// padding: { top: 20 } // Just top
}
}Common Mistakes
1. Too Many Animations Causing Performance Issues
Each animated property on each data point creates a separate tween. On large datasets (100+ points), this can cause jank. Disable unnecessary animations with animation: false or animate only the properties you need.
2. Custom Tooltip Not Updating Position
External tooltips must read tooltip.caretX and tooltip.caretY, which are set during the draw phase. If your external function doesn’t use these exact properties (or uses stale references), the tooltip appears in the wrong place.
3. Plugin Not Firing Because It’s Not Registered
Custom plugins must be registered globally with Chart.register(myPlugin). If you define a plugin but don’t register it, nothing happens — no error, no warning. It silently does nothing.
4. Legend Items Overlapping with Chart Area
When position: "left" or "right", the legend can take too much space, compressing the chart. Set maxWidth on the legend or reduce labels.boxWidth and labels.padding.
5. Animation Delay Not Applied to Entering Elements
The delay function receives the animation context. For stagger effects on entering elements, check both context.dataIndex and context.type to distinguish between entering and updating elements.
6. Not Destroying Charts on Single-Page Apps
If you use Chart.js in a SPA and navigate away without calling chart.destroy(), the chart continues to listen to resize events and update the DOM — causing memory leaks. Always destroy on component unmount.
Practice Questions
What is the difference between
intersect: trueandintersect: falsein interaction settings?truerequires the cursor to be directly over a data element;falsetriggers interaction based on proximity, creating a crosshair-like experience.How do you make bars animate one after another in a wave effect? Use the
delaycallback in animation config:delay: function(ctx) { return ctx.dataIndex * 100; }. Each bar delays an additional 100ms.What’s the difference between
beforeDrawandafterDrawplugin hooks?beforeDrawruns before any chart elements are rendered (good for backgrounds).afterDrawruns after everything (good for overlays and annotations).Why would you use an external tooltip instead of the built-in one? External tooltips are real HTML elements and support images, links, formatted tables, and custom styling that Canvas-rendered tooltips cannot.
What happens if you define a custom plugin but don’t call
Chart.register()? Nothing. The plugin silently does nothing. No error is thrown.
Challenge
Build a dashboard page that shows:
- A bar chart with staggered animation (each bar drops in one after another with
easeOutBounce) - A custom plugin that draws a horizontal target line at a configurable value
- An external tooltip styled as a dark card with rounded corners, showing dataset label, value, and a color dot
- Three buttons: “Animate New Data” (randomizes data with animation), “Toggle Animation” (disables/enables), “Reset”
FAQ
Try It Yourself: Animated Dashboard with Custom Plugins
This mini project combines animations, a custom threshold line plugin, and interactive controls:
<!DOCTYPE html>
<html>
<head>
<title>Animated Dashboard</title>
<style>
body { font-family: sans-serif; padding: 20px; }
.chart-box { width: 600px; max-width: 100%; margin: 20px 0; }
.controls { margin: 10px 0; display: flex; gap: 10px; flex-wrap: wrap; }
.controls button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
</style>
</head>
<body>
<h2>Animated Sales Dashboard</h2>
<div class="controls">
<button onclick="animateChart()"
style="background:#4CAF50;color:white;">Animate New Data</button>
<button onclick="resetChart()"
style="background:#f44336;color:white;">Reset</button>
<button onclick="toggleAnimation()"
style="background:#FF9800;color:white;">Toggle Animation</button>
</div>
<div class="chart-box">
<canvas id="animChart"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// Custom plugin: draw a horizontal target line
var targetLinePlugin = {
id: "targetLine",
afterDraw: function(chart) {
if (!chart.options.plugins.targetLine) return;
var yScale = chart.scales.y;
var ctx = chart.ctx;
var value = chart.options.plugins.targetLine.value;
var color = chart.options.plugins.targetLine.color || "red";
var y = yScale.getPixelForValue(value);
ctx.save();
ctx.beginPath();
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.setLineDash([6, 4]);
ctx.moveTo(chart.chartArea.left, y);
ctx.lineTo(chart.chartArea.right, y);
ctx.stroke();
ctx.fillStyle = color;
ctx.font = "12px sans-serif";
ctx.textAlign = "right";
ctx.fillText("Target: " + value, chart.chartArea.right - 5, y - 5);
ctx.restore();
}
};
Chart.register(targetLinePlugin);
var animated = true;
var ctx = document.getElementById("animChart").getContext("2d");
var originalData = [12, 19, 8, 15, 22, 18, 25];
var chart = new Chart(ctx, {
type: "bar",
data: {
labels: ["Week 1", "Week 2", "Week 3", "Week 4", "Week 5", "Week 6", "Week 7"],
datasets: [{
label: "Sales",
data: originalData.slice(),
backgroundColor: "rgba(54, 162, 235, 0.7)",
borderColor: "rgb(54, 162, 235)",
borderWidth: 1,
borderRadius: 4
}]
},
options: {
responsive: true,
animation: {
duration: 1000,
easing: "easeOutBounce"
},
plugins: {
title: { display: true, text: "Weekly Sales (Target: 20)" },
targetLine: { value: 20, color: "#e91e63" }
},
scales: {
y: { beginAtZero: true, max: 30 }
}
}
});
function animateChart() {
chart.data.datasets[0].data = originalData.map(function() {
return Math.floor(Math.random() * 25) + 3;
});
chart.update();
}
function resetChart() {
chart.data.datasets[0].data = originalData.slice();
chart.update();
}
function toggleAnimation() {
animated = !animated;
chart.options.animation.duration = animated ? 1000 : 0;
}
</script>
</body>
</html>What to try: Click “Animate New Data” to see the bounce easing in action. Watch the pink dashed line at 20 — bars that exceed it represent above-target weeks. Click “Toggle Animation” to see instant updates vs animated transitions.
What’s Next
| Tutorial | What You’ll Learn |
|---|---|
| Chart.js Reference & Cheatsheet | Complete API reference for daily development |
| Scatter & Bubble Charts | Correlation analysis with scatter and bubble charts |
Related topics: JavaScript Canvas API basics help you understand what plugins do under the hood. CSS responsive design principles apply to chart container sizing.
What’s Next
Congratulations on completing this Chartjs Advanced tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro