La donnée graphique avec HTML et CSS

Retrouvez les slides en ligne sur
ffoodd.fr/web2day
et la documentation complète sur
ffoodd.github.io.

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.

Un diagramme en barres

  
<table style="--scale: 3000">
  <caption>Temps de chargement pour ffoodd.fr</caption>
  <thead>
    <tr>
      <td></td>
      <th scope="col">Temps de chargement cumulé</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Trafic HTTP terminé</th>
      <td style="--value: 2980">
        <span>2980 ms</span>
      </td>
    </tr>
    <tr>[…]</tr>
  </tbody>
</table>
  

Le tableau nu

Temps de chargement pour ffoodd.fr
Temps de chargement cumulé
Temps : backend ms
Temps : Frontend 96 ms
Délai : premier octet 102 ms
Délai : dernier octet 129 ms
Délai : première image 188 ms
Délai : premier CSS 194 ms
Délai : premier JS 326 ms
DOM Interactif 836 ms
Chargement du DOM 836 ms
DOM complet 2561 ms
Trafic HTTP terminé 2980 ms

Un peu d’habillage

  
@media screen and (min-width: 30em) {
  table {
    border-collapse: collapse;
    border-spacing: 0;
    caption-side: bottom;
    contain: content;
    empty-cells: hide;
    font-feature-settings: "tnum";    
  }
  
  th,
  td {
    border: 0;
    padding: 0;
  }
}

Le tableau en slip

Temps de chargement pour ffoodd.fr
Temps de chargement cumulé
Temps : backend ms
Temps : Frontend 96 ms
Délai : premier octet 102 ms
Délai : dernier octet 129 ms
Délai : première image 188 ms
Délai : premier CSS 194 ms
Délai : premier JS 326 ms
DOM Interactif 836 ms
Chargement du DOM 836 ms
DOM complet 2561 ms
Trafic HTTP terminé 2980 ms

Un habillage structuré

Basé sur un article de Miriam Suzanne sur CSS Tricks.

  
@supports (grid-template-columns: repeat(var(--scale, 100), minmax(0, 1fr))) {
    tr {
      display: grid;
      grid-auto-rows: 1fr;  
      grid-row-gap: .5rem;
      grid-template-columns: 
        minmax(min-content, 15em) 
        repeat(var(--scale, 100), minmax(0, 1fr)) 
        10ch;
    }

    th {
      grid-column: 1 / 1;
    }

    td {
      grid-column: 2 / var(--value, 0);
    }
}
  

Le tableau en jeans

Temps de chargement pour ffoodd.fr
Temps de chargement cumulé
Temps : backend ms
Temps : Frontend 96 ms
Délai : premier octet 102 ms
Délai : dernier octet 129 ms
Délai : première image 188 ms
Délai : premier CSS 194 ms
Délai : premier JS 326 ms
DOM Interactif 836 ms
Chargement du DOM 836 ms
DOM complet 2561 ms
Trafic HTTP terminé 2980 ms

Un détail

  
td span {
  background: inherit;
  -webkit-text-fill-color: transparent;
  -webkit-background-clip: text;
  left: 100%;
  position: absolute;
}
  

Le tableau décemment vêtu

Temps de chargement pour ffoodd.fr
Temps de chargement cumulé
Temps : backend ms
Temps : Frontend 96 ms
Délai : premier octet 102 ms
Délai : dernier octet 129 ms
Délai : première image 188 ms
Délai : premier CSS 194 ms
Délai : premier JS 326 ms
DOM Interactif 836 ms
Chargement du DOM 836 ms
DOM complet 2561 ms
Trafic HTTP terminé 2980 ms

Un peu de classe

Combinant une technique proposée par Trys Mudford et les astuces de Taylor Hunt.

  
table {
  --stripes: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E");
  --zig: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E");
}

tr:nth-child(5n+5) {
  --background: var(--stripes);
}

td {  
  background: 
    linear-gradient(to right, #3cb371, #444, #0000cd, #639, crimson) 
    calc(var(--value, 0) / var(--scale, 100) * 100%) 0% / 
    calc(var(--scale, 100) * 100%) 100%,
    transparent var(--background) center / contain;
  background-blend-mode: hard-light;
}
  

Le tableau bien sapé

Temps de chargement pour ffoodd.fr
Temps de chargement cumulé
Temps : backend ms
Temps : Frontend 96 ms
Délai : premier octet 102 ms
Délai : dernier octet 129 ms
Délai : première image 188 ms
Délai : premier CSS 194 ms
Délai : premier JS 326 ms
DOM Interactif 836 ms
Chargement du DOM 836 ms
DOM complet 2561 ms
Trafic HTTP terminé 2980 ms

On peaufine les détails

  
table:hover tr {
  opacity: .5;
}

table:hover tr:hover {
  opacity: 1;
}
          
@media screen and (-ms-high-contrast: active) {
  td {
     background-image: 
       linear-gradient(to right, Window, ButtonFace, ButtonShadow, ButtonText, highlight),
       var(--background);
  }
}
  

Le tableau du samedi soir

Temps de chargement pour ffoodd.fr
Temps de chargement cumulé
Temps : backend ms
Temps : Frontend 96 ms
Délai : premier octet 102 ms
Délai : dernier octet 129 ms
Délai : première image 188 ms
Délai : premier CSS 194 ms
Délai : premier JS 326 ms
DOM Interactif 836 ms
Chargement du DOM 836 ms
DOM complet 2561 ms
Trafic HTTP terminé 2980 ms

On fait des vagues

Dans le HTML

  
<table style="--scale: 3000; --1: 4; --2: 96; --3: 102; --4: 129; --5: 188;">
  

Dans le CSS

  
tr:nth-of-type(5) td {
  grid-column: var(--4, 0) / var(--value, 0);
}
  

Le tableau du Niagara

Temps de chargement pour ffoodd.fr
Temps de chargement cumulé
Temps : backend ms
Temps : Frontend 96 ms
Délai : premier octet 102 ms
Délai : dernier octet 129 ms
Délai : première image 188 ms
Délai : premier CSS 194 ms
Délai : premier JS 326 ms
DOM Interactif 836 ms
Chargement du DOM 836 ms
DOM complet 2561 ms
Trafic HTTP terminé 2980 ms

Un diagramme linéaire

  
<table style="--y: 32; --x: 13; --t-1: 'Jan.'; --t-2: 'Fév.'; […]">
  <caption>Température mensuelle moyenne en 2017</caption>
  <thead>
    <tr>
      <th scope="col">Année</th>
      <th scope="col">Jan.</th>
      <th scope="col">[…]</th>
    </tr>
  </thead>
  <tbody>
    <tr style="--year: '2017'; --1: 8; --2: 6; --3: 9; --4: 12; --5: 15; --6: 21; --7: 24; --8: 25; --9: 22; --10: 19; --11: 14; --12: 9;">
      <th scope="row">2017</th>
      <td>8 °C</td>
      <td>[…]</td>
    </tr>
  </tbody>
</table>

Le tableau linéaire

Température mensuelle moyenne en 2017
Année Jan. Fév. Mars Avr. Mai Juin Juil. Août Sep. Oct. Nov. Déc.
2017 8 °C 6 °C 9 °C 12 °C 15 °C 21 °C 24 °C 25 °C 22 °C 19 °C 14 °C 9 °C

Une échelle

  
table {
    padding: calc(24em - 2rem) 0 1rem;
}
        
table::after {
  --scale: calc( ( 100% - ( var(--y) * 1px) ) / var(--y) );
  background-image: repeating-linear-gradient(
      to bottom, 
      white, 
      white var(--scale), 
      rgba(0, 0, 0, .25) calc(var(--scale) + .25rem)
  );
}
Température mensuelle moyenne en 2017
Année Jan. Fév. Mars Avr. Mai Juin Juil. Août Sep. Oct. Nov. Déc.
2017 8 °C 6 °C 9 °C 12 °C 15 °C 21 °C 24 °C 25 °C 22 °C 19 °C 14 °C 9 °C

Un tracé

  
@supports (clip-path: polygon(0% calc( 100% - ( var(--1) * 100% / var(--y))))) {
    table {
        --offset: calc((100% / var(--x)) / 2);
    }
    
    table [style]::before {
        background: linear-gradient(to top, blue, red 75%);
        clip-path: polygon(
            0% 100%,
            calc((100% / var(--x) * 1)) 100%,
            calc((100% / var(--x) * 1)) calc(100% - (var(--1) / var(--y) * 100%)),
            /* Premier point */
            calc((100% / var(--x) * 1) + var(--offset)) calc(100% - (var(--1) / var(--y) * 100%)),
            /* Deuxième point */
            calc((100% / var(--x) * 2) + var(--offset)) calc(100% - (var(--2) / var(--y) * 100%)),
            100% calc(100% - (var(--12) / var(--y) * 100%)),
            100% 100%,
            0% 100%
        );
    }
}
Température mensuelle moyenne en 2017
Année Jan. Fév. Mars Avr. Mai Juin Juil. Août Sep. Oct. Nov. Déc.
2017 8 °C 6 °C 9 °C 12 °C 15 °C 21 °C 24 °C 25 °C 22 °C 19 °C 14 °C 9 °C

Des infobulles

Reposant sur une astuce de Cassie Evans utilisant les compteurs CSS et une idée de Lea Verou pour gérer le retour à la ligne.

  
td {
    
}

td:first-child {
    --value: var(--1);
    --term: var(--t-1);
}

td::after {
    --top: calc( var(--height) - (var(--value) / var(--y) * 100%) );
    counter-reset: value var(--value);
    content: var(--term) " " var(--year) "\A " counter(value) "\A0°C";
    pointer-events: none;
    position: absolute;
    top: var(--top, 100);
    will-change: opacity, transform;
    white-space: pre;
}
Température mensuelle moyenne en 2017
Année Jan. Fév. Mars Avr. Mai Juin Juil. Août Sep. Oct. Nov. Déc.
2017 8 °C 6 °C 9 °C 12 °C 15 °C 21 °C 24 °C 25 °C 22 °C 19 °C 14 °C 9 °C
Température mensuelle moyenne en 2017
Année Jan. Fév. Mars Avr. Mai Juin Juil. Août Sep. Oct. Nov. Déc.
2017 8 °C 6 °C 9 °C 12 °C 15 °C 21 °C 24 °C 25 °C 22 °C 19 °C 14 °C 9 °C
Température mensuelle moyenne en 2017
Année Jan. Fév. Mars Avr. Mai Juin Juil. Août Sep. Oct. Nov. Déc.
2017 8 °C 6 °C 9 °C 12 °C 15 °C 21 °C 24 °C 25 °C 22 °C 19 °C 14 °C 9 °C
2018 10 °C 4 °C 7 °C 13 °C 17 °C 20 °C 22 °C 23 °C 26 °C 17 °C 14 °C 10 °C

Au radar

    
<table 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>Niveau d’intérêt par domaine, sur 20</caption>
  <thead>
    <tr>
      <th scope="col">Accessibilité</th>
      <th scope="col">Référencement</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><span>14</span></td>
      <td><span>11</span></td>
    </tr>
  </tbody>
</table>
  
Niveau d’intérêt par domaine, sur 20
Accessibilité Référencement Performance Compatibilité Sécurité Qualité de code Test
14 11 13 16 10 12 4

Étalonnage

  
table {
    --radius: 10em;
    --size: calc( var(--radius) / var(--scale) );
    background-image:
      repeating-radial-gradient(
        circle at 50%, black, black 2px, white 0, white calc(var(--size) * var(--step))
      ),
      repeating-radial-gradient(
        circle at 50%, grey, grey 2px, white 0, white var(--size)
      );
    border-radius: 50%;
    height: calc( var(--radius) * 2 );
    width: calc( var(--radius) * 2 );
}
  
Niveau d’intérêt par domaine, sur 20
Accessibilité Référencement Performance Compatibilité Sécurité Qualité de code Test
14 11 13 16 10 12 4

Encerclement

Basé sur une explication d’Ana Tudor.

  
table { 
    --part: calc( 360deg / var(--items) );
}

[scope="col"] {
    --away: calc( (var(--radius) * -1) - 50% );
    left: 50%;
    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) );
}

Niveau d’intérêt par domaine, sur 20
Accessibilité Référencement Performance Compatibilité Sécurité Qualité de code Test
14 11 13 16 10 12 4

Placer les points

  
td:nth-of-type(3) span {
    --pos: calc( 100% - (var(--4) * 100% / (var(--scale) * var(--ratio) ) ) );
    background: linear-gradient( to top left, rebeccapurple 10%, indigo 75% );
    clip-path: polygon(
        100% var(--pos),
        calc( 100% - ( var(--3) * 100% / var(--scale) ) ) 100%,
        100% 100%
    );
    height: 100%;
    position: absolute;
    width: 100%;
}
  
Niveau d’intérêt par domaine, sur 20
Accessibilité Référencement Performance Compatibilité Sécurité Qualité de code Test
14 11 13 16 10 12 4

Distribution des parts

Détournement d’une technique publiée par Sara Soueidan sur Codrops.

  
td {
    --skew: calc( 90deg - var(--part) );
    border-bottom: 1px solid rebeccapurple;
    transform:
        rotate( calc(var(--part) * var(--index, 1)) )
        skew( var(--skew) );
    transform-origin: 100% 100%;
}

Niveau d’intérêt par domaine, sur 20
Accessibilité Référencement Performance Compatibilité Sécurité Qualité de code Test
14 11 13 16 10 12 4

Houston

Illustration de la déformation du carré par la fonction skew()

Un peu de trigonométrie

Figurez-vous que Stereokai a implémenté plusieurs fonctions de trigonométrie pour calculer les sinus, cosinus et tangente d’un angle.

  
td span {
      --opposite: calc( 180 - (90 + (90 - (360 / var(--items)))) );
      /* Trouver l’angle opposé, en radians */
      --angle: calc( var(--opposite) * 0.01745329251 );
      /* calc()uler le sinus, sorcellerie ! */
      --sin-term1: var(--angle);
      --sin-term2: calc((var(--angle) * var(--angle) * var(--angle)) / 6);
      --sin-term3: calc((var(--angle) * var(--angle) * var(--angle) * var(--angle) * var(--angle)) / 120);
      --sin-term4: calc((var(--angle) * var(--angle) * var(--angle) * var(--angle) * var(--angle) * var(--angle) * var(--angle)) / 5040);
      --sin-term5: calc((var(--angle) * var(--angle) * var(--angle) * var(--angle) * var(--angle) * var(--angle) * var(--angle) * var(--angle) * var(--angle)) / 362880);
      --sin: calc(var(--sin-term1) - var(--sin-term2) + var(--sin-term3) - var(--sin-term4) + var(--sin-term5));
      /* calc()uler l’hypothénuse : largeur initiale / sinus de l’angle opposé */
      --hypo: calc( var(--unitless-radius) / var(--sin) );
      /* trouver le ratio : largeur après déformation / largeur initiale */
      --ratio: calc( var(--hypo) / var(--unitless-radius) );
}

Niveau d’intérêt par domaine, sur 20
Accessibilité Référencement Performance Compatibilité Sécurité Qualité de code Test
14 11 13 16 10 12 4
Niveau d’intérêt par domaine, sur 20
Accessibilité Référencement Performance Compatibilité Sécurité Qualité de code Test
Gaël 14 11 13 16 14 10 4
Luc 18 10 11 16 10 12 11

Un diagramme en tarte

  
<table>
  <caption>Répartition du poids des ressources pour ffoodd.fr</caption>
  <thead class="sr-only">
    <tr>
      <th scope="col">Ressource</th>
      <th scope="col">Proportion</th>
    </tr>
  </thead>
  <tbody>
    <tr style="--color: #734bf9; --term: 'HTML';">
      <th scope="row">HTML</th>
      <td style="--value: 2; --start: 0; ">2 %</td>
    </tr>
    <tr>[…]</tr>
  </tbody>
</table>
Répartition du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
Json 1 %
Images 44 %
Webfonts 17 %
Autres 2 %

Devenir légende

  
tbody {
    display: table-row;
}

tbody tr {
    display: table-cell;
}

tbody [scope="row"]::before {
    background: var(--color, currentColor) var(--background);
    content: "";
    display: inline-block;
    height: 1rem;
    transform: translate3d(-.2rem, .1rem, 0);
    width: 1rem;
}
  
Répartition du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
Json 1 %
Images 44 %
Webfonts 17 %
Autres 2 %

Devenir tarte

Valeur entre 25 et 50%
Valeur entre 50 et 75%
Valeur entre 75 et 100%

Devenir tarte

On réitère nos calculs de trigo précédents, et on ajoute une petite couche de complexité avec des variables pseudo-booléennes, sur une idée de Roman Komarov.

  
td::before {
    clip-path: polygon(
      50% 50%,
      50% 0%,
      100% 0%,
      calc(50% + (var(--pos-B) * 1% * var(--lt-25, 1)) + (var(--gt-25, 0) * 50%)) calc(50% - (var(--pos-A) * 1% * var(--lt-25, 1))),
      calc(50% + (var(--gt-25, 0) * 50% ))                                        calc(50% + (var(--gt-25, 0) * 50%)),
      calc(50% + (var(--pos-A) * 1% * var(--lt-50, 1)) + (var(--gt-50, 0) * 50%)) calc(50% + (var(--pos-B) * 1% * var(--lt-50, 1)) + (var(--gt-50, 0) * 50%)),
      calc(50% - (var(--gt-50, 0) * 50% ))                                        calc(50% + (var(--gt-50, 0) * 50%)),
      calc(50% - (var(--pos-B) * 1% * var(--lt-75, 1)) - (var(--gt-75, 0) * 50%)) calc(50% + (var(--pos-A) * 1% * var(--lt-75, 1))),
      calc(50% - (var(--gt-75, 0) * 50% ))                                        calc(50% - (var(--gt-75, 0) * 50%)),
      calc(50% - (var(--pos-A) * 1% * var(--gt-75, 0)))                           calc(50% - (var(--pos-B) * 1% * var(--gt-75, 0))),
      50% 50%
    );
}
  
Répartition du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
Json 1 %
Images 44 %
Webfonts 17 %
Autres 2 %

Devenir doughnut

Inspiré par les expérimentations d’Ana Tudor avec mask-* sur CodePen.

  
@supports (mask: var(--mask)) {
   table {
     mask-image: radial-gradient(
       circle at 50% calc(50% - 2.5rem),
       transparent 0%,
       transparent var(--offset),
       white calc(var(--offset) + 1px),
       white 100%
     );
   }
}
  
Répartition du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
Json 1 %
Images 44 %
Webfonts 17 %
Autres 2 %

Devenir polaire

  
td::before {
  --zoom: 50;
  transform:
    translate3d(-50%, -50%, 0)
    rotate( var(--position) )
    scale( calc((var(--zoom) + (var(--value) / (100 / var(--zoom)))) / 100) );
}
  
Répartition du poids des ressources pour ffoodd.fr
Ressource Proportion
HTML 2 %
CSS 2 %
JS 32 %
Json 1 %
Images 44 %
Webfonts 17 %
Autres 2 %

Interrupteur de styles

Adrian Roselli a découvert que tripatouiller le display des tableaux abime 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é.

  
document.addEventListener( "DOMContentLoaded", function () {
  var switches = document.querySelectorAll( '[role="switch"]' );

  Array.prototype.forEach.call( switches, function( el, i ) {
    el.addEventListener( 'click', function() {
      var checked = this.getAttribute( 'aria-checked' ) === 'true' || false;
      this.setAttribute( 'aria-checked', !checked );
      
      var chart = this.parentNode.nextElementSibling;
      chart.classList.toggle( 'table-charts' );
    });
  });
});

Commutateur
Permet de désactiver les styles sur le tableau suivant.

Temps de chargement pour ffoodd.fr
Temps de chargement cumulé
Temps : backend ms
Temps : Frontend 96 ms
Délai : premier octet 102 ms
Délai : dernier octet 129 ms
Délai : première image 188 ms
Délai : premier CSS 194 ms
Délai : premier JS 326 ms
DOM Interactif 836 ms
Chargement du DOM 836 ms
DOM complet 2561 ms
Trafic HTTP terminé 2980 ms

Explorez

Les pistes ne manquent pas :

  • les propriétés all — avec les valeurs initial, inherit mais surtout revert — et contain ;
  • scroll-snap pour des diagrammes « déroulables » ;
  • attr() et counter() avec un typage faible et la possibilité de s’en servir ailleurs que dans la propriété content ;
  • les shapes, regions et exclusions pour explorer d’autres types de graphiques ;
  • Houdini, un ensemble de spécifications du futur — jetez donc un œil aux essais de Vincent De Oliveira ;
  • et probablement beaucoup d’autres idées et techniques à découvrir…

Conclusion

Les avantages sont multiples — tout comme les inconvénients :

  • pas de JavaScript pour l’aspect graphique ;
  • mais JavaScript est requis pour l’accessibilité ;
  • le balisage est libre et maîtrisable ;
  • rwd, whcm et autres préférences utilisateurs sont gérables grâce à CSS ;
  • HTML et CSS sont statiques ;
  • mais il faut travailler en amélioration progressive.

Learning is fun

Merci

Et à bientôt

Crédits