Charts

Radar chart

Up to three series compared across three to eight axes on a web, with a legend and a hidden data table.

Built on
<svg>vector-effect@starting-stylecolor-mix()
Client JavaScript
None
Works without React
Yes, as HTML and CSS
Source, gzipped
3.2 kB
Built from
Chart legend
Laptop review scores, out of 10
AxisScore
Speed9
Battery7
Screen8
Weight6
Price5
Ports4
Try its options

Loading its options.

Code
import { RadarChart } from "@/components/equip/radar-chart";

export default function Demo() {
  return (
    <RadarChart
      label="Laptop review scores, out of 10"
      axes={["Speed", "Battery", "Screen", "Weight", "Price", "Ports"]}
      series={[{ label: "Score", values: [9, 7, 8, 6, 5, 4] }]}
      max={10}
      size={88}
      style={{ width: 216 }}
    />
  );
}
Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<figure aria-label="Laptop review scores, out of 10. Strongest in Speed, 9" class="eq-radar-chart" style="width:216px;--_size:88px;--_pad:4.2em">
  <div class="eq-radar-chart-plot"><svg class="eq-radar-chart-svg" aria-hidden="true" viewBox="0 0 100 100"><polygon class="eq-radar-chart-ring" points="50,38.5 59.959,44.25 59.959,55.75 50,61.5 40.041,55.75 40.041,44.25" /><polygon class="eq-radar-chart-ring" points="50,27 69.919,38.5 69.919,61.5 50,73 30.081,61.5 30.081,38.5" /><polygon class="eq-radar-chart-ring" points="50,15.5 79.878,32.75 79.878,67.25 50,84.5 20.122,67.25 20.122,32.75" /><polygon class="eq-radar-chart-ring" data-outer="true" points="50,4 89.837,27 89.837,73 50,96 10.163,73 10.163,27" /><line class="eq-radar-chart-spoke" x1="50" y1="50" x2="50" y2="4" /><line class="eq-radar-chart-spoke" x1="50" y1="50" x2="89.837" y2="27" /><line class="eq-radar-chart-spoke" x1="50" y1="50" x2="89.837" y2="73" /><line class="eq-radar-chart-spoke" x1="50" y1="50" x2="50" y2="96" /><line class="eq-radar-chart-spoke" x1="50" y1="50" x2="10.163" y2="73" /><line class="eq-radar-chart-spoke" x1="50" y1="50" x2="10.163" y2="27" /><g class="eq-radar-chart-series" data-series="0"><polygon class="eq-radar-chart-area" points="50,8.6 77.886,33.9 81.87,68.4 50,77.6 30.081,61.5 34.065,40.8" /><circle class="eq-radar-chart-point" cx="50" cy="8.6" r="1.6"><title>Score, Speed: 9</title></circle><circle class="eq-radar-chart-point" cx="77.886" cy="33.9" r="1.6"><title>Score, Battery: 7</title></circle><circle class="eq-radar-chart-point" cx="81.87" cy="68.4" r="1.6"><title>Score, Screen: 8</title></circle><circle class="eq-radar-chart-point" cx="50" cy="77.6" r="1.6"><title>Score, Weight: 6</title></circle><circle class="eq-radar-chart-point" cx="30.081" cy="61.5" r="1.6"><title>Score, Price: 5</title></circle><circle class="eq-radar-chart-point" cx="34.065" cy="40.8" r="1.6"><title>Score, Ports: 4</title></circle></g></svg><span class="eq-radar-chart-axis" aria-hidden="true" data-side="middle" style="--_sx:0;--_cy:1">Speed</span><span class="eq-radar-chart-axis" aria-hidden="true" data-side="start" style="--_sx:0.866;--_cy:0.5">Battery</span><span class="eq-radar-chart-axis" aria-hidden="true" data-side="start" style="--_sx:0.866;--_cy:-0.5">Screen</span><span class="eq-radar-chart-axis" aria-hidden="true" data-side="middle" style="--_sx:0;--_cy:-1">Weight</span><span class="eq-radar-chart-axis" aria-hidden="true" data-side="end" style="--_sx:-0.866;--_cy:-0.5">Price</span><span class="eq-radar-chart-axis" aria-hidden="true" data-side="end" style="--_sx:-0.866;--_cy:0.5">Ports</span></div>
  <table class="eq-radar-chart-data">
    <caption>Laptop review scores, out of 10</caption>
    <thead>
      <tr>
        <th scope="col">Axis</th>
        <th scope="col">Score</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">Speed</th>
        <td>9</td>
      </tr>
      <tr>
        <th scope="row">Battery</th>
        <td>7</td>
      </tr>
      <tr>
        <th scope="row">Screen</th>
        <td>8</td>
      </tr>
      <tr>
        <th scope="row">Weight</th>
        <td>6</td>
      </tr>
      <tr>
        <th scope="row">Price</th>
        <td>5</td>
      </tr>
      <tr>
        <th scope="row">Ports</th>
        <td>4</td>
      </tr>
    </tbody>
  </table>
</figure>

Examples

Skills against the role

Each series has its own dash as well as its own strength, and a hidden table holds every value for screen readers.

Design skills, self-assessed against the senior role, out of 5
AxisSenior roleSelf-assessed
Research43
Interaction55
Visual44
Prototyping45
Writing43
Systems42
Facilitation43
Code
import { RadarChart } from "@/components/equip/radar-chart";

export default function Example() {
  return (
    <RadarChart
      label="Design skills, self-assessed against the senior role, out of 5"
      axes={["Research", "Interaction", "Visual", "Prototyping", "Writing", "Systems", "Facilitation"]}
      series={[
        { label: "Senior role", values: [4, 5, 4, 4, 4, 4, 4] },
        { label: "Self-assessed", values: [3, 5, 4, 5, 3, 2, 3] },
      ]}
      max={5}
      levels={5}
      size={260}
    />
  );
}
HTMLworks in any framework, with the part's CSS
<figure aria-label="Design skills, self-assessed against the senior role, out of 5. Senior role strongest in Interaction, 5; Self-assessed strongest in Interaction, 5" class="eq-radar-chart" style="--_size:260px;--_pad:7em">
  <ul role="list" aria-hidden="true" class="eq-chart-legend eq-radar-chart-legend" data-orientation="row">
    <li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Senior role</span></li>
    <li class="eq-chart-legend-item" data-series="1"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Self-assessed</span></li>
  </ul>
  <div class="eq-radar-chart-plot"><svg class="eq-radar-chart-svg" aria-hidden="true" viewBox="0 0 100 100"><polygon class="eq-radar-chart-ring" points="50,40.8 57.193,44.264 58.969,52.047 53.992,58.289 46.008,58.289 41.031,52.047 42.807,44.264" /><polygon class="eq-radar-chart-ring" points="50,31.6 64.386,38.528 67.939,54.094 57.983,66.578 42.017,66.578 32.061,54.094 35.614,38.528" /><polygon class="eq-radar-chart-ring" points="50,22.4 71.579,32.792 76.908,56.142 61.975,74.867 38.025,74.867 23.092,56.142 28.421,32.792" /><polygon class="eq-radar-chart-ring" points="50,13.2 78.771,27.056 85.877,58.189 65.967,83.156 34.033,83.156 14.123,58.189 21.229,27.056" /><polygon class="eq-radar-chart-ring" data-outer="true" points="50,4 85.964,21.319 94.847,60.236 69.959,91.445 30.041,91.445 5.153,60.236 14.036,21.319" /><line class="eq-radar-chart-spoke" x1="50" y1="50" x2="50" y2="4" /><line class="eq-radar-chart-spoke" x1="50" y1="50" x2="85.964" y2="21.319" /><line class="eq-radar-chart-spoke" x1="50" y1="50" x2="94.847" y2="60.236" /><line class="eq-radar-chart-spoke" x1="50" y1="50" x2="69.959" y2="91.445" /><line class="eq-radar-chart-spoke" x1="50" y1="50" x2="30.041" y2="91.445" /><line class="eq-radar-chart-spoke" x1="50" y1="50" x2="5.153" y2="60.236" /><line class="eq-radar-chart-spoke" x1="50" y1="50" x2="14.036" y2="21.319" /><g class="eq-radar-chart-series" data-series="0"><polygon class="eq-radar-chart-area" points="50,13.2 85.964,21.319 85.877,58.189 65.967,83.156 34.033,83.156 14.123,58.189 21.229,27.056" /><circle class="eq-radar-chart-point" cx="50" cy="13.2" r="1.6"><title>Senior role, Research: 4</title></circle><circle class="eq-radar-chart-point" cx="85.964" cy="21.319" r="1.6"><title>Senior role, Interaction: 5</title></circle><circle class="eq-radar-chart-point" cx="85.877" cy="58.189" r="1.6"><title>Senior role, Visual: 4</title></circle><circle class="eq-radar-chart-point" cx="65.967" cy="83.156" r="1.6"><title>Senior role, Prototyping: 4</title></circle><circle class="eq-radar-chart-point" cx="34.033" cy="83.156" r="1.6"><title>Senior role, Writing: 4</title></circle><circle class="eq-radar-chart-point" cx="14.123" cy="58.189" r="1.6"><title>Senior role, Systems: 4</title></circle><circle class="eq-radar-chart-point" cx="21.229" cy="27.056" r="1.6"><title>Senior role, Facilitation: 4</title></circle></g><g class="eq-radar-chart-series" data-series="1"><polygon class="eq-radar-chart-area" points="50,22.4 85.964,21.319 85.877,58.189 69.959,91.445 38.025,74.867 32.061,54.094 28.421,32.792" /><circle class="eq-radar-chart-point" cx="50" cy="22.4" r="1.6"><title>Self-assessed, Research: 3</title></circle><circle class="eq-radar-chart-point" cx="85.964" cy="21.319" r="1.6"><title>Self-assessed, Interaction: 5</title></circle><circle class="eq-radar-chart-point" cx="85.877" cy="58.189" r="1.6"><title>Self-assessed, Visual: 4</title></circle><circle class="eq-radar-chart-point" cx="69.959" cy="91.445" r="1.6"><title>Self-assessed, Prototyping: 5</title></circle><circle class="eq-radar-chart-point" cx="38.025" cy="74.867" r="1.6"><title>Self-assessed, Writing: 3</title></circle><circle class="eq-radar-chart-point" cx="32.061" cy="54.094" r="1.6"><title>Self-assessed, Systems: 2</title></circle><circle class="eq-radar-chart-point" cx="28.421" cy="32.792" r="1.6"><title>Self-assessed, Facilitation: 3</title></circle></g></svg><span class="eq-radar-chart-axis" aria-hidden="true" data-side="middle" style="--_sx:0;--_cy:1">Research</span><span class="eq-radar-chart-axis" aria-hidden="true" data-side="start" style="--_sx:0.782;--_cy:0.623">Interaction</span><span class="eq-radar-chart-axis" aria-hidden="true" data-side="start" style="--_sx:0.975;--_cy:-0.223">Visual</span><span class="eq-radar-chart-axis" aria-hidden="true" data-side="start" style="--_sx:0.434;--_cy:-0.901">Prototyping</span><span class="eq-radar-chart-axis" aria-hidden="true" data-side="end" style="--_sx:-0.434;--_cy:-0.901">Writing</span><span class="eq-radar-chart-axis" aria-hidden="true" data-side="end" style="--_sx:-0.975;--_cy:-0.223">Systems</span><span class="eq-radar-chart-axis" aria-hidden="true" data-side="end" style="--_sx:-0.782;--_cy:0.623">Facilitation</span></div>
  <table class="eq-radar-chart-data">
    <caption>Design skills, self-assessed against the senior role, out of 5</caption>
    <thead>
      <tr>
        <th scope="col">Axis</th>
        <th scope="col">Senior role</th>
        <th scope="col">Self-assessed</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">Research</th>
        <td>4</td>
        <td>3</td>
      </tr>
      <tr>
        <th scope="row">Interaction</th>
        <td>5</td>
        <td>5</td>
      </tr>
      <tr>
        <th scope="row">Visual</th>
        <td>4</td>
        <td>4</td>
      </tr>
      <tr>
        <th scope="row">Prototyping</th>
        <td>4</td>
        <td>5</td>
      </tr>
      <tr>
        <th scope="row">Writing</th>
        <td>4</td>
        <td>3</td>
      </tr>
      <tr>
        <th scope="row">Systems</th>
        <td>4</td>
        <td>2</td>
      </tr>
      <tr>
        <th scope="row">Facilitation</th>
        <td>4</td>
        <td>3</td>
      </tr>
    </tbody>
  </table>
</figure>

Comparing three plans

Up to three series; where webs overlap the translucent fills stack, and hovering one deepens its fill.

Hosting plans compared, scored out of 10
AxisStarterGrowthScale
Uptime7910
Support479
Price963
Regions369
Storage4710
Code
import { RadarChart } from "@/components/equip/radar-chart";

export default function Example() {
  return (
    <RadarChart
      label="Hosting plans compared, scored out of 10"
      axes={["Uptime", "Support", "Price", "Regions", "Storage"]}
      series={[
        { label: "Starter", values: [7, 4, 9, 3, 4] },
        { label: "Growth", values: [9, 7, 6, 6, 7] },
        { label: "Scale", values: [10, 9, 3, 9, 10] },
      ]}
      max={10}
      levels={5}
      size={240}
    />
  );
}
HTMLworks in any framework, with the part's CSS
<figure aria-label="Hosting plans compared, scored out of 10. Starter strongest in Price, 9; Growth strongest in Uptime, 9; Scale strongest in Uptime, 10" class="eq-radar-chart" style="--_size:240px;--_pad:4.2em">
  <ul role="list" aria-hidden="true" class="eq-chart-legend eq-radar-chart-legend" data-orientation="row">
    <li class="eq-chart-legend-item" data-series="0"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Starter</span></li>
    <li class="eq-chart-legend-item" data-series="1"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Growth</span></li>
    <li class="eq-chart-legend-item" data-series="2"><svg class="eq-chart-legend-mark" data-mark="line" aria-hidden="true" width="18" height="10" viewBox="0 0 18 10"><line x1="1" y1="5" x2="17" y2="5" /></svg><span class="eq-chart-legend-name">Scale</span></li>
  </ul>
  <div class="eq-radar-chart-plot"><svg class="eq-radar-chart-svg" aria-hidden="true" viewBox="0 0 100 100"><polygon class="eq-radar-chart-ring" points="50,40.8 58.75,47.157 55.408,57.443 44.592,57.443 41.25,47.157" /><polygon class="eq-radar-chart-ring" points="50,31.6 67.499,44.314 60.815,64.886 39.185,64.886 32.501,44.314" /><polygon class="eq-radar-chart-ring" points="50,22.4 76.249,41.471 66.223,72.329 33.777,72.329 23.751,41.471" /><polygon class="eq-radar-chart-ring" points="50,13.2 84.999,38.628 71.63,79.772 28.37,79.772 15.001,38.628" /><polygon class="eq-radar-chart-ring" data-outer="true" points="50,4 93.749,35.785 77.038,87.215 22.962,87.215 6.251,35.785" /><line class="eq-radar-chart-spoke" x1="50" y1="50" x2="50" y2="4" /><line class="eq-radar-chart-spoke" x1="50" y1="50" x2="93.749" y2="35.785" /><line class="eq-radar-chart-spoke" x1="50" y1="50" x2="77.038" y2="87.215" /><line class="eq-radar-chart-spoke" x1="50" y1="50" x2="22.962" y2="87.215" /><line class="eq-radar-chart-spoke" x1="50" y1="50" x2="6.251" y2="35.785" /><g class="eq-radar-chart-series" data-series="0"><polygon class="eq-radar-chart-area" points="50,17.8 67.499,44.314 74.334,83.493 41.889,61.164 32.501,44.314" /><circle class="eq-radar-chart-point" cx="50" cy="17.8" r="1.6"><title>Starter, Uptime: 7</title></circle><circle class="eq-radar-chart-point" cx="67.499" cy="44.314" r="1.6"><title>Starter, Support: 4</title></circle><circle class="eq-radar-chart-point" cx="74.334" cy="83.493" r="1.6"><title>Starter, Price: 9</title></circle><circle class="eq-radar-chart-point" cx="41.889" cy="61.164" r="1.6"><title>Starter, Regions: 3</title></circle><circle class="eq-radar-chart-point" cx="32.501" cy="44.314" r="1.6"><title>Starter, Storage: 4</title></circle></g><g class="eq-radar-chart-series" data-series="1"><polygon class="eq-radar-chart-area" points="50,8.6 80.624,40.05 66.223,72.329 33.777,72.329 19.376,40.05" /><circle class="eq-radar-chart-point" cx="50" cy="8.6" r="1.6"><title>Growth, Uptime: 9</title></circle><circle class="eq-radar-chart-point" cx="80.624" cy="40.05" r="1.6"><title>Growth, Support: 7</title></circle><circle class="eq-radar-chart-point" cx="66.223" cy="72.329" r="1.6"><title>Growth, Price: 6</title></circle><circle class="eq-radar-chart-point" cx="33.777" cy="72.329" r="1.6"><title>Growth, Regions: 6</title></circle><circle class="eq-radar-chart-point" cx="19.376" cy="40.05" r="1.6"><title>Growth, Storage: 7</title></circle></g><g class="eq-radar-chart-series" data-series="2"><polygon class="eq-radar-chart-area" points="50,4 89.374,37.207 58.111,61.164 25.666,83.493 6.251,35.785" /><circle class="eq-radar-chart-point" cx="50" cy="4" r="1.6"><title>Scale, Uptime: 10</title></circle><circle class="eq-radar-chart-point" cx="89.374" cy="37.207" r="1.6"><title>Scale, Support: 9</title></circle><circle class="eq-radar-chart-point" cx="58.111" cy="61.164" r="1.6"><title>Scale, Price: 3</title></circle><circle class="eq-radar-chart-point" cx="25.666" cy="83.493" r="1.6"><title>Scale, Regions: 9</title></circle><circle class="eq-radar-chart-point" cx="6.251" cy="35.785" r="1.6"><title>Scale, Storage: 10</title></circle></g></svg><span class="eq-radar-chart-axis" aria-hidden="true" data-side="middle" style="--_sx:0;--_cy:1">Uptime</span><span class="eq-radar-chart-axis" aria-hidden="true" data-side="start" style="--_sx:0.951;--_cy:0.309">Support</span><span class="eq-radar-chart-axis" aria-hidden="true" data-side="start" style="--_sx:0.588;--_cy:-0.809">Price</span><span class="eq-radar-chart-axis" aria-hidden="true" data-side="end" style="--_sx:-0.588;--_cy:-0.809">Regions</span><span class="eq-radar-chart-axis" aria-hidden="true" data-side="end" style="--_sx:-0.951;--_cy:0.309">Storage</span></div>
  <table class="eq-radar-chart-data">
    <caption>Hosting plans compared, scored out of 10</caption>
    <thead>
      <tr>
        <th scope="col">Axis</th>
        <th scope="col">Starter</th>
        <th scope="col">Growth</th>
        <th scope="col">Scale</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">Uptime</th>
        <td>7</td>
        <td>9</td>
        <td>10</td>
      </tr>
      <tr>
        <th scope="row">Support</th>
        <td>4</td>
        <td>7</td>
        <td>9</td>
      </tr>
      <tr>
        <th scope="row">Price</th>
        <td>9</td>
        <td>6</td>
        <td>3</td>
      </tr>
      <tr>
        <th scope="row">Regions</th>
        <td>3</td>
        <td>6</td>
        <td>9</td>
      </tr>
      <tr>
        <th scope="row">Storage</th>
        <td>4</td>
        <td>7</td>
        <td>10</td>
      </tr>
    </tbody>
  </table>
</figure>

Props

PropTypeDefaultWhat it does
axesstring[]noneThree to eight axis names, clockwise from the top. Past eight the rest are left out.
series{ label: string; values: number[] }[]noneOne to three series, one value per axis. Missing and negative values count as zero. Past three the rest are left out.
labelstringnoneNames the figure and captions the data table. Each series' strongest axis is added to it.
maxnumbernoneFixes the outer ring, such as 5 for a five-point scale. Defaults to the largest value.
levelsnumber4How many rings to draw.
sizenumber240Largest width of the web in pixels. It shrinks to fit a narrower container.
showLegendbooleanseries.length > 1Shows the legend above the web.
formatIntl.NumberFormatOptionsnoneHow values are written.
localestring"en-US"Passed to Intl.NumberFormat.
...restComponentProps<"figure">noneEvery native figure attribute.

Anatomy

root.eq-radar-chartA figure named with the summary.
legend.eq-radar-chart-legendA Chart legend with line marks.
plot.eq-radar-chart-plotKeeps room beside the web for the longest axis name.
svg.eq-radar-chart-svg
ring.eq-radar-chart-ringdata-outer marks the outermost ring.
spoke.eq-radar-chart-spoke
series.eq-radar-chart-seriesGrows from the centre on the spring; data-series sets its strength and dash.
area.eq-radar-chart-area
point.eq-radar-chart-pointShows its value in a native tooltip on hover.
axis.eq-radar-chart-axisA real text name just outside its spoke.
empty.eq-radar-chart-emptyShown when there are fewer than three axes or nothing above zero.
data.eq-radar-chart-dataEvery value as a visually hidden table.

Notes

  • The web is hidden from assistive tech; a visually hidden table holds every value by axis and series.
  • Series differ in ink strength and in dash, and the legend names each, so none relies on colour alone.
  • Axes share one scale, so give them comparable units, such as a score out of 5.
  • Renders on the server with no client JavaScript.
  • The grow-in uses @starting-style; where it is missing the webs 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/radar-chart.tsx159 lines
import type { CSSProperties, ComponentProps } from "react";
import { ChartLegend } from "./chart-legend";
import "./radar-chart.css";

export type RadarSeries = {
  label: string;
  /** One value per axis, in the same order. Missing and negative values count as zero. */
  values: number[];
};

export type RadarChartProps = ComponentProps<"figure"> & {
  /** Three to eight axis names, clockwise from the top. Past eight, the rest are left out. */
  axes: string[];
  /** One to three series. Past three, the rest are left out. */
  series: RadarSeries[];
  /** Names the chart, such as "Skills, design team". Each series' strongest axis is added to it. */
  label: string;
  /** Fixes the outer ring instead of using the largest value. */
  max?: number;
  /** How many rings to draw. */
  levels?: number;
  /** Largest width of the web in pixels; it shrinks to fit a narrower container. */
  size?: number;
  showLegend?: boolean;
  format?: Intl.NumberFormatOptions;
  locale?: string;
};

const MAX_AXES = 8;
const MAX_SERIES = 3;
const R = 46;

export function RadarChart({
  axes,
  series,
  label,
  max,
  levels = 4,
  size = 240,
  showLegend,
  format,
  locale = "en-US",
  className,
  style,
  ...rest
}: RadarChartProps) {
  const number = new Intl.NumberFormat(locale, format);
  const names = axes.slice(0, MAX_AXES);
  const n = names.length;
  const sets = series.slice(0, MAX_SERIES).map((s) => ({
    label: s.label,
    values: names.map((_, i) => {
      const v = s.values[i];
      return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
    }),
  }));
  const peak = Math.max(0, ...sets.flatMap((s) => s.values));
  const top = max && max > 0 ? max : peak;
  const empty = n < 3 || sets.length === 0 || top === 0;
  const rings = Math.max(1, Math.floor(levels));

  const angle = (i: number) => (i / n) * Math.PI * 2;
  const at = (i: number, r: number) => [round(50 + r * Math.sin(angle(i))), round(50 - r * Math.cos(angle(i)))] as const;
  const polygon = (r: (i: number) => number) => names.map((_, i) => at(i, r(i)).join(",")).join(" ");
  const longest = Math.max(0, ...names.map((a) => a.length));
  const pad = Math.min(7, Math.max(2.5, longest * 0.6));

  const summary = empty
    ? `${label}: no data`
    : `${label}. ${sets
        .map((s) => {
          const best = s.values.indexOf(Math.max(...s.values));
          return `${sets.length > 1 ? `${s.label} strongest` : "Strongest"} in ${names[best]}, ${number.format(s.values[best] ?? 0)}`;
        })
        .join("; ")}`;
  const legend = showLegend ?? sets.length > 1;

  return (
    <figure
      aria-label={summary}
      {...rest}
      className={["eq-radar-chart", className].filter(Boolean).join(" ")}
      style={{ ...style, "--_size": `${size}px`, "--_pad": `${pad}em` } as CSSProperties}
    >
      {legend && !empty && (
        <ChartLegend className="eq-radar-chart-legend" mark="line" items={sets.map((s) => ({ label: s.label }))} aria-hidden="true" />
      )}
      <div className="eq-radar-chart-plot">
        <svg className="eq-radar-chart-svg" aria-hidden="true" viewBox="0 0 100 100">
          {n >= 3 &&
            Array.from({ length: rings }, (_, k) => (
              <polygon key={k} className="eq-radar-chart-ring" data-outer={k === rings - 1 || undefined} points={polygon(() => (R * (k + 1)) / rings)} />
            ))}
          {n >= 3 &&
            names.map((_, i) => {
              const [x, y] = at(i, R);
              return <line key={i} className="eq-radar-chart-spoke" x1="50" y1="50" x2={x} y2={y} />;
            })}
          {!empty &&
            sets.map((s, k) => (
              <g key={k} className="eq-radar-chart-series" data-series={k}>
                <polygon className="eq-radar-chart-area" points={polygon((i) => (R * Math.min(top, s.values[i] ?? 0)) / top)} />
                {s.values.map((v, i) => {
                  const [x, y] = at(i, (R * Math.min(top, v)) / top);
                  return (
                    <circle key={i} className="eq-radar-chart-point" cx={x} cy={y} r="1.6">
                      <title>{`${s.label}, ${names[i]}: ${number.format(v)}`}</title>
                    </circle>
                  );
                })}
              </g>
            ))}
        </svg>
        {n >= 3 &&
          names.map((a, i) => (
            <span
              key={`${a}-${i}`}
              className="eq-radar-chart-axis"
              aria-hidden="true"
              data-side={Math.sin(angle(i)) > 0.2 ? "start" : Math.sin(angle(i)) < -0.2 ? "end" : "middle"}
              style={{ "--_sx": round(Math.sin(angle(i))), "--_cy": round(Math.cos(angle(i))) } as CSSProperties}
            >
              {a}
            </span>
          ))}
        {empty && <span className="eq-radar-chart-empty">No data</span>}
      </div>
      {!empty && (
        <table className="eq-radar-chart-data">
          <caption>{label}</caption>
          <thead>
            <tr>
              <th scope="col">Axis</th>
              {sets.map((s, k) => (
                <th key={k} scope="col">
                  {s.label}
                </th>
              ))}
            </tr>
          </thead>
          <tbody>
            {names.map((a, i) => (
              <tr key={`${a}-${i}`}>
                <th scope="row">{a}</th>
                {sets.map((s, k) => (
                  <td key={k}>{number.format(s.values[i] ?? 0)}</td>
                ))}
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </figure>
  );
}

function round(n: number) {
  return Math.round(n * 1000) / 1000;
}
components/equip/radar-chart.css142 lines
@layer equip {
  .eq-radar-chart {
    position: relative;
    display: grid;
    justify-items: center;
    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 line marks: ink strength plus a dash. */
  .eq-radar-chart [data-series] {
    --_tone: var(--eq-chart-1);
    --_dash: none;
  }

  .eq-radar-chart [data-series="1"] {
    --_tone: var(--eq-chart-2);
    --_dash: 5 4;
  }

  .eq-radar-chart [data-series="2"] {
    --_tone: var(--eq-chart-3);
    --_dash: 0.1 4;
  }

  /* The web keeps room on each side for the longest axis name, and on top and bottom for one line.
     In a narrow chart the side room is capped at a quarter of the width, so the web keeps at least
     half of it and the longest names wrap instead. */
  .eq-radar-chart-plot {
    --_room: min(var(--_pad), 25%);
    position: relative;
    width: min(var(--_size), 100% - 2 * var(--_room));
    aspect-ratio: 1;
    margin: calc(var(--eq-text-xs) * 1.3 + var(--eq-space-2)) var(--_room);
  }

  .eq-radar-chart-svg {
    display: block;
    width: 100%;
    height: 100%;
    overflow: visible;
  }

  .eq-radar-chart-ring,
  .eq-radar-chart-spoke {
    fill: none;
    stroke: var(--eq-line);
    stroke-width: 1;
    vector-effect: non-scaling-stroke;
  }

  .eq-radar-chart-ring[data-outer] {
    stroke: var(--eq-line-strong);
  }

  /* Each web grows out from the centre on the spring. */
  .eq-radar-chart-series {
    transform-box: view-box;
    transform-origin: 50% 50%;
    transition: scale var(--eq-dur-move) var(--eq-spring);
  }

  @starting-style {
    .eq-radar-chart-series {
      scale: 0;
    }
  }

  .eq-radar-chart-area {
    fill: color-mix(in srgb, var(--_tone, var(--eq-ink)) 16%, transparent);
    stroke: var(--_tone, var(--eq-ink));
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
    stroke-dasharray: var(--_dash);
    vector-effect: non-scaling-stroke;
    transition: fill var(--eq-dur) var(--eq-ease);
  }

  .eq-radar-chart-point {
    fill: var(--_tone, var(--eq-ink));
  }

  @media (hover: hover) {
    .eq-radar-chart-series:hover .eq-radar-chart-area {
      fill: color-mix(in srgb, var(--_tone, var(--eq-ink)) 32%, transparent);
    }
  }

  /* Each name sits just outside its spoke and grows away from the web. */
  .eq-radar-chart-axis {
    position: absolute;
    left: calc(50% + var(--_sx) * (46% + var(--eq-space-2)));
    top: calc(50% - var(--_cy) * (46% + var(--eq-space-2)));
    width: max-content;
    /* Half the web is the side room once that room is capped. */
    max-width: min(var(--_pad), 50%);
    translate: calc((var(--_sx) - 1) * 50%) calc((-1 * var(--_cy) - 1) * 50%);
    color: var(--eq-ink-2);
    overflow-wrap: break-word;
    hyphens: auto;
    text-align: center;
  }

  .eq-radar-chart-axis[data-side="start"] {
    text-align: start;
  }

  .eq-radar-chart-axis[data-side="end"] {
    text-align: end;
  }

  .eq-radar-chart-axis[data-side="middle"] {
    max-width: min(var(--_pad) * 2, 100%);
  }

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

  .eq-radar-chart-data {
    /* A table ignores height and overflow, so it is made a block before it is clipped. */
    display: block;
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

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