Dessine-moi un graphique (en CSS)

Suite de « La donnée graphique avec HTML et CSS » donnée à Paris Web 2018 (Viméo).

Slides en ligne
ffoodd.fr/devquest/
Documentation complète
ffoodd.github.io/chaarts

Le principe de moindre pouvoir

C’est un des axiomes du web, proposés par Tim Berners-Lee en 1998 — à côté de kiss, du design modulaire, de la tolérance ou de la décentralisation.

Le mantra de l’amélioration progressive, en somme…

Définition

Un graphique de données est :

  1. un ensemble de clés,
  2. associées à une ou plusieurs valeurs,
  3. disposées sur une échelle,
  4. mises en forme afin d’en faciliter la compréhension.

Vous pourrez en apprendre plus sur la représentation graphique de données sur Wikipédia.

Piqûre de rappel

Le plus important, c’est le contenu — et par extension, sa sémantique.
Pour décrire un ensemble de clés et valeurs en html, il n’existe pas moult choix — en fait, deux :

  1. les listes de définitions : <dl>, <dt> et <dd> ;
  2. les tableaux de données, solution plébiscitée.

Et oui, ça sert à ça en vrai.

Avec en vrac des idées de Trys Mudford, Heydon Pickering, Greg Whitworth, Miriam Suzanne, Andy Bell, Sebastian Shaw, Hidde De vries, Ire Aderinokun, Roman Komarov, Sara Soueidan, Michelle Barker, Cassie Evans, Carter Li, Ana Tudor, Lea Verou, Stereokai et Patrick Brosset…

En résumé :

Image de l’interrupteur permettant de désactiver les styles sur les tableaux, passant du mode « guilleret » au mode « sobre ».

C’est donc accessible, performant, autonome et statique. Mais nous sommes en 2024 : on peut améliorer tout ça ! Lançons-nous avec un camembert interactif !

Le tableau nu

<table>
	<caption>[…]</caption>
	<thead>
	<tr>
		<th scope="col">[…]</th>
		<th scope="col">[…]</th>
	</tr>
	</thead>
	<tbody>
	<tr>
		<th scope="row">HTML</th>
		<td>2 %</td>
	</tr>
	<tr>[…]</tr>
	</tbody>
</table>
Distribution du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
JSON 1 %
Images 44 %
Fontes 17 %
Autre 2 %

Un peu d’habillage

@layer framework, element, chaarts;

@import url("https://bootstrap.min.css") layer(framework);

@layer element {
	table {
		border-collapse: collapse;
		caption-side: top;
		font-feature-settings: "tnum";
		inline-size: 100%;
		margin-block-end: 1.5rem;
		vertical-align: top;
	}

	table > caption:first-child {
		font-style: italic;
	}
}
Distribution du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
JSON 1 %
Images 44 %
Fontes 17 %
Autre 2 %

Un vrai défilé

Technique créée par Lea verou, adaptée aux tableaux par Chen Hui Jing.

@layer element {
	.scrollable-container {
		background:
			local linear-gradient(to right, #fff 30%, #0000) 0 0 / 2.5rem 100% no-repeat,
			local linear-gradient(to right, #0000, #fff 70%) 100% / 2.5rem 100% no-repeat,
			scroll radial-gradient(farthest-side at 0% 50%, #0004, #0000) 0 0 / 1rem 100% no-repeat,
			#fff
			scroll radial-gradient(farthest-side at 100% 50%, #0004, #0000) 100% / 1rem 100% no-repeat;
		max-inline-size: 100%;
		-webkit-overflow-scrolling: touch;
		overflow-x: auto;
		overflow-y: hidden;
	}
}
Distribution du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
JSON 1 %
Images 44 %
Fontes 17 %
Autre 2 %

Un défilé accessible

Pour qu’un élément défilable soit accessible au clavier, on a besoin d’un petit bout de JavaScript.

const regions = document.querySelectorAll('.scrollable-container');
regions.forEach(region => {
	const width = region.offsetWidth;
	const child = region.querySelector('table') || region.querySelector('code');

	if (child.offsetWidth > width) {
		region.setAttribute('tabindex', '0');
	} else {
		if (region.hasAttribute('tabindex')) {
			region.removeAttribute('tabindex');
		}
	}
});

Début de chantier

@layer container {
	.chaarts {
		border-collapse: separate;
		border-spacing: 0;
		caption-side: bottom;
		contain: content;
		empty-cells: hide;
		overflow: hidden;
	}

	.chaarts > caption:first-child {
		font-style: normal;
	}

	.chaarts th,
	.chaarts td	{
		border: 0;
		padding: 0;
	}
}
Distribution du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
JSON 1 %
Images 44 %
Fontes 17 %
Autre 2 %

Devenir légende

@layer chaarts {
	@media screen and (min-width: 60rem) {
		.chaarts tbody {
			display: flex;
		}

		.chaarts tbody tr {
			flex: 1 1 auto;
		}
	}

	.chaarts tbody tr[scope="row"] {
		padding-inline-end: .5rem;
	}

	.chaarts tbody [scope="row"]::before {
		background: var(--color, currentcolor);
		block-size: 1rem;
		content: "";
		display: inline-block;
		inline-size: 1rem;
		transform: translate3d(-.2rem, .1rem, 0);
	}
}
Distribution du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
JSON 1 %
Images 44 %
Fontes 17 %
Autre 2 %

Forger la légende

<table class="chaarts pie">
	<caption>[…]</caption>
	<thead class="sr-only">
		<tr>
			<th scope="col">[…]</th>
			<th scope="col">[…]</th>
		</tr>
	</thead>
	<tbody>
		<tr style="--color: purple;">
			<th scope="row">HTML</th>
			<td>2 %</td>
		</tr>
		<tr>[…]</tr>
	</tbody>
</table>

					
Distribution du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
JSON 1 %
Images 44 %
Fontes 17 %
Autre 2 %

Une palette de couleurs automagique

Sara Soueidan a détaillé la combinaison de hsl() et calc().

.chaarts-container {
	--chaarts-purple: #639;
	--chaarts-pink: #e11a81;
	--chaarts-blue: #0172f0;
	--chaarts-yellow: #fdc02f;
	--chaarts-green: #39ca74;
	--chaarts-red: #ff2d40;
	--chaarts-gray: #585462;
}
.chaarts-container {
	--hue: 270;
	--lightness: 50%;
	--saturation: 100%;
	--chaarts-purple: hsl(var(--hue) calc(var(--saturation) / 2) var(--lightness));
	--chaarts-pink: hsl(calc(var(--hue) + 60) var(--saturation) var(--lightness));
	--chaarts-blue: hsl(calc(var(--hue) - 60) var(--saturation) var(--lightness));
	--chaarts-yellow: hsl(40 var(--saturation) var(--lightness));
	--chaarts-green: hsl(calc(var(--hue) - 120) var(--saturation) var(--lightness));
	--chaarts-red: hsl(calc(var(--hue) + 90) var(--saturation) var(--lightness));
	--chaarts-gray: hsl(0 0% var(--lightness));
}

Démonstration live

Mode sombre / clair

Vous avez compris le principe : tous les arguments de hsl() peuvent être ajustés !
Vous avez remarqué l’absence de virgules ? C’est la notation séparée par des espaces : rgb(0 0 0 / .5).

.chaarts-container {
	color-scheme: light;
	--background-lightness: 100%;
	--foreground-lightness: 0%;
	--background: hsl(0 0% var(--background-lightness));
	--foreground: hsl(0 0% var(--foreground-lightness));
	background-color: var(--background);
	color: var(--foreground);
}

@media (prefers-color-scheme: dark) {
	.chaarts-container {
		color-scheme: dark;
		--background-lightness: 0%;
		--foreground-lightness: 100%;
	}
}

Mode contrastes élevés

Adrian Roselli a partagé une mine d’informations sur les contrastes élevés.

@media (prefers-contrast: more) {
	.chaarts-container {
		--background: Canvas;
		--foreground: CanvasText;
		--chaarts-purple: LinkText;
		--chaarts-pink: CanvasText;
		--chaarts-blue: Highlight;
		--chaarts-yellow: ActiveText;
		--chaarts-green: InactiveBorder;
		--chaarts-red: VisitedText;
		--chaarts-gray: ButtonShadow;
	}
}

Si la couleur n’existait pas…

Basé sur une idée de Trys Mudford pour limiter les requêtes et une recommandation de Chris Coyier (via Charlotte Dann) sur ce qu’il faut encoder — en résumé, seulement l’octhotorpe (#).

.chaarts-container {
	--checkers: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'><g fill='hsl(0 0% 100% / .6)'><path fill-rule='evenodd' d='M0 0h4v4H0V0zm4 4h4v4H4V4z'/></g></svg>");
	--hexagons: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='24' viewBox='0 0 28 49'><g fill-rule='evenodd'><g fill='hsl(0 0% 100% / .6)' fill-rule='nonzero'><path d='M13.99 9.25l13 7.5v15l-13 7.5L1 31.75v-15l12.99-7.5zM3 17.9v12.7l10.99 6.34 11-6.35V17.9l-11-6.34L3 17.9zM0 15l12.98-7.5V0h-2v6.35L0 12.69v2.3zm0 18.5L12.98 41v8h-2v-6.85L0 35.81v-2.3zM15 0v7.5L27.99 15H28v-2.31h-.01L17 6.35V0h-2zm0 49v-8l12.99-7.5H28v2.31h-.01L17 42.15V49h-2z'/></g></g></svg>");
	--triangles: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='8' height='16' viewBox='0 0 36 72'><g fill-rule='evenodd'><g fill='hsl(0 0% 100% / .6)'><path d='M2 6h12L8 18 2 6zm18 36h12l-6 12-6-12z'/></g></g></svg>");
	--zig: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='12' viewBox='0 0 20 12'><g fill-rule='evenodd'><g fill='hsl(0 0% 100% / .6)'><path d='M9.8 12L0 2.2V.8l10 10 10-10v1.4L10.2 12h-.4zm-4 0L0 6.2V4.8L7.2 12H5.8zm8.4 0L20 6.2V4.8L12.8 12h1.4zM9.8 0l.2.2.2-.2h-.4zm-4 0L10 4.2 14.2 0h-1.4L10 2.8 7.2 0H5.8z'/></g></g></svg>");
	--stripes: url("data:image/svg+xml,<svg width='6' height='6' viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'><g fill='hsl(0 0% 100% / .6)' fill-rule='evenodd'><path d='M5 0h1L0 6V5zM6 5v1H5z'/></g></svg>");
	--dots: url("data:image/svg+xml,<svg width='10' height='10' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'><g fill='hsl(0 0% 100% / .6)' fill-rule='evenodd'><circle cx='3' cy='3' r='3'/><circle cx='13' cy='13' r='3'/></g></svg>");
}

.chaarts tr {
	background: var(--color) var(--background);
}

.chaarts tr:nth-of-type(1n + 1) {
 --background: var(--checkers);
}

.chaarts tr:nth-of-type(2n + 2) {
 --background: var(--hexagons);
}

Un peu d’espace

.chaarts {
	--radius: 32rem;

	margin: 0 auto;
	padding-block-start: calc(var(--radius) + 2rem);
	position: relative;
}
Distribution du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
JSON 1 %
Images 44 %
Fontes 17 %
Autre 2 %

Coloniser l’espace

.chaarts td::before {
	background: var(--color, currentcolor) var(--background);
	block-size: var(--radius);
	border-radius: 50%;
	content: '';
	inline-size: var(--radius);
	inset-block-start: calc(var(--radius) / 2);
	inset-inline-start: 50%;
	position: absolute;
	transform: translate3d(-50%, -50%, 0);
	transform-origin: center;
}
Distribution du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
JSON 1 %
Images 44 %
Fontes 17 %
Autre 2 %

Ça va couper

Découper la tarte

Vous connaissez l’inspecteur de Firefox ?

.chaarts td::before {
	clip-path: polygon(
		50% 50%,
		50% 0%,
		100% 0%,
		93.8153% 25.9123%
	);
}
Distribution du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
JSON 1 %
Images 44 %
Fontes 17 %
Autre 2 %

Cherchez l’angle

Pour trouver le point sur le cercle :

  1. on identifie le triangle rectangle formé par le rayon du cercle, son centre, et notre point cible ;
  2. on connait son hypoténuse — le rayon ;
  3. mais également l’angle opposé — qu’on convertit en radians en le multipliant par 0.01745329251, donc…
  4. on peut appliquer la loi des sinus pour trouver la longueur des deux autres côtés ! 😇
Distribution du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
JSON 1 %
Images 44 %
Fontes 17 %
Autre 2 %

Les fonctions mathématiques

En 2018 🥵

--sin-α-1: var(--α);
--sin-α-2: calc((var(--α) * var(--α) * var(--α)) / 6);
--sin-α-3: calc((var(--α) * var(--α) * var(--α) * var(--α) * var(--α)) / 120);
--sin-α-4: calc((var(--α) * var(--α) * var(--α) * var(--α) * var(--α) * var(--α) * var(--α)) / 5040);
--sin-α-5: calc((var(--α) * var(--α) * var(--α) * var(--α) * var(--α) * var(--α) * var(--α) * var(--α) * var(--α)) / 362880);
/* Finalement ! */
--sin-α: calc(var(--sin-α-1) - var(--sin-α-2) + var(--sin-α-3) - var(--sin-α-4) + var(--sin-α-5));
						

En 2024 🤯

--sin-α: sin(var(--a));
						

Placer le point

Pour vérifier vos résultats, window.getComputedStyle(temp0, ':before').clipPath dans la console !

.chaarts {
	--to-radians: 0.01745329251;
}

.chaarts td::before {
	--part: calc(var(--value) * 3.6);
	--β: calc(var(--part) * var(--to-radians));
	--α: calc((90 - var(--part)) * var(--to-radians));
	--pos-B: calc(sin(var(--β)) * 50%);
	--pos-A: calc(sin(var(--α)) * 50%);
	clip-path: polygon(
		50% 50%,
		50% 0%,
		100% 0%,
		calc(50% + var(--pos-B)) calc(50% - var(--pos-A))
	);
}
Distribution du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
JSON 1 %
Images 44 %
Fontes 17 %
Autre 2 %

Placer les points

Les décalages par rapport au centre sont différents selon la tranche de valeur.

Nous avons quatre cadrants différents à gérer — mais un seul polygone…
Certains points doivent donc basculer d’une position à une autre selon la valeur.

Il nous faudra dix points, au total.

Un booléen en CSS

Basé sur une idée de Roma Komarov.

Des drapeaux

.chaarts td {
	--lt-25: clamp(0, 25 - var(--value), 1);
	--gt-25: calc(1 - var(--lt-25));
}

Des conditions

.chaarts td::before {
	clip-path: polygon(…
		calc(var(--gt-25, 0) * 100%)
	…);
}

Des comparaisons

.chaarts td::before {
	clip-path: polygon(…
		max(50%, var(--gt-25, 0) * 100%)
		max(50%, var(--gt-25, 0) * 100%)
	…);
}
Distribution du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
JSON 1 %
Images 44 %
Fontes 17 %
Autre 2 %

Distribution des parts

Côté HTML

<table style="--1: 2; --2: 2; --3: 32; --4: 1; --5: 44; --6: 17; --7: 2;">
	<tbody>
		<tr>
			<td style="--value: 17;">
						

Côté CSS

.chaarts.pie tr:first-child {
  --value: var(--1);
  --position: 0turn;
}

.chaarts.pie tr:nth-of-type(4n + 4) {
  --value: var(--4);
  --position: calc((var(--1) + var(--2) + var(--3)) * .01turn);
}

.chaarts td::before {
	transform:
		translate3d(-50%, -50%, 0)
		rotate(var(--position)
	);
}
Distribution du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
JSON 1 %
Images 44 %
Fontes 17 %
Autre 2 %

Au survol

.chaarts {
	--enter: cubic-bezier(0, .5, .5, 1);
	--exit: cubic-bezier(.5, 0, 1, .5);
	--move: cubic-bezier(.5, 0, .5, 1);
}

.chaarts td::before {
	--zoom: .75;
	transform:
		translate3d(-50%, -50%, 0)
		rotate(var(--position))
		scale(var(--zoom));
	transition: transform .2s var(--move);
}

.chaarts tr:hover td::before {
	--zoom: .8;
}

.chaarts tbody:hover tr {
	opacity: .5;
}

.chaarts tbody:hover tr:hover {
	opacity: 1;
}
Distribution du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
JSON 1 %
Images 44 %
Fontes 17 %
Autre 2 %

Encore plus d’altitude

Tiré d’une réponse d’Ana Tudor sur la distribution autour d’un cercle.

.chaarts td::after {
	--axis: calc(
		var(--position) - .25turn
	  + var(--value) * .005turn
	);
	--away: calc(var(--radius) / 2 - 1rem);

	background-color: var(--foreground-lighter);
	color: var(--background-lighter);
	content: "";
	opacity: 0;
	padding: .5rem;
	pointer-events: none;
	transform:
		translate3d(-50%, -50%, 0)
		rotate(var(--axis))
		translate(var(--away))
		rotate(calc(var(--axis) * -1))
		perspective(1000px)
		rotate3d(1, 0, 0, 45deg);
	transform-origin: 50% calc(100% + 10px);
	transition: opacity .2s var(--enter),
	            transform .2s var(--enter);
	z-index: 3;
}
Distribution du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
JSON 1 %
Images 44 %
Fontes 17 %
Autre 2 %

Conversion de types

Basé sur une idée de Cassie Evans pour convertir un entier en chaîne.

@property --integer {
	inherits: false;
	initial-value: 0;
	syntax: "<integer>";
}

.chaarts td::after {
	--integer: calc(var(--value));

	content: var(--term) "\A0: " counter(value) "\A0%";
	counter-reset: value var(--integer);
}
Distribution du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
JSON 1 %
Images 44 %
Fontes 17 %
Autre 2 %

Pourquoi pas conic-gradient()

Ça nous épargnerait la trigonométrie, les conditions et comparaisons. Oui, mais…

.chaarts td::before {
	mask-image: conic-gradient(#ffff var(--value), #0000 0);
}
Distribution du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
JSON 1 %
Images 44 %
Fontes 17 %
Autre 2 %

Le RTL grâce aux propriétés logiques

Pour nous, l’axe block est l’axe vertical et l’axe inline est l’axe horizontal.
Image tirée de l’article Logical properties sur web.dev
Distribution du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
JSON 1 %
Images 44 %
Fontes 17 %
Autre 2 %

Quelques ajustements

Ahmad Shadeed détaille les subtilités du RTL sur un site dédié.

[dir="rtl"] .chaarts {
	--is-rtl: -1;
}

[dir="rtl"] .chaarts [scope="row"]::before {
	translate: calc(-.2rem * var(--is-rtl, 1)) .1rem 0;
}

[dir="rtl"] .chaarts td::before {
	transform:
		translate3d(calc(-50% * var(--is-rtl, 1)), -50%, 0)
		rotate(var(--position))
		scale(var(--zoom));
}
Distribution du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
JSON 1 %
Images 44 %
Fontes 17 %
Autre 2 %

Devenir donut

.chaarts {
	--offset: 8rem;
	--mask: radial-gradient(
		circle at 50% calc(50% - 2rem),
		#0000 0 var(--offset),
		#ffff calc(var(--offset) + 1px) 100%
	);
	mask-image: var(--mask);
}

.chaarts td::after {
	--away: calc(var(--radius) / 2 - 2.5rem);
}
Distribution du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
JSON 1 %
Images 44 %
Fontes 17 %
Autre 2 %

Devenir polaire

.chaarts td::before,
.chaarts tr:hover td::before {
	--zoom: 50;
	transform:
		translate3d(calc(-50% * var(--is-rtl, 1)), -50%, 0)
		rotate(var(--position))
		scale(calc((var(--zoom) + (var(--value) / (100 / var(--zoom)))) / 100));
}
Distribution du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
JSON 1 %
Images 44 %
Fontes 17 %
Autre 2 %

Interrupteur de styles

Adrian Roselli a découvert que tripatouiller le display des tableaux abîme sa sémantique exposée — c’est pourquoi j’implémente le composant inclusif du toggle button conçu par Heydon Pickering pour désactiver les styles à volonté.

const switches = document.querySelectorAll('[role="switch"]');
switches.forEach(el => {
	el.addEventListener('click', () => {
		const checked = el.getAttribute('aria-checked') === 'true' || false;
		el.setAttribute('aria-checked', !checked);

		if (el.classList.contains('disable-css')) {
			const chart = el.parentNode.nextElementSibling;
			chart.classList.toggle('chaarts');
		}
	});
});

Interrupteur
Désactiver les styles sur le tableau ci-après.

Distribution du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
JSON 1 %
Images 44 %
Fontes 17 %
Autre 2 %

Vers l’infini et l’au-delà

Il y a d’autres types de graphiques sur chaarts, basés sur d’autres astuces et avec d’autres opportunités d’améliorations !

Rappelez-vous : ce n’est que du CSS !

Merci

Et à bientôt

Crédits

Laissez votre feedback OpenFeedback