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:
-
on the
<table>
element, the circular scale is created using two successiverepeating-radial-gradient()
— depending on the --scale and --step variables; -
each element
<th>
is moved outside the circle thus formed, thanks to a technique borrowed from Ana Tudor, who explains it on StackOverflow; -
then — still on header cells — the value corresponding to each column
is displayed in a pseudo-element
::after
using a trick from Cassie Evans, usingcounter-reset
andcounter()
to display a numerical variable in thecontent
property. Howevercounter-reset
only works with integers, and our value might be a number. So we rely on Carter Li's ruse usingcalc()
to convert numbers to integers— and@property
for Chromium-based browsers. Boum! -
Then the magic works on the
<td>
elements:- each of which is adjusted to form a square with a side equal to the radius of the circle on the background;
- then transformed to represent a portion of the circle according to the number of elements — specified with --items — using a trick shared by Sara Soueidan on Codrops,
- each one is decorated with a border at the bottom;
-
then we use again
clip-path
polygon()
function on each direct child<span>
— extended to occupy the whole surface of its parent<td>
— in order to form a triangle, based for one side on the ratio value of the current element / scale, and on another side a ratio based on the value of the next element (yumcalc()
) — but on another scale… -
because to compensate for the distortion
due to the
skew()
function, we need to correct the scale on which the second ratio is calculated using a little trigonometry:- we know one side of the right-angled triangle obtained after the deformation, as well as two angles — the right one, of course, and we deduce the second from the angle used to deform the element;
- so we can calculate the hypothenuse using the sine law, — as before in the pie chart;
- and finally, all we have to do is calculate the ratio between the initial dimension — the side — and the final dimension — the hypothenuse — and apply this ratio to the scale on which the second point of the polygon is placed.
-
The third point of the polygon is the bottom right corner, whose coordinates are
100% 100%
;
- one last trick is necessary to close the shape you have seen, we use the current and next value for each element. But when we get to the last element, there is no next! So we need to add a value, to which we assign the first value — in this example, --8: var(--1);.
And that's it!
Switch
Allows you to disable styles on the following table.
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.
Accessibility | SEO | Performance | Compatibility | Security | Code quality | Test |
---|---|---|---|---|---|---|
14 | 11 | 13 | 16 | 10 | 12 | 4 |
A Firefox feature
Overlapping radars
Very few changes compared to the previous version:
-
the
<table>
element no longer carries the values, but has a new --areas custom property to indicate the number of rows in the table; -
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;
-
the rest is relatively common now — if you've gone through the previous examples:
- a color for each row, presented on the header cells and serving as a background for the data cells;
-
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.
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;
}
}