Skip to content

Chart.js Шпаргалка

Simple yet flexible JavaScript charting library.

01

Getting Started

Setup & First Chart

Chart.js renders on a canvas element. The type property sets the chart type. data contains labels and datasets with values and styling.

chartjs
<!-- include Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

<canvas id="myChart" width="400" height="400"></canvas>

<script>
const ctx = document.getElementById('myChart').getContext('2d');
new Chart(ctx, {
  type: 'bar',
  data: {
    labels: ['Red', 'Blue', 'Green'],
    datasets: [{
      label: 'Votes',
      data: [12, 19, 3],
      backgroundColor: ['red', 'blue', 'green']
    }]
  }
});
</script>

npm Install & ES Module Import

Use 'chart.js/auto' for the simplest setup — it imports Chart.js plus all controllers/elements and registers them automatically. For smaller production bundles prefer tree-shaking: import from 'chart.js' and register only the pieces you use.

chartjs
# install
npm install chart.js

// import the auto-bundled build (registers everything)
import Chart from 'chart.js/auto';

const ctx = document.getElementById('myChart');
const chart = new Chart(ctx, {
  type: 'line',
  data: { /* ... */ },
  options: { responsive: true }
});

Canvas & 2D Context

Chart.js can take either the <canvas> element or its 2d rendering context. When responsive is true the canvas is resized to fill its container, so the width/height HTML attributes are only fallbacks. Each canvas can hold exactly one Chart instance.

chartjs
// Chart.js accepts a canvas element OR a 2D context
const canvas = document.getElementById('myChart');
const chart = new Chart(canvas, { type: 'bar', data: {...} });

// equivalent: pass the 2d context
const ctx = canvas.getContext('2d');
const chart2 = new Chart(ctx, { type: 'bar', data: {...} });

// one chart per canvas; the canvas size is controlled by
// responsive options, NOT the width/height attributes

Available Chart Types

Chart.js ships eight chart types. Bar, line, scatter and bubble use Cartesian axes; pie, doughnut and polarArea are circular; radar has its own radial axis. The 'type' field at the top level selects the controller used for every dataset unless a dataset overrides it with its own type.

chartjs
// the 8 built-in chart types
const types = ['bar', 'line', 'pie', 'doughnut',
               'radar', 'polarArea', 'bubble', 'scatter'];

// each type maps to a controller:
//   bar/line    -> Cartesian controllers
//   pie/doughnut/polarArea -> circular controllers
//   radar       -> radar controller
//   bubble/scatter -> cartesian with point parsing

new Chart(ctx, { type: 'polarArea', data: {...} });

Tree-Shaking & Register

When importing from 'chart.js' (not /auto) nothing is registered, so you must register controllers, elements, scales and plugins yourself. This tree-shakeable approach can cut bundle size significantly. Forgetting to register a component is the most common 'scale is not a registered scale' error.

chartjs
import {
  Chart,
  BarController,
  BarElement,
  CategoryScale,
  LinearScale,
  Legend,
  Title,
  Tooltip,
} from 'chart.js';

// register ONLY what you use -> smaller bundle
Chart.register(
  BarController, BarElement,
  CategoryScale, LinearScale,
  Legend, Title, Tooltip,
);

new Chart(ctx, {
  type: 'bar',
  data: { labels: ['A', 'B'], datasets: [{ data: [1, 2] }] },
});

Destroy & Lifecycle

Each Chart instance owns its canvas and attached event listeners. Calling destroy() tears everything down so the canvas can be reused — essential in SPAs and React/Vue effects. Creating a second chart on an active canvas leaks memory and produces double-rendered graphics.

chartjs
const chart = new Chart(ctx, config);

// ...later, to re-render with new config:
chart.destroy();        // cleans up listeners and canvas
const fresh = new Chart(ctx, newConfig);

// never create two Chart instances on the same canvas;
// always destroy() the old one first.
02

Bar Charts

Basic Bar Chart

A bar chart maps each label to a bar whose height equals the data value. backgroundColor sets bar fill, borderColor/borderWidth the outline. Set y.beginAtZero so bars always start from zero — without it Chart.js may autoscale the axis minimum and visually distort the differences.

chartjs
new Chart(ctx, {
  type: 'bar',
  data: {
    labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
    datasets: [{
      label: 'Revenue',
      data: [12, 19, 7, 15, 22],
      backgroundColor: 'rgba(54, 162, 235, 0.6)',
      borderColor: 'rgba(54, 162, 235, 1)',
      borderWidth: 1
    }]
  },
  options: { scales: { y: { beginAtZero: true } } }
});

Horizontal Bar (indexAxis)

In Chart.js v3+ horizontal bars are created with indexAxis: 'y' on a 'bar' type — the old 'horizontalBar' type was removed. The x scale becomes the value axis. Horizontal bars are ideal for long category labels that would overlap on a vertical chart.

chartjs
new Chart(ctx, {
  type: 'bar',
  data: {
    labels: ['Task A', 'Task B', 'Task C'],
    datasets: [{ label: 'Hours', data: [5, 8, 3] }]
  },
  options: {
    indexAxis: 'y',   // horizontal bars (v3+ syntax)
    scales: { x: { beginAtZero: true } }
  }
});

Grouped Bars (Multiple Datasets)

Multiple datasets in a bar chart are placed side by side (grouped) by default. Chart.js auto-assigns colors only if you omit backgroundColor, so set explicit colors per dataset for predictable output. Each dataset shares the same labels array on the x axis.

chartjs
new Chart(ctx, {
  type: 'bar',
  data: {
    labels: ['Q1', 'Q2', 'Q3', 'Q4'],
    datasets: [
      { label: '2023', data: [20, 35, 30, 35],
        backgroundColor: 'rgba(255,99,132,0.6)' },
      { label: '2024', data: [25, 32, 34, 40],
        backgroundColor: 'rgba(54,162,235,0.6)' }
    ]
  },
  options: { scales: { y: { beginAtZero: true } } }
});

Stacked Bars

Stacked bars require stacked: true on BOTH the x and y scales. Each dataset's bars are drawn on top of the previous one, summing to a total. Stacking shows composition, but makes the upper series harder to compare across categories than grouped bars.

chartjs
new Chart(ctx, {
  type: 'bar',
  data: {
    labels: ['A', 'B', 'C'],
    datasets: [
      { label: 'Men',   data: [20, 35, 30], backgroundColor: '#36A2EB' },
      { label: 'Women', data: [25, 32, 34], backgroundColor: '#FF6384' }
    ]
  },
  options: {
    scales: {
      x: { stacked: true },
      y: { stacked: true, beginAtZero: true }
    }
  }
});

Bar Styling

borderRadius rounds bar corners (a single number applies to all corners; an object like {topLeft:8} targets specific corners). borderSkipped defaults to 'start' which omits one border — set false to draw all four. barPercentage and categoryPercentage together control bar thickness and spacing.

chartjs
new Chart(ctx, {
  type: 'bar',
  data: {
    labels: ['A', 'B', 'C'],
    datasets: [{
      data: [10, 20, 15],
      backgroundColor: '#FF6384',
      borderColor: '#fff',
      borderWidth: 2,
      borderRadius: 8,           // rounded corners (px or %)
      borderSkipped: false,      // draw border on all sides
      barPercentage: 0.8,        // bar width within category
      categoryPercentage: 0.7    // category width
    }]
  }
});

Floating Bars ([min, max])

Passing [min, max] pairs as data values produces floating bars that don't start at zero — perfect for ranges like daily temperature low-high, open-close, or confidence intervals. The y axis should NOT begin at zero for range bars. This also works on horizontal bars.

chartjs
new Chart(ctx, {
  type: 'bar',
  data: {
    labels: ['Mon', 'Tue', 'Wed'],
    datasets: [{
      label: 'Low-High temp',
      // each value is [min, max] -> a floating bar
      data: [[10, 18], [12, 20], [8, 15]],
      backgroundColor: 'rgba(75,192,192,0.6)'
    }]
  },
  options: { scales: { y: { beginAtZero: false } } }
});
03

Line Charts

Basic Line Chart

A line chart connects data points in order. borderColor is the line color; backgroundColor is used for the fill (if fill is enabled). tension (0-1) controls curve smoothness — 0 is straight segments, ~0.3-0.4 gives gentle bezier curves. Use 0 for precise data like financial prices.

chartjs
new Chart(ctx, {
  type: 'line',
  data: {
    labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
    datasets: [{
      label: 'Visitors',
      data: [30, 45, 28, 60, 52],
      borderColor: 'rgba(75,192,192,1)',
      backgroundColor: 'rgba(75,192,192,0.2)',
      tension: 0.3
    }]
  },
  options: { scales: { y: { beginAtZero: true } } }
});

Multiple Lines

Each dataset in a line chart becomes its own line, auto-assigned a color from the default palette (overridable with borderColor). Lines are drawn in dataset order, so the last dataset renders on top. Pass a label to each dataset so the legend can identify them.

chartjs
new Chart(ctx, {
  type: 'line',
  data: {
    labels: ['Jan', 'Feb', 'Mar', 'Apr'],
    datasets: [
      { label: 'Plan',  data: [10, 20, 25, 30], borderColor: 'red' },
      { label: 'Actual', data: [8, 22, 24, 33], borderColor: 'blue' }
    ]
  },
  options: {
    scales: { y: { beginAtZero: true } },
    plugins: { legend: { position: 'top' } }
  }
});

Line Tension & Interpolation

tension adds bezier smoothing but can overshoot data points (creating dips below the minimum). cubicInterpolationMode: 'monotone' smooths without overshoot — preferred for monotonic data like stock prices. borderDash is a [dash, gap] pattern for dashed/dotted lines.

chartjs
datasets: [{
  data: [1, 5, 2, 6, 3],
  tension: 0,                  // straight lines
  // tension: 0.4,             // smooth bezier
  cubicInterpolationMode: 'monotone',  // no overshoot
  borderDash: [5, 5],          // dashed line
  borderWidth: 2,
  borderColor: 'purple'
}]

Filled Lines (fill)

fill controls the area under the line. true/'origin' fills down to the x axis. A number (1, -1, +1) fills toward another dataset by index, useful for band charts. A negative number fills toward the previous dataset. Set backgroundColor with alpha so the line stays visible through the fill.

chartjs
datasets: [
  { label: 'A', data: [3,5,4,6], fill: true,  backgroundColor: 'rgba(255,99,132,0.3)' },
  { label: 'B', data: [1,2,3,2], fill: 'origin', backgroundColor: 'rgba(54,162,235,0.3)' },
  { label: 'C', data: [2,4,3,5], fill: 1,       backgroundColor: 'rgba(75,192,192,0.3)' }
]
// fill values: false | true/'origin' | 1/-1/+1 | '-1' | {target:...}

Point Styling

Point properties accept a single value or an array (one per data point). pointStyle supports many shapes ('circle', 'rect', 'rectRot', 'triangle', 'star', 'cross', 'crossRot'). Set pointRadius: 0 to hide points entirely (useful for dense line charts), and pointHoverRadius larger for an interactive pop effect.

chartjs
datasets: [{
  data: [4, 6, 5, 7],
  showLine: true,
  pointRadius: 5,           // point size (0 hides points)
  pointHoverRadius: 9,
  pointBackgroundColor: 'white',
  pointBorderColor: 'black',
  pointBorderWidth: 2,
  pointStyle: 'rectRot',    // 'circle','rect','triangle','star','cross'...
  // per-point arrays also work:
  // pointRadius: [0, 0, 6, 0],
}]

Span Gaps & Missing Data

null, undefined and NaN represent missing data and break the line by default. spanGaps: true connects the points on either side of a gap with a single segment. Distinguish NaN (missing) from 0 (a real zero) — confusing them is a common source of misleading charts.

chartjs
datasets: [{
  data: [5, null, 7, NaN, 9, undefined, 11],
  spanGaps: true,    // connect across null/NaN gaps
  // spanGaps: false -> the line breaks at missing points
}]

// also skip points entirely by setting them to NaN
// NaN is "no data"; 0 is a legitimate zero value
04

Pie & Doughnut Charts

Basic Pie Chart

A pie chart shows parts of a whole: each value becomes a wedge whose angle is proportional to its share. Provide one backgroundColor per slice (an array), otherwise Chart.js cycles a default palette. Pie charts work best with 3-6 categories — more becomes unreadable.

chartjs
new Chart(ctx, {
  type: 'pie',
  data: {
    labels: ['Rent', 'Food', 'Fun', 'Save'],
    datasets: [{
      data: [1200, 600, 400, 800],
      backgroundColor: ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0']
    }]
  }
});

Basic Doughnut Chart

A doughnut is a pie with a hole in the middle. The cutout property (percentage string or number of pixels) controls the hole size — '50%' is the classic donut look. The center hole can hold a summary label via a plugin, and ring length encodes value slightly better than wedge angle.

chartjs
new Chart(ctx, {
  type: 'doughnut',
  data: {
    labels: ['A', 'B', 'C'],
    datasets: [{
      data: [40, 35, 25],
      backgroundColor: ['#FF6384', '#36A2EB', '#FFCE56'],
      cutout: '50%'      // size of the inner hole
    }]
  }
});

Cutout, Rotation & Circumference

rotation sets the start angle in degrees (default 0 = right; -90 = top). circumference controls how much of the circle is drawn — 180 makes a half-doughnut, useful for gauge-style charts. Both live under options (not the dataset) in v3+, though cutout can appear in either.

chartjs
new Chart(ctx, {
  type: 'doughnut',
  data: { labels: ['A','B','C'], datasets: [{ data: [30,40,30], backgroundColor: ['#f00','#0f0','#00f'] }] },
  options: {
    cutout: '60%',
    rotation: -90,        // start at top (degrees)
    circumference: 360    // full circle; 180 = half doughnut
  }
});

Border & Offset

borderColor with borderWidth separates wedges visually (white borders give a clean segmented look). hoverOffset pushes a slice outward when the user hovers it, providing clear interactive feedback. offset is an array (one per slice) to permanently 'explode' specific wedges for emphasis.

chartjs
datasets: [{
  data: [30, 40, 30],
  backgroundColor: ['#FF6384', '#36A2EB', '#FFCE56'],
  borderColor: 'white',
  borderWidth: 3,
  hoverOffset: 16,        // pop slice out on hover
  offset: [0, 20, 0]      // permanently offset 2nd slice
}]

Half Doughnut (Gauge)

A half-doughnut (rotation: -90, circumference: 180) reads as a gauge/progress meter. Pair it with a center-text plugin to show the percentage, and disable the legend for a clean KPI look. The 'remaining' slice in a neutral gray conveys the uncompleted portion.

chartjs
new Chart(ctx, {
  type: 'doughnut',
  data: {
    labels: ['Done', 'Remaining'],
    datasets: [{
      data: [72, 28],
      backgroundColor: ['#4BC0C0', '#E0E0E0']
    }]
  },
  options: {
    rotation: -90,
    circumference: 180,
    plugins: { legend: { display: false } }
  }
});

Tooltip Percentage Callback

Pie/doughnut tooltips show raw values by default. Use the tooltip callbacks.label to compute and display percentages instead — ctx.parsed is the slice value and ctx.dataset.data holds all values for the sum. Template literals in callbacks must escape backticks and ${} when embedded in this cheatsheet's code string.

chartjs
options: {
  plugins: {
    tooltip: {
      callbacks: {
        label: (ctx) => {
          const total = ctx.dataset.data.reduce((a, b) => a + b, 0);
          const pct = (ctx.parsed / total * 100).toFixed(1);
          return `${ctx.label}: ${ctx.parsed} (${pct}%)`;
        }
      }
    }
  }
}
05

Radar Charts

Basic Radar Chart

A radar chart plots each label as a vertex on a polygon, connecting them into a closed shape. fill: true shades the interior. The radial scale is configured under scales.r (not x/y). Radars compare multivariate profiles but can mislead with too many axes or non-comparable units.

chartjs
new Chart(ctx, {
  type: 'radar',
  data: {
    labels: ['Speed', 'Power', 'Range', 'Comfort', 'Price'],
    datasets: [{
      label: 'Model X',
      data: [8, 7, 6, 9, 5],
      borderColor: 'rgba(255,99,132,1)',
      backgroundColor: 'rgba(255,99,132,0.2)',
      fill: true
    }]
  },
  options: { scales: { r: { beginAtZero: true } } }
});

Radar Styling

Radar datasets accept the same styling as line charts: borderColor, fill, point styling, tension. A small tension (0.1) slightly rounds the vertices for a softer look; 0 keeps straight spikes. Use translucent backgroundColor so overlapping radar shapes remain visible.

chartjs
datasets: [{
  data: [7, 8, 6, 9, 5],
  borderColor: 'blue',
  backgroundColor: 'rgba(0,0,255,0.2)',
  borderWidth: 2,
  pointRadius: 4,
  pointBackgroundColor: 'blue',
  fill: true,
  tension: 0.1            // slight curve between vertices
}]

Radar Scale (r) Settings

The radial scale r controls rings and labels. min/max fix the value range across all axes (essential when comparing datasets). ticks.stepSize sets ring spacing; pointLabels styles the category names around the perimeter. Set ticks.backdropColor transparent so ring numbers don't obscure the gridlines.

chartjs
options: {
  scales: {
    r: {
      min: 0,
      max: 10,
      ticks: { stepSize: 2, backdropColor: 'transparent' },
      pointLabels: { font: { size: 13 }, color: '#333' },
      grid: { color: '#ccc' },
      angleLines: { color: '#ccc' }
    }
  }
}

Radar with Multiple Datasets

Multiple datasets overlay as concentric polygons, ideal for before/after or competitor comparisons. Always set the same min/max on r so the shapes are comparable, and use translucent fills so both profiles remain visible where they overlap.

chartjs
new Chart(ctx, {
  type: 'radar',
  data: {
    labels: ['A', 'B', 'C', 'D', 'E'],
    datasets: [
      { label: 'Before', data: [5,6,4,7,5], borderColor: 'red',  backgroundColor: 'rgba(255,0,0,0.15)', fill: true },
      { label: 'After',  data: [7,8,6,9,7], borderColor: 'blue', backgroundColor: 'rgba(0,0,255,0.15)', fill: true }
    ]
  }
});

Radar Angle Lines & Grid

angleLines are the spokes from center to each vertex; grid is the concentric polygon rings. Hiding ticks (display:false) removes the numeric ring labels for a cleaner aesthetic while keeping the rings themselves. pointLabels controls only the category text at each vertex.

chartjs
options: {
  scales: {
    r: {
      angleLines: { display: true, color: 'rgba(0,0,0,0.2)', lineWidth: 1 },
      grid: { display: true, color: 'rgba(0,0,0,0.1)' },
      pointLabels: { display: true, color: '#000', font: { weight: 'bold' } },
      ticks: { display: false }   // hide numeric ring labels for a cleaner look
    }
  }
}
06

Polar Area Charts

Basic Polar Area Chart

A polar area chart draws each value as a sector spanning the same angle (360/N each), with the radius encoding the value. Unlike pie (where angle = value), here the radius = value, so all slices have equal angular width but different lengths.

chartjs
new Chart(ctx, {
  type: 'polarArea',
  data: {
    labels: ['North', 'South', 'East', 'West'],
    datasets: [{
      data: [11, 16, 7, 14],
      backgroundColor: ['#FF6384', '#4BC0C0', '#FFCE56', '#36A2EB']
    }]
  },
  options: { scales: { r: { beginAtZero: true } } }
});

Polar Area vs Pie

The key difference: in a pie, each slice's ANGLE is proportional to its value; in a polar area, each slice's RADIUS is proportional to its value while angles are equal. Use polar area when you want every category equally visible but sized by magnitude.

chartjs
// PIE:    angle = value,  radius = constant
new Chart(ctx, { type: 'pie',     data: { datasets: [{ data: [10, 20, 30] }] } });

// POLAR:  angle = constant, radius = value
new Chart(ctx, { type: 'polarArea', data: { datasets: [{ data: [10, 20, 30] }] } });

// polar area is better when values differ widely AND
// you want every category to occupy an equal angular share

Polar Scale (r)

Polar area uses a single radial scale r shared by all sectors — so all values are measured against the same maximum radius. Setting an explicit max makes the chart honest about scale; without it Chart.js autoscales to the largest value, which can exaggerate small differences.

chartjs
options: {
  scales: {
    r: {
      min: 0,
      max: 20,
      ticks: { stepSize: 5 },
      grid: { color: 'rgba(0,0,0,0.15)' },
      angleLines: { color: 'rgba(0,0,0,0.15)' }
    }
  }
}

Polar Border Styling

Polar area sectors accept the same border/fill styling as pie/doughnut. White borders separate adjacent sectors clearly. hoverBackgroundColor changes a sector's fill on hover for interactive feedback. Unlike doughnut there is no cutout — the sectors meet at the center.

chartjs
datasets: [{
  data: [11, 16, 7, 14],
  backgroundColor: ['#FF6384', '#4BC0C0', '#FFCE56', '#36A2EB'],
  borderColor: 'white',
  borderWidth: 2,
  hoverBackgroundColor: '#333'
}]

Polar with Hover Offset

Polar area supports hoverBackgroundColor and hoverBorderColor but does NOT support hoverOffset / offset the same way doughnut does, because sectors share a radial scale. If 'popping out' a slice is essential, choose doughnut instead. Use color and tooltip changes for polar interactivity.

chartjs
datasets: [{
  data: [11, 16, 7, 14],
  backgroundColor: ['#FF6384', '#4BC0C0', '#FFCE56', '#36A2EB'],
  hoverOffset: 12     // not supported on polarArea the same way as doughnut;
                      // use hoverBackgroundColor + tooltips for interactivity
}]
// for true "pop out" use 'doughnut' instead
07

Bubble Charts

Basic Bubble Chart

Bubble chart data points are objects {x, y, r} where x and y are coordinates and r is the bubble RADIUS in pixels. Unlike scatter, the bubble size encodes a third variable. Note r is a radius (not area), so a 2x larger value is visually 4x the area — consider sqrt-scaling your size data.

chartjs
new Chart(ctx, {
  type: 'bubble',
  data: {
    datasets: [{
      label: 'Products',
      data: [
        { x: 10, y: 20, r: 15 },
        { x: 15, y: 10, r: 10 },
        { x:  7, y: 25, r:  8 }
      ],
      backgroundColor: 'rgba(255,99,132,0.6)'
    }]
  },
  options: { scales: { y: { beginAtZero: true } } }
});

Bubble Size Mapping

Because humans perceive bubble AREA rather than radius, map your value through Math.sqrt before assigning r, so doubling the value doubles the visible area. Pick a divisor that keeps the largest bubble from overlapping too many neighbors. This makes the chart perceptually honest.

chartjs
const raw = [{x:1,y:2,pop:1000}, {x:2,y:3,pop:5000}, {x:3,y:1,pop:20000}];

// map a real-world value to a pixel radius with sqrt scaling
// so bubble AREA (not radius) is proportional to the value
const data = raw.map(d => ({
  x: d.x,
  y: d.y,
  r: Math.sqrt(d.pop) / 10    // tune the divisor for your dataset
}));

new Chart(ctx, { type: 'bubble', data: { datasets: [{ data }] } });

Bubble Colors per Point

Like other Chart.js datasets, bubble styling properties accept arrays to style each bubble individually. Use this to color-code bubbles by category while size encodes a separate numeric value, giving a 4-dimensional chart (x, y, size, color).

chartjs
datasets: [{
  data: [
    { x: 10, y: 20, r: 15 },
    { x: 15, y: 10, r: 10 }
  ],
  // arrays give per-point styling
  backgroundColor: ['rgba(255,99,132,0.6)', 'rgba(54,162,235,0.6)'],
  borderColor:     ['rgba(255,99,132,1)',   'rgba(54,162,235,1)'],
  borderWidth: 2
}]

Multiple Bubble Datasets

Multiple datasets render as differently-colored bubble groups, each in the legend. Keep bubble radii modest when overlaying groups so bubbles don't fully obscure one another. alpha-transparent fills (0.5-0.6) help overlapping bubbles remain readable.

chartjs
new Chart(ctx, {
  type: 'bubble',
  data: {
    datasets: [
      { label: 'Asia',   data: [{x:1,y:2,r:10},{x:3,y:4,r:8}],  backgroundColor: 'rgba(255,99,132,0.6)' },
      { label: 'Europe', data: [{x:2,y:5,r:12},{x:4,y:2,r:7}],  backgroundColor: 'rgba(54,162,235,0.6)' }
    ]
  }
});

Bubble Hover Styling

Hover properties (hoverBackgroundColor, hoverBorderColor, hoverBorderWidth, hoverRadius) apply only to the bubble under the pointer. hoverRadius adds extra pixels to the radius on hover, making the targeted bubble visibly pop. Combined with tooltips this gives clear interactive focus.

chartjs
datasets: [{
  data: [{x:1,y:2,r:10}],
  backgroundColor: 'rgba(75,192,192,0.5)',
  hoverBackgroundColor: 'rgba(75,192,192,0.9)',
  hoverBorderColor: 'black',
  hoverBorderWidth: 3,
  hoverRadius: 2      // EXTRA radius added on hover (v3+ uses hoverRadius)
}]
08

Scatter Charts

Basic Scatter Chart

Scatter chart data points are {x, y} objects (no radius needed). The x scale should be type: 'linear' (NOT category) so numeric x values map to actual positions. Scatter is the standard chart for showing correlation between two continuous variables.

chartjs
new Chart(ctx, {
  type: 'scatter',
  data: {
    datasets: [{
      label: 'Observations',
      data: [
        { x: 1.2, y: 2.3 },
        { x: 1.8, y: 3.1 },
        { x: 2.5, y: 4.0 },
        { x: 3.1, y: 4.8 }
      ],
      backgroundColor: 'rgba(75,192,192,0.7)'
    }]
  },
  options: { scales: { x: { type: 'linear', position: 'bottom' } } }
});

Scatter with Many Points

For thousands of points, shrink pointRadius (1-2px) and use semi-transparent backgroundColor (alpha 0.2-0.4) so overlapping points reveal density through darker regions. Disable point hover for very large datasets (pointHitRadius: 0) to keep interaction responsive.

chartjs
const n = 2000;
const data = Array.from({ length: n }, () => ({
  x: Math.random() * 100,
  y: Math.random() * 100
}));

new Chart(ctx, {
  type: 'scatter',
  data: { datasets: [{ data, pointRadius: 1.5, backgroundColor: 'rgba(0,0,0,0.3)' }] }
});

Scatter Point Styling

pointStyle supports many shapes beyond the default circle: 'rect', 'rectRot', 'triangle', 'star', 'cross', 'crossRot', 'dash'. pointRotation rotates non-circular shapes. All point* properties accept arrays for per-point styling — useful for highlighting specific observations.

chartjs
datasets: [{
  data: [{x:1,y:2},{x:2,y:3}],
  pointRadius: 8,
  pointHoverRadius: 12,
  pointStyle: 'triangle',          // 'circle','rect','star','cross',...
  pointBackgroundColor: 'red',
  pointBorderColor: 'darkred',
  pointBorderWidth: 2,
  pointRotation: 45                // rotation in degrees (for shapes)
}]

Scatter with Trend Line

Chart.js has no built-in trend line, so compute a regression (least-squares here) and add a 'line' dataset with two endpoints and pointRadius:0. The line dataset's type overrides the chart type per-dataset — this is also how mixed charts work. fill:false keeps it as just a line.

chartjs
// simple least-squares fit
const pts = [{x:1,y:2},{x:2,y:3.5},{x:3,y:4.2},{x:4,y:5.1}];
const n = pts.length;
const mX = pts.reduce((s,p)=>s+p.x,0)/n;
const mY = pts.reduce((s,p)=>s+p.y,0)/n;
const slope = pts.reduce((s,p)=>s+(p.x-mX)*(p.y-mY),0) / pts.reduce((s,p)=>s+(p.x-mX)**2,0);
const intercept = mY - slope*mX;

new Chart(ctx, {
  type: 'scatter',
  data: { datasets: [
    { data: pts, backgroundColor: 'blue' },
    { type: 'line', data: [{x:0,y:intercept},{x:5,y:slope*5+intercept}],
      borderColor: 'red', pointRadius: 0, fill: false }
  ]}
});

Scatter to Line (showLine)

showLine: true on a scatter dataset connects the points with a line, effectively turning scatter into a line chart that uses numeric x coordinates. This is the right choice when x values are real numbers (not category labels) but you still want connecting segments — e.g. time series with irregular spacing.

chartjs
new Chart(ctx, {
  type: 'scatter',
  data: { datasets: [{
    data: [{x:1,y:2},{x:2,y:3},{x:3,y:5}],
    showLine: true,            // connect points with a line
    borderColor: 'green',
    backgroundColor: 'green',
    pointRadius: 4
  }]}
});
09

Data — Datasets

Datasets Structure

data holds labels (shared across all datasets) and datasets (an array of series). Each dataset has a label (shown in legend/tooltips), a data array aligned with labels, and styling properties. Properties set on the dataset apply to every point unless overridden by an array.

chartjs
data: {
  labels: ['A', 'B', 'C'],          // shared category labels
  datasets: [
    {
      label: 'Series 1',             // legend + tooltip label
      data: [10, 20, 30],            // values (one per label)
      backgroundColor: 'rgba(0,0,255,0.5)',
      borderColor: 'blue',
      borderWidth: 1,
      // ... type-specific styling
    }
  ]
}

labels & data Alignment

Each dataset's data array is positionally aligned with the shared labels array — data[i] is the value for labels[i]. A shorter data array produces undefined values for the missing tail, which line charts treat as gaps. Always keep labels and data the same length to avoid surprising gaps.

chartjs
data: {
  labels: ['Jan', 'Feb', 'Mar'],
  datasets: [
    { data: [10, 20, 30] },          // Jan=10, Feb=20, Mar=30
    { data: [5, 15] }                // Jan=5, Feb=15, Mar=undefined
  ]
}
// data[i] always corresponds to labels[i]; a shorter data array
// leaves the remaining positions as undefined (missing data)

Multiple Datasets

Multiple datasets render as parallel series (grouped bars, multiple lines, overlapping radars). They share one labels array. Set distinct colors per dataset — Chart.js only auto-colors when backgroundColor is omitted, and even then explicit colors are more predictable across versions.

chartjs
data: {
  labels: ['Q1','Q2','Q3','Q4'],
  datasets: [
    { label: '2023', data: [20,35,30,35], backgroundColor: 'rgba(255,99,132,0.6)' },
    { label: '2024', data: [25,32,34,40], backgroundColor: 'rgba(54,162,235,0.6)' },
    { label: '2025', data: [28,38,36,45], backgroundColor: 'rgba(75,192,192,0.6)' }
  ]
}

Dataset-Level Styling

Dataset-level style applies to every point in that dataset. hover* variants activate on hover. hidden:true starts the dataset hidden (the legend toggle can show it again). order controls z-order — lower numbers draw later (on top) — useful in mixed/overlapping charts.

chartjs
datasets: [{
  label: 'Revenue',
  data: [10, 20, 30],
  backgroundColor: 'rgba(54,162,235,0.6)',
  borderColor: 'rgba(54,162,235,1)',
  borderWidth: 2,
  hoverBackgroundColor: 'rgba(54,162,235,0.9)',
  hidden: false,                  // set true to hide from view + legend toggle
  order: 0                        // lower order draws on top
}]

Parsing Object Data

When your data is an array of objects, tell Chart.js which fields hold the axis values via parsing: {xAxisKey, yAxisKey}. This avoids manual mapping to {x, y} pairs. The keys default to 'x' and 'y', so {x, y} objects work without a parsing config.

chartjs
new Chart(ctx, {
  type: 'bar',
  data: {
    datasets: [{
      data: [
        { name: 'Red', votes: 12 },
        { name: 'Blue', votes: 19 }
      ],
      parsing: { xAxisKey: 'name', yAxisKey: 'votes' }
    }]
  }
});

Per-Point Styling Arrays

Any styling property can be a single value (applies to all points) or an array (one per point). This enables highlighting specific points — e.g. set most backgroundColor to gray and one to red to draw attention. Array length should match the data length; shorter arrays cycle.

chartjs
datasets: [{
  data: [10, 20, 30, 40],
  // every styling property accepts an array (one value per point)
  backgroundColor: ['red', 'green', 'blue', 'orange'],
  borderColor:     ['darkred', 'darkgreen', 'darkblue', 'darkorange'],
  borderWidth: [1, 2, 3, 4],
  pointRadius: [3, 6, 9, 12]   // (line/scatter only)
}]
10

Options — Responsive & Scales

Options Structure

options is the top-level configuration object. It groups concerns: plugins (legend/title/tooltip), scales (x/y/r), layout (padding), animation, interaction (hover mode), and events. Almost every Chart.js behavior is tuned here rather than on individual datasets.

chartjs
new Chart(ctx, {
  type: 'bar',
  data: { /* ... */ },
  options: {
    responsive: true,
    maintainAspectRatio: true,
    plugins: { legend: {}, title: {}, tooltip: {} },
    scales:  { x: {}, y: {} },
    layout:  { padding: 10 },
    animation: { duration: 800 },
    interaction: { mode: 'nearest', intersect: false },
    events: ['mousemove', 'click', 'touchstart']
  }
});

Responsive & maintainAspectRatio

responsive:true makes the chart fill its container's width and re-render on resize. maintainAspectRatio:false releases the height so the container's CSS height is honored — set both and wrap the canvas in a sized <div>. With maintainAspectRatio:true (default) the chart keeps a fixed width/height ratio.

chartjs
<div style="width:100%; height:400px;">
  <canvas id="c"></canvas>
</div>

<script>
new Chart(document.getElementById('c'), {
  type: 'line',
  data: { /* ... */ },
  options: {
    responsive: true,           // resize with container
    maintainAspectRatio: false  // let the container control height
  }
});
</script>

Scales Overview

scales configures each axis. The id (x, y, r) is the scale key; type selects the scale class. Common types: 'category' (text labels), 'linear' (numbers), 'logarithmic', 'time' (requires a date adapter), 'radialLinear' (radar/polar). title.display + text adds an axis label.

chartjs
options: {
  scales: {
    x: { type: 'category', title: { display: true, text: 'Month' } },
    y: { type: 'linear',   title: { display: true, text: 'Value' },
         beginAtZero: true },
    r: { type: 'radialLinear' }   // for radar/polarArea
  }
}
// built-in scale types: category, linear, logarithmic, time,
//                       timeseries, radialLinear

Plugins Overview

plugins groups the built-in title, legend and tooltip plugins plus any registered custom plugins' options by their id. title adds a chart title above the canvas. legend controls the dataset key. tooltip controls hover popups. Custom plugins read their options from this same plugins object.

chartjs
options: {
  plugins: {
    title:   { display: true, text: 'My Chart', font: { size: 18 } },
    legend:  { display: true, position: 'top' },
    tooltip: { enabled: true, mode: 'index', intersect: false },
    // a custom inline plugin:
    myPlugin: { prop: 'value' }
  }
}

Layout (Padding)

layout.padding adds space inside the canvas between the chart area and the canvas edge — useful when titles, axis labels or large tooltips would otherwise be clipped. A single number pads all sides equally; the object form allows per-side values.

chartjs
options: {
  layout: {
    padding: {
      top: 10, right: 20, bottom: 10, left: 20
    }
  }
}
// or a single number for all four sides:
// layout: { padding: 15 }

Global Defaults (Chart.defaults)

Chart.defaults holds the global default configuration — set values once at app startup and every chart inherits them. This is the right place for branding (font family, base color, default animation). Per-chart options always override defaults, so you can still customize individual charts.

chartjs
// set once, applies to every chart created afterwards
Chart.defaults.font.family = "'Segoe UI', sans-serif";
Chart.defaults.font.size = 13;
Chart.defaults.color = '#333';
Chart.defaults.borderColor = '#ddd';
Chart.defaults.plugins.legend.labels.color = '#333';
Chart.defaults.animation.duration = 600;

// per-chart override still wins:
new Chart(ctx, { options: { font: { size: 16 } } });
11

Axes — X & Y

Linear Axis

A linear axis maps numbers proportionally. beginAtZero forces the axis to start at 0 (recommended for bar charts). min/max set hard limits; suggestedMin/suggestedMax are soft hints that Chart.js may expand to fit ticks. ticks.callback customizes the label text (e.g. append a unit).

chartjs
options: {
  scales: {
    y: {
      type: 'linear',
      beginAtZero: true,
      min: 0, max: 100,        // hard limits
      ticks: { stepSize: 10, callback: (v) => v + '%' }
    }
  }
}

Category Axis

A category axis shows text labels at fixed positions. It's the default x axis for bar/line charts. ticks.autoSkip:false forces every label to show (otherwise Chart.js drops overlapping ones). maxRotation tilts labels to prevent overlap when there are many categories.

chartjs
options: {
  scales: {
    x: {
      type: 'category',
      labels: ['Mon','Tue','Wed'],   // optional; usually from data.labels
      ticks: { autoSkip: false, maxRotation: 45, minRotation: 0 }
    }
  }
}

Time Axis

A time axis plots Date objects or ISO strings proportionally to time, with gaps where there is no data. It requires a date adapter package (chartjs-adapter-date-fns, -luxon, -dayjs). time.unit fixes the tick granularity ('day','month','year'); without it Chart.js auto-picks a unit.

chartjs
# install a date adapter
npm install chart.js date-fns date-fns-tz

import 'chartjs-adapter-date-fns';

new Chart(ctx, {
  type: 'line',
  data: { datasets: [{ data: [
    {x: new Date('2024-01-01'), y: 10},
    {x: new Date('2024-02-01'), y: 20},
    {x: new Date('2024-04-01'), y: 15}
  ]}]},
  options: { scales: { x: { type: 'time', time: { unit: 'month' } } } }
});

Logarithmic Axis

A logarithmic axis spaces ticks by powers of 10, essential for data spanning orders of magnitude (population, prices, frequencies). It cannot display 0 or negative values. ticks.callback lets you format the powers-of-ten labels compactly (e.g. scientific notation).

chartjs
new Chart(ctx, {
  type: 'line',
  data: { datasets: [{ data: [1, 10, 100, 1000, 10000] }] },
  options: {
    scales: {
      y: { type: 'logarithmic',
           ticks: { callback: (v) => v >= 1 ? v.toExponential(0) : v } }
    }
  }
});

Axis Title & Ticks Callback

title.display + title.text add an axis label (always off by default — set display:true). ticks.callback receives each tick value and returns the display string, letting you format numbers as currency, percentages, k/M abbreviations, or any custom text without altering the underlying data.

chartjs
options: {
  scales: {
    y: {
      title: { display: true, text: 'Revenue (USD)', color: '#333', font: { size: 14, weight: 'bold' } },
      ticks: {
        callback: function(value) {
          if (value >= 1000) return (value/1000) + 'k';
          return value;
        }
      }
    }
  }
}

Min/Max vs suggestedMin/Max

min/max are HARD limits: the axis stays within them even if data exceeds the range (data is clipped). suggestedMin/suggestedMax are SOFT hints: Chart.js uses them as a baseline but may expand to produce round tick values. Prefer suggested* when you want nice round numbers; use hard min/max for fixed scales like percentages.

chartjs
options: {
  scales: {
    y: {
      // hard limits — chart never goes outside this range
      min: 0, max: 100,
      // OR soft hints — Chart.js may expand to fit nice ticks:
      // suggestedMin: 0, suggestedMax: 100,
      ticks: { stepSize: 20 }
    }
  }
}
12

Legend

Legend Basics

The legend lists each dataset's label with a color swatch. display:true shows it (default), display:false hides it. position accepts 'top', 'bottom', 'left', 'right' or 'center' (chartArea). Clicking a legend item toggles that dataset's visibility by default — a built-in filtering interaction.

chartjs
new Chart(ctx, {
  type: 'bar',
  data: { labels: ['A','B'], datasets: [{ label: 'Sales', data: [10,20] }] },
  options: {
    plugins: {
      legend: { display: true, position: 'top' }
    }
  }
});

Legend Position & Align

position sets the legend's side; align controls its placement along that side ('start'/'center'/'end'). labels configures the swatch (boxWidth/boxHeight) and text styling. maxWidth limits the legend area so very long legends wrap instead of pushing the chart aside.

chartjs
plugins: {
  legend: {
    position: 'bottom',
    align: 'center',     // 'start' | 'center' | 'end'
    maxWidth: 300,
    labels: {
      boxWidth: 20,
      boxHeight: 20,
      padding: 15,
      color: '#333',
      font: { size: 12 }
    }
  }
}

Legend onClick (Custom Toggle)

Override legend.onClick to customize click behavior. The default toggles dataset visibility. The signature is (event, legendItem, legend). Calling chart.update() after changing state re-renders. Use this to confirm before hiding, sync with external UI, or implement single-select (radio) behavior.

chartjs
plugins: {
  legend: {
    onClick(e, item, legend) {
      const chart = legend.chart;
      const dataset = chart.data.datasets[item.datasetIndex];
      // custom: toggle visibility AND log the action
      dataset.hidden = !dataset.hidden;
      console.log('Toggled', dataset.label, '->', dataset.hidden);
      chart.update();
    }
  }
}

Legend Labels Callback

generateLabels returns an array of label objects that fully control what appears in the legend. Each item's text, swatch colors, and hidden state can be customized — useful for adding counts, units, or deriving a legend from non-dataset data. The hidden flag and datasetIndex link clicks back to the right dataset.

chartjs
plugins: {
  legend: {
    labels: {
      generateLabels(chart) {
        return chart.data.datasets.map((ds, i) => ({
          text: `${ds.label} (${ds.data.length} pts)`,
          fillStyle: ds.backgroundColor,
          strokeStyle: ds.borderColor,
          lineWidth: ds.borderWidth,
          hidden: ds.hidden,
          datasetIndex: i
        }));
      }
    }
  }
}

Hide Legend & External Legend

For full styling control, disable the built-in legend (display:false) and render your own HTML legend. Iterate chart.data.datasets, render buttons/swatches, and toggle dataset.hidden + chart.update() on click. This is common when matching a design system where the default canvas legend won't fit in.

chartjs
// hide the built-in legend entirely
options: { plugins: { legend: { display: false } } }

// build your own external legend HTML, then wire it to the chart:
const chart = new Chart(ctx, { /* ... */, options: { plugins: { legend: { display: false } } } });

chart.data.datasets.forEach((ds, i) => {
  const btn = document.createElement('button');
  btn.textContent = ds.label;
  btn.onclick = () => { ds.hidden = !ds.hidden; chart.update(); };
  document.getElementById('legend').appendChild(btn);
});
13

Tooltips

Tooltip Basics

Tooltips appear on hover showing the dataset label and value. enabled:true by default. Styling properties (backgroundColor, titleColor, bodyColor, padding, cornerRadius) control the look. displayColors toggles the small color swatches next to each item — useful when many datasets overlap.

chartjs
options: {
  plugins: {
    tooltip: {
      enabled: true,           // show tooltips
      backgroundColor: 'rgba(0,0,0,0.8)',
      titleColor: 'white',
      bodyColor: 'white',
      borderColor: 'gray',
      borderWidth: 1,
      padding: 10,
      cornerRadius: 4,
      displayColors: true      // show color boxes
    }
  }
}

Tooltip label Callback

callbacks.label returns the text for each tooltip row. ctx.dataset.label is the series name, ctx.parsed.y holds the value (use ctx.parsed for pie/radar). Format currency, append units, or compute totals here. The returned string (or array of strings) becomes the tooltip body.

chartjs
plugins: {
  tooltip: {
    callbacks: {
      label: (ctx) => {
        const label = ctx.dataset.label || '';
        const value = ctx.parsed.y ?? ctx.parsed;
        return `${label}: $${value}`;
      }
    }
  }
}

Tooltip Title Callback

callbacks.title controls the tooltip header (default: the x-axis label). The function receives an array of tooltip items (useful when mode:'index' groups multiple datasets). Returning an empty string '' suppresses the title entirely. Combine title and label callbacks for fully custom tooltip content.

chartjs
plugins: {
  tooltip: {
    callbacks: {
      title: (items) => {
        // items[0].label is the x-axis label / category
        return 'Period: ' + items[0].label;
      },
      label: (ctx) => ctx.dataset.label + ': ' + ctx.formattedValue
    }
  }
}

External HTML Tooltip

external lets you render the tooltip as real HTML — for rich styling, images, or links impossible on canvas. Set enabled:false so the default tooltip doesn't double-render. The callback receives a context with a tooltip object holding caretX/caretY (position) and body lines. Position the HTML element at those coordinates.

chartjs
plugins: {
  tooltip: {
    enabled: false,                  // disable default canvas tooltip
    external(context) {
      const { tooltip } = context;
      let el = document.getElementById('tt');
      if (!el) { el = document.createElement('div'); el.id = 'tt'; document.body.appendChild(el); }
      if (tooltip.opacity === 0) { el.style.opacity = 0; return; }
      el.innerHTML = tooltip.body.map(b => b.lines.join('')).join('<br>');
      el.style.position = 'fixed';
      el.style.left = tooltip.caretX + 'px';
      el.style.top  = tooltip.caretY + 'px';
      el.style.opacity = 1;
    }
  }
}

Tooltip Mode (interaction)

interaction.mode controls what the tooltip targets. 'point' shows only the hovered point; 'index' shows all datasets at the same x (great for grouped bars/lines); 'nearest' finds the closest single point. intersect:false triggers the tooltip when the pointer is anywhere in the column, not just on a point — usually what users expect.

chartjs
options: {
  interaction: {
    mode: 'index',     // 'point' | 'nearest' | 'index' | 'x' | 'y' | 'dataset'
    intersect: false   // trigger even when not directly over a point
  },
  plugins: { tooltip: { mode: 'index', intersect: false } }
}

Tooltip Styling & Filters

filter skips tooltip rows that don't meet a condition (e.g. hide zero/empty values). callbacks.footer adds a summary row after all items — perfect for showing a total. bodyFont/titleFont style the text; caretSize and caretPadding control the little pointer arrow.

chartjs
plugins: {
  tooltip: {
    filter: (item) => item.parsed.y !== 0,   // hide zero values
    callbacks: { footer: (items) => 'Total: ' + items.reduce((s,i)=>s+i.parsed.y,0) },
    bodyFont: { size: 13, weight: 'bold' },
    titleFont: { size: 14 },
    caretSize: 6,
    caretPadding: 8
  }
}
14

Animation

Animation Duration

animation.duration sets the total animation length in milliseconds (default 1000). easing controls the acceleration curve — 'easeOutQuart' (the default) starts fast and decelerates, which feels responsive. Shorten duration (200-400ms) for data-dashboards that update frequently.

chartjs
options: {
  animation: {
    duration: 1000,      // total duration in ms
    easing: 'easeOutQuart'
  }
}
// easing options: 'linear','easeInQuad','easeOutQuad','easeInOutQuad',
// 'easeInCubic','easeOutCubic','easeInOutCubic','easeOutBounce',...

Disable Animation

Set animation:false to turn off all animations entirely — best for performance-critical dashboards or when feeding rapid updates. animation:{duration:0} keeps the animation system active but instant (hover still works). The plural animations object lets you disable specific properties like y while animating others.

chartjs
// disable ALL animations
options: { animation: false }

// disable only the initial draw, keep hover animations:
options: { animation: { duration: 0 } }

// per-property: animate everything except the y axis
options: {
  animations: {
    y: { duration: 0 }
  }
}

Easing Functions

Easing changes how an animation progresses over time, dramatically affecting feel. 'easeOutQuart' (default) feels professional and responsive. 'easeOutBounce' or 'easeInOutBack' are playful but can feel gimmicky on data charts. Match the easing to your app's overall motion language.

chartjs
options: {
  animation: {
    duration: 1200,
    easing: 'easeInOutBack'   // slight overshoot for a playful feel
  }
}
// common easings:
// 'linear'            constant speed (mechanical)
// 'easeOutQuart'      default; snappy start, smooth end
// 'easeInOutCubic'    symmetric in/out
// 'easeOutBounce'     bouncy landing
// 'easeInOutBack'     overshoots at both ends

Animation onComplete

onComplete fires when an animation finishes — useful for chaining actions like enabling an export button or triggering a follow-up render. onProgress fires every animation frame with the step count. Avoid heavy work in onProgress as it runs 60x/second.

chartjs
options: {
  animation: {
    onComplete: function() {
      console.log('Animation finished');
      // e.g. enable a "download PNG" button now that the chart is stable
      document.getElementById('downloadBtn').disabled = false;
    },
    onProgress: function(ctx) {
      // fired each frame; ctx.currentStep / ctx.numSteps
    }
  }
}

Per-Property Animation (animations)

The plural animations object configures individual properties independently. Each entry can specify from/to (or a function of the parsing context), duration, easing, and loop. Animating tension from 1 to 0.3 produces a 'line settles' effect; animating y from the axis baseline produces a 'grow up' effect.

chartjs
options: {
  animations: {
    tension: {
      duration: 1500,
      easing: 'easeOutBounce',
      from: 1,           // start very curved
      to: 0.3,           // settle at a gentle curve
      loop: false
    },
    colors: { duration: 800 },
    y: { from: ctx => ctx.chart.scales.y.getPixelForValue(0) }
  }
}
15

Events & Clicks

onClick Handler

options.onClick fires when the user clicks anywhere on the chart. The elements argument is an array of clicked chart elements (bars/points), each with .datasetIndex and .index. If elements is empty the click hit blank canvas. This is the standard way to make charts interactive (drill-down, selection).

chartjs
new Chart(ctx, {
  type: 'bar',
  data: { labels: ['A','B','C'], datasets: [{ data: [10,20,30] }] },
  options: {
    onClick(event, elements, chart) {
      if (elements.length > 0) {
        const el = elements[0];
        const label = chart.data.labels[el.index];
        const value = chart.data.datasets[el.datasetIndex].data[el.index];
        alert(`Clicked ${label} = ${value}`);
      }
    }
  }
});

getElementsAtEventForMode

getElementsAtEventForMode finds the chart elements under a native mouse event, with the same mode options as interaction ('nearest', 'index', 'x', etc.). The 4th arg useFinalPosition should be true for hit-testing the final rendered positions. Use this when you need element lookups outside options.onClick.

chartjs
const chart = new Chart(ctx, { /* ... */ });

canvas.addEventListener('click', (e) => {
  const points = chart.getElementsAtEventForMode(
    e, 'nearest', { intersect: true }, true   // (event, mode, options, useFinalPosition)
  );
  if (points.length) {
    const p = points[0];
    console.log('dataset', p.datasetIndex, 'index', p.index);
  }
});

onHover

options.onHover fires on every mouse move over the chart. Use it for cheap UI feedback like changing the cursor (as shown) or highlighting related external elements. Avoid expensive operations here — it fires very frequently. For heavier logic, use onClick or debounce the handler.

chartjs
options: {
  onHover(event, elements, chart) {
    // change cursor to a pointer when hovering a bar
    event.native.target.style.cursor =
      elements.length ? 'pointer' : 'default';
  }
}

Hover & Active Elements

The hover config controls how the chart highlights elements under the pointer. mode:'nearest' + intersect:true highlights the single nearest point. animationDuration smooths the highlight transition. chart.setActiveElements programmatically triggers the hover state — useful for syncing chart highlights with external UI.

chartjs
options: {
  hover: {
    mode: 'nearest',
    intersect: true,
    animationDuration: 200   // smooth transition when hover target changes
  }
}
// programmatically set the hovered element:
chart.setActiveElements([{ datasetIndex: 0, index: 2 }]);
chart.update();

Events List

events limits which DOM events the chart listens to. Reducing the list (e.g. to just 'click') disables hover effects for a static feel and improves performance on dashboards. For touch-only mobile contexts, include touchstart/touchmove. An empty array disables all interaction.

chartjs
options: {
  events: ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove']
  // restrict to just click for a non-interactive "static" feel:
  // events: ['click']
}
// supported events: mousemove, mouseout, click, touchstart, touchmove,
//                   touchend, pointer events (Chart.js v3.4+)

Drill-Down Example

A common pattern: clicking a bar swaps the dataset to a more detailed view (drill-down). Capture the clicked index, replace chart.data.labels and chart.data.datasets[...].data, then call chart.update() to animate the transition. Keep a stack of views if you want a back button.

chartjs
const monthly = { labels: ['Jan','Feb','Mar'], data: [100,120,90] };
const daily = { labels: Array.from({length:28},(_,i)=>'D'+(i+1)),
                data: Array.from({length:28},()=>Math.random()*50) };

const chart = new Chart(ctx, {
  type: 'bar',
  data: { labels: monthly.labels, datasets: [{ data: monthly.data }] },
  options: { onClick(evt, els) {
    if (!els.length) return;
    const idx = els[0].index;
    chart.data.labels = daily.labels;
    chart.data.datasets[0].data = daily.data;
    chart.update();
  }}
});
16

Mixed Charts

Bar + Line Mixed Chart

Mix chart types by setting type on individual datasets — it overrides the chart-level type for that series. A bar + line combo is the most common mix (e.g. revenue as bars, growth rate as a line). Use a second y axis (y1) when the series have different units or scales.

chartjs
new Chart(ctx, {
  type: 'bar',                          // default type for all datasets
  data: {
    labels: ['Jan','Feb','Mar','Apr'],
    datasets: [
      { type: 'bar',  label: 'Revenue', data: [20,35,30,40], backgroundColor: '#36A2EB', yAxisID: 'y' },
      { type: 'line', label: 'Growth',  data: [5,8,6,12],    borderColor: '#FF6384', yAxisID: 'y1', fill: false }
    ]
  },
  options: { scales: { y: { position: 'left' }, y1: { position: 'right', grid: { drawOnChartArea: false } } } }
});

Dataset type Override

Any dataset can override the chart-level type, letting you combine bar, line and scatter in one chart. Datasets without an explicit type inherit the chart's type. Mind the z-order: datasets are drawn in array order, so place background series (bars) before foreground series (lines).

chartjs
new Chart(ctx, {
  type: 'line',                // chart-level default
  data: {
    labels: ['A','B','C'],
    datasets: [
      { data: [1,2,3] },                          // inherits 'line'
      { type: 'bar', data: [3,2,1] },              // becomes a bar
      { type: 'scatter', data: [{x:0,y:2},{x:2,y:3}], pointRadius: 5 }  // scatter points
    ]
  }
});

Multiple Y Axes (y, y1, y2)

Define multiple y axes with custom ids (y, y1, y2) and assign datasets via yAxisID. grid.drawOnChartArea:false prevents the second axis from drawing gridlines over the first. position:'right' stacks on the right; add offset:true to a third axis so it doesn't overlap the second. Color each axis to match its series.

chartjs
options: {
  scales: {
    y:  { type: 'linear', position: 'left',  title: { display: true, text: 'Price ($)' } },
    y1: { type: 'linear', position: 'right', title: { display: true, text: 'Volume' }, grid: { drawOnChartArea: false } },
    y2: { type: 'linear', position: 'right', offset: true, title: { display: true, text: '%' } }
  }
}
// assign each dataset: yAxisID: 'y' | 'y1' | 'y2'

Mixed Data with Different Shapes

Mixed charts can combine a value series (bars) with reference lines (constant target, trend). Set pointRadius:0 and fill:false on the line datasets so they render as pure lines without markers or fills. This pattern is common in performance dashboards (actual vs target).

chartjs
new Chart(ctx, {
  type: 'bar',
  data: {
    labels: ['Mon','Tue','Wed','Thu','Fri'],
    datasets: [
      { type:'bar',  label:'Tasks', data:[8,6,7,9,5], backgroundColor:'rgba(54,162,235,0.6)' },
      { type:'line', label:'Target',data:[7,7,7,7,7], borderColor:'red', borderWidth:2, pointRadius:0, fill:false },
      { type:'line', label:'Trend', data:[6,6.5,7,8,8.5], borderColor:'green', borderDash:[5,5], pointRadius:0, fill:false }
    ]
  }
});

Mixed Styling Tips

In mixed charts, set dataset.order so the line draws above the bars (lower order = on top). Color-code each axis's ticks and title to match its series so readers know which scale belongs to which line. Disable the second axis's grid (drawOnChartArea:false) to avoid double gridlines.

chartjs
// 1. assign order so lines draw on top of bars
datasets: [
  { type:'bar',  data:[...], order: 2 },
  { type:'line', data:[...], order: 1 }   // lower order draws later (on top)
]
// 2. give each axis a matching color
options: { scales: {
  y:  { ticks: { color: '#36A2EB' }, title: { text: 'Revenue', color: '#36A2EB' } },
  y1: { ticks: { color: '#FF6384' }, title: { text: 'Growth',  color: '#FF6384' }, position: 'right', grid: { drawOnChartArea: false } }
}}
17

Plugins

Built-in Plugins

Chart.js ships three built-in plugins: Title (chart title above the canvas), Legend (the dataset key), and Tooltip (hover popups). They are configured under options.plugins by their id and are enabled by default (except Title, which needs display:true to show its text).

chartjs
// three built-in plugins (always available):
//   Legend   -> options.plugins.legend
//   Title    -> options.plugins.title
//   Tooltip  -> options.plugins.tooltip

new Chart(ctx, {
  type: 'line',
  data: { /* ... */ },
  options: {
    plugins: {
      title:  { display: true, text: 'Sales 2024', font: { size: 18 } },
      legend: { position: 'bottom' },
      tooltip:{ mode: 'index', intersect: false }
    }
  }
});

Custom Inline Plugin

Pass an array of plugin objects to a single chart via the plugins option. Each plugin has an id and hook functions (beforeInit, afterInit, beforeDraw, afterDraw, beforeUpdate, etc.). Inline plugins apply only to that chart instance. ctx.save()/restore() bracket any canvas state changes you make.

chartjs
new Chart(ctx, {
  type: 'line',
  data: { /* ... */ },
  options: { /* ... */ },
  plugins: [{
    id: 'centerText',
    afterDraw(chart) {
      const { ctx, chartArea } = chart;
      ctx.save();
      ctx.font = 'bold 24px Arial';
      ctx.fillStyle = 'black';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText('Hello', (chartArea.left+chartArea.right)/2,
                            (chartArea.top+chartArea.bottom)/2);
      ctx.restore();
    }
  }]
});

Plugin Hooks (lifecycle)

Plugin hooks run at every stage of the chart lifecycle: init, update, render, draw, resize, destroy. before* hooks can cancel an action by returning false. The most common for custom drawing is afterDraw (draw on top of the chart) or beforeDraw (draw behind the data). Each hook receives the chart instance.

chartjs
const plugin = {
  id: 'myHook',
  beforeInit(chart)  { console.log('before init'); },
  afterInit(chart)   { console.log('chart ready'); },
  beforeUpdate(chart){ console.log('about to update'); },
  beforeDraw(chart)  { console.log('before draw'); },
  afterDraw(chart)   { console.log('after draw'); },
  beforeRender(chart){ console.log('before first render'); },
  resize(chart, size){ console.log('resized', size); },
  destroy(chart)     { console.log('destroyed'); }
};

Plugin: Vertical Line at Index

A common custom plugin draws a vertical guideline at the tooltip position (like the default crosshair). afterDraw reads the active tooltip element's x coordinate and strokes a dashed line from top to bottom of the chart area. Wrap canvas state in save()/restore() so the dashed line style doesn't leak.

chartjs
const verticalLine = {
  id: 'verticalLine',
  afterDraw(chart) {
    if (chart.tooltip?._active?.length) {
      const x = chart.tooltip._active[0].element.x;
      const { top, bottom } = chart.chartArea;
      const ctx = chart.ctx;
      ctx.save();
      ctx.beginPath();
      ctx.moveTo(x, top);
      ctx.lineTo(x, bottom);
      ctx.lineWidth = 1;
      ctx.strokeStyle = 'gray';
      ctx.setLineDash([4, 4]);
      ctx.stroke();
      ctx.restore();
    }
  }
};

Plugin: Center Text for Doughnut

Drawing centered summary text is the classic use case for a doughnut plugin. afterDatasetsDraw runs after the wedges so text sits on top. Compute the total (or any summary) and render it at the geometric center of the chartArea. This turns a doughnut into an informative KPI widget.

chartjs
const centerText = {
  id: 'centerText',
  afterDatasetsDraw(chart) {
    const { ctx, data } = chart;
    const total = data.datasets[0].data.reduce((a,b)=>a+b, 0);
    ctx.save();
    ctx.font = 'bold 28px Arial';
    ctx.fillStyle = '#333';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    const { left, right, top, bottom } = chart.chartArea;
    ctx.fillText(total, (left+right)/2, (top+bottom)/2);
    ctx.restore();
  }
};
new Chart(ctx, { type:'doughnut', data:{...}, plugins:[centerText] });

Register Plugin Globally

Chart.register(plugin) makes a plugin apply to every chart created afterwards — ideal for app-wide plugins like watermarks, branding, or analytics. For a one-off chart use the inline plugins array instead. Chart.unregister removes it. Registered plugins read their config from options.plugins[id].

chartjs
import { Chart } from 'chart.js';

const myPlugin = { id: 'watermark', afterDraw(chart) { /* ... */ } };

// register once -> applies to EVERY chart
Chart.register(myPlugin);

// or unregister later
Chart.unregister(myPlugin);

// per-chart-only: skip Chart.register and pass via options.plugins instead
18

Update & Destroy

chart.update()

After mutating chart.data or chart.options, call chart.update() to re-render. By default the change animates from the previous state. Pass 'none' as the mode to skip the animation (useful for high-frequency updates). Mutating then updating is far cheaper than destroying and recreating the chart.

chartjs
const chart = new Chart(ctx, config);

// mutate the data/config, then update:
chart.data.labels.push('Jun');
chart.data.datasets[0].data.push(28);
chart.options.scales.y.max = 50;

chart.update();   // animate to the new state
// chart.update('none');   // skip animation

Update Modes

update(mode) accepts a mode string controlling behavior. 'none' disables animation for a silent refresh. 'reset' rewinds to the pre-animation values then re-animates (good for replaying the entrance). 'active' updates only hovered elements for cheap hover re-renders. The default (no arg) animates normally.

chartjs
chart.update();             // default: animates the change
chart.update('none');       // no animation
chart.update('reset');      // reset to pre-animation state, then animate
chart.update('resize');     // re-compute layout (call after container resize)
chart.update('show');       // show currently-hidden dataset animations
chart.update('hide');       // hide dataset animations
chart.update('active');     // update only the active (hovered) elements

Update Data & Labels

You can replace data/labels wholesale or mutate them in place (push/shift). For live-streaming charts (e.g. a 60-second window), append the new value and shift the oldest, then update('none') for a smooth non-animated refresh — calling the default animated update on every tick causes a janky lag.

chartjs
// replace the entire data array
chart.data.datasets[0].data = [5, 10, 15, 20];
chart.data.labels = ['A','B','C','D'];
chart.update();

// or append streaming data (and trim the oldest):
const arr = chart.data.datasets[0].data;
arr.push(newValue);
if (arr.length > 60) arr.shift();
chart.update('none');   // instant update for live data

chart.destroy()

destroy() tears down the chart: it removes event listeners, cancels animations, clears the canvas and frees memory. Always call destroy when a chart's container is removed (React useEffect cleanup, route change). Failing to destroy leaks memory and can throw 'Canvas is already in use' errors when reusing a canvas.

chartjs
const chart = new Chart(ctx, config);

// when done (route change, component unmount, etc.):
chart.destroy();

// after destroy the canvas is free; a new Chart can reuse it:
const fresh = new Chart(ctx, newConfig);

// destroy also removes event listeners and frees memory

chart.resize() & clear()

chart.resize() recomputes the chart size — with responsive:true this happens automatically on window resize, so manual calls are rare. chart.clear() blanks the canvas without destroying the chart (the next update redraws). chart.resize(width, height) forces specific pixel dimensions, overriding responsive sizing.

chartjs
const chart = new Chart(ctx, config);

// manually trigger a resize (rarely needed; responsive:true auto-resizes)
chart.resize();

// clear the canvas (chart stays alive, just blank until next render):
chart.clear();
chart.draw();   // re-draw without recomputing layout

// resize the canvas to explicit dimensions:
chart.resize(800, 400);
19

Responsive & Device Pixel Ratio

Responsive Container

For a responsive chart, wrap the canvas in a positioned container with explicit width/height and set responsive:true + maintainAspectRatio:false. The canvas fills the container and re-renders on window resize. Without maintainAspectRatio:false the chart keeps a fixed aspect ratio and ignores the container's height.

chartjs
<!-- container controls the size -->
<div style="position: relative; width: 100%; height: 400px;">
  <canvas id="c"></canvas>
</div>

<script>
new Chart(document.getElementById('c'), {
  type: 'line',
  data: { /* ... */ },
  options: { responsive: true, maintainAspectRatio: false }
});
</script>

maintainAspectRatio

maintainAspectRatio:true (default) keeps the canvas at a fixed width/height ratio set by aspectRatio (default 2 = 2:1). maintainAspectRatio:false releases the height to the container. Pie/doughnut default to aspectRatio:1 (square) since circles look wrong in a wide rectangle.

chartjs
// default: chart keeps a 2:1 (width:height) aspect ratio
options: { responsive: true, maintainAspectRatio: true, aspectRatio: 2 }

// let the container control height:
options: { responsive: true, maintainAspectRatio: false }

// square charts:
options: { responsive: true, maintainAspectRatio: true, aspectRatio: 1 }

aspectRatio

aspectRatio = width / height. With maintainAspectRatio:true the canvas dimensions follow this ratio. Use a higher ratio (3-4) for wide dashboard tiles, lower (1-1.5) for tall panels. Pie/doughnut default to 1 because circles need a square canvas to avoid looking like ellipses.

chartjs
new Chart(ctx, {
  type: 'bar',
  data: { /* ... */ },
  options: {
    responsive: true,
    maintainAspectRatio: true,
    aspectRatio: 3    // width:height = 3:1 (wide, short chart)
  }
});
// common ratios:
//   2  -> default for bar/line
//   1  -> pie/doughnut (square)
//   3  -> wide dashboards
//   16/9 -> video-like

chart.resize() Method

Chart.js listens to window resize events automatically when responsive:true, but layout changes from CSS transitions, sidebar toggles, or tab switches may not fire a window resize. In those cases call chart.resize() manually after the container's size has settled (e.g. in a setTimeout or transitionend handler).

chartjs
const chart = new Chart(ctx, { /* responsive: true */ });

// manually trigger a re-layout (e.g. after a sidebar collapses)
window.dispatchEvent(new Event('resize'));
// or:
chart.resize();

// explicit size:
chart.resize(600, 300);

devicePixelRatio (sharp rendering)

devicePixelRatio scales the canvas backing store so lines stay sharp on high-DPI/retina displays (default uses window.devicePixelRatio, usually 2 or 3). Capping at 2 keeps text crisp while limiting the pixel workload on 3x phones. Setting 1 produces blurry output on retina but maximizes render speed.

chartjs
// let Chart.js use the screen's full pixel density (default):
new Chart(ctx, { options: { devicePixelRatio: window.devicePixelRatio } });

// cap DPR for performance on retina displays (fewer pixels to render):
options: { devicePixelRatio: 2 }

// ignore DPR (renders at 1x — blurry on retina but fastest):
options: { devicePixelRatio: 1 }

ResizeObserver Pattern

A ResizeObserver on the chart's container catches size changes from CSS flexbox/grid, collapsible sidebars, and tab switches that window resize misses. Disconnect the observer and call chart.destroy() in your teardown to prevent leaks. This is the most robust responsive pattern for complex layouts.

chartjs
const canvas = document.getElementById('c');
const chart = new Chart(canvas, config);

// observe the CONTAINER, not the canvas:
const ro = new ResizeObserver(() => chart.resize());
ro.observe(canvas.parentElement);

// cleanup on destroy:
function teardown() {
  ro.disconnect();
  chart.destroy();
}

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.