Performance and security
Large-data settings, measured numbers, bundle sizes, XSS, CSP, CSV, dependencies.
This page collects what you need to know to run WestCoast Charts with large data and on pages with strict security requirements. Every number on this page comes from a measurement that is recorded in the repository; the source is named under each table. Numbers come from one machine, so treat them as orders of magnitude and as a regression baseline, not as a promise for every device.
Performance
Settings for large data
Line and area charts with many points, and bar or scatter charts with thousands of marks, are handled by two techniques that are on by default:
- Decimation (
wcc.performance.decimate, defaulttrue): plain line and area paths keep only the first, last, lowest and highest point of every device-pixel column. The drawn result is the same. The decimation code ships in the light entrywest-coast-charts/types/decimate(also part ofwest-coast-charts/typesandfull). Without it the core draws every point and logs one hint. - Dense marks (
wcc.performance.denseMarks, default'auto'): fromdenseThresholdpoints per series (default 2,000), columns narrower than a pixel and scatter points are drawn as one combined path per series instead of one element per point.
import WestCoastCharts from 'west-coast-charts'
import 'west-coast-charts/types/decimate' // light entry with the decimation code
const points = Array.from({ length: 50000 }, (_, i) => [Date.UTC(2026, 0, 1) + i * 60000, Math.round(50 + 30 * Math.sin(i / 500) + (i % 7))])
const chart = new WestCoastCharts(document.querySelector('#chart'), {
chart: { type: 'line', height: 320, animations: { enabled: false } },
series: [{ name: 'Sensor', data: points }],
xaxis: { type: 'datetime' },
wcc: {
performance: {
decimate: true, // default
denseMarks: 'auto', // default; true forces it, false turns it off
denseThreshold: 2000, // default
},
a11y: { dataTableMaxRows: 500 }, // the hidden data table renders this many rows and states the total
},
})
await chart.render()More tips:
- Turn animations off for very large series or high-frequency streams:
chart.animations.enabled: false. - Update instead of re-creating.
updateSeries()reuses the chart; destroying and re-creating costs a full render. - Canvas renderer for millions of points.
west-coast-charts/features/renderer-canvasdraws line, area and scatter series into a canvas while axes, tooltips and accessibility stay as before. See docs/features/renderer-canvas.md (library repository). - Dense mode changes a few details (for example, per-point hover styling on a combined path). They are listed in docs/architecture/performance.md (library repository).
Measured: core charts
Large data sets. Longest main-thread task in ms, headless Chromium, on the Linux build box after the 2026-10-23 performance fixes:
| Case | render | longest render task | update (sync) | longest update task |
|---|---|---|---|---|
| line, 10k points | 73 | 73 | 34 | 0 |
| line, 100k points | 167 | 167 | 111 | 126 |
| area, 10k points | 83 | 83 | 35 | 0 |
| column, 10k points | 62 | 61 | 32 | 0 |
| column, 100k points | 121 | 120 | 96 | 104 |
| scatter, 10k points | 108 | 108 | 45 | 53 |
| scatter, 100k points | 223 | 223 | 163 | 170 |
Source: docs/architecture/performance.md (library repository), section "2026-10-23". A longest task of 0 means no single task above the long-task threshold.
Against the 4.7.0 code base, longest render task in ms (Chromium, perf test cases):
| Case | 4.7.0-based | now |
|---|---|---|
| column, 10k points | 553 | 68 |
| scatter, 10k points | 558 | 69 |
| line, 10k points | 121 | 79 |
| area, 10k points | 135 | 67 |
| line, 100k points | 383–489 | 207 |
Source: docs/architecture/performance.md (library repository), section "Numbers".
Animation and interaction, 720 bars, Chrome 126, 3 runs:
| Measure | Result |
|---|---|
| first render | 83.1 ms |
| animated morph between data sets | 60 fps (p95 frame interval 21.8 ms) |
| streaming updates | 39 fps at 39 updates per second |
| hover | 60 fps (p95 frame interval 16.8 ms) |
Source: verified site facts at commit bd511576 (2026-10-23).
Performance pass A (radialColumn, 100k redraw, canvas 1M), medians before → after:
| Case | Measure | Before | After |
|---|---|---|---|
| radialColumn, 720 bars, animated update | longest frame per run | 27–43 ms | 22–26 ms |
| radialColumn, 720 bars, animated update | synchronous update | 13.4 ms | 11.4 ms |
| radialColumn, 720 bars, animations off | updates per second | 45.2 | 61 (display limit) |
| line, 100k points | redraw frame | 105 / 95 ms | 86 / 73 ms |
| canvas renderer, line 1M points | first render | 397.8 ms | 352.9 ms |
Source: docs/perf/perf-a.md (library repository) (box, headless Chromium with software raster).
Measured: premium charts
Medians of 3 runs, chart 1,200 × 700 px, animations off unless noted, headless Chromium with software raster on a shared 8 vCPU Linux box (2026-10-23):
| Case | render | update | interaction |
|---|---|---|---|
| Gantt, 5k tasks | 77 ms | 98 ms | wheel scroll and bar drag 60 fps |
| Gantt, 20k tasks | 132 ms | 164 ms | wheel scroll and bar drag 60 fps |
| Tree, 2k nodes | 105 ms | 103 ms | pan and hover 60 fps; expand all 111 ms |
| Tree, 10k nodes | 497 ms | 466 ms | pan and hover 60 fps; expand all 442 ms (one long frame) |
| Sankey, 300 nodes / 1.5k links | 168 ms | 186 ms | hover 57 fps, node drag 59 fps |
| Network, 2k nodes / 6k edges, animated layout | 227 ms to the first frame | 250 ms | layout settles in 1,339 ms at 49 fps |
| Network, 2k nodes / 6k edges, static layout | 828 ms | 837 ms | node drag 55 fps |
| Map, all countries + 5k bubbles | 90 ms | 98 ms | pan, zoom and hover 60 fps |
| Stock, 100k candles + volume | 145 ms | 137 ms | pan 59 fps, zoom and crosshair 60 fps |
After destroy() the measured heap returns to within 0.2 MB of an empty page in every case.
Source: docs/perf/premium.md (library repository), which also lists DOM sizes, heap figures and the method.
Bundle sizes
gzip level 9, minified bundles, from the WestCoast Charts release record (library commit 95f093ba8d388c579d9f94701d3322c728c77632):
| File | gzip |
|---|---|
core west-coast-charts.min.js |
194,836 B |
all chart types west-coast-charts-types.min.js |
83,472 B |
premium west-coast-charts-premium.min.js |
165,491 B |
everything except premium west-coast-charts.full.min.js |
380,285 B |
For comparison, ApexCharts 4.7.0 apexcharts.min.js is 152,686 B. The difference buys accessibility,
security fixes, theming and robustness; the itemised list is in
docs/architecture/size-ledger.md (library repository). Single chart types
(west-coast-charts/types/<name>) and features load on their own and cost nothing until imported.
Known limits
- Network, static layout: with animations off the layout runs to completion before the first paint (about 0.8 s for 2k nodes / 6k edges). Use the default animated layout or pass positions.
- Tree, expand all at 10k nodes is one long frame (about 0.44 s); panning and hover stay smooth.
- Sankey keeps one gradient per link (about 9.5k elements at 1.5k links); there is no virtualisation.
- Scatter, 100k points: each update still forces one layout of the dense path for axis label measurement.
- Measured in headless Chromium with software raster only. GPU-backed browsers should be faster on paint-heavy charts, but that is not measured. 120 Hz displays are not measured.
Sources: docs/perf/premium.md (library repository) ("Known limits"), docs/perf/perf-a.md (library repository) ("Gaps").
Security
The fixes behind this section are documented in detail, with the affected code, in SECURITY-FIXES.md (library repository).
HTML and XSS
- Strings from data and configuration (series names, categories, labels, titles, annotation texts, locale strings) are always rendered as text, never as HTML.
- HTML returned by your functions (formatters,
tooltip.custom,legend.markers.customHTML) and markup options (toolbar icons, annotationcustomSVG) runs through a built-in allow-list sanitizer. chart.sanitizeHtml:true(default), a function(html, { svg }) => string | Nodeto use your own sanitizer, orfalsefor raw HTML (only for fully trusted output).- Escape data you mix into your own HTML with
WestCoastCharts.escapeHtml(value). - The built-in sanitizer is designed for chart labels and tooltips. If you render untrusted rich HTML in a custom tooltip, plug in a dedicated sanitizer such as DOMPurify:
import WestCoastCharts from 'west-coast-charts'
// stand-in for DOMPurify.sanitize so this example runs without the dependency
const sanitize = (html) => html.replace(/<script[\s\S]*?<\/script>/gi, '').replace(/\son\w+="[^"]*"/gi, '')
const chart = new WestCoastCharts(document.querySelector('#chart'), {
chart: { type: 'bar', height: 260, sanitizeHtml: sanitize }, // e.g. DOMPurify.sanitize
series: [{ name: 'Incidents', data: [2, 5, 1] }],
xaxis: { categories: ['api', 'web', 'db'] },
tooltip: {
custom: ({ series, seriesIndex, dataPointIndex, w }) =>
`<div class="tip"><b>${WestCoastCharts.escapeHtml(w.globals.labels[dataPointIndex])}</b>: ${series[seriesIndex][dataPointIndex]}</div>`,
},
})
await chart.render()Content Security Policy and Trusted Types
| Need | What to allow |
|---|---|
| Scripts | Nothing extra: no eval, no new Function, no inline scripts. A nonce-based script-src with 'strict-dynamic' works; load the bundles with your nonce. |
Library <style> elements |
Pass your nonce as chart.nonce; every <style> the core, premium and feature bundles inject carries it. Alternatively serve dist/west-coast-charts.css yourself. |
| Style attributes | The charts set inline style attributes (tooltips, legends, data tables, labels). Allow them with style-src-attr 'unsafe-inline' (or 'unsafe-inline' in style-src). They cannot run code. |
Trusted Types (require-trusted-types-for 'script') |
Add the policy name west-coast-charts to trusted-types. Every HTML the library writes goes through that policy: markup it builds from escaped text, and formatter or tooltip HTML after sanitizeHtml. With chart.sanitizeHtml: false or your own sanitizer function, your HTML passes through the policy unchanged, so only use that for markup you trust. |
PNG export and dataURI() |
img-src must allow blob: (tried first) or data: (fallback). Pages that allow neither cannot rasterise SVG in the browser. |
A policy that the release check renders charts under without a single violation (core, types, premium
and feature bundles; hover, menu, legend toggle, data update, drill-down, dataURI()):
default-src 'none';
script-src 'nonce-{random}' 'strict-dynamic';
style-src 'nonce-{random}'; style-src-elem 'nonce-{random}'; style-src-attr 'unsafe-inline';
img-src 'self' blob: data:; font-src 'self'; connect-src 'self';
base-uri 'none'; object-src 'none'; form-action 'none';
require-trusted-types-for 'script'; trusted-types west-coast-chartsSend it as an HTTP header: <meta> CSP tags ignore some of these directives. {random} is a fresh
nonce per response. The check is tools/docs/check-csp.cjs (headless Chromium).
import WestCoastCharts from 'west-coast-charts'
// the same nonce your server puts in the Content-Security-Policy header
const nonce = document.querySelector('meta[name="csp-nonce"]')?.content ?? 'r4nd0m'
await new WestCoastCharts(document.querySelector('#chart'), {
chart: { type: 'line', height: 240, nonce },
series: [{ name: 'Latency', data: [120, 98, 143, 110] }],
}).render()CSV export
exportToCSV neutralises spreadsheet formulas: text cells starting with =, +, -, @, tab or
carriage return (and their full-width forms) get a leading ', so a category such as
=HYPERLINK(...) cannot become a live formula when the file is opened in a spreadsheet. Numbers are
unchanged. Cells containing delimiters, quotes or line breaks are quoted per RFC 4180. The same rules
apply to the output of csv.categoryFormatter and csv.valueFormatter.
Robustness
- Malformed options (
series: null,nullentries,data: null,xaxis.categories: null,annotations: null,chart: null, toolbar icons withoutclick, …) are normalised to their empty equivalents instead of throwing, in the constructor and in all update methods. - A randomised configuration test found 0 exceptions in 900 random configurations
(size ledger (library repository,
architecture/size-ledger.md), "Robustness and security"). - Premium charts guard against very large arrays (no argument spreading over unbounded data).
Licence check and privacy
The premium licence check is offline: no network request, no telemetry, no cookies or storage. See Premium and licensing.
Dependencies
The runtime dependencies are @svgdotjs/* and @yr/monotone-cubic-spline. npm audit --omit=dev
reports 0 vulnerabilities for them (see SECURITY-FIXES.md (library repository)).
Nothing from the build or test tool chain is shipped in dist/.
Reporting a vulnerability
Please report suspected vulnerabilities privately to West Coast Software rather than in a public issue.
WestCoast Charts 1.1.5 · generated from the library guide at commit 95f093ba. Code examples in the guide are executed by the library's docs check.