Chart.js v4.5.1 is a canvas-based charting library for the browser and Node.js. This guide covers everything an administrator, integrator, or contributing developer needs to understand the internals, configure builds, deploy at scale, and troubleshoot production issues. See the User Guide for usage examples.
1. Architecture Overview
1.1 Design Philosophy
Chart.js is a modular, plugin-driven rendering engine that draws charts onto HTML5 <canvas> elements. The architecture follows a registry pattern where controllers, elements, scales, and plugins are registered at runtime. This enables tree-shaking for ESM consumers while providing a batteries-included UMD bundle for CDN usage.
1.2 Core Subsystems
The engine is organized into seven subsystems, each with a well-defined responsibility:
| Subsystem | Location | Responsibility |
|---|---|---|
| Chart Controller | src/core/core.controller.js |
Main entry point; orchestrates lifecycle, binds events, manages datasets |
| Config Resolver | src/core/core.config.js |
Scope-chain resolution, scriptable options, caching |
| Registry | src/core/core.registry.js |
Component registration (controllers, elements, scales, plugins) |
| Animation | src/core/core.animation.js, core.animations.js, core.animator.js |
RAF-based interpolation with 31 easing functions |
| Layout Engine | src/core/core.layouts.js |
Box-model layout for titles, legends, scales, chart area |
| Interaction | src/core/core.interaction.js |
Hit detection across six modes |
| Platform | src/platform/ |
Browser DOM or OffscreenCanvas/Node abstraction |
1.3 Component Flow
User Config
│
▼
Config Resolver ──► Scope Chain (dataset → type overrides → defaults)
│
▼
Registry ──► Resolves controllers, scales, elements, plugins by ID
│
▼
Chart Controller
├── Layout Engine (positions boxes: title, legend, scales, chart area)
├── Dataset Controllers (bar, line, pie, etc.)
│ └── Elements (Arc, Bar, Line, Point)
├── Scales (category, linear, log, time, radial)
├── Plugins (lifecycle hooks: beforeInit → ... → destroy)
└── Animator (singleton RAF loop)
└── Animation instances (from → to interpolation)
1.4 Class Hierarchy
| Class | Parent | Source | Purpose |
|---|---|---|---|
Chart |
-- | src/core/core.controller.js |
Main chart instance (~1500 lines) |
Defaults |
-- | src/core/core.defaults.js |
Global defaults store |
Registry |
-- | src/core/core.registry.js |
Component registry |
Animator |
-- | src/core/core.animator.js |
Singleton animation loop |
DateAdapterBase |
-- | src/core/core.adapters.ts |
Abstract date adapter interface |
Legend |
Element |
src/plugins/plugin.legend.js |
Legend rendering |
Title |
Element |
src/plugins/plugin.title.js |
Title rendering |
Tooltip |
Element |
src/plugins/plugin.tooltip.js |
Tooltip rendering (~1100 lines) |
TimeSeriesScale |
TimeScale |
src/scales/scale.timeseries.js |
Evenly-spaced time axis |
simpleArc |
-- | src/plugins/plugin.filler/simpleArc.js |
Arc fill helper |
1.5 Runtime Dependency
Chart.js has exactly one runtime dependency:
| Package | Version | Purpose |
|---|---|---|
@kurkle/color |
^0.3.0 | Color parsing, manipulation, and conversion |
All other devDependencies (Rollup, Karma, Jasmine, SWC, etc.) are build-time only.
Important
Chart.js has zero CLI commands, zero REST API endpoints, zero server-side actions, and zero filters. It is a pure client-side library. Do not look for backend configuration sections -- they do not exist.
2. System Requirements
2.1 Build Environment
| Requirement | Version | Notes |
|---|---|---|
| Node.js | 16+ | Required for build and test |
| pnpm | >=8 (pinned: 8.13.0) | packageManager field in package.json enforces this |
| TypeScript | Included in devDeps | For type declarations only; source is JS + TS mix |
2.2 Browser Support
Chart.js targets ES2018 (per .eslintrc.yml) and transpiles to ES2022 via SWC in the build step. Runtime requirements:
| Feature | Used By | Fallback |
|---|---|---|
<canvas> 2D context |
Core rendering | None -- required |
ResizeObserver |
DomPlatform container resize detection |
Manual resize() calls |
MutationObserver |
DomPlatform canvas attach/detach |
None |
Intl.NumberFormat |
helpers.intl.js tick formatting |
Basic toFixed() |
requestAnimationFrame |
Animator |
Polyfillable via helpers.extras.js |
OffscreenCanvas |
BasicPlatform (SSR/worker) |
Falls back to DomPlatform |
2.3 Node.js / SSR Requirements
For server-side rendering, Chart.js needs a canvas implementation (e.g., canvas or @napi-rs/canvas) that provides a 2D context. The BasicPlatform is auto-selected when OffscreenCanvas is available or the context is not a browser DOM element. The BasicPlatform disables animations automatically.
2.4 pnpm Workspace
The repository uses a pnpm workspace defined in pnpm-workspace.yaml:
packages:
- docs
- test/integration/*
This means pnpm install at root handles all sub-packages, including the VuePress documentation site and integration test projects.
3. Configuration Reference
3.1 Config Resolution Chain
The config resolver (src/core/core.config.js) implements a scope-chain lookup. When Chart.js resolves an option value, it traverses scopes in this order:
- Dataset-level options (highest priority)
- Chart type overrides (
Chart.overrides[type]) - Global defaults (
Chart.defaults) - Descriptors (internal metadata)
Key resolver methods:
| Method | Purpose |
|---|---|
datasetScopeKeys(type) |
Returns scope keys for dataset options |
datasetElementScopeKeys(type, elementType) |
Scope keys for per-element options within a dataset |
pluginScopeKeys(plugin) |
Scope keys for plugin options |
chartOptionScopes() |
All scope keys for chart-level options |
_createResolver() |
Builds a proxy-based resolver for a scope chain |
_attachContext() |
Wraps a resolver with scriptable option evaluation |
Note
The config resolver uses aggressive caching for scope keys and resolvers. If you modify
Chart.defaultsafter chart creation, callchart.update()to force re-resolution. Mutations to nested default objects are picked up automatically through the proxy mechanism, but adding entirely new scope branches requires a full update cycle.
3.2 Scale Auto-Detection
The config system auto-detects axis type from the scale ID or its position property:
- ID starting with
xor positiontop/bottommaps to the x-axis - ID starting with
yor positionleft/rightmaps to the y-axis - ID
rmaps to the radial axis
Scale defaults are then merged per chart type, so a bar chart's x-axis gets CategoryScale defaults while a scatter chart's x-axis gets LinearScale defaults.
3.3 Animation Configuration
| Property | Default | Description |
|---|---|---|
animation.duration |
1000 |
Base animation duration (ms) |
animation.easing |
'easeOutQuart' |
Easing function name |
animation.delay |
undefined |
Delay before animation start |
transitions.active.animation.duration |
400 |
Hover animation speed |
transitions.resize.animation.duration |
0 |
Instant on resize |
transitions.show.animations.colors.from |
'transparent' |
Fade in from transparent |
transitions.hide.animations.colors.to |
'transparent' |
Fade out to transparent |
resizeDelay |
0 |
Debounce delay for resize events (ms) |
Available easing functions (31 total in helpers.easing.js): linear, easeInQuad, easeOutQuad, easeInOutQuad, easeInCubic, easeOutCubic, easeInOutCubic, easeInQuart, easeOutQuart, easeInOutQuart, easeInQuint, easeOutQuint, easeInOutQuint, easeInSine, easeOutSine, easeInOutSine, easeInExpo, easeOutExpo, easeInOutExpo, easeInCirc, easeOutCirc, easeInOutCirc, easeInElastic, easeOutElastic, easeInOutElastic, easeInBack, easeOutBack, easeInOutBack, easeInBounce, easeOutBounce, easeInOutBounce.
3.4 Colors Plugin Palette
The built-in ColorsPlugin (src/plugins/plugin.colors.ts) assigns default colors when the user has not specified any:
| Index | Color (R,G,B) | Visual |
|---|---|---|
| 0 | 54, 162, 235 | Blue |
| 1 | 255, 99, 132 | Red |
| 2 | 255, 159, 64 | Orange |
| 3 | 255, 205, 86 | Yellow |
| 4 | 75, 192, 192 | Green |
| 5 | 153, 102, 255 | Purple |
| 6 | 201, 203, 207 | Grey |
Background colors are the same with 0.5 opacity. For Doughnut and PolarArea charts, colors rotate per data point (slice). For all other chart types, colors apply per dataset. The plugin skips assignment if the user has already defined backgroundColor or borderColor, unless forceOverride: true is set.
3.5 Decimation Plugin Configuration
| Option | Default | Description |
|---|---|---|
enabled |
false |
Enable data decimation |
algorithm |
'min-max' |
Algorithm: 'min-max' or 'lttb' |
threshold |
4 * chartWidth |
Minimum samples before decimation activates |
samples |
-- | Target sample count (LTTB only) |
Warning
The decimation plugin only works with line charts using a linear or time x-axis and
parsing: false. If any of these conditions are not met, decimation silently does nothing. The plugin replacesdataset.datavia a getter/setter, so the original data array is preserved internally.
3.6 Interaction Modes
| Mode | Behavior |
|---|---|
'point' |
Returns elements directly under the mouse pointer |
'nearest' |
Returns the single closest element |
'index' |
Returns all elements sharing the same data index |
'dataset' |
Returns all elements from the hovered dataset |
'x' |
Returns all elements at the same x coordinate |
'y' |
Returns all elements at the same y coordinate |
3.7 Plugin Lifecycle Hooks
Plugins receive these hooks in order during the chart lifecycle:
install → start → beforeInit → afterInit →
[per update cycle:]
beforeUpdate → beforeElementsUpdate → beforeDatasetsUpdate →
beforeDatasetUpdate → afterDatasetUpdate (per dataset) →
afterDatasetsUpdate → afterUpdate →
beforeLayout → afterLayout →
beforeRender →
beforeDraw →
beforeDatasetsDraw →
beforeDatasetDraw → afterDatasetDraw (per dataset) →
afterDatasetsDraw →
afterDraw →
afterRender →
[per event:]
beforeEvent → afterEvent →
resize →
stop → uninstall → destroy
3.8 Fill Modes (Filler Plugin)
The filler plugin (src/plugins/plugin.filler/, 8 files, ~20K chars) supports these fill targets:
| Value | Behavior |
|---|---|
| Dataset index (number) | Fill to another dataset's line |
'origin' |
Fill to the axis origin (zero line) |
'start' |
Fill to the start of the axis |
'end' |
Fill to the end of the axis |
'stack' |
Fill as stacked area |
true |
Alias for 'origin' |
false |
Disable fill |
4. Deployment & Scaling
4.1 Build System
Chart.js uses Rollup with SWC transpilation. The build produces four output variants from rollup.config.js:
| Output | Entry Point | Format | File |
|---|---|---|---|
| UMD (minified) | src/index.umd.ts |
UMD | dist/chart.umd.min.js |
| UMD (readable) | src/index.umd.ts |
UMD | dist/chart.umd.js |
| ESM | src/index.ts + src/helpers/index.ts |
ESM | dist/chart.js + dist/helpers.js |
| CJS | src/index.ts + src/helpers/index.ts |
CJS | dist/chart.cjs + dist/helpers.cjs |
Build plugins in use:
| Plugin | Purpose |
|---|---|
@rollup/plugin-json |
Import JSON files |
@rollup/plugin-node-resolve |
Resolve node_modules |
rollup-plugin-swc3 |
Transpile TS/JS (target: es2022) |
rollup-plugin-terser |
Minify UMD build |
rollup-plugin-cleanup |
Remove comments from non-minified builds |
4.2 Package Exports
The package.json exports map provides three entry points:
{
"chart.js": {
"import": "dist/chart.js",
"require": "dist/chart.cjs"
},
"chart.js/auto": "auto-registers all built-in components",
"chart.js/helpers": "dist/helpers.js / dist/helpers.cjs"
}
chart.js-- Tree-shakeable ESM/CJS entry. Consumers must manuallyChart.register(...)the components they need.chart.js/auto-- Auto-registers all controllers, elements, scales, and plugins. Convenient but prevents tree-shaking.chart.js/helpers-- Utility functions for plugin/extension authors.
Type definitions ship as dist/types.d.ts. The sideEffects field marks auto and UMD builds as having side effects (they auto-register components on import).
4.3 Reproducible Builds
The Rollup config reads the SOURCE_DATE_EPOCH environment variable for reproducible builds. Set this to a Unix timestamp to ensure deterministic output:
SOURCE_DATE_EPOCH=$(date +%s) pnpm build
4.4 CDN Deployment
The UMD bundle is designed for CDN delivery:
<!-- jsDelivr -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js"></script>
<!-- unpkg -->
<script src="https://unpkg.com/chart.js@4.5.1/dist/chart.umd.min.js"></script>
The UMD entry (src/index.umd.ts) auto-registers all built-in components, so no additional setup is needed for <script> tag usage.
Tip
For production deployments, always pin a specific version (e.g.,
chart.js@4.5.1) rather than using@latest. Chart.js major and minor versions can change default behavior, scale types, or plugin APIs.
4.5 npm Scripts Reference
| Script | Command | Purpose |
|---|---|---|
build |
rollup -c && pnpm emitDeclarations |
Full production build + type declarations |
autobuild |
rollup -c -w |
Watch mode for development |
dev |
karma start ./karma.conf.cjs --auto-watch --no-single-run --browsers chrome --grep |
Interactive test runner |
test |
pnpm lint && pnpm test-ci |
Full test suite |
test-ci |
concurrently "pnpm:test-ci-*" |
CI test run (Karma + integration) |
test-ci-karma |
Karma with coverage | Browser tests with Istanbul |
test-ci-integration |
Integration suite | Node ESM/CJS, React, TypeScript |
lint |
concurrently "pnpm:lint-*" |
All linters in parallel |
lint-js |
eslint "src/**/*.{js,ts}" "test/**/*.js" "docs/**/*.js" --cache |
ESLint |
docs |
pnpm run build && pnpm --filter "./docs/**" build |
Build docs site |
4.6 Tree-Shaking Guide
To minimize bundle size in modern bundlers (webpack, Rollup, Vite, esbuild):
- Import from
chart.js(notchart.js/auto) - Register only the components you use:
import {
Chart,
LineController,
LineElement,
PointElement,
LinearScale,
CategoryScale,
Tooltip,
Legend
} from 'chart.js';
Chart.register(
LineController,
LineElement,
PointElement,
LinearScale,
CategoryScale,
Tooltip,
Legend
);
- The
sideEffectsfield inpackage.jsoncorrectly markschart.js/autoand UMD builds, so bundlers will not tree-shake those entry points.
5. Operations
5.1 Chart Instance Management
The Chart class maintains a singleton instances map keyed by a numeric chart ID:
Chart.instances // Map of all active chart instances
Chart.getChart(canvasOrId) // Retrieve chart bound to a canvas element or ID string
Caution
Chart.js prevents reuse of a canvas that already has a chart bound to it. You must call
chart.destroy()before creating a new chart on the same canvas. Failure to do so throws an error and leaves the canvas in an inconsistent state. This is the single most common issue in SPA frameworks (React, Vue, Angular) where components remount.
5.2 Memory Management
Each Chart instance holds references to:
- The canvas element and its 2D rendering context
- All dataset data arrays
- Parsed data caches
- Animation state
- Event listener proxies on the DOM
Cleanup checklist:
- Call
chart.destroy()-- this removes event listeners, clears animations, and deletes the instance fromChart.instances - In SPA frameworks, destroy in the unmount/cleanup lifecycle hook
- The decimation plugin replaces
dataset.datawith getter/setter proxies;destroy()cleans these up
5.3 Resize Behavior
The DomPlatform uses ResizeObserver to detect container size changes and MutationObserver for canvas attach/detach events. It also listens for devicePixelRatio changes (e.g., dragging a window between monitors with different DPIs).
| Config | Default | Effect |
|---|---|---|
responsive |
true |
Auto-resize to container |
maintainAspectRatio |
true |
Preserve width/height ratio |
resizeDelay |
0 |
Debounce resize (ms) |
The internal _doResize method is debounced using the resizeDelay option. Set this to a non-zero value (e.g., 100) if resize events cause excessive redraws.
5.4 Testing Infrastructure
Browser tests: Karma + Jasmine (karma.conf.cjs)
- Fixture-based visual regression tests in
test/fixtures/ - Coverage via Istanbul when running
test-ci-karma
Integration tests (in test/integration/):
| Test Suite | What It Validates |
|---|---|
| Node ESM | import { Chart } from 'chart.js' works in Node |
| Node CJS | require('chart.js') works in Node |
| React browser | Chart.js renders in a React app |
| TypeScript Node | Type declarations compile without errors |
| TypeScript NodeNext | Module resolution with moduleResolution: NodeNext |
5.5 Linting & Code Quality
ESLint (.eslintrc.yml):
- Extends the
chartjsshared config - Enforces ES2018 language restrictions
- TypeScript support for
.tsfiles - Cache enabled for incremental linting
Code Climate (.codeclimate.yml):
- Duplication detection enabled
- FIXME/TODO comment detection
- Argument count threshold: 5
- Method complexity threshold: 7
6. Performance
6.1 Rendering Pipeline
Each chart.update() call triggers this pipeline:
- Config resolution -- resolves all scriptable options against context
- Layout -- positions boxes (title, legend, scales) using the box layout engine
- Scale update -- tick generation, auto-skip, label sizing
- Dataset update -- maps data to pixel positions, applies stacking
- Animation -- creates/updates
Animationinstances for changed properties - Render -- draws to canvas via the
Animatorsingleton's RAF loop
6.2 Data Decimation
For large datasets (thousands of points), the decimation plugin is critical:
Min-Max algorithm (default):
- Preserves local minima and maxima
- Fast O(n) single pass
- Good for data where peaks matter
LTTB algorithm (Largest Triangle Three Buckets):
- Better visual fidelity for general datasets
- O(n) complexity
- Configured via
samplesoption
Tip
Enable decimation for any line chart with more than a few hundred data points. Set
parsing: falseand use a linear or time x-axis. The threshold defaults to4 * chartWidthpixels, meaning decimation only activates when data points significantly outnumber pixels.
6.3 Animation Performance
The Animator is a singleton that runs a single requestAnimationFrame loop for all active chart instances. Optimizations:
- Animations that have completed are automatically cleaned up
- The loop stops entirely when no animations are active
transitions.resize.animation.duration = 0ensures resize is instant- Set
animation: falseglobally to disable all animations (recommended for dashboards with frequent updates)
// Disable animations for real-time dashboards
new Chart(ctx, {
options: {
animation: false,
transitions: {
active: { animation: { duration: 0 } }
}
}
});
6.4 Tick Auto-Skip
The auto-skip system (src/core/core.scale.autoskip.js) prevents label overlap on axes by calculating maximum fitting labels based on:
- Label rotation angle
- Available pixel space
- Font size and padding
This runs on every scale update. For extremely dense tick sets, auto-skip can become a bottleneck; consider setting explicit ticks.maxTicksLimit to cap computation.
6.5 Performance Checklist
| Technique | Impact | When to Use |
|---|---|---|
animation: false |
High | Real-time/streaming data, >10 updates/sec |
| Decimation plugin | High | Line charts with >500 points |
parsing: false + pre-formatted data |
Medium | Large datasets; skip internal parsing |
resizeDelay: 100 |
Medium | Frequent container resizes |
| Tree-shake imports | Medium | Bundle size reduction |
spanGaps: true |
Low | Skip null-point processing |
7. Security
7.1 Threat Model
Chart.js is a client-side rendering library. The primary security concerns are:
- Cross-Site Scripting (XSS) via data injection -- Chart.js renders to
<canvas>, which does not interpret HTML or execute scripts. Canvas drawing operations are inherently safe against XSS. - Prototype pollution -- The config resolution chain uses deep merging. Chart.js's
helpers.core.jsimplementsisObject()checks but consumers must sanitize untrusted config input. - Denial of Service -- Extremely large datasets or deeply nested configs can cause browser tab crashes. Apply data limits at the application layer.
Warning
While canvas rendering is XSS-safe, the tooltip plugin can render custom HTML via the
externalcallback. If you useexternaltooltips that inject user-supplied data into the DOM, you must sanitize that data. The default tooltip uses canvas rendering and is safe.
7.2 Supply Chain
Chart.js has exactly one runtime dependency (@kurkle/color). The attack surface is minimal. The build toolchain (Rollup, SWC, Karma, etc.) runs at development time only and is not shipped to consumers.
7.3 Content Security Policy
For CSP-compliant environments:
- Chart.js does not use
eval()ornew Function() - No inline styles are injected into the DOM
- Canvas operations do not require
unsafe-inlineorunsafe-eval - Required CSP directives: none beyond the defaults
7.4 Data Validation
Chart.js does not validate incoming data types at the boundary. It expects numeric values where numbers are needed and trusts the caller. For applications accepting user-supplied chart data, validate at the application layer before passing to Chart.js.
8. Developer Internals
8.1 Chart Class Deep Dive
The Chart class (src/core/core.controller.js, ~1500 lines) is the central orchestrator.
Constructor accepts:
- An
HTMLCanvasElement - A string ID (looks up canvas by DOM ID)
- A
CanvasRenderingContext2D - A jQuery-like wrapper (extracts first element)
Static members:
| Member | Type | Purpose |
|---|---|---|
Chart.defaults |
Defaults |
Global defaults store |
Chart.instances |
Object |
Map of all active charts by ID |
Chart.overrides |
Object |
Per-type option overrides |
Chart.registry |
Registry |
Component registry |
Chart.version |
string |
Library version string |
Chart.getChart(key) |
method | Find chart by canvas or ID |
Chart.register(...) |
method | Register components |
Chart.unregister(...) |
method | Unregister components |
Instance methods:
| Method | Purpose |
|---|---|
update(mode?) |
Full update cycle (config resolve → layout → render) |
render() |
Render only (no data/layout recalculation) |
resize(width?, height?) |
Manual resize |
destroy() |
Full cleanup (events, animations, DOM, instance map) |
clear() |
Clear canvas without destroying |
stop() |
Stop running animations |
toBase64Image(type?, quality?) |
Export chart as data URL |
getElementsAtEventForMode(e, mode, options, useFinalPosition) |
Hit detection |
getSortedVisibleDatasetMetas() |
Dataset metadata in draw order |
getDatasetMeta(index) |
Metadata for specific dataset |
isDatasetVisible(index) |
Visibility check |
show(datasetIndex) / hide(datasetIndex) |
Toggle dataset visibility |
toggleDataVisibility(index) / getDataVisibility(index) |
Toggle individual data point visibility |
notifyPlugins(hook, args?) |
Trigger plugin lifecycle hook |
data (getter/setter) |
Access chart data |
options (getter/setter) |
Access resolved options |
bindEvents() |
Attach DOM event listeners |
8.2 Registry Internals
The Registry class holds four TypedRegistry instances:
Registry
├── controllers (TypedRegistry<DatasetController>)
├── elements (TypedRegistry<Element>)
├── scales (TypedRegistry<Scale>)
└── plugins (TypedRegistry<Plugin>)
Each TypedRegistry enforces that registered components have a string id property. Registration fails with the error "class does not have id: " if this is missing.
Lookup methods: getController(id), getElement(id), getScale(id), getPlugin(id).
The add(...) and remove(...) methods accept varargs of individual components or arrays, enabling batch registration:
Chart.register(BarController, BarElement, CategoryScale, LinearScale);
Chart.register([Tooltip, Legend, Title]);
8.3 Controllers (8 Chart Types)
| Controller | File | Element(s) Used |
|---|---|---|
BarController |
controller.bar.js |
BarElement |
BubbleController |
controller.bubble.js |
PointElement |
DoughnutController |
controller.doughnut.js |
ArcElement |
LineController |
controller.line.js |
LineElement, PointElement |
PieController |
controller.pie.js |
ArcElement |
PolarAreaController |
controller.polarArea.js |
ArcElement |
RadarController |
controller.radar.js |
LineElement, PointElement |
ScatterController |
controller.scatter.js |
PointElement |
All controllers extend DatasetController (src/core/core.datasetController.js), which provides the base update/draw/hit-test cycle.
8.4 Scale System
Hierarchy:
Scale (core.scale.js)
├── CategoryScale (scale.category.js)
├── LinearScale (scale.linear.js)
│ └── extends LinearScaleBase (scale.linearbase.js)
├── LogarithmicScale (scale.logarithmic.js)
│ └── extends LinearScaleBase
├── RadialLinearScale (scale.radialLinear.js) — radar/polar with point labels
├── TimeScale (scale.time.js) — requires DateAdapter
│ └── TimeSeriesScale (scale.timeseries.js) — evenly-spaced variant
Tick generation:
- Linear scales use the niceNum algorithm for human-friendly intervals
- Logarithmic scales use log10-based tick placement
- Time scales delegate to a
DateAdapterimplementation
Important
The
TimeScaleandTimeSeriesScalerequire aDateAdapterimplementation. Chart.js ships only the abstractDateAdapterBase(src/core/core.adapters.ts). You must install an adapter likechartjs-adapter-date-fns,chartjs-adapter-luxon, orchartjs-adapter-moment. Without one, any method call throws:"This method is not implemented: Check that a complete date adapter is provided."
8.5 Elements (4 Visual Primitives)
| Element | File | Used By |
|---|---|---|
ArcElement |
elements/element.arc.js |
Doughnut, Pie, PolarArea |
BarElement |
elements/element.bar.js |
Bar |
LineElement |
elements/element.line.js |
Line, Radar |
PointElement |
elements/element.point.js |
Line, Bubble, Scatter, Radar |
Elements are the lowest-level visual components. Each element knows how to draw itself on a canvas context and calculate its bounding box for hit detection.
8.6 Platform Abstraction
The platform layer (src/platform/) provides environment-specific functionality:
Platform detection (_detectPlatform(canvas)):
- If
canvasis anOffscreenCanvasor not a DOM element: BasicPlatform - Otherwise: DomPlatform
DomPlatform provides:
ResizeObserverfor container size monitoringMutationObserverfor canvas DOM attachment changesdevicePixelRatiochange listener (multi-monitor support)- Throttled event proxies for pointer/touch/mouse events
- Touch-to-mouse event mapping:
touchstart→mousedown,touchmove→mousemove,touchend→mouseup,pointerenter→mouseenter
BasicPlatform provides:
- Minimal
acquireContextthat works with non-DOM canvases - Animations disabled by default
- No event binding
8.7 Helpers Library (18 Modules)
The src/helpers/ directory (100K chars) provides utility functions exported via chart.js/helpers:
| Module | Key Functions |
|---|---|
helpers.canvas.js |
drawPoint(), clipArea(), unclipArea(), renderText() |
helpers.collection.js |
_lookup(), _lookupByKey(), _rlookupByKey() |
helpers.color.js |
Color parsing/conversion (wraps @kurkle/color) |
helpers.config.ts |
_createResolver(), _attachContext(), recursion detection |
helpers.core.js |
isObject(), each(), merge(), uid(), valueOrDefault() |
helpers.curve.js |
Bezier control point computation (monotone, spline) |
helpers.dom.js |
getRelativePosition(), getMaximumSize(), readUsedSize() |
helpers.easing.js |
31 easing functions |
helpers.extras.js |
requestAnimFrame(), throttled(), debounce() |
helpers.interpolation.js |
_steppedInterpolation(), _bezierInterpolation() |
helpers.intl.js |
formatNumber() via Intl.NumberFormat |
helpers.math.js |
toRadians(), toDegrees(), distanceBetweenPoints(), sign() |
helpers.options.js |
toFont(), toPadding(), toLineHeight(), resolve() |
helpers.rtl.js |
RTL layout support utilities |
helpers.segment.js |
Line segment computation for fills and decimation |
8.8 Built-in Plugins (7)
| Plugin | File | Lines | Purpose |
|---|---|---|---|
| Colors | plugin.colors.ts |
~80 | Auto-assigns the 7-color palette |
| Decimation | plugin.decimation.js |
~280 | LTTB or min-max data reduction |
| Filler | plugin.filler/ (8 files) |
~800 | Area fills between datasets/axes |
| Legend | plugin.legend.js |
~500 | Interactive legend with click handlers |
| Subtitle | plugin.subtitle.js |
~30 | Secondary title below the main title |
| Title | plugin.title.js |
~200 | Chart title rendering |
| Tooltip | plugin.tooltip.js |
~1100 | Hover tooltips (canvas or external HTML) |
8.9 Writing Custom Plugins
A Chart.js plugin is an object with an id property and any subset of the lifecycle hooks:
const myPlugin = {
id: 'myPlugin',
// Called once when plugin is registered
install(chart, args, options) {},
// Called when chart starts using this plugin
start(chart, args, options) {},
// Before/after each update cycle
beforeUpdate(chart, args, options) {},
afterUpdate(chart, args, options) {},
// Before/after each draw
beforeDraw(chart, args, options) {},
afterDraw(chart, args, options) {},
// Event handling
beforeEvent(chart, args, options) {},
afterEvent(chart, args, options) {},
// Cleanup
destroy(chart, args, options) {},
uninstall(chart, args, options) {},
};
Chart.register(myPlugin);
Plugin options are resolved through the config scope chain at options.plugins[pluginId].
8.10 Writing Custom Scales
Extend the Scale base class and register:
class CustomScale extends Chart.Scale {
static id = 'custom';
static defaults = {};
determineDataLimits() {
// Set this.min and this.max
}
buildTicks() {
// Return array of {value, label} tick objects
return [];
}
getPixelForValue(value) {
// Convert data value to pixel coordinate
}
getValueForPixel(pixel) {
// Convert pixel coordinate to data value
}
}
Chart.register(CustomScale);
9. Integration Points
9.1 Framework Integration Patterns
React: Use useRef for the canvas and useEffect for lifecycle:
useEffect(() => {
const chart = new Chart(canvasRef.current, config);
return () => chart.destroy(); // Critical cleanup
}, []);
Vue: Use onMounted/onBeforeUnmount:
onMounted(() => {
chart = new Chart(canvasRef.value, config);
});
onBeforeUnmount(() => {
chart?.destroy();
});
Angular: Use ngAfterViewInit/ngOnDestroy with @ViewChild.
Note
In all SPA frameworks, the most critical requirement is calling
chart.destroy()in the cleanup/unmount hook. Without this, you will see errors about canvas reuse, memory leaks from retained event listeners, and orphaned animation loops.
9.2 Server-Side Rendering
For generating chart images on the server:
- Install a canvas implementation:
npm install canvas(node-canvas) or@napi-rs/canvas - Import and create a canvas:
import { createCanvas } from 'canvas';
import { Chart } from 'chart.js/auto';
const canvas = createCanvas(800, 400);
const chart = new Chart(canvas, { /* config */ });
const buffer = canvas.toBuffer('image/png');
The BasicPlatform is auto-selected and disables animations. No DOM is needed.
9.3 Web Worker Rendering
Chart.js can render in a Web Worker using OffscreenCanvas:
// Main thread
const offscreen = canvas.transferControlToOffscreen();
worker.postMessage({ canvas: offscreen }, [offscreen]);
// Worker
self.onmessage = ({ data: { canvas } }) => {
const chart = new Chart(canvas, config);
};
The BasicPlatform is auto-detected for OffscreenCanvas instances.
9.4 Date Adapter Integration
Time-based scales require a date adapter. The adapter must extend DateAdapterBase and implement:
| Method | Purpose |
|---|---|
init(options) |
Initialize with locale/timezone options |
formats() |
Return format strings for each time unit |
parse(value, format) |
Parse a value into a timestamp |
format(timestamp, format) |
Format a timestamp into a display string |
add(timestamp, amount, unit) |
Add time to a timestamp |
diff(a, b, unit) |
Difference between two timestamps |
startOf(timestamp, unit) |
Start of a time unit |
endOf(timestamp, unit) |
End of a time unit |
Popular adapters: chartjs-adapter-date-fns, chartjs-adapter-luxon, chartjs-adapter-moment, chartjs-adapter-dayjs-4.
9.5 TypeScript Integration
Type definitions are exported from dist/types.d.ts. Key types:
import type {
ChartConfiguration,
ChartData,
ChartOptions,
ChartType,
Plugin,
Scale,
Element,
DatasetController
} from 'chart.js';
Tip
The integration test suite validates TypeScript compatibility under both
moduleResolution: NodeandmoduleResolution: NodeNext. If you encounter type resolution issues, ensure yourtsconfig.jsonmatches one of these tested configurations.
10. Troubleshooting
10.1 Error Reference
| Error Message | Source | Cause | Solution |
|---|---|---|---|
"Recursion detected: " |
helpers.config.ts:263 |
Circular reference in scriptable option evaluation | Check that scriptable options do not reference themselves through the context chain |
"This method is not implemented: Check that a complete date adapter is provided." |
core.adapters.ts:66 |
Time scale used without a date adapter | Install and import a date adapter (e.g., chartjs-adapter-date-fns) |
"Cannot determine type of " |
core.config.js:52 |
Scale configuration has an unrecognizable type | Ensure the scale's type matches a registered scale ID |
"No dataset found at index " |
core.controller.js:1101 |
Referencing a dataset index that does not exist | Check data.datasets.length before accessing by index |
"class does not have id: " |
core.typedRegistry.js:38 |
Registering a component without a static id |
Add static id = 'myComponent' to your class |
"Unsupported decimation algorithm " |
plugin.decimation.js:277 |
Invalid value for plugins.decimation.algorithm |
Use 'min-max' or 'lttb' only |
"Cannot find a dataset at index " |
plugin.tooltip.js:1112 |
Tooltip references a non-existent dataset | Usually caused by data mutation during tooltip render; ensure atomic data updates |
10.2 Known Issues (from GitHub)
| Issue | Symptom | Workaround |
|---|---|---|
| #12239 | Axis titles misaligned when scale is reversed | Manually adjust title padding |
| #12234 | chart.umd.js should have readable types |
Use ESM import for full type support |
| #12232 | Array of tuples not working when parsing disabled | Enable parsing or restructure data |
| #12177 | Chart no longer resizes correctly when enlarging window (v4.5.1) | Set explicit resizeDelay or call chart.resize() manually |
| #12143 | Changes to resize events and animation in 4.5.1 | Review migration notes for ResizeObserver changes |
| #10901 | TypeError: Cannot read properties of null (reading 'getLabelAndValue') |
Ensure datasets are not mutated during tooltip evaluation |
| #10406 | PolarArea chart broken when all data values equal 0 | Add a minimum value guard in data preprocessing |
| #10059 | Inconsistent Doughnut arc spacing | Adjust borderWidth and spacing options |
| #9965 | Using TypeScript type definitions without modules | Use chart.js/auto with <script> and a global types reference |
10.3 Common Pitfalls
Canvas reuse without destroy:
Error: Canvas is already in use. Chart with ID '0' must be destroyed before...
Prevention: Always call chart.destroy() before creating a new chart on the same canvas element.
Missing scale registration:
Chart renders empty with no visible errors.
Prevention: When using tree-shaken imports from chart.js, ensure you register all required scale types (CategoryScale, LinearScale, etc.).
Time scale without adapter:
Throws "This method is not implemented" on first render.
Prevention: Install and import a date adapter before creating a time-based chart.
Decimation not activating:
Data points still render at full density despite enabling the plugin.
Prevention: Verify all three prerequisites: (1) line chart, (2) linear or time x-axis, (3) parsing: false.
Caution
When updating chart data in-place (e.g.,
chart.data.datasets[0].data.push(newPoint)), you must callchart.update()afterward. Chart.js does not observe array mutations. Failing to callupdate()leaves the visual state stale, and can cause tooltip/interaction errors when internal metadata is out of sync with the data.