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
- BuildsFiles changed 120Minutes 2.1
- BuildsFiles changed 180Minutes 2.6
- BuildsFiles changed 240Minutes 3.4
- BuildsFiles changed 310Minutes 3.9
- BuildsFiles changed 360Minutes 4.8
- BuildsFiles changed 420Minutes 5.1
- BuildsFiles changed 470Minutes 6.2
- BuildsFiles changed 530Minutes 6.6
- 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.
- LarderTeamSeats 8Weekly active 81%
- Halcyon LabsTeamSeats 14Weekly active 72%
- BrightlineTeamSeats 22Weekly active 64%
- FernwayTeamSeats 11Weekly active 58%
- Atlas FreightBusinessSeats 46Weekly active 69%
- Kestrel HealthBusinessSeats 72Weekly active 61%
- Orchard RetailBusinessSeats 58Weekly active 47%
- Meridian BankEnterpriseSeats 180Weekly active 52%
- 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.
- United StatesMonthly traffic growth 4.1%Conversion 3.2%Revenue $412K
- GermanyMonthly traffic growth 3.4%Conversion 4.1%Revenue $128K
- JapanMonthly traffic growth 2.2%Conversion 2.7%Revenue $96K
- BrazilMonthly traffic growth 5.6%Conversion 1.9%Revenue $74K
- IndiaMonthly traffic growth 7.8%Conversion 1.4%Revenue $151K
- 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
| Prop | Type | Default | What it does |
|---|---|---|---|
series | { label: string; points: { x: number; y: number; size?: number; label?: string }[] }[] | none | One to four groups, each with its own shape. Points with a size become bubbles. Past four groups the rest are left out. |
label | string | none | Names the figure. The point count and both ranges are added to it. |
xLabel | string | none | What the horizontal axis measures. Shown under it and in every readout. |
yLabel | string | none | What the vertical axis measures. Shown above it and in every readout. |
sizeLabel | string | "Size" | What a bubble's size measures, for the readout. |
height | number | 180 | Height of the plot area in pixels. |
xMin | number | none | Fixes the left end of the x scale. xMax, yMin and yMax work the same way. |
xMax | number | none | Fixes the right end of the x scale. |
yMin | number | none | Fixes the bottom of the y scale. |
yMax | number | none | Fixes the top of the y scale. |
ticks | number | 4 | About how many gridlines to draw on each axis. |
showLegend | boolean | series.length > 1 | Shows the legend above the plot. |
xFormat | Intl.NumberFormatOptions | none | How x values and ticks are written. |
yFormat | Intl.NumberFormatOptions | none | How y values and ticks are written. |
sizeFormat | Intl.NumberFormatOptions | none | How sizes are written in the readout. |
locale | string | "en-US" | Passed to Intl.NumberFormat. |
...rest | ComponentProps<"figure"> | none | Every native figure attribute. |
Anatomy
| root | .eq-scatter-plot | A figure named with the summary. |
|---|---|---|
| legend | .eq-scatter-plot-legend | A Chart legend with point marks. |
| body | .eq-scatter-plot-body | The 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-gridline | data-base marks the two axis lines. |
| empty | .eq-scatter-plot-empty | Shown when there are no points. |
| points | .eq-scatter-plot-points | A list with one focusable item per point. |
| point | .eq-scatter-plot-point | At least 24px to aim at; data-series sets its strength. |
| mark | .eq-scatter-plot-mark | A circle, square, diamond or triangle per group; grows in on the spring. |
| tip | .eq-scatter-plot-tip | The 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-xtick | data-minor ticks step aside on a narrow chart. |
| x title | .eq-scatter-plot-xtitle |
Keyboard
| Tab | Moves 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;
}
}
}