Radar charts

This one's kind of fun. We define some CSS variables on the table: the scale (and tiers), the number of elements, and the values.

On the CSS side, it's getting complicated:

And that's it!

Switch
Allows you to disable styles on the following table.

Level of interest by domain, out of 20
Accessibility SEO Performance Compatibility Security Code quality Test
14 11 13 16 10 12 4
HTML
<table class="chaarts radar" id="radar" style="--scale: 20; --step: 5; --items: 7; --1: 14; --2: 11; --3: 13; --4: 16; --5: 10; --6: 12; --7: 4; --8: var(--1);">
	<caption id="caption-9">[…]</caption>
	<thead>
		<tr>
			<th scope="col">[…]</th>
			<th scope="col">[…]</th>
		</tr>
	</thead>
	<tbody>
		<tr>
			<td><span>14</span></td>
			<td><span>11</span></td>
		</tr>
	</tbody>
</table>
css
@charset "UTF-8";
.chaarts[class*=radar] {
  --radius: 12.8em;
  --unitless-radius: calc(1024 / 16 / 5);
  --size: calc(var(--radius) / var(--scale));
  --part: calc(360deg / var(--items));
  --integer: calc(var(--scale));
  background-image: repeating-radial-gradient(circle at 50%, var(--foreground-o-25) 0 2px, transparent 0 calc(var(--size) * var(--step))), repeating-radial-gradient(circle at 50%, var(--foreground-o-10) 0 2px, transparent 0 var(--size));
  block-size: calc(var(--radius) * 2);
  border: 2px solid;
  border-radius: 50%;
  contain: layout;
  counter-reset: scale var(--integer);
  inline-size: calc(var(--radius) * 2);
  margin: 6rem auto 12rem;
  overflow: visible;
  position: relative;
}

.chaarts[class*=radar] caption {
  background: none;
  inset-block-end: -10rem;
  position: absolute;
}

.chaarts[class*=radar] [scope=col] {
  --away: calc((var(--radius) * -1) - 50%);
  background-color: transparent;
  left: 50%;
  margin: 0;
  padding: 0 1rem;
  position: absolute;
  top: 50%;
  transform: translate3d(-50%, -50%, 0) rotate(calc(var(--part) * var(--index, 1))) translate(var(--away)) rotate(calc(var(--part) * var(--index, 1) * -1));
}

.chaarts[class*=radar] tr > *:nth-of-type(1) {
  --index: 1;
}

.chaarts[class*=radar] tr > *:nth-of-type(2) {
  --index: 2;
}

.chaarts[class*=radar] tr > *:nth-of-type(3) {
  --index: 3;
}

.chaarts[class*=radar] tr > *:nth-of-type(4) {
  --index: 4;
}

.chaarts[class*=radar] tr > *:nth-of-type(5) {
  --index: 5;
}

.chaarts[class*=radar] tr > *:nth-of-type(6) {
  --index: 6;
}

.chaarts[class*=radar] tr > *:nth-of-type(7) {
  --index: 7;
}

.chaarts[class*=radar] td {
  --skew: calc(90deg - var(--part));
  block-size: 50%;
  border-block-end: 1px solid var(--chaarts-purple);
  inline-size: 50%;
  left: 0;
  margin: 0;
  position: absolute;
  top: 0;
  transform: rotate(calc(var(--part) * var(--index, 1))) skew(var(--skew));
  transform-origin: 100% 100%;
}

.chaarts[class*=radar] td:nth-of-type(1) span {
  --point: var(--1);
  --pos: calc(100% - (var(--2) * 100% / (var(--scale) * var(--ratio))));
}

.chaarts[class*=radar] td:nth-of-type(2) span {
  --point: var(--2);
  --pos: calc(100% - (var(--3) * 100% / (var(--scale) * var(--ratio))));
}

.chaarts[class*=radar] td:nth-of-type(3) span {
  --point: var(--3);
  --pos: calc(100% - (var(--4) * 100% / (var(--scale) * var(--ratio))));
}

.chaarts[class*=radar] td:nth-of-type(4) span {
  --point: var(--4);
  --pos: calc(100% - (var(--5) * 100% / (var(--scale) * var(--ratio))));
}

.chaarts[class*=radar] td:nth-of-type(5) span {
  --point: var(--5);
  --pos: calc(100% - (var(--6) * 100% / (var(--scale) * var(--ratio))));
}

.chaarts[class*=radar] td:nth-of-type(6) span {
  --point: var(--6);
  --pos: calc(100% - (var(--7) * 100% / (var(--scale) * var(--ratio))));
}

.chaarts[class*=radar] td:nth-of-type(7) span {
  --point: var(--7);
  --pos: calc(100% - (var(--8) * 100% / (var(--scale) * var(--ratio))));
}

.chaarts[class*=radar] td::after, .chaarts[class*=radar] td::before {
  display: none;
}

.chaarts[class*=radar] span {
  --opposite: calc(180 - (90 + (90 - (360 / var(--items)))));
  --angle: calc(var(--opposite) * var(--to-radians));
  --sin-angle: sin(var(--angle));
  --hypo: calc(var(--unitless-radius) / var(--sin-angle));
  --ratio: calc(var(--hypo) / var(--unitless-radius));
  --polygon: polygon(
  		100% var(--pos),
  		calc(100% - (var(--point) * 100% / var(--scale))) 100%,
  		100% 100%
  );
  background: var(--chaarts-purple);
  block-size: 100%;
  clip-path: var(--polygon);
  filter: drop-shadow(0 0 1rem var(--chaarts-purple));
  inline-size: 100%;
  position: absolute;
}

.chaarts.radar [scope=col]::after {
  color: var(--foreground-lighter);
  display: block;
  font-size: small;
  font-weight: 400;
}

.chaarts.radar [scope=col]:nth-child(1)::after {
  --integer: calc(var(--1));
  content: counter(value) " / " counter(scale);
  counter-reset: value var(--integer);
}

.chaarts.radar [scope=col]:nth-child(2)::after {
  --integer: calc(var(--2));
  content: counter(value) " / " counter(scale);
  counter-reset: value var(--integer);
}

.chaarts.radar [scope=col]:nth-child(3)::after {
  --integer: calc(var(--3));
  content: counter(value) " / " counter(scale);
  counter-reset: value var(--integer);
}

.chaarts.radar [scope=col]:nth-child(4)::after {
  --integer: calc(var(--4));
  content: counter(value) " / " counter(scale);
  counter-reset: value var(--integer);
}

.chaarts.radar [scope=col]:nth-child(5)::after {
  --integer: calc(var(--5));
  content: counter(value) " / " counter(scale);
  counter-reset: value var(--integer);
}

.chaarts.radar [scope=col]:nth-child(6)::after {
  --integer: calc(var(--6));
  content: counter(value) " / " counter(scale);
  counter-reset: value var(--integer);
}

.chaarts.radar [scope=col]:nth-child(7)::after {
  --integer: calc(var(--7));
  content: counter(value) " / " counter(scale);
  counter-reset: value var(--integer);
}
A Chromium bug

There is currently a bug in Chromium — I filled an issue on bugs.chromium.org — when using the border-spacing property on the table: it prevents Chrome to define the dimensions of the table… For Chrome user, use the inspector to uncheck this property on the <table> tag of these examples!

Reduced test case

Switch
Allows you to disable styles on the following table.

Level of interest by domain, out of 20
Accessibility SEO Performance Compatibility Security Code quality Test
14 11 13 16 10 12 4
A Firefox feature
The skew() function deforms the element by tilting it.
Screenshot of the deformation caused by skew() – props to Patrick Brosset who made CSS transform Highlighter happen in Firefox DevTools.

Overlapping radars

Very few changes compared to the previous version:

  1. the <table> element no longer carries the values, but has a new --areas custom property to indicate the number of rows in the table;
  2. however we multiply the number of rows in the body of the table:
    • each one carries several variables: --color then the values — --1, etc.;
    • and contains several cells: a <th scope="row"> row header cell and <td> data cells;
  3. the rest is relatively common now — if you've gone through the previous examples:
    1. a color for each row, presented on the header cells and serving as a background for the data cells;
    2. a distinctive hover effect over each row: the values appear verbatim on hovering, and the hovered row is highlighted. In order not to deprive users who do not have a good hover pointer, this effect is a progressive enhancement based on the @media (hover: hover) { … } media query.

Switch
Allows you to disable styles on the following table.

Level of interest by domain, out of 20
Accessibility SEO Performance Compatibility Security Code quality Test
Gaël 14 11 13 16 14 10 4
Luc 18 10 11 16 10 12 11
HTML
<table class="chaarts radar-multiple" id="radar-multiple" style="--scale: 20; --step: 5; --items: 7; --areas: 2;">
	<caption id="caption-9">[…]</caption>
	<thead>
		<tr>
			<th scope="col">[…]</th>
			<th scope="col">[…]</th>
		</tr>
	</thead>
	<tbody>
		<tr style="--color: var(--chaarts-purple); --1: 14; --2: 11; --3: 13; --4: 16; --5: 14; --6: 10; --7: 4; --8: var(--1);">
			<th scope="row">Gaël</th>
			<td><span>14</span></td>
		</tr>
		<tr style="-color: var(--chaarts-pink); --1: 18; --2: 10; --3: 11; --4: 16; --5: 10; --6: 12; --7: 11; --8: var(--1);">
			<th scope="row">Luc</th>
			<td><span>18</span></td>
		</tr>
	</tbody>
</table>
css
.chaarts.radar-multiple {
  margin-block-end: 12rem;
}

.chaarts.radar-multiple tbody {
  columns: var(--areas);
  vertical-align: bottom;
}

.chaarts.radar-multiple [scope=row] {
  block-size: 2rem;
  bottom: -8rem;
  left: 1rem;
  position: absolute;
}

.chaarts.radar-multiple [scope=row]::before {
  background: var(--color, currentcolor);
  block-size: 1rem;
  content: "";
  display: inline-block;
  inline-size: 1rem;
  margin-inline-end: 0.25rem;
  translate: 0 0.1rem 0;
}

.chaarts.radar-multiple tr:nth-child(2) [scope=row] {
  left: calc(1rem + 100% / var(--areas) * 1);
}

.chaarts.radar-multiple td {
  align-items: flex-end;
  border-color: var(--color, currentcolor);
  display: flex;
  justify-content: flex-end;
  opacity: 0.5;
  pointer-events: none;
  z-index: 0;
}

.chaarts.radar-multiple td::after {
  color: var(--foreground);
  display: block;
  font-size: small;
  font-weight: 700;
  inline-size: 100%;
  text-indent: -0.5rem;
  transform: skew(calc(var(--skew) * -1)) rotate(calc(var(--part) * var(--index, 1) * -1));
  transform-origin: 0 0;
  white-space: nowrap;
}

.chaarts.radar-multiple td:nth-of-type(1)::after {
  --integer: calc(var(--1));
  content: counter(value);
  counter-reset: value var(--integer);
  inline-size: calc(var(--1) * 100% / var(--scale));
}

.chaarts.radar-multiple td:nth-of-type(2)::after {
  --integer: calc(var(--2));
  content: counter(value);
  counter-reset: value var(--integer);
  inline-size: calc(var(--2) * 100% / var(--scale));
}

.chaarts.radar-multiple td:nth-of-type(3)::after {
  --integer: calc(var(--3));
  content: counter(value);
  counter-reset: value var(--integer);
  inline-size: calc(var(--3) * 100% / var(--scale));
}

.chaarts.radar-multiple td:nth-of-type(4)::after {
  --integer: calc(var(--4));
  content: counter(value);
  counter-reset: value var(--integer);
  inline-size: calc(var(--4) * 100% / var(--scale));
}

.chaarts.radar-multiple td:nth-of-type(5)::after {
  --integer: calc(var(--5));
  content: counter(value);
  counter-reset: value var(--integer);
  inline-size: calc(var(--5) * 100% / var(--scale));
}

.chaarts.radar-multiple td:nth-of-type(6)::after {
  --integer: calc(var(--6));
  content: counter(value);
  counter-reset: value var(--integer);
  inline-size: calc(var(--6) * 100% / var(--scale));
}

.chaarts.radar-multiple td:nth-of-type(7)::after {
  --integer: calc(var(--7));
  content: counter(value);
  counter-reset: value var(--integer);
  inline-size: calc(var(--7) * 100% / var(--scale));
}

.chaarts.radar-multiple span {
  --mask: radial-gradient(circle at bottom right, var(--foreground), var(--foreground-o-50));
  background: var(--color, currentcolor);
  mask-image: var(--mask);
  pointer-events: auto;
}

@media (hover: hover) {
  .chaarts.radar-multiple td {
    opacity: 0.25;
    transition: opacity 0.2s var(--move);
  }
  .chaarts.radar-multiple td::after {
    opacity: 0;
    transition: inherit;
  }
  .chaarts.radar-multiple tr:hover td {
    opacity: 1;
    z-index: 1;
  }
  .chaarts.radar-multiple tr:hover td::after {
    opacity: inherit;
  }
}