Chart.js Scriptable Options for Dynamic Point Styling
Learn how Chart.js scriptable options let you style individual points based on data values—without mutating your dataset—and how to keep performance smooth with memoization.
11 Feb 2026, 14:17 UTC

The problem: static styling hides important data nuances
When you configure a Chart.js line or scatter chart with a single pointBackgroundColor or pointRadius value, every point looks the same. That works for simple series, but it obscures patterns such as values that exceed a safety threshold, outliers, or categories that need visual emphasis. To highlight those cases you would normally have to preprocess the data array, adding extra fields just for styling, which couples presentation logic to your dataset and makes reuse harder.
Thesis: scriptable options let you compute styles per point without mutating data
Chart.js accepts a function instead of a static value for most visual options (colors, fonts, radii, borders, etc.). The function receives a context object that tells you which chart, dataset, and data point is being rendered, plus the parsed values and interaction state. By returning a value from that function you can drive styling directly from the data, keeping the original dataset untouched.
How scriptable options work in Chart.js v4
In v4 the context shape includes:
type– e.g.,'data','dataset','tick'dataIndexanddatasetIndex– indices of the point and its datasetparsed– the parsedxandyvalues (orrfor polar)dataset– the original dataset object, useful for reading custom fields like a per‑point thresholdchart– reference to the chart instance, giving access to other datasets and scalesmode– interaction state such as'hover'or'default'
The function’s return type must match the option’s expected type (string for colours, number for radii, boolean for visibility, etc.). Returning undefined falls back to the global default, not the dataset default, so always return an explicit value.
Worked example: threshold‑based point colour and radius
Suppose you have a line chart of temperature readings and you want any reading above 30 °C to appear as a larger red point, while readings at or below 30 °C stay blue and normal size.
import { Chart } from 'chart.js';
// Sample data – note we keep a custom `threshold` field per point
const data = {
labels: ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'],
datasets: [{
label: 'Temperature (°C)',
data: [
{ x: 0, y: 28, threshold: 30 },
{ x: 1, y: 32, threshold: 30 },
{ x: 2, y: 29, threshold: 30 },
{ x: 3, y: 31, threshold: 30 },
{ x: 4, y: 27, threshold: 30 },
{ x: 5, y: 33, threshold: 30 },
{ x: 6, y: 30, threshold: 30 }
],
// Scriptable options
pointBackgroundColor: ctx => {
const y = ctx.parsed.y;
return y > ctx.dataset.data[ctx.dataIndex].threshold ? 'red' : 'blue';
},
pointRadius: ctx => {
const y = ctx.parsed.y;
return y > ctx.dataset.data[ctx.dataIndex].threshold ? 6 : 4;
}
}]
};
const config = {
type: 'line',
data: data,
options: {
animation: false, // easier to see immediate effect while testing
scales: { y: { beginAtZero: true } }
}
};
new Chart(document.getElementById('myChart'), config);
In this snippet:
- The
pointBackgroundColorfunction readsctx.parsed.y(the current point’s value) and compares it to thethresholdstored alongside the original data point. - The
pointRadiusfunction does the same comparison to decide the size. - Because the functions are pure (no side effects) and only depend on the context, Chart.js can call them each frame without altering the dataset.
Trade‑off: performance considerations and memoization
Scriptable options execute on every animation frame. For a chart with a few hundred points this is negligible, but with tens of thousands of points the per‑point function calls can become a bottleneck.
Two practical strategies:
- Memoize expensive calculations – store results in a Map keyed by
${ctx.datasetIndex}-${ctx.dataIndex}-${ctx.mode}and return the cached value when available. - Pre‑compute and stash – compute the style once (e.g., during data loading) and attach it to
dataset._cached; the scriptable option then simply returnsctx.dataset._cached[ctx.dataIndex].
Both approaches keep the render loop lightweight while preserving the flexibility of per‑point logic.
Actionable steps: add scriptable options safely
- Identify the visual property you want to vary (color, radius, border width, etc.).
- Write a pure function that receives the Chart.js context and returns the appropriate value.
- Test the function in isolation by logging the context:
console.log(ctx)to confirm which fields are present for your chart type. - Attach the function to the dataset option (or global options if you want it to apply to all datasets).
- If the function does non‑trivial work, add memoization or pre‑computation as described above.
- Verify the chart renders as expected and open the Chrome DevTools Performance tab to ensure frame times stay below ~16 ms (60 fps) for your typical dataset size.
By following these steps you gain dynamic, data‑driven styling without polluting your data model, and you retain the ability to scale to larger datasets with simple performance tweaks.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.