GitHub repository

ScatterPlot

Two-axis scatter with optional quadrants and bubble sizing.

A token-driven scatter plot for comparing entities on two metrics at once. Each datum is a dot positioned by `x`/`y`, optionally sized by `size` (sqrt-scaled so area is true) and tinted by `color`. Pass `xRef`/`yRef` to split the plane into quadrants and `quadrantLabels` to caption them — ideal for "high score vs low wins"-style comparisons. Axes use the padded data extent (not forced to zero), every point is keyboard-focusable with an accessible tooltip, and empty input shows an em-dash placeholder.

Demo

live · storybook Open in Storybook

Demo loads from https://dssoca-storybook.vercel.app/. Run pnpm storybook for it to render locally.

Usage

svelte
<script>
  import { ScatterPlot } from 'dssoca';

  const points = [
    { label: 'Ana', x: 8.9, y: 7.5, size: 27 },
    { label: 'Bruno', x: 8.5, y: 5.7, size: 14 },
    { label: 'Caio', x: 7.7, y: 4.5, size: 7 },
  ];
</script>

<ScatterPlot
  {points}
  xRef={8}
  yRef={5}
  xLabel="Consistency"
  yLabel="Power rating"
  quadrantLabels={{ tr: 'steady star', br: 'spiky' }}
/>

HTML

CSS only — static markup The exact markup the component renders, generated at build time. Use it with dssoca/vanilla.css — see Plain HTML & CSS.

html
<div class="ss-scatter" role="group" aria-label="Scatter plot with 3 points (api, db, cache).">
  <div class="plot">
    <svg class="canvas" viewBox="0 0 420 320" width="420" height="320" preserveAspectRatio="xMidYMid meet" role="presentation">
      <g transform="translate(48,12)">
        <line class="grid" x1="0" x2="356" y1="272" y2="272">
      </line>
      <text class="tick y" x="-6" y="272" dy="0.32em" text-anchor="end">90</text>
      <line class="grid" x1="0" x2="356" y1="217.60000000000002" y2="217.60000000000002">
    </line>
    <text class="tick y" x="-6" y="217.60000000000002" dy="0.32em" text-anchor="end">92</text>
    <line class="grid" x1="0" x2="356" y1="163.2" y2="163.2">
  </line>
  <text class="tick y" x="-6" y="163.2" dy="0.32em" text-anchor="end">94</text>
  <line class="grid" x1="0" x2="356" y1="108.80000000000001" y2="108.80000000000001">
</line>
<text class="tick y" x="-6" y="108.80000000000001" dy="0.32em" text-anchor="end">96</text>
<line class="grid" x1="0" x2="356" y1="54.39999999999999" y2="54.39999999999999">
</line>
<text class="tick y" x="-6" y="54.39999999999999" dy="0.32em" text-anchor="end">98</text>
<line class="grid" x1="0" x2="356" y1="0" y2="0">
</line>
<text class="tick y" x="-6" y="0" dy="0.32em" text-anchor="end">100</text>
<text class="tick x" x="0" y="288" text-anchor="middle">0</text>
<text class="tick x" x="79.1111111111111" y="288" text-anchor="middle">10</text>
<text class="tick x" x="158.2222222222222" y="288" text-anchor="middle">20</text>
<text class="tick x" x="237.33333333333331" y="288" text-anchor="middle">30</text>
<text class="tick x" x="316.4444444444444" y="288" text-anchor="middle">40</text>
<line class="axis" x1="0" x2="0" y1="0" y2="272">
</line>
<line class="axis" x1="0" x2="356" y1="272" y2="272">
</line>
<circle class="point" cx="94.93333333333334" cy="54.39999999999999" r="5" style="fill:var(--ss-accent)" tabindex="0" role="button" aria-label="api: 12, 98">
</circle>
<text class="point-label" x="103.93333333333334" y="54.39999999999999" dy="0.32em">api</text>
<circle class="point" cx="316.4444444444444" cy="244.8" r="5" style="fill:var(--ss-blue)" tabindex="0" role="button" aria-label="db: 40, 91">
</circle>
<text class="point-label" x="325.4444444444444" y="244.8" dy="0.32em">db</text>
<circle class="point" cx="23.733333333333334" cy="27.199999999999996" r="5" style="fill:var(--ss-magenta)" tabindex="0" role="button" aria-label="cache: 3, 99">
</circle>
<text class="point-label" x="32.733333333333334" y="27.199999999999996" dy="0.32em">cache</text>
<text class="axis-label x" x="178" y="304" text-anchor="middle">latency ms</text>
<text class="axis-label y" transform="translate(-36,136) rotate(-90)" text-anchor="middle">uptime %</text>
</g>
</svg>
</div>
</div>

Rendered geometry: copy as-is. Hover tooltips are not wired by vanilla.js.

Props

PropTypeDefaultDescription
pointsScatterPoint[]—Required. Each point has a `label`, `x`, `y`, and optional `size` (bubble area) and `color`.
xLabelstring—X axis caption drawn under the axis.
yLabelstring—Y axis caption drawn rotated beside the axis.
xRefnumber—Dashed vertical reference line at this x (e.g. the median) — splits into quadrants.
yRefnumber—Dashed horizontal reference line at this y — splits into quadrants.
quadrantLabels{ tl?; tr?; bl?; br? }—Corner captions for the four quadrants (reads best with xRef + yRef).
xDomain[number, number]—Force the x domain; defaults to the padded data extent.
yDomain[number, number]—Force the y domain; defaults to the padded data extent.
xFormat(x: number) => string—Format an x value for ticks + tooltip.
yFormat(y: number) => string—Format a y value for ticks + tooltip.
showLabelsbooleantrueDraw each point's label next to its dot.
heightnumber320Intrinsic drawing height in px.
widthnumber420Intrinsic drawing width in px.
fluidbooleanfalseStretch to the container width via the viewBox.
tooltipbooleantrueReveal a tooltip on hover/focus of a point. Each point is keyboard-focusable.
summarystring—Screen-reader summary + accessible name; auto-generated when absent.
size'sm' | 'md' | 'lg'—Per-instance size override; inherits the ancestor `data-size-variant` when unset.
↑↓ navigate · ↵ open · esc close

Keyboard shortcuts

No shortcuts registered

Shortcuts added through the dssoca registry will show up here.

Single-key shortcuts