La donnée graphique avec HTML et CSS

Retrouvez les slides en ligne sur
ffoodd.fr/paris-web.2018.

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;
    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

  
td {
  background-blend-mode: hard-light;
  background-color: transparent;
  background-position: calc(var(--value, 0) / var(--scale, 100) * 100%) 0%, center;
  background-size: calc(var(--scale, 100) * 100%) 100%, contain;
}

tr:nth-child(5n+5) td {
  background-image: 
    linear-gradient(to right, #3cb371, #444, #0000cd, #639, crimson),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E");
}
  

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) {
  tr:nth-child(5n + 5) td {
     background-image: 
       linear-gradient(to right, Window, ButtonFace, ButtonShadow, ButtonText, highlight),
       url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E");
  }
}
  

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: 12; --1: 8; --2: 6; --3: 9;">
    <caption>Température mensuelle moyenne en 2017</caption>
    <thead>
        <tr>
            <th scope="row">Mois</th>
            <th scope="col">Jan.</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <th scope="row" class="sr-only">Température</th> 
            <td style="--value: 8">
                <span>8 °C</span>
            </td>
        </tr>
    </tbody>
</table>
  

Le tableau linéaire

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

Une échelle

  
table {
    padding: calc(24em - 2rem) 0 1rem;
}
        
table::after {
  --scale: calc(100% / 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
Mois Jan. Fév. Mars Avr. Mai Juin Juil. Août Sep. Oct. Nov. Déc.
Température °C °C °C 12 °C 15 °C 21 °C 24 °C 25 °C 22 °C 19 °C 14 °C °C

Un tracé

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

Un diagramme en tarte

  
<table>
    <caption>Répartition du poids des ressources pour ffoodd.fr</caption>
    <thead>
        <tr>
            <th scope="col">Ressource</th>
            <th scope="col">Proportion</th>
        </tr>
    </thead>
    <tbody>
        <tr style="--color: #734BF9">
            <th scope="row">HTML</th> 
            <td>
                2 %
                <span role="presentation" style="--value: 2; --start: 0;"></span>
            </td>
        </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 th::before {
    background: var(--color, currentColor);
    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

Basé sur un article de Léa Verou sur Smashing Magazine.

  
@supports (--position: calc(var(--start, 0) * .01turn)) {
    [role="presentation"] {
        --position: calc(var(--start, 0) * .01turn);
        clip-path: circle(50% at 0% 50%);
        overflow: hidden;
        transform: rotate(var(--position)) scale(.75);
        transform-origin: left center;
    }

    [role="presentation"]::before {
        --position: calc(var(--value, 0) * .01turn + 1deg);
        background-color: var(--color, currentColor);
        content: "";
        transform: rotate(var(--position));
        transform-origin: right center;
    }
}
  
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 %

Au radar

  
<figure role="group">
    <dl role="list" style="--scale: 20; --step: 5; --items: 7; --1: 14; --8: var(--1);">
        <dt role="term listitem" id="def_a11y" data-value="14" data-scale="20">
            <dfn>Accessibilité</dfn>
        </dt>
        <dd role="definition listitem" aria-labelledby="def_a11y" data-scale="20">
            <span>14</span>
        </dd>
    </dl>
    <figcaption>Niveau d’intérêt par domaine, sur 20</figcaption>
</figure>
  
Accessibilité
14
Référencement
11
Performance
13
Compatibilité
16
Sécurité
10
Qualité de code
12
Test
4
Niveau d’intérêt par domaine, sur 20

Étalonnage

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

Encerclement

Basé sur une explication d’Ana Tudor.

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

dt {
    --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) );
}

dt::after {
    content: attr(data-value) "\A0/\A0" attr(data-scale);
}

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

Placer les points

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

Distribution des parts

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

  
dd {
    --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%;
}
  
Accessibilité
14
Référencement
11
Performance
13
Compatibilité
16
Sécurité
10
Qualité de code
12
Test
4
Niveau d’intérêt par domaine, sur 20

Houston

Illustration de la déformation du carré par la valeur 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.

  
dd span {
    --opposite: calc( 180 - (90 + (90 - (360 / var(--items)))) );
    /* get opposite angle in radians */
    --angle: calc( var(--opposite) * 0.01745329251 );
    /* calc() sin, dark wizardry! */
    --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() hypothenuse: initial width / opposite angle's sinus */
    --hypo: calc( var(--unitless-radius) / var(--sin) );
    /* get the ratio: skewed / initial width */
    --ratio: calc( var(--hypo) / var(--unitless-radius) );
}
  
Accessibilité
14
Référencement
11
Performance
13
Compatibilité
16
Sécurité
10
Qualité de code
12
Test
4
Niveau d’intérêt par domaine, sur 20

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