Skip to content
Chart.js Advanced — Animation, Custom Plugins & Interactive Dashboards

Chart.js Advanced — Animation, Custom Plugins & Interactive Dashboards

DodaTech Updated Jun 6, 2026 16 min read

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.

Prerequisites: You should understand Chart.js basics and be comfortable with the line and bar chart configuration structure.

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:

EasingFeelUse Case
linearConstant speedData updates, no flourish
easeInOutQuadSmooth start and endGeneral purpose
easeOutBounceBouncy landingFun, playful charts
easeInOutElasticOvershoot with wobbleAttention-grabbing
easeOutCubicFast start, slow endBars 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

ModeWhat It HighlightsBest For
"point"Single point directly under mouseDense scatter plots
"nearest"Closest point(s) to mouseGeneral use
"index"All points with same index across datasetsComparing series at a point in time
"dataset"All points in the hovered datasetHighlighting one series
"x"All points near the mouse on x-axisLine charts, crosshair feel
"y"All points near the mouse on y-axisFinding 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:

  1. getPixelForValue(50) converts the data value 50 to a pixel position on the y-axis
  2. The plugin draws a dashed line across the chart area at that pixel position
  3. The afterDraw hook 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

  1. What is the difference between intersect: true and intersect: false in interaction settings? true requires the cursor to be directly over a data element; false triggers interaction based on proximity, creating a crosshair-like experience.

  2. How do you make bars animate one after another in a wave effect? Use the delay callback in animation config: delay: function(ctx) { return ctx.dataIndex * 100; }. Each bar delays an additional 100ms.

  3. What’s the difference between beforeDraw and afterDraw plugin hooks? beforeDraw runs before any chart elements are rendered (good for backgrounds). afterDraw runs after everything (good for overlays and annotations).

  4. 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.

  5. 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

Can I use CSS animations instead of Chart.js animations?

No. Chart.js renders on Canvas, not DOM elements. CSS animations don’t apply to Canvas content. Use Chart.js’s built-in animation configuration.

How do I reset a chart to its original state?

Call chart.reset() to return to the initial animation state. To restore original data, store the initial data array and reassign it, then call chart.update().

What is the difference between beforeDraw and afterDatasetsDraw?

beforeDraw runs before anything is rendered — grid, axes, data, everything. afterDatasetsDraw runs after the data is drawn but before ticks and labels. Use beforeDraw for background fills, afterDatasetsDraw for grid overlays, and afterDraw for top-layer annotations.

Can I add custom CSS classes to chart elements?

No. Chart.js renders on Canvas, not DOM. All styling must be done through chart configuration properties or custom plugins that draw directly on the Canvas context.

How do I change the cursor on chart hover?

Set options.onHover to modify the cursor style, or set options.hover to configure which elements trigger hover effects.

How do I update chart data without animation?

Pass 'none' to chart.update('none'). This updates instantly without any transition.

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

TutorialWhat You’ll Learn
Chart.js Reference & CheatsheetComplete API reference for daily development
Scatter & Bubble ChartsCorrelation 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