Skip to content

Scatter Plot

Correlation

Does more of X really move Y, or are you chasing a coincidence? Plot your points and the trend, the clusters, and the outliers all surface at a glance, with bubble size carrying a third variable for free. The Pearson correlation comes back in getContext(), so you can quote the number instead of squinting at the cloud.

Example
canvas · responsive
Go deeper: Insights guide·DevTools guide

The chart above is the same engine in every framework - only the integration code below differs.

When to reach for it

  • Testing a hypothesis. Does spend move conversion? Does tenure move churn? The cloud, the trend and the outliers answer at a glance, and getContext() hands you the Pearson r to quote in the write-up.
  • Finding segments before the average hides them. Clusters and outliers jump out of a scatter long before they surface in a summary table - the analyst's first look at any new dataset.
  • If one axis is time, use a Line chart - a scatter treats time as just another number and loses the reading order your audience expects.

Heavy data on WebGPU Experimental

ScatterChart has an opt-in renderer="webgpu" that paints the point cloud as GPU-instanced circles while axes, labels and tooltips stay on the SVG layer. It is capability-gated: on a browser without WebGPU it downgrades to canvas automatically, and getContext().renderer reports whichever actually painted.

The demo below is a nod to particle physics: 50,000 simulated dimuon events over a falling continuum background. The sharp vertical bands are the J/ψ, ψ(2S) and Υ(1S/2S/3S) resonances, the same structure an LHC dimuon spectrum shows, and exactly the kind of point cloud a GPU renderer exists for.

⚗️ Experimental - not yet stable. WebGPU rendering is an opt-in preview. It needs a WebGPU-capable browser (Chrome / Edge, or Safari 26+); everywhere else it falls back to canvas automatically. Axes, labels and tooltips stay on the SVG layer - only the data marks are painted on the GPU.
Heavy-data demo · 50,000 simulated dimuon events… detecting

Play through the years

The Gapminder move: tag each point with a date, turn on timeline, and watch the scatter drift year by year with the built-in play button and scrubber. Off by default - nothing changes until a chart opts in.

Press the play button under the chart: it steps through the years, one snapshot at a time. Drag the scrubber to jump to any year.

tsx
const ref = useRef<ScatterChartHandle>(null);

<ScatterChart ref={ref} {...props} timeline={{ speedMs: 1000, loop: true }} />;
// ref.current?.timeline() -> play() / pause() / seek(year) / stepForward()
vue
<ScatterChart :options="{ ...props, timeline: { speedMs: 1000, loop: true } }" />
svelte
<div use:scatterChart={{ ...props, timeline: { speedMs: 1000, loop: true } }}></div>
ts
applyScatterChartProps(this.c.nativeElement, { ...props, timeline: { speedMs: 1000, loop: true } });
html
<michi-vz-scatter-chart id="c"></michi-vz-scatter-chart>
<script>
  const el = document.getElementById("c");
  el.timeline = { speedMs: 1000, loop: true };
  // el.getTimeline() -> play() / pause() / seek(year)
</script>
  • speedMs sets the pace, loop wraps around, autoplay: true starts on mount, showControl: false hides the built-in bar.
  • Values glide between periods by default (interpolate); tune the motion with tweenMs and easing, or set interpolate: false for hard cuts. Reduced motion always gets the hard cut.
  • The headless controller is always available: chart.timeline() exposes play() / pause() / toggle() / seek(period) / stepForward() / stepBack(), plus onStep and formatPeriod in the config for custom UI.
  • Pair it with pointLabels so every bubble stays named while it moves; a filter still applies inside each period.
  • Points without a date stay visible in every period.

Usage

tsx
import { ScatterChart } from "@michi-vz/react";

export default () => <ScatterChart {...props} />; // props = the chart options
vue
<script setup>
import { ScatterChart } from "@michi-vz/vue";
</script>

<template>
  <ScatterChart :options="props" />
</template>
svelte
<script>
  import { scatterChart } from "@michi-vz/svelte";
</script>

<div use:scatterChart={props}></div>
ts
// main.ts - register the elements once
import "@michi-vz/angular";
import { applyScatterChartProps } from "@michi-vz/angular";

// component (uses CUSTOM_ELEMENTS_SCHEMA)
// template: <michi-vz-scatter-chart #c></michi-vz-scatter-chart>
applyScatterChartProps(this.c.nativeElement, props);
html
<script type="module" src="https://cdn.jsdelivr.net/npm/@michi-vz/wc/dist/michi-vz-wc.bundle.js"></script>

<michi-vz-scatter-chart id="c"></michi-vz-scatter-chart>
<script>
  Object.assign(document.getElementById("c"), props); // dataSet/series, title, …
</script>
ts
import { mountScatterChart } from "@michi-vz/core";

const chart = mountScatterChart(el, props);
chart.update(next);
chart.getContext(); // renderer-agnostic, LLM-ready
chart.destroy();

API

Props are typed as ScatterChartProps in @michi-vz/core. Shared across all charts: width, height, margin, colors / colorsMapping, renderer ("svg", "canvas", or experimental "webgpu"), highlightItems, disabledItems, and the on* callbacks. onChartDataProcessed / getContext() return the renderer-agnostic ChartContext.