Performance and security

Large-data settings, measured numbers, bundle sizes, XSS, CSP, CSV, dependencies.

View .md Agent setup

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, default true): 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 entry west-coast-charts/types/decimate (also part of west-coast-charts/types and full). Without it the core draws every point and logs one hint.
  • Dense marks (wcc.performance.denseMarks, default 'auto'): from denseThreshold points 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.
js
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-canvas draws 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, annotation customSVG) runs through a built-in allow-list sanitizer.
  • chart.sanitizeHtml: true (default), a function (html, { svg }) => string | Node to use your own sanitizer, or false for 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:
js
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()):

text
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-charts

Send 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).

js
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, null entries, data: null, xaxis.categories: null, annotations: null, chart: null, toolbar icons without click, …) 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.