Diagrammes circulaires flexibles avec CSS et SVG

Lea Verou vient de publier un livre extraordinaire proposant des techniques CSS et SVG originales. Cet article est technique, mais l'approche, la méthode, la créativité sont une sacrée source d'inspiration!

Par

Les diagrammes circulaires (a.k.a. diagrammes en camembert), même dans une bichromie la plus basique, n’ont jamais été la chose la plus facile à créer avec les technologies du web, bien qu’extrêmement courants dans l’information, depuis la présentation de simples statistiques jusqu’aux indicateurs de progression et aux timers. Les implémentations font généralement appel à un éditeur d’images externe pour créer des images multiples pour les diverses valeurs du diagramme ou des frameworks JavaScript conçus pour des diagrammes bien plus complexes.

Bien que l’exploit ne soit pas aussi impossible qu’autrefois, il n’existe toujours pas de solution simple. Pourtant, il y a des façons d’y parvenir aujourd’hui, meilleures et plus aisées à maintenir.

Solution basée sur CSS transform

Cette solution est la meilleure en termes de markup : elle ne nécessite qu’un seul élément et le reste est l’affaire de pseudo-éléments, de transformations et de dégradés css. Commençons avec un élément simple :

<div class="pie"></div>

Pour l’instant, supposons que nous voulions un diagramme circulaire qui affiche le pourcentage 20% (codé en dur). Nous le rendrons flexible plus tard. Appliquons un style à notre élément pour en faire un cercle qui sera notre background (Figure 1).

Figure 1 : notre point de départ (un diagramme montrant 0%)
.pie {
  width: 100px; height: 100px;
  border-radius: 50%;
  background: yellowgreen;
}

Notre diagramme sera vert (plus exactement yellowgreen) et le marron (#655) montrera le pourcentage. Nous pourrions être tentés d’utiliser des transformations de type skew pour la partie du pourcentage, mais après avoir expérimenté sur ce terrain, la solution s’avère compliquée. À la place, nous allons colorer les parties gauche et droite de notre cercle en deux couleurs et utiliser un pseudo-élément tournant pour dévoiler le pourcentage dont nous avons besoin.

Pour colorer en marron la partie droite de notre cercle, nous utilisons un simple dégradé linéaire :

background-image: linear-gradient(to right, transparent 50%, #655 0);
Figure 2 : Coloration de la partie droite de notre cercle, avec un dégradé linéaire.

Comme le montre la Figure 2, c’est tout ce dont nous avons besoin. Maintenant, nous pouvons continuer à styler le pseudo-élément qui agira comme un masque :

.pie::before {
  content: ’’;
  display: block;
  margin-left: 50%;
  height: 100%;
}
Figure 3 : Le pseudo-élément qui servira de masque est montré ici par une ligne pointillée.

Vous pouvez voir dans la Figure 3 où se trouve notre pseudo-élément par rapport à l’élément diagramme. Actuellement, il n’est pas stylé et ne recouvre rien, ce n’est qu’un rectangle invisible. Pour lui donner un style, commençons par quelques observations :

  • Nous voulons qu’il recouvre la partie marron de notre cercle, par conséquent nous devons lui donner un background vert, en utilisant background-color: inherit pour éviter la duplication de code puisque nous voulons qu’il ait la même couleur que son élément parent.
  • Nous voulons qu’il tourne autour du centre du cercle, qui se trouve au milieu du côté gauche du pseudo-élément, donc nous devons lui appliquer une transform-origin de 0 50%, ou simplement left.
  • Nous ne voulons pas que ce soit un rectangle car il dépasserait des bords du diagramme, donc nous devons soit appliquer un overflow: hidden, soit lui donner un border-radius approprié pour le transformer en demi-cercle.

Si nous prenons tous ces éléments en compte, voici ce à quoi pourrait ressembler notre pseudo-élément :

.pie::before {
  content: ’’;
  display: block;
  margin-left: 50%;
  height: 100%;
  border-radius: 0 100% 100% 0 / 50%;
  background-color: inherit;
  transform-origin: left;
}
Figure 4 : Notre pseudo-élément (représenté en pointillés) après application des styles

Note : Faites attention de ne pas utiliser background: inherit à la place de background-color: inherit, sinon le dégradé sera également hérité !

Notre diagramme a donc l’apparence qu’on lui voit dans la Figure 4. C’est ici que nous allons commencer à nous amuser ! Nous pouvons commencer à faire tourner le pseudo-élément en lui appliquant une transformation rotate(). Pour les 20% que nous voulions montrer, nous pouvons utiliser une valeur de 72deg (0.2 × 360 = 72), ou .2turn, qui est plus facile à lire. La Figure 5 montre ce que cela donne , également avec d’autres valeurs :

Figure 5 : Notre diagramme simple montrant différents pourcentages ; de gauche à droite, on a 10% (36deg ou .1turn), 20% (72deg ou .2turn), 40% (144deg ou .4turn)

Nous pourrions penser que nous avons fait l’essentiel, malheureusement ce n’est pas si simple. Notre diagramme circulaire fonctionne bien tant qu’on affiche des pourcentages de 0 à 50%, mais si nous essayons d’afficher 60% (en utilisant .6turn) on obtient le résultat de la Figure 6. Ne perdez pas espoir, nous allons réparer cela très vite !

Figure 6: Notre diagramme ne fonctionne plus pour les pourcentages supérieurs à 50% (ici : 60%)

Si nous considérons les pourcentages de 50% à 100% comme un problème à part, nous pouvons remarquer qu’il nous est possible de leur appliquer une version inversée de notre solution précédente : un pseudo-élément marron, qui tourne de 0 à 0.5turn respectivement. Donc, pour un affichage de 60%, le code du pseudo-élément pourrait être :

.pie::before {
  content: ’’;
  display: block;
  margin-left: 50%;
  height: 100%;
  border-radius: 0 100% 100% 0 / 50%;
  background: #655;
  transform-origin: left;
  transform: rotate(.1turn);
}
Figure 7 : Notre diagramme à 60% maintenant correct

Vous pouvez le voir en action dans la Figure 7. Maintenant que nous avons trouvé un moyen d’afficher n’importe quel pourcentage, nous pouvons même animer le diagramme circulaire de 0% à 100% avec les animations CSS, par exemple pour créer un indicateur de progression :

@keyframes spin {
  to { transform: rotate(.5turn); }
}

@keyframes bg {
  50% { background: #655; }
}

.pie::before {
  content: ’’;
  display: block;
  margin-left: 50%;
  height: 100%;
  border-radius: 0 100% 100% 0 / 50%;
  background-color: inherit;
  transform-origin: left;
  animation: spin 3s linear infinite,
             bg 6s step-end infinite;
}

Voir le diagramme animé sur Dabblet.

Tout ceci est parfait, mais comment pourrions-nous styler plusieurs diagrammes statiques présentant des pourcentages différents, ce qui est le cas d’usage le plus fréquent ? Idéalement nous voulons pouvoir écrire ceci :

<div class="pie">20%</div>
<div class="pie">60%</div>

…et obtenir deux diagrammes, l’un montrant 20%, l’autre 60%. Nous allons d’abord voir comment nous pourrions y parvenir avec des styles en ligne, puis nous verrons s'il ne vaut pas mieux écrire un petit script pour parser le contenu du texte afin d’ajouter les styles. Préférable pour l'élégance du code, l'encapsulation, la maintenabilité et peut-être de manière plus importante encore, pour l'accessibilité.

Le défi auquel nous sommes confrontés lorsque nous voulons contrôler le pourcentage du diagramme avec des styles en ligne est que le code CSS qui se charge de gérer le pourcentage est appliqué au pseudo-élément. Et comme vous le savez, on ne peut pas appliquer de styles en ligne sur les pseudo-éléments, donc... il va nous falloir faire preuve d’inventivité.

Note : Vous pouvez utiliser la même technique pour d’autres cas où vous voulez utiliser des valeurs comprises dans un spectre sans répétition ni calculs complexes, ainsi que pour debugger des animations en les faisant avancer pas à pas. Voir un exemple simple de cette technique.

La solution est plutôt inattendue. Nous allons utiliser l’animation que nous venons de présenter, mais elle sera en mode pause. Plutôt que de la lancer comme une animation normale, nous allons utiliser des retards d’animation négatifs pour avancer de manière statique vers n’importe quel point de l’animation et y rester. Ça vous paraît confus ? Et pourtant les animation-delay négatifs sont non seulement autorisés par la spécification, mais ils s’avèrent très pratiques dans des cas comme le nôtre.

Un retard négatif est valide. Comme un retard de 0s, il signifie que l’animation est exécutée immédiatement, mais elle commence à la valeur absolue du retard, comme si l’animation avait commencé depuis ce temps dans le passé — et par conséquent elle semble démarrer à mi-chemin de sa durée active (CSS Animations Level 1).

Puisque notre animation est en pause, la première image (définie par notre animation-delay) sera la seule affichée. Le pourcentage montré sur le diagramme sera un pourcentage de la durée totale de notre animation-delay. Par exemple, avec la durée totale actuelle de 6s, nous aurions besoin d’un animation-delay de -1.2s pour afficher un pourcentage de 20%. Pour simplifier les calculs, nous allons fixer une durée de 100s. N’oubliez pas que l’animation est en pause éternelle, par conséquent la durée que nous spécifions n’a aucun autre effet.

Il nous reste un dernier problème : l’animation est sur le pseudo-élément, mais nous voulons mettre un style en ligne sur l’élément .pie. Toutefois, comme il n’y a pas d’animation sur l’élément <div>, nous pouvons régler l’ animation-delay sur le pseudo-élément. Pour résumer tout cela, notre markup pour les diagrammes de 20% et 60% ressemble à ceci :

<div class="pie"
     style="animation-delay: -20s"></div>
<div class="pie"
     style="animation-delay: -60s"></div>

Et le code CSS devient maintenant (sans les règles relatives à .pie qui restent inchangées) :

@keyframes spin {
  to { transform: rotate(.5turn); }
}

@keyframes bg {
  50% { background: #655; }
}

.pie::before {
  /* [Le reste du style est inchangé] */
  animation: spin 50s linear infinite,
             bg 100s step-end infinite;
  animation-play-state: paused;
  animation-delay: inherit;
}

À partir d’ici, nous pouvons convertir le markup pour utiliser les pourcentages comme contenu, comme nous le voulions, et ajouter les styles d’ animation-delay en ligne via un script simple :

$$(’.pie’).forEach(function(pie) {
  var p = parseFloat(pie.textContent);
  pie.style.animationDelay = ’-’ + p + ’s’;
});

Comme vous le voyez, nous laissons le texte intact car nous en avons besoin pour l’accessibilité. Maintenant, nos diagrammes ressemblent à la Figure 8.

20%
60%
Figure 8 : Notre texte, avant qu’il ne soit caché

Nous devons cacher le texte, ce que nous pouvons faire (en respectant l’accessibilité) via color: transparent, le texte peut être sélectionné et imprimé. Dernier petit perfectionnement, nous pouvons centrer le pourcentage dans le diagramme. Pour cela, nous devons :

  • Convertir la hauteur height du diagramme en line-height (ou ajouter une line-height égale à height mais c’est une duplication de code inutile car la valeur de line-height serait reprise par la hauteur height calculée).
  • Dimensionner et positionner le pseudo-élément via un positionnement absolu, de façon à ce qu’il ne tire pas le texte vers le bas.
  • Ajouter text-align: center pour centrer le texte horizontalement.

Voici le code final :

.pie {
  position: relative;
  width: 100px;
  line-height: 100px;
  border-radius: 50%;
  background: yellowgreen;
  background-image:
    linear-gradient(to right, transparent 50%, #655 0);
  color: transparent;
  text-align: center;
}

@keyframes spin {
  to { transform: rotate(.5turn); }
}
@keyframes bg {
  50% { background: #655; }
}

.pie::before {
  content: ’’;
  position: absolute;
  top: 0; left: 50%;
  width: 50%; height: 100%;
  border-radius: 0 100% 100% 0 / 50%;
  background-color: inherit;
  transform-origin: left;
  animation: spin 50s linear infinite,
             bg 100s step-end infinite;
  animation-play-state: paused;
  animation-delay: inherit;
}

Voir le diagramme statique et le code sur Dabblet.


Solution SVG

SVG simplifie la plupart des tâches graphiques et les diagrammes n’y font pas exception. Cependant, plutôt que de créer un diagramme circulaire avec des chemins, ce qui ferait appel à des formules mathématiques complexes, nous allons utiliser une petite astuce.

Commençons avec un cercle :

<svg width="100" height="100">
  <circle r="30" cx="50" cy="50" />
</svg>

Appliquons lui un style de base :

//CSS
circle {
  fill: yellowgreen;
  stroke: #655;
  stroke-width: 30;
}

Note : Comme vous le savez sans doute, ces propriétés CSS sont également disponibles en tant qu’attributs de l’élément SVG, ce qui peut être pratique pour la portabilité.

Figure 9 : Notre point de départ : un cercle SVG vert avec un trait épais de couleur #655 marron

La Figure 9 montre notre cercle et son trait (stroke) épais. Les propriétés des traits en SVG ne se limitent pas à stroke et stroke-width, il y en a bien d’autres, dont certaines insuffisamment connues. L’une d’entre elles est stroke-dasharray qui sert à à créer des traits pointillés. Par exemple, nous pourrions l’utiliser ainsi :

stroke-dasharray: 20 10;
Figure 10 : Un simple pointillé, créé avec stroke-dasharray

Le code signifie que nous voulons des traits de longueur 20 séparés par un gap de longueur 10 comme ceux de la Figure 10.

Vous vous demandez peut-être ce que cette intro rapide aux traits SVG a à voir avec les diagrammes en camembert. Cela deviendra plus clair lorsque nous appliquerons un trait avec un tiret ayant une longueur de 0 et une distance de séparation supérieure ou égale à la circonférence de notre cercle (C = 2π, c’est à dire dans notre cas C = 2π × 30 ≈ 189).

stroke-dasharray: 0 189;

Figure 11 : Plusieurs stroke-dasharray valeurs et leur effet respectif, de gauche à droite :0 189; 40 189; 95 189; 150 189

Comme vous pouvez le voir dans le premier cercle de la Figure 11, cela supprime complètement tous les traits pour ne nous laisser que le cercle vert. Mais ça commence à devenir intéressant lorsque nous augmentons la première valeur : du fait que le gap (la distance séparant chaque tiret à l’intérieur du pointillé) est si long, nous n’obtenons plus un trait pointillé mais juste un trait qui couvre le pourcentage de la circonférence du cercle que nous lui spécifions.

Vous avez deviné où tout cela nous mène. Si nous réduisons le rayon de notre cercle jusqu’à ce qu’il soit complètement recouvert par le trait, nous obtenons quelque chose qui ressemble à s’y méprendre à un diagramme circulaire. Par exemple, vous pouvez voir dans la Figure 12 ce que cela donne pour un rayon de 25 et une stroke-width de 50 :

Figure 12: Notre graphique SVG commence à ressembler à un diagramme circulaire

N’oubliez pas : les traits SVG sont à moitié à l’intérieur et à moitié à l’extérieur de l’élément auquel ils s’appliquent. Dans le futur, nous pourrons contrôler ce comportement.

<svg width="100" height="100">
  <circle r="25" cx="50" cy="50" />
</svg>

Et notre CSS :

circle {
  fill: yellowgreen;
  stroke: #655;
  stroke-width: 50;
  stroke-dasharray: 60 158; /* 2π × 25 ≈ 158 */
}

À partir d’ici, transformer le graphique en diagramme circulaire similaire à ceux que nous avons vus précédemment est assez facile. Nous avons juste à ajouter un cercle vert plus grand sous le trait et à le faire tourner de 90° dans le sens des aiguilles d’une montre pour qu’il commence au centre et au sommet. L’élément <svg> étant aussi un élément HTML, nous pouvons le styler ainsi :

svg {
  transform: rotate(-90deg);
  background: yellowgreen;
  border-radius: 50%;
}
Figure 13 : Le diagramme circulaire final en SVG

La Figure 13 montre le résultat final. Avec cette technique, il est encore plus facile d’animer le diagramme de 0% à 100%. Il suffit de créer une animation CSS qui anime stroke-dasharray de 0 158 à 158 158 :

@keyframes fillup {
  to { stroke-dasharray: 158 158; }
}

circle {
  fill: yellowgreen;
  stroke: #655;
  stroke-width: 50;
  stroke-dasharray: 0 158;
  animation: fillup 5s linear infinite;
}

Petite amélioration supplémentaire : nous pouvons spécifier un certain rayon de façon à ce que la longueur de sa circonférence soit (infinitésimalement proche) de 100, ainsi les longueurs de stroke-dasharray seront données en pourcentages sans avoir à faire de calculs supplémentaires. La circonférence est égale à 2πr donc notre rayon doit être égal à 100 ÷ 2πr ≈ 15,915 ce que nous pouvons arrondir à 16. Nous allons aussi spécifier les dimensions du SVG dans l’attribut viewBox à la place des attributs width et height afin qu’il s’ajuste à la taille de son container.

Après ces modifications, le markup du diagramme de la Figure 13 devient :

<svg viewBox="0 0 32 32">
  <circle r="16" cx="16" cy="16" />
</svg>

…et le CSS :

svg {
  width: 100px; height: 100px;
  transform: rotate(-90deg);
  background: yellowgreen;
  border-radius: 50%;
}

circle {
  fill: yellowgreen;
  stroke: #655;
  stroke-width: 32;
  stroke-dasharray: 38 100; /* for 38% */
}

Remarquez à quel point il est simple de changer le pourcentage. Bien sûr, même avec cette simplification, nous n’avons pas envie de répéter tout ce markup SVG pour chaque diagramme. Il est temps de faire appel à JavaScript pour une petite automatisation. Nous allons écrire un script simple qui va chercher un HTML facile comme celui-ci :

<div class="pie">20%</div>
<div class="pie">60%</div>

…et ajoute un SVG en ligne à l’intérieur de chaque élément .pie avec tous les éléments et attributs nécessaires. Il ajoutera également un élément <title> pour l’accessibilité, afin que les utilisateurs de lecteurs d’écran puissent savoir quel pourcentage est affiché. Voici le script final :

$$(’.pie’).forEach(function(pie) {
  var p = parseFloat(pie.textContent);
  var NS = "http://www.w3.org/2000/svg";
  var svg = document.createElementNS(NS, "svg");
  var circle = document.createElementNS(NS, "circle");
  var title = document.createElementNS(NS, "title");
  circle.setAttribute("r", 16);
  circle.setAttribute("cx", 16);
  circle.setAttribute("cy", 16);
  circle.setAttribute("stroke-dasharray", p + " 100");
  svg.setAttribute("viewBox", "0 0 32 32");
  title.textContent = pie.textContent;
  pie.textContent = ’’;
  svg.appendChild(title);
  svg.appendChild(circle);
  pie.appendChild(svg);
});

Et voilà ! Vous pensez peut-être que la méthode CSS est meilleure parce que son code est plus simple et moins étrange. Cependant, la méthode SVG présente des avantages par rapport à la solution en pur CSS :

  • Il est très simple d’ajouter une troisième couleur : il suffit d’ajouter un cercle réalisé avec un trait et de décaler son trait avec stroke-dashoffset. Une autre possibilité est d’ajouter sa longueur à celle du cercle précédent (en-dessous). Comment imagineriez-vous d’ajouter une troisème couleur à un diagramme réalisé avec la première solution ?
  • Aucun souci à se faire pour l’impression car les éléments SVG sont considérés comme du contenu et sont imprimés comme les éléments <img>. La première solution repose sur les backgrounds et du coup ne sera pas imprimée.
  • Nous pouvons modifier les couleurs avec des styles en ligne, ce qui signifie que nous pouvons facilement les changer via un script (par exemple en fonction d’un input utilisateur). la première solution repose sur les pseudo-éléments qui n’acceptent pas de styles en ligne excepté via héritage, ce qui ne convient pas toujours.

Voir les diagrammes SVG sur Dabblet.

Voir les spécifications en fin d’article.


Les diagrammes circulaires du futur

Les dégradés coniques seraient extrêmement utiles ici aussi. Tout ce dont nous aurions besoin pour un diagramme circulaire serait un élément circulaire et un dégradé conique à deux stops couleur. Par exemple, le diagramme 40% de la Figure 5 serait aussi simple que ceci :

.pie {
  width: 100px; height: 100px;
  border-radius: 50%;
  background: conic-gradient(#655 40%, yellowgreen 0);
}

De plus, une fois que la fonction attr() définie dans CSS Values Level 3 sera mise à jour et largement implémentée, nous pourrons contrôler les pourcentages avec un simple attribut HTML :

background: conic-gradient(#655 attr(data-value %), yellowgreen 0);

Cette solution facilite grandement l’ajout d’une troisième couleur. Par exemple, pour un diagramme comme ci-dessus, il suffirait d’ajouter deux autres stops couleur :

background: conic-gradient(deeppink 20%, #fb3 0, #fb3 30%, yellowgreen 0);

Note de l’éditeur (Smashing Magazine) : Vous pouvez utiliser les dégradés coniques dès aujourd’hui grâce au polyfill de Lea, publié peu après sa conférence à SmashingConf.


Intéressé par CSS ? Retrouvez une liste des meilleurs articles et ressources du web.

Tous les articles sur CSS parus dans la Cascade.

Intéressé par SVG ? Retrouvez une liste des meilleurs articles et ressources du web.

Tous les articles sur SVG parus dans la Cascade.

Du même auteur, dans la Cascade :

Les motifs CSS3

Spécifications (en anglais) :

Retour à l’article   ↩︎


original paru le dans Smashing Magazine.

Sur l’auteur : est actuellement chercheur en interaction homme-machine à MIT SAIL. Elle a auparavant écrit un ouvrage avancé sur CSS pour O’Reilly (CSS Secrets) et a travaillé pour le W3C. Elle a une passion de longue date pour les standards de l’open web, et elle est une des expertes invitées du CSS Working Group. Elle est responsable de plusieurs projets open source et applications web populaires, tels que Prism, Dabblet et -prefix-free et elle tient un blog technique à lea.verou.me. Malgré son parcours académique en informatique, Lea est une de ces inadaptées qui aiment autant le code que le design. Vous pouvez la suivre sur Twitter.

Traduit avec l’aimable autorisation de Smashing Magazine et de l’auteur.
Copyright Smashing Magazine © 2015.