Scales, axes and time-series data
Configure category, linear, logarithmic, time and timeseries scales, install a date adapter, and format ticks without fighting the defaults.
Choosing the scale and formatting ticks
import { Chart } from './register.js';
const chart = new Chart(canvas, {
type: 'line',
data: {
datasets: [{
label: 'Requests',
data: points, // with a time scale, [{ x: '2026-01-01', y: 120 }, ...]
parsing: false // skip parsing: the objects are already {x, y}
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
type: 'time',
time: {
unit: 'day', // 'hour' | 'day' | 'week' | 'month' | 'year'
tooltipFormat: 'PPpp', // date-fns format for the tooltip
displayFormats: { day: 'd MMM', month: 'MMM yyyy' },
round: 'day'
},
ticks: {
maxRotation: 0, // keep labels horizontal when possible
autoSkip: true,
autoSkipPadding: 24,
callback(value) {
// 'this' is the scale; use it to format conditionally
return new Date(value).toLocaleDateString('en', { month: 'short', day: 'numeric' });
}
},
grid: { display: false }
},
y: {
type: 'linear',
beginAtZero: true,
grace: '10%', // padding above the highest point
ticks: {
stepSize: 500,
callback: (value) => '$' + value.toLocaleString()
},
grid: { color: 'rgba(148, 163, 184, 0.25)' },
title: { display: true, text: 'Revenue' }
}
}
}
});| Scale type | Data it expects | Note |
|---|---|---|
category | Labels or an index | The default; spacing is uniform |
linear | Numbers | beginAtZero, stepSize, grace |
logarithmic | Positive numbers | Zero is invalid; set a min |
time | Dates, evenly spaced by value | Requires a date adapter |
timeseries | Dates, evenly spaced on screen | Better for financial and monitoring data |
radialLinear | Numbers on a radial axis | For radar and polar area charts |
💡
A
time scale needs a date adapter. Without one, Chart.js logs a single line at startup saying so and every date is treated as a number, which looks like a data problem rather than a missing dependency. Install chartjs-adapter-date-fns and date-fns, and import the adapter for its side effect.Stacking, multiple axes and category labels
// Stacked bars on one axis: stacking is enabled on the scale, not the dataset.
const stacked = {
type: 'bar',
data: {
labels: ['Jan', 'Feb', 'Mar'],
datasets: [
{ label: 'Direct', data: [320, 380, 410], backgroundColor: '#6d28d9' },
{ label: 'Referral', data: [180, 210, 195], backgroundColor: '#0ea5e9' },
{ label: 'Organic', data: [90, 120, 140], backgroundColor: '#10b981' }
]
},
options: {
scales: {
x: { stacked: true },
y: { stacked: true, beginAtZero: true }
}
}
};
// Percentage stacking: normalise in the data, because Chart.js does not do it.
const toPercent = (datasets) => {
const totals = datasets[0].data.map((_, i) =>
datasets.reduce((sum, dataset) => sum + dataset.data[i], 0)
);
return datasets.map((dataset) => ({
...dataset,
data: dataset.data.map((value, i) => (value / totals[i]) * 100)
}));
};
// Two y axes: bind each dataset to an axis by id.
const dualAxis = {
type: 'line',
data: {
datasets: [
{ label: 'Requests', data: requests, yAxisID: 'y', borderColor: '#0ea5e9' },
{ label: 'Error rate', data: rates, yAxisID: 'y1', borderColor: '#ef4444' }
]
},
options: {
scales: {
y: { type: 'linear', position: 'left', title: { display: true, text: 'Requests' } },
y1: { type: 'linear', position: 'right', beginAtZero: true, grid: { drawOnChartArea: false },
ticks: { callback: (value) => value + '%' } }
}
}
};// Category labels: a plain array works, but an object gives you control.
const withObjects = {
data: {
labels: ['Q1', 'Q2', 'Q3'],
datasets: [{
label: 'Revenue',
data: [820, 932, 901]
}]
},
options: {
scales: {
x: {
ticks: {
// Distinguish a total row from a real period
callback(index) {
const label = this.getLabelForValue(Number(index));
return label === 'Q1' ? label + ' (partial)' : label;
}
}
}
}
}
};
// Reading the scale back for a custom interaction
const xScale = chart.scales.x;
console.log(xScale.min, xScale.max, xScale.getPixelForValue(932));- Stacking is a scale option. Setting
stacked: trueon a dataset does nothing, which is a common source of confusion when porting from Chart.js 2. - With two y axes,
grid: { drawOnChartArea: false }on the secondary axis stops two sets of grid lines from fighting on the same plot. - Chart.js does not normalise to percentages. If you want a 100% stacked chart, transform the data — and label the axis as a percentage.
graceon a linear axis is more robust thansuggestedMaxbecause it adapts as the data changes.
Parsing and data shape
| Data shape | Parsing | Note |
|---|---|---|
[10, 20, 30] with labels | Default | The simplest case |
[{ x: 'Jan', y: 10 }] | Default | x and y are read by key |
[[1, 10], [2, 20]] | Default | Index 0 is x, index 1 is y |
[{ t: '2026-01-01', v: 10 }] | parsing: { xAxisKey: 't', yAxisKey: 'v' } | Custom property names |
Already { x, y } objects | parsing: false | Skips a pass; requires a time or linear scale |
| A typed array | parsing: false | Fastest for very large series |
// Custom property names, mapped explicitly
const mapped = {
type: 'line',
data: {
datasets: [{
label: 'Latency',
data: rows, // [{ timestamp: '...', p95: 142 }, ...]
parsing: { xAxisKey: 'timestamp', yAxisKey: 'p95' }
}]
},
options: { scales: { x: { type: 'time' } } }
};
// Turning an array of index/value pairs into {x, y} with disabled parsing is the
// fastest path for a large series: Chart.js stores what you give it.
const fast = {
data: {
datasets: [{
label: 'Throughput',
data: new Array(rows.length),
parsing: false
}]
}
};
for (let i = 0; i < rows.length; i++) fast.data.datasets[0].data[i] = { x: rows[i].ts, y: rows[i].value };The timeseries scale is usually the better choice for monitoring data: it spaces points evenly on screen regardless of the gaps in the timestamps, so an outage with no data does not silently compress the axis. Use time when the true spacing between points carries meaning.
FAQ
Why are my dates showing as years like 1970?
The
time scale was requested without a date adapter, so the timestamp string was parsed as a number. Install and import chartjs-adapter-date-fns, and confirm the adapter import runs before the chart is created.How do I stop tick labels overlapping?
Set
ticks.autoSkip: true (the default) with a larger autoSkipPadding, and cap maxRotation. If the labels still collide, reduce ticks.maxTicksLimit rather than rotating them 90 degrees.Related
Chart types and datasets Options, built-in plugins and tooltips
Last refreshed 2026-09-18.