Charts

Scatter plot

Points on two numeric axes, in up to four groups, with optional bubble size and a readout per point.

Built on
<svg>vector-effectcontainer queries@starting-stylecolor-mix()
Client JavaScript
None
Works without React
Yes, as HTML and CSS
Source, gzipped
4.4 kB
Built from
Chart legend
  1. BuildsFiles changed 120Minutes 2.1
  2. BuildsFiles changed 180Minutes 2.6
  3. BuildsFiles changed 240Minutes 3.4
  4. BuildsFiles changed 310Minutes 3.9
  5. BuildsFiles changed 360Minutes 4.8
  6. BuildsFiles changed 420Minutes 5.1
  7. BuildsFiles changed 470Minutes 6.2
  8. BuildsFiles changed 530Minutes 6.6
  9. BuildsFiles changed 590Minutes 7.9
Try its options

Loading its options.

Code
import { ScatterPlot } from "@/components/equip/scatter-plot";

const builds = [
  { x: 120, y: 2.1 },
  { x: 180, y: 2.6 },
  { x: 240, y: 3.4 },
  { x: 310, y: 3.9 },
  { x: 360, y: 4.8 },
  { x: 420, y: 5.1 },
  { x: 470, y: 6.2 },
  { x: 530, y: 6.6 },
  { x: 590, y: 7.9 },
];

export default function Demo() {
  return (
    <ScatterPlot
      label="Build time against changed files"
      series={[{ label: "Builds", points: builds }]}
      xLabel="Files changed"
      yLabel="Minutes"
      height={52}
      ticks={2}
      style={{ width: 216 }}
    />
  );
}
Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<figure aria-label="Build time against changed files: 9 points, Files changed from 120 to 590, Minutes from 2.1 to 7.9" class="eq-scatter-plot" style="width:216px;--_h:52px">
  <div class="eq-scatter-plot-body">
    <span class="eq-scatter-plot-ytitle" aria-hidden="true">Minutes</span>
    <div class="eq-scatter-plot-y" aria-hidden="true">
      <span class="eq-scatter-plot-ytick" style="--_y:100%">0</span>
      <span class="eq-scatter-plot-ytick" style="--_y:50%">5</span>
      <span class="eq-scatter-plot-ytick" style="--_y:0%">10</span>
    </div>
    <div class="eq-scatter-plot-plot">
      <svg class="eq-scatter-plot-grid" aria-hidden="true" viewBox="0 0 100 100" preserveAspectRatio="none">
        <line class="eq-scatter-plot-gridline" x1="0" x2="100" y1="100" y2="100" />
        <line class="eq-scatter-plot-gridline" x1="0" x2="100" y1="50" y2="50" />
        <line class="eq-scatter-plot-gridline" x1="0" x2="100" y1="0" y2="0" />
        <line class="eq-scatter-plot-gridline" x1="0" x2="0" y1="0" y2="100" />
        <line class="eq-scatter-plot-gridline" x1="33.33" x2="33.33" y1="0" y2="100" />
        <line class="eq-scatter-plot-gridline" x1="66.67" x2="66.67" y1="0" y2="100" />
        <line class="eq-scatter-plot-gridline" x1="100" x2="100" y1="0" y2="100" />
        <line class="eq-scatter-plot-gridline" data-base="" x1="0" x2="100" y1="100" y2="100" />
        <line class="eq-scatter-plot-gridline" data-base="" x1="0" x2="0" y1="0" y2="100" />
      </svg>
      <ol class="eq-scatter-plot-points" role="list" aria-label="Build time against changed files">
        <li class="eq-scatter-plot-point" tabindex="0" data-series="0" data-side="start" data-half="bottom" style="--_x:16%;--_y:79%;--_d:9px">
          <svg class="eq-scatter-plot-mark" aria-hidden="true" viewBox="0 0 10 10">
            <circle cx="5" cy="5" r="4.25" />
          </svg>
          <span class="eq-scatter-plot-tip">
            <span class="eq-scatter-plot-tip-label">Builds</span>
            <span class="eq-scatter-plot-tip-row"><span>Files changed</span> <b>120</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Minutes</span> <b>2.1</b></span>
          </span>
        </li>
        <li class="eq-scatter-plot-point" tabindex="0" data-series="0" data-side="start" data-half="bottom" style="--_x:24%;--_y:74%;--_d:9px">
          <svg class="eq-scatter-plot-mark" aria-hidden="true" viewBox="0 0 10 10">
            <circle cx="5" cy="5" r="4.25" />
          </svg>
          <span class="eq-scatter-plot-tip">
            <span class="eq-scatter-plot-tip-label">Builds</span>
            <span class="eq-scatter-plot-tip-row"><span>Files changed</span> <b>180</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Minutes</span> <b>2.6</b></span>
          </span>
        </li>
        <li class="eq-scatter-plot-point" tabindex="0" data-series="0" data-side="start" data-half="bottom" style="--_x:32%;--_y:66%;--_d:9px">
          <svg class="eq-scatter-plot-mark" aria-hidden="true" viewBox="0 0 10 10">
            <circle cx="5" cy="5" r="4.25" />
          </svg>
          <span class="eq-scatter-plot-tip">
            <span class="eq-scatter-plot-tip-label">Builds</span>
            <span class="eq-scatter-plot-tip-row"><span>Files changed</span> <b>240</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Minutes</span> <b>3.4</b></span>
          </span>
        </li>
        <li class="eq-scatter-plot-point" tabindex="0" data-series="0" data-side="start" data-half="bottom" style="--_x:41.33%;--_y:61%;--_d:9px">
          <svg class="eq-scatter-plot-mark" aria-hidden="true" viewBox="0 0 10 10">
            <circle cx="5" cy="5" r="4.25" />
          </svg>
          <span class="eq-scatter-plot-tip">
            <span class="eq-scatter-plot-tip-label">Builds</span>
            <span class="eq-scatter-plot-tip-row"><span>Files changed</span> <b>310</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Minutes</span> <b>3.9</b></span>
          </span>
        </li>
        <li class="eq-scatter-plot-point" tabindex="0" data-series="0" data-side="start" data-half="bottom" style="--_x:48%;--_y:52%;--_d:9px">
          <svg class="eq-scatter-plot-mark" aria-hidden="true" viewBox="0 0 10 10">
            <circle cx="5" cy="5" r="4.25" />
          </svg>
          <span class="eq-scatter-plot-tip">
            <span class="eq-scatter-plot-tip-label">Builds</span>
            <span class="eq-scatter-plot-tip-row"><span>Files changed</span> <b>360</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Minutes</span> <b>4.8</b></span>
          </span>
        </li>
        <li class="eq-scatter-plot-point" tabindex="0" data-series="0" data-side="end" data-half="top" style="--_x:56%;--_y:49%;--_d:9px">
          <svg class="eq-scatter-plot-mark" aria-hidden="true" viewBox="0 0 10 10">
            <circle cx="5" cy="5" r="4.25" />
          </svg>
          <span class="eq-scatter-plot-tip">
            <span class="eq-scatter-plot-tip-label">Builds</span>
            <span class="eq-scatter-plot-tip-row"><span>Files changed</span> <b>420</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Minutes</span> <b>5.1</b></span>
          </span>
        </li>
        <li class="eq-scatter-plot-point" tabindex="0" data-series="0" data-side="end" data-half="top" style="--_x:62.67%;--_y:38%;--_d:9px">
          <svg class="eq-scatter-plot-mark" aria-hidden="true" viewBox="0 0 10 10">
            <circle cx="5" cy="5" r="4.25" />
          </svg>
          <span class="eq-scatter-plot-tip">
            <span class="eq-scatter-plot-tip-label">Builds</span>
            <span class="eq-scatter-plot-tip-row"><span>Files changed</span> <b>470</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Minutes</span> <b>6.2</b></span>
          </span>
        </li>
        <li class="eq-scatter-plot-point" tabindex="0" data-series="0" data-side="end" data-half="top" style="--_x:70.67%;--_y:34%;--_d:9px">
          <svg class="eq-scatter-plot-mark" aria-hidden="true" viewBox="0 0 10 10">
            <circle cx="5" cy="5" r="4.25" />
          </svg>
          <span class="eq-scatter-plot-tip">
            <span class="eq-scatter-plot-tip-label">Builds</span>
            <span class="eq-scatter-plot-tip-row"><span>Files changed</span> <b>530</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Minutes</span> <b>6.6</b></span>
          </span>
        </li>
        <li class="eq-scatter-plot-point" tabindex="0" data-series="0" data-side="end" data-half="top" style="--_x:78.67%;--_y:21%;--_d:9px">
          <svg class="eq-scatter-plot-mark" aria-hidden="true" viewBox="0 0 10 10">
            <circle cx="5" cy="5" r="4.25" />
          </svg>
          <span class="eq-scatter-plot-tip">
            <span class="eq-scatter-plot-tip-label">Builds</span>
            <span class="eq-scatter-plot-tip-row"><span>Files changed</span> <b>590</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Minutes</span> <b>7.9</b></span>
          </span>
        </li>
      </ol>
    </div>
    <div class="eq-scatter-plot-x" aria-hidden="true"><span class="eq-scatter-plot-xtick" data-edge="start" style="--_x:0%">0</span><span class="eq-scatter-plot-xtick" data-minor="true" style="--_x:33.33%">250</span><span class="eq-scatter-plot-xtick" data-minor="true" style="--_x:66.67%">500</span><span class="eq-scatter-plot-xtick" data-edge="end" style="--_x:100%">750</span></div>
    <span class="eq-scatter-plot-xtitle" aria-hidden="true">Files changed</span>
  </div>
</figure>

Examples

Groups by plan

Each group has its own shape as well as its own strength, and a named point reads its name, group and both values.

  1. LarderTeamSeats 8Weekly active 81%
  2. Halcyon LabsTeamSeats 14Weekly active 72%
  3. BrightlineTeamSeats 22Weekly active 64%
  4. FernwayTeamSeats 11Weekly active 58%
  5. Atlas FreightBusinessSeats 46Weekly active 69%
  6. Kestrel HealthBusinessSeats 72Weekly active 61%
  7. Orchard RetailBusinessSeats 58Weekly active 47%
  8. Meridian BankEnterpriseSeats 180Weekly active 52%
  9. Polar LogisticsEnterpriseSeats 240Weekly active 38%
Code
import { ScatterPlot } from "@/components/equip/scatter-plot";

export default function Example() {
  return (
    <ScatterPlot
      label="Accounts by seats and weekly active share"
      xLabel="Seats"
      yLabel="Weekly active"
      yFormat={{ style: "percent" }}
      series={[
        {
          label: "Team",
          points: [
            { x: 8, y: 0.81, label: "Larder" },
            { x: 14, y: 0.72, label: "Halcyon Labs" },
            { x: 22, y: 0.64, label: "Brightline" },
            { x: 11, y: 0.58, label: "Fernway" },
          ],
        },
        {
          label: "Business",
          points: [
            { x: 46, y: 0.69, label: "Atlas Freight" },
            { x: 72, y: 0.61, label: "Kestrel Health" },
            { x: 58, y: 0.47, label: "Orchard Retail" },
          ],
        },
        {
          label: "Enterprise",
          points: [
            { x: 180, y: 0.52, label: "Meridian Bank" },
            { x: 240, y: 0.38, label: "Polar Logistics" },
          ],
        },
      ]}
      style={{ maxWidth: 520 }}
    />
  );
}
HTMLworks in any framework, with the part's CSS
<figure aria-label="Accounts by seats and weekly active share: 9 points, Seats from 8 to 240, Weekly active from 38% to 81%" class="eq-scatter-plot" style="max-width:520px;--_h:180px">
  <ul role="list" aria-hidden="true" class="eq-chart-legend eq-scatter-plot-legend" data-orientation="row">
    <li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="point" aria-hidden="true" width="10" height="10" viewBox="0 0 10 10"><circle cx="5" cy="5" r="3.75" /></svg><span class="eq-chart-legend-name">Team</span></li>
    <li class="eq-chart-legend-item" data-series="1"><svg class="eq-chart-legend-mark" data-mark="point" aria-hidden="true" width="10" height="10" viewBox="0 0 10 10"><rect x="1.5" y="1.5" width="7" height="7" /></svg><span class="eq-chart-legend-name">Business</span></li>
    <li class="eq-chart-legend-item" data-series="2"><svg class="eq-chart-legend-mark" data-mark="point" aria-hidden="true" width="10" height="10" viewBox="0 0 10 10"><path d="M5 0.75 9.25 5 5 9.25 0.75 5Z" /></svg><span class="eq-chart-legend-name">Enterprise</span></li>
  </ul>
  <div class="eq-scatter-plot-body">
    <span class="eq-scatter-plot-ytitle" aria-hidden="true">Weekly active</span>
    <div class="eq-scatter-plot-y" aria-hidden="true">
      <span class="eq-scatter-plot-ytick" style="--_y:100%">20%</span>
      <span class="eq-scatter-plot-ytick" style="--_y:75%">40%</span>
      <span class="eq-scatter-plot-ytick" style="--_y:50%">60%</span>
      <span class="eq-scatter-plot-ytick" style="--_y:25%">80%</span>
      <span class="eq-scatter-plot-ytick" style="--_y:0%">100%</span>
    </div>
    <div class="eq-scatter-plot-plot">
      <svg class="eq-scatter-plot-grid" aria-hidden="true" viewBox="0 0 100 100" preserveAspectRatio="none">
        <line class="eq-scatter-plot-gridline" x1="0" x2="100" y1="100" y2="100" />
        <line class="eq-scatter-plot-gridline" x1="0" x2="100" y1="75" y2="75" />
        <line class="eq-scatter-plot-gridline" x1="0" x2="100" y1="50" y2="50" />
        <line class="eq-scatter-plot-gridline" x1="0" x2="100" y1="25" y2="25" />
        <line class="eq-scatter-plot-gridline" x1="0" x2="100" y1="0" y2="0" />
        <line class="eq-scatter-plot-gridline" x1="0" x2="0" y1="0" y2="100" />
        <line class="eq-scatter-plot-gridline" x1="33.33" x2="33.33" y1="0" y2="100" />
        <line class="eq-scatter-plot-gridline" x1="66.67" x2="66.67" y1="0" y2="100" />
        <line class="eq-scatter-plot-gridline" x1="100" x2="100" y1="0" y2="100" />
        <line class="eq-scatter-plot-gridline" data-base="" x1="0" x2="100" y1="100" y2="100" />
        <line class="eq-scatter-plot-gridline" data-base="" x1="0" x2="0" y1="0" y2="100" />
      </svg>
      <ol class="eq-scatter-plot-points" role="list" aria-label="Accounts by seats and weekly active share">
        <li class="eq-scatter-plot-point" tabindex="0" data-series="0" data-side="start" data-half="top" style="--_x:2.67%;--_y:23.75%;--_d:9px">
          <svg class="eq-scatter-plot-mark" aria-hidden="true" viewBox="0 0 10 10">
            <circle cx="5" cy="5" r="4.25" />
          </svg>
          <span class="eq-scatter-plot-tip">
            <span class="eq-scatter-plot-tip-label">Larder</span>
            <span class="eq-scatter-plot-tip-row">Team</span>
            <span class="eq-scatter-plot-tip-row"><span>Seats</span> <b>8</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Weekly active</span> <b>81%</b></span>
          </span>
        </li>
        <li class="eq-scatter-plot-point" tabindex="0" data-series="0" data-side="start" data-half="top" style="--_x:4.67%;--_y:35%;--_d:9px">
          <svg class="eq-scatter-plot-mark" aria-hidden="true" viewBox="0 0 10 10">
            <circle cx="5" cy="5" r="4.25" />
          </svg>
          <span class="eq-scatter-plot-tip">
            <span class="eq-scatter-plot-tip-label">Halcyon Labs</span>
            <span class="eq-scatter-plot-tip-row">Team</span>
            <span class="eq-scatter-plot-tip-row"><span>Seats</span> <b>14</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Weekly active</span> <b>72%</b></span>
          </span>
        </li>
        <li class="eq-scatter-plot-point" tabindex="0" data-series="0" data-side="start" data-half="top" style="--_x:7.33%;--_y:45%;--_d:9px">
          <svg class="eq-scatter-plot-mark" aria-hidden="true" viewBox="0 0 10 10">
            <circle cx="5" cy="5" r="4.25" />
          </svg>
          <span class="eq-scatter-plot-tip">
            <span class="eq-scatter-plot-tip-label">Brightline</span>
            <span class="eq-scatter-plot-tip-row">Team</span>
            <span class="eq-scatter-plot-tip-row"><span>Seats</span> <b>22</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Weekly active</span> <b>64%</b></span>
          </span>
        </li>
        <li class="eq-scatter-plot-point" tabindex="0" data-series="0" data-side="start" data-half="bottom" style="--_x:3.67%;--_y:52.5%;--_d:9px">
          <svg class="eq-scatter-plot-mark" aria-hidden="true" viewBox="0 0 10 10">
            <circle cx="5" cy="5" r="4.25" />
          </svg>
          <span class="eq-scatter-plot-tip">
            <span class="eq-scatter-plot-tip-label">Fernway</span>
            <span class="eq-scatter-plot-tip-row">Team</span>
            <span class="eq-scatter-plot-tip-row"><span>Seats</span> <b>11</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Weekly active</span> <b>58%</b></span>
          </span>
        </li>
        <li class="eq-scatter-plot-point" tabindex="0" data-series="1" data-side="start" data-half="top" style="--_x:15.33%;--_y:38.75%;--_d:9px">
          <svg class="eq-scatter-plot-mark" aria-hidden="true" viewBox="0 0 10 10">
            <rect x="1" y="1" width="8" height="8" />
          </svg>
          <span class="eq-scatter-plot-tip">
            <span class="eq-scatter-plot-tip-label">Atlas Freight</span>
            <span class="eq-scatter-plot-tip-row">Business</span>
            <span class="eq-scatter-plot-tip-row"><span>Seats</span> <b>46</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Weekly active</span> <b>69%</b></span>
          </span>
        </li>
        <li class="eq-scatter-plot-point" tabindex="0" data-series="1" data-side="start" data-half="top" style="--_x:24%;--_y:48.75%;--_d:9px">
          <svg class="eq-scatter-plot-mark" aria-hidden="true" viewBox="0 0 10 10">
            <rect x="1" y="1" width="8" height="8" />
          </svg>
          <span class="eq-scatter-plot-tip">
            <span class="eq-scatter-plot-tip-label">Kestrel Health</span>
            <span class="eq-scatter-plot-tip-row">Business</span>
            <span class="eq-scatter-plot-tip-row"><span>Seats</span> <b>72</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Weekly active</span> <b>61%</b></span>
          </span>
        </li>
        <li class="eq-scatter-plot-point" tabindex="0" data-series="1" data-side="start" data-half="bottom" style="--_x:19.33%;--_y:66.25%;--_d:9px">
          <svg class="eq-scatter-plot-mark" aria-hidden="true" viewBox="0 0 10 10">
            <rect x="1" y="1" width="8" height="8" />
          </svg>
          <span class="eq-scatter-plot-tip">
            <span class="eq-scatter-plot-tip-label">Orchard Retail</span>
            <span class="eq-scatter-plot-tip-row">Business</span>
            <span class="eq-scatter-plot-tip-row"><span>Seats</span> <b>58</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Weekly active</span> <b>47%</b></span>
          </span>
        </li>
        <li class="eq-scatter-plot-point" tabindex="0" data-series="2" data-side="end" data-half="bottom" style="--_x:60%;--_y:60%;--_d:9px">
          <svg class="eq-scatter-plot-mark" aria-hidden="true" viewBox="0 0 10 10">
            <path d="M5 0.5 9.5 5 5 9.5 0.5 5Z" />
          </svg>
          <span class="eq-scatter-plot-tip">
            <span class="eq-scatter-plot-tip-label">Meridian Bank</span>
            <span class="eq-scatter-plot-tip-row">Enterprise</span>
            <span class="eq-scatter-plot-tip-row"><span>Seats</span> <b>180</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Weekly active</span> <b>52%</b></span>
          </span>
        </li>
        <li class="eq-scatter-plot-point" tabindex="0" data-series="2" data-side="end" data-half="bottom" style="--_x:80%;--_y:77.5%;--_d:9px">
          <svg class="eq-scatter-plot-mark" aria-hidden="true" viewBox="0 0 10 10">
            <path d="M5 0.5 9.5 5 5 9.5 0.5 5Z" />
          </svg>
          <span class="eq-scatter-plot-tip">
            <span class="eq-scatter-plot-tip-label">Polar Logistics</span>
            <span class="eq-scatter-plot-tip-row">Enterprise</span>
            <span class="eq-scatter-plot-tip-row"><span>Seats</span> <b>240</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Weekly active</span> <b>38%</b></span>
          </span>
        </li>
      </ol>
    </div>
    <div class="eq-scatter-plot-x" aria-hidden="true"><span class="eq-scatter-plot-xtick" data-edge="start" style="--_x:0%">0</span><span class="eq-scatter-plot-xtick" data-minor="true" style="--_x:33.33%">100</span><span class="eq-scatter-plot-xtick" data-minor="true" style="--_x:66.67%">200</span><span class="eq-scatter-plot-xtick" data-edge="end" style="--_x:100%">300</span></div>
    <span class="eq-scatter-plot-xtitle" aria-hidden="true">Seats</span>
  </div>
</figure>

Bubbles

Give points a size and they become bubbles whose area, not width, follows the value; the readout names what size means.

  1. United StatesMonthly traffic growth 4.1%Conversion 3.2%Revenue $412K
  2. GermanyMonthly traffic growth 3.4%Conversion 4.1%Revenue $128K
  3. JapanMonthly traffic growth 2.2%Conversion 2.7%Revenue $96K
  4. BrazilMonthly traffic growth 5.6%Conversion 1.9%Revenue $74K
  5. IndiaMonthly traffic growth 7.8%Conversion 1.4%Revenue $151K
  6. AustraliaMonthly traffic growth 3.1%Conversion 4.5%Revenue $38K
Code
import { ScatterPlot } from "@/components/equip/scatter-plot";

const markets = [
  { label: "United States", x: 4.1, y: 0.032, size: 412000 },
  { label: "Germany", x: 3.4, y: 0.041, size: 128000 },
  { label: "Japan", x: 2.2, y: 0.027, size: 96000 },
  { label: "Brazil", x: 5.6, y: 0.019, size: 74000 },
  { label: "India", x: 7.8, y: 0.014, size: 151000 },
  { label: "Australia", x: 3.1, y: 0.045, size: 38000 },
];

export default function Example() {
  return (
    <ScatterPlot
      label="Markets by traffic growth, conversion and revenue"
      series={[{ label: "Markets", points: markets }]}
      xLabel="Monthly traffic growth"
      yLabel="Conversion"
      sizeLabel="Revenue"
      xFormat={{ style: "unit", unit: "percent", maximumFractionDigits: 1 }}
      yFormat={{ style: "percent", maximumFractionDigits: 1 }}
      sizeFormat={{ style: "currency", currency: "USD", notation: "compact" }}
      xMin={0}
      yMin={0}
      height={200}
      style={{ maxWidth: 520 }}
    />
  );
}
HTMLworks in any framework, with the part's CSS
<figure aria-label="Markets by traffic growth, conversion and revenue: 6 points, Monthly traffic growth from 2.2% to 7.8%, Conversion from 1.4% to 4.5%" class="eq-scatter-plot" style="max-width:520px;--_h:200px">
  <div class="eq-scatter-plot-body">
    <span class="eq-scatter-plot-ytitle" aria-hidden="true">Conversion</span>
    <div class="eq-scatter-plot-y" aria-hidden="true">
      <span class="eq-scatter-plot-ytick" style="--_y:100%">0%</span>
      <span class="eq-scatter-plot-ytick" style="--_y:66.67%">2%</span>
      <span class="eq-scatter-plot-ytick" style="--_y:33.33%">4%</span>
      <span class="eq-scatter-plot-ytick" style="--_y:0%">6%</span>
    </div>
    <div class="eq-scatter-plot-plot">
      <svg class="eq-scatter-plot-grid" aria-hidden="true" viewBox="0 0 100 100" preserveAspectRatio="none">
        <line class="eq-scatter-plot-gridline" x1="0" x2="100" y1="100" y2="100" />
        <line class="eq-scatter-plot-gridline" x1="0" x2="100" y1="66.67" y2="66.67" />
        <line class="eq-scatter-plot-gridline" x1="0" x2="100" y1="33.33" y2="33.33" />
        <line class="eq-scatter-plot-gridline" x1="0" x2="100" y1="0" y2="0" />
        <line class="eq-scatter-plot-gridline" x1="0" x2="0" y1="0" y2="100" />
        <line class="eq-scatter-plot-gridline" x1="25" x2="25" y1="0" y2="100" />
        <line class="eq-scatter-plot-gridline" x1="50" x2="50" y1="0" y2="100" />
        <line class="eq-scatter-plot-gridline" x1="75" x2="75" y1="0" y2="100" />
        <line class="eq-scatter-plot-gridline" x1="100" x2="100" y1="0" y2="100" />
        <line class="eq-scatter-plot-gridline" data-base="" x1="0" x2="100" y1="100" y2="100" />
        <line class="eq-scatter-plot-gridline" data-base="" x1="0" x2="0" y1="0" y2="100" />
      </svg>
      <ol class="eq-scatter-plot-points" role="list" aria-label="Markets by traffic growth, conversion and revenue">
        <li class="eq-scatter-plot-point" tabindex="0" data-series="0" data-side="end" data-half="top" style="--_x:51.25%;--_y:46.67%;--_d:28px">
          <svg class="eq-scatter-plot-mark" aria-hidden="true" viewBox="0 0 10 10">
            <circle cx="5" cy="5" r="4.25" />
          </svg>
          <span class="eq-scatter-plot-tip">
            <span class="eq-scatter-plot-tip-label">United States</span>
            <span class="eq-scatter-plot-tip-row"><span>Monthly traffic growth</span> <b>4.1%</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Conversion</span> <b>3.2%</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Revenue</span> <b>$412K</b></span>
          </span>
        </li>
        <li class="eq-scatter-plot-point" tabindex="0" data-series="0" data-side="start" data-half="top" style="--_x:42.5%;--_y:31.67%;--_d:18.26px">
          <svg class="eq-scatter-plot-mark" aria-hidden="true" viewBox="0 0 10 10">
            <circle cx="5" cy="5" r="4.25" />
          </svg>
          <span class="eq-scatter-plot-tip">
            <span class="eq-scatter-plot-tip-label">Germany</span>
            <span class="eq-scatter-plot-tip-row"><span>Monthly traffic growth</span> <b>3.4%</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Conversion</span> <b>4.1%</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Revenue</span> <b>$128K</b></span>
          </span>
        </li>
        <li class="eq-scatter-plot-point" tabindex="0" data-series="0" data-side="start" data-half="bottom" style="--_x:27.5%;--_y:55%;--_d:16.62px">
          <svg class="eq-scatter-plot-mark" aria-hidden="true" viewBox="0 0 10 10">
            <circle cx="5" cy="5" r="4.25" />
          </svg>
          <span class="eq-scatter-plot-tip">
            <span class="eq-scatter-plot-tip-label">Japan</span>
            <span class="eq-scatter-plot-tip-row"><span>Monthly traffic growth</span> <b>2.2%</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Conversion</span> <b>2.7%</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Revenue</span> <b>$96K</b></span>
          </span>
        </li>
        <li class="eq-scatter-plot-point" tabindex="0" data-series="0" data-side="end" data-half="bottom" style="--_x:70%;--_y:68.33%;--_d:15.32px">
          <svg class="eq-scatter-plot-mark" aria-hidden="true" viewBox="0 0 10 10">
            <circle cx="5" cy="5" r="4.25" />
          </svg>
          <span class="eq-scatter-plot-tip">
            <span class="eq-scatter-plot-tip-label">Brazil</span>
            <span class="eq-scatter-plot-tip-row"><span>Monthly traffic growth</span> <b>5.6%</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Conversion</span> <b>1.9%</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Revenue</span> <b>$74K</b></span>
          </span>
        </li>
        <li class="eq-scatter-plot-point" tabindex="0" data-series="0" data-side="end" data-half="bottom" style="--_x:97.5%;--_y:76.67%;--_d:19.32px">
          <svg class="eq-scatter-plot-mark" aria-hidden="true" viewBox="0 0 10 10">
            <circle cx="5" cy="5" r="4.25" />
          </svg>
          <span class="eq-scatter-plot-tip">
            <span class="eq-scatter-plot-tip-label">India</span>
            <span class="eq-scatter-plot-tip-row"><span>Monthly traffic growth</span> <b>7.8%</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Conversion</span> <b>1.4%</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Revenue</span> <b>$151K</b></span>
          </span>
        </li>
        <li class="eq-scatter-plot-point" tabindex="0" data-series="0" data-side="start" data-half="top" style="--_x:38.75%;--_y:25%;--_d:12.68px">
          <svg class="eq-scatter-plot-mark" aria-hidden="true" viewBox="0 0 10 10">
            <circle cx="5" cy="5" r="4.25" />
          </svg>
          <span class="eq-scatter-plot-tip">
            <span class="eq-scatter-plot-tip-label">Australia</span>
            <span class="eq-scatter-plot-tip-row"><span>Monthly traffic growth</span> <b>3.1%</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Conversion</span> <b>4.5%</b></span>
            <span class="eq-scatter-plot-tip-row"><span>Revenue</span> <b>$38K</b></span>
          </span>
        </li>
      </ol>
    </div>
    <div class="eq-scatter-plot-x" aria-hidden="true"><span class="eq-scatter-plot-xtick" data-edge="start" style="--_x:0%">0%</span><span class="eq-scatter-plot-xtick" data-minor="true" style="--_x:25%">2%</span><span class="eq-scatter-plot-xtick" data-minor="true" style="--_x:50%">4%</span><span class="eq-scatter-plot-xtick" data-minor="true" style="--_x:75%">6%</span><span class="eq-scatter-plot-xtick" data-edge="end" style="--_x:100%">8%</span></div>
    <span class="eq-scatter-plot-xtitle" aria-hidden="true">Monthly traffic growth</span>
  </div>
</figure>

Props

PropTypeDefaultWhat it does
series{ label: string; points: { x: number; y: number; size?: number; label?: string }[] }[]noneOne to four groups, each with its own shape. Points with a size become bubbles. Past four groups the rest are left out.
labelstringnoneNames the figure. The point count and both ranges are added to it.
xLabelstringnoneWhat the horizontal axis measures. Shown under it and in every readout.
yLabelstringnoneWhat the vertical axis measures. Shown above it and in every readout.
sizeLabelstring"Size"What a bubble's size measures, for the readout.
heightnumber180Height of the plot area in pixels.
xMinnumbernoneFixes the left end of the x scale. xMax, yMin and yMax work the same way.
xMaxnumbernoneFixes the right end of the x scale.
yMinnumbernoneFixes the bottom of the y scale.
yMaxnumbernoneFixes the top of the y scale.
ticksnumber4About how many gridlines to draw on each axis.
showLegendbooleanseries.length > 1Shows the legend above the plot.
xFormatIntl.NumberFormatOptionsnoneHow x values and ticks are written.
yFormatIntl.NumberFormatOptionsnoneHow y values and ticks are written.
sizeFormatIntl.NumberFormatOptionsnoneHow sizes are written in the readout.
localestring"en-US"Passed to Intl.NumberFormat.
...restComponentProps<"figure">noneEvery native figure attribute.

Anatomy

root.eq-scatter-plotA figure named with the summary.
legend.eq-scatter-plot-legendA Chart legend with point marks.
body.eq-scatter-plot-bodyThe axes and plot; a size container.
y title.eq-scatter-plot-ytitle
y axis.eq-scatter-plot-y
y tick.eq-scatter-plot-ytick
plot.eq-scatter-plot-plot
grid.eq-scatter-plot-grid
gridline.eq-scatter-plot-gridlinedata-base marks the two axis lines.
empty.eq-scatter-plot-emptyShown when there are no points.
points.eq-scatter-plot-pointsA list with one focusable item per point.
point.eq-scatter-plot-pointAt least 24px to aim at; data-series sets its strength.
mark.eq-scatter-plot-markA circle, square, diamond or triangle per group; grows in on the spring.
tip.eq-scatter-plot-tipThe readout: visually hidden until its point is hovered or focused.
tip label.eq-scatter-plot-tip-label
tip row.eq-scatter-plot-tip-row
x axis.eq-scatter-plot-x
x tick.eq-scatter-plot-xtickdata-minor ticks step aside on a narrow chart.
x title.eq-scatter-plot-xtitle

Keyboard

TabMoves through the points, group by group; each one shows and reads its name and values.

Notes

  • Groups differ in shape as well as ink strength, and every readout names the group, so nothing relies on colour alone.
  • Marks are translucent, so overlapping points stay visible.
  • Each point is a tab stop, which suits a few dozen; for hundreds of points summarise them first.
  • Bubble area, not diameter, follows size, so a value twice as large does not look four times as large.
  • Renders on the server with no client JavaScript.
  • The grow-in uses @starting-style; where it is missing the points are simply there.

Source

Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.

components/equip/scatter-plot.tsx226 lines
import type { CSSProperties, ComponentProps } from "react";
import { ChartLegend } from "./chart-legend";
import "./scatter-plot.css";

export type ScatterPoint = {
  x: number;
  y: number;
  /** Turns the point into a bubble whose area follows this value. */
  size?: number;
  /** Names the point in its readout, such as a customer or a region. */
  label?: string;
};

export type ScatterSeries = { label: string; points: ScatterPoint[] };

export type ScatterPlotProps = ComponentProps<"figure"> & {
  /** One to four groups, each with its own shape. Past four, the rest are left out. */
  series: ScatterSeries[];
  /** Names the chart, such as "Accounts by seats and usage". The ranges are added to it. */
  label: string;
  /** What the horizontal axis measures. */
  xLabel: string;
  /** What the vertical axis measures. */
  yLabel: string;
  /** What a bubble's size measures, for the readout. */
  sizeLabel?: string;
  /** Height of the plot area in pixels. */
  height?: number;
  xMin?: number;
  xMax?: number;
  yMin?: number;
  yMax?: number;
  /** About how many gridlines to draw on each axis. */
  ticks?: number;
  showLegend?: boolean;
  xFormat?: Intl.NumberFormatOptions;
  yFormat?: Intl.NumberFormatOptions;
  sizeFormat?: Intl.NumberFormatOptions;
  locale?: string;
};

const MAX_SERIES = 4;

export function ScatterPlot({
  series,
  label,
  xLabel,
  yLabel,
  sizeLabel = "Size",
  height = 180,
  xMin,
  xMax,
  yMin,
  yMax,
  ticks = 4,
  showLegend,
  xFormat,
  yFormat,
  sizeFormat,
  locale = "en-US",
  className,
  style,
  ...rest
}: ScatterPlotProps) {
  const fx = new Intl.NumberFormat(locale, xFormat);
  const fy = new Intl.NumberFormat(locale, yFormat);
  const fs = new Intl.NumberFormat(locale, sizeFormat);
  const groups = series.slice(0, MAX_SERIES).map((s) => ({
    label: s.label,
    points: s.points.filter((p) => Number.isFinite(p.x) && Number.isFinite(p.y)),
  }));
  const all = groups.flatMap((g) => g.points);
  const empty = all.length === 0;

  const xs = all.map((p) => p.x);
  const ys = all.map((p) => p.y);
  const sizes = all.map((p) => (typeof p.size === "number" && p.size > 0 ? p.size : 0));
  const bubbles = sizes.some((s) => s > 0);
  const biggest = Math.max(0, ...sizes);

  const sx = niceScale(empty ? 0 : Math.min(...xs), empty ? 1 : Math.max(...xs), ticks, xMin, xMax);
  const sy = niceScale(empty ? 0 : Math.min(...ys), empty ? 1 : Math.max(...ys), ticks, yMin, yMax);
  const px = (v: number) => round(((Math.min(sx.hi, Math.max(sx.lo, v)) - sx.lo) / (sx.hi - sx.lo)) * 100);
  const py = (v: number) => round((1 - (Math.min(sy.hi, Math.max(sy.lo, v)) - sy.lo) / (sy.hi - sy.lo)) * 100);
  const diameter = (p: ScatterPoint) =>
    bubbles ? round(6 + Math.sqrt((typeof p.size === "number" && p.size > 0 ? p.size : 0) / (biggest || 1)) * 22) : 9;

  const summary = empty
    ? `${label}: no data`
    : `${label}: ${all.length} ${all.length === 1 ? "point" : "points"}, ${xLabel} from ${fx.format(Math.min(...xs))} to ${fx.format(Math.max(...xs))}, ${yLabel} from ${fy.format(Math.min(...ys))} to ${fy.format(Math.max(...ys))}`;
  const legend = showLegend ?? groups.length > 1;

  return (
    <figure
      aria-label={summary}
      {...rest}
      className={["eq-scatter-plot", className].filter(Boolean).join(" ")}
      style={{ ...style, "--_h": `${height}px` } as CSSProperties}
    >
      {legend && !empty && (
        <ChartLegend className="eq-scatter-plot-legend" mark="point" items={groups.map((g) => ({ label: g.label }))} aria-hidden="true" />
      )}
      <div className="eq-scatter-plot-body">
        <span className="eq-scatter-plot-ytitle" aria-hidden="true">
          {yLabel}
        </span>
        <div className="eq-scatter-plot-y" aria-hidden="true">
          {!empty &&
            sy.ticks.map((t) => (
              <span key={t} className="eq-scatter-plot-ytick" style={{ "--_y": `${py(t)}%` } as CSSProperties}>
                {fy.format(t)}
              </span>
            ))}
        </div>
        <div className="eq-scatter-plot-plot">
          <svg className="eq-scatter-plot-grid" aria-hidden="true" viewBox="0 0 100 100" preserveAspectRatio="none">
            {!empty &&
              sy.ticks.map((t) => (
                <line key={`y${t}`} className="eq-scatter-plot-gridline" x1="0" x2="100" y1={py(t)} y2={py(t)} />
              ))}
            {!empty &&
              sx.ticks.map((t) => (
                <line key={`x${t}`} className="eq-scatter-plot-gridline" x1={px(t)} x2={px(t)} y1="0" y2="100" />
              ))}
            <line className="eq-scatter-plot-gridline" data-base="" x1="0" x2="100" y1="100" y2="100" />
            <line className="eq-scatter-plot-gridline" data-base="" x1="0" x2="0" y1="0" y2="100" />
          </svg>
          {empty ? (
            <span className="eq-scatter-plot-empty">No data</span>
          ) : (
            <ol className="eq-scatter-plot-points" role="list" aria-label={label}>
              {groups.flatMap((g, k) =>
                g.points.map((p, i) => {
                  const x = px(p.x);
                  const y = py(p.y);
                  return (
                    <li
                      key={`${k}-${i}`}
                      className="eq-scatter-plot-point"
                      tabIndex={0}
                      data-series={k}
                      data-side={x > 50 ? "end" : "start"}
                      data-half={y < 50 ? "top" : "bottom"}
                      style={{ "--_x": `${x}%`, "--_y": `${y}%`, "--_d": `${diameter(p)}px` } as CSSProperties}
                    >
                      <svg className="eq-scatter-plot-mark" aria-hidden="true" viewBox="0 0 10 10">
                        <Shape series={k} />
                      </svg>
                      <span className="eq-scatter-plot-tip">
                        <span className="eq-scatter-plot-tip-label">{p.label ?? g.label}</span>
                        {p.label && groups.length > 1 && <span className="eq-scatter-plot-tip-row">{g.label}</span>}
                        <span className="eq-scatter-plot-tip-row">
                          <span>{xLabel}</span> <b>{fx.format(p.x)}</b>
                        </span>
                        <span className="eq-scatter-plot-tip-row">
                          <span>{yLabel}</span> <b>{fy.format(p.y)}</b>
                        </span>
                        {bubbles && typeof p.size === "number" && (
                          <span className="eq-scatter-plot-tip-row">
                            <span>{sizeLabel}</span> <b>{fs.format(p.size)}</b>
                          </span>
                        )}
                      </span>
                    </li>
                  );
                }),
              )}
            </ol>
          )}
        </div>
        <div className="eq-scatter-plot-x" aria-hidden="true">
          {!empty &&
            sx.ticks.map((t, i) => (
              <span
                key={t}
                className="eq-scatter-plot-xtick"
                data-edge={i === 0 && px(t) === 0 ? "start" : i === sx.ticks.length - 1 && px(t) === 100 ? "end" : undefined}
                data-minor={(sx.ticks.length > 3 && i !== 0 && i !== sx.ticks.length - 1) || undefined}
                style={{ "--_x": `${px(t)}%` } as CSSProperties}
              >
                {fx.format(t)}
              </span>
            ))}
        </div>
        <span className="eq-scatter-plot-xtitle" aria-hidden="true">
          {xLabel}
        </span>
      </div>
    </figure>
  );
}

function Shape({ series }: { series: number }) {
  if (series === 1) return <rect x="1" y="1" width="8" height="8" />;
  if (series === 2) return <path d="M5 0.5 9.5 5 5 9.5 0.5 5Z" />;
  if (series === 3) return <path d="M5 0.75 9.5 9H0.5Z" />;
  return <circle cx="5" cy="5" r="4.25" />;
}

/* Round steps of 1, 2, 2.5 or 5 times a power of ten, so gridlines land on numbers people say. */
function niceScale(lo: number, hi: number, count: number, fixLo?: number, fixHi?: number) {
  let a = fixLo ?? lo;
  let b = fixHi ?? hi;
  if (!(b > a)) b = a + (Math.abs(a) || 1);
  const raw = (b - a) / Math.max(1, count);
  const mag = 10 ** Math.floor(Math.log10(raw));
  const f = raw / mag;
  /* Take the first round step at or above the ideal one that lands on both ends, so a fixed end gets its own line. */
  const steps = [1, 2, 2.5, 5, 10, 20, 25, 50].map((m) => m * mag).filter((s) => s >= raw * (1 - 1e-9) || s === 10 * mag);
  const fits = (s: number) => {
    const lo2 = fixLo ?? Math.floor(a / s) * s;
    const hi2 = fixHi ?? Math.ceil(b / s) * s;
    const k = (hi2 - lo2) / s;
    return Math.abs(k - Math.round(k)) < 1e-6 && k <= count * 2;
  };
  const step = steps.find(fits) ?? (f <= 1 ? 1 : f <= 2 ? 2 : f <= 2.5 ? 2.5 : f <= 5 ? 5 : 10) * mag;
  if (fixLo === undefined) a = Math.floor(a / step) * step;
  if (fixHi === undefined) b = Math.ceil(b / step) * step;
  const ticks: number[] = [];
  for (let k = Math.ceil(a / step - 1e-9); k * step <= b + step * 1e-9; k++) ticks.push(Math.round(k * step * 1e9) / 1e9 || 0);
  return { lo: a, hi: b, ticks };
}

function round(n: number) {
  return Math.round(n * 100) / 100;
}
components/equip/scatter-plot.css296 lines
@layer equip {
  .eq-scatter-plot {
    display: grid;
    gap: var(--eq-space-3);
    width: 100%;
    min-width: 0;
    margin: 0;
    font: var(--eq-weight) var(--eq-text-xs) / 1.3 var(--eq-font);
    color: var(--eq-ink-2);
  }

  /* The series steps, the same as Chart legend's point marks: ink strength plus a shape. */
  .eq-scatter-plot [data-series] {
    --_tone: var(--eq-chart-1);
  }

  .eq-scatter-plot [data-series="1"] {
    --_tone: var(--eq-chart-2);
  }

  .eq-scatter-plot [data-series="2"] {
    --_tone: var(--eq-chart-3);
  }

  .eq-scatter-plot [data-series="3"] {
    --_tone: var(--eq-chart-4);
  }

  .eq-scatter-plot-body {
    display: grid;
    grid-template-columns: max-content minmax(0, 1fr);
    grid-template-rows: auto var(--_h) auto auto;
    column-gap: var(--eq-space-2);
    container-type: inline-size;
  }

  .eq-scatter-plot-ytitle {
    grid-column: 1 / -1;
    margin-block-end: var(--eq-space-2);
    color: var(--eq-ink-2);
  }

  .eq-scatter-plot-xtitle {
    grid-column: 2;
    margin-block-start: var(--eq-space-1);
    color: var(--eq-ink-2);
    text-align: end;
  }

  .eq-scatter-plot-y {
    display: grid;
    grid-template-rows: var(--_h);
    color: var(--eq-ink-3);
    font-variant-numeric: tabular-nums;
    text-align: end;
  }

  /* Every tick shares one cell, so the column is as wide as the widest label. */
  .eq-scatter-plot-ytick {
    grid-area: 1 / 1;
    align-self: start;
    position: relative;
    top: var(--_y);
    translate: 0 -50%;
    white-space: nowrap;
  }

  .eq-scatter-plot-plot {
    position: relative;
    min-width: 0;
    container-type: inline-size;
  }

  .eq-scatter-plot-grid {
    position: absolute;
    inset: 0;
    display: block;
    width: 100%;
    height: 100%;
    overflow: visible;
  }

  .eq-scatter-plot-gridline {
    stroke: var(--eq-line);
    stroke-width: 1;
    vector-effect: non-scaling-stroke;
  }

  .eq-scatter-plot-gridline[data-base] {
    stroke: var(--eq-line-strong);
  }

  .eq-scatter-plot-empty {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    color: var(--eq-ink-3);
  }

  .eq-scatter-plot-points {
    position: absolute;
    inset: 0;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  /* At least 24px to aim at, whatever the drawn size. */
  .eq-scatter-plot-point {
    position: absolute;
    left: var(--_x);
    top: var(--_y);
    display: grid;
    place-items: center;
    width: max(24px, var(--_d));
    height: max(24px, var(--_d));
    border-radius: var(--eq-radius-full);
    translate: -50% -50%;
    outline: none;
    cursor: default;
  }

  .eq-scatter-plot-mark {
    display: block;
    width: var(--_d);
    height: var(--_d);
    overflow: visible;
    transition: scale var(--eq-dur-move) var(--eq-spring);
  }

  @starting-style {
    .eq-scatter-plot-mark {
      scale: 0;
    }
  }

  .eq-scatter-plot-mark > * {
    fill: color-mix(in srgb, var(--_tone, var(--eq-ink)) 35%, transparent);
    stroke: var(--_tone, var(--eq-ink));
    stroke-width: 1.5;
    stroke-linejoin: round;
    vector-effect: non-scaling-stroke;
    transition: fill var(--eq-dur) var(--eq-ease);
  }

  /* The readout. Out of sight it is still text, so a screen reader reads it with its point. */
  .eq-scatter-plot-tip {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .eq-scatter-plot-tip-label {
    display: block;
    color: var(--eq-ink);
    font-weight: var(--eq-weight-strong);
  }

  .eq-scatter-plot-tip-row {
    display: flex;
    justify-content: space-between;
    gap: var(--eq-space-3);
  }

  .eq-scatter-plot-tip-row b {
    color: var(--eq-ink);
    font-weight: var(--eq-weight-strong);
    font-variant-numeric: tabular-nums;
  }

  .eq-scatter-plot-point:focus-visible {
    z-index: 2;
    outline: 2px solid var(--eq-focus);
    outline-offset: 2px;
  }

  .eq-scatter-plot-point:focus-visible .eq-scatter-plot-mark > * {
    fill: var(--_tone, var(--eq-ink));
  }

  .eq-scatter-plot-point:focus-visible .eq-scatter-plot-tip {
    z-index: 2;
    width: max-content;
    max-width: calc(50cqi - var(--eq-space-3));
    height: auto;
    margin: 0;
    padding: var(--eq-space-1) var(--eq-space-2);
    overflow: visible;
    clip-path: none;
    white-space: normal;
    border: 1px solid var(--eq-line-strong);
    border-radius: var(--eq-radius-m);
    background: var(--eq-raised);
    box-shadow: var(--eq-shadow);
    -webkit-backdrop-filter: var(--eq-blur);
    backdrop-filter: var(--eq-blur);
  }

  .eq-scatter-plot-point[data-side="start"]:focus-visible .eq-scatter-plot-tip {
    left: calc(100% + var(--eq-space-1));
  }

  .eq-scatter-plot-point[data-side="end"]:focus-visible .eq-scatter-plot-tip {
    right: calc(100% + var(--eq-space-1));
  }

  .eq-scatter-plot-point[data-half="top"]:focus-visible .eq-scatter-plot-tip {
    top: 0;
  }

  .eq-scatter-plot-point[data-half="bottom"]:focus-visible .eq-scatter-plot-tip {
    bottom: 0;
  }

  @media (hover: hover) {
    .eq-scatter-plot-point:hover {
      z-index: 2;
    }

    .eq-scatter-plot-point:hover .eq-scatter-plot-mark > * {
      fill: var(--_tone, var(--eq-ink));
    }

    .eq-scatter-plot-point:hover .eq-scatter-plot-tip {
      z-index: 2;
      width: max-content;
      max-width: calc(50cqi - var(--eq-space-3));
      height: auto;
      margin: 0;
      padding: var(--eq-space-1) var(--eq-space-2);
      overflow: visible;
      clip-path: none;
      white-space: normal;
      border: 1px solid var(--eq-line-strong);
      border-radius: var(--eq-radius-m);
      background: var(--eq-raised);
      box-shadow: var(--eq-shadow);
      -webkit-backdrop-filter: var(--eq-blur);
      backdrop-filter: var(--eq-blur);
    }

    .eq-scatter-plot-point[data-side="start"]:hover .eq-scatter-plot-tip {
      left: calc(100% + var(--eq-space-1));
    }

    .eq-scatter-plot-point[data-side="end"]:hover .eq-scatter-plot-tip {
      right: calc(100% + var(--eq-space-1));
    }

    .eq-scatter-plot-point[data-half="top"]:hover .eq-scatter-plot-tip {
      top: 0;
    }

    .eq-scatter-plot-point[data-half="bottom"]:hover .eq-scatter-plot-tip {
      bottom: 0;
    }
  }

  .eq-scatter-plot-x {
    position: relative;
    grid-column: 2;
    height: calc(var(--eq-text-xs) * 1.3);
    margin-block-start: var(--eq-space-2);
    color: var(--eq-ink-3);
    font-variant-numeric: tabular-nums;
  }

  .eq-scatter-plot-xtick {
    position: absolute;
    left: var(--_x);
    translate: -50% 0;
    white-space: nowrap;
  }

  .eq-scatter-plot-xtick[data-edge="start"] {
    translate: 0 0;
  }

  .eq-scatter-plot-xtick[data-edge="end"] {
    translate: -100% 0;
  }

  /* On a narrow chart only the two end labels stay, so none collide. */
  @container (max-width: 260px) {
    .eq-scatter-plot-xtick[data-minor] {
      display: none;
    }
  }
}

EqUIp is in beta: free for non-commercial use, no warranty, and things can change. Terms

Nothing picked yet. Press A on a part, or use the plus on the board, and the install command builds here.

Loading the index.

Loading the index