Fermer

février 9, 2024

Comment dessiner des graphiques radar sur le Web

Comment dessiner des graphiques radar sur le Web


J’ai pu travailler avec un nouveau type de graphique pour la visualisation de données appelé carte radar lorsqu’un projet le demandait. C’était nouveau pour moi, mais l’idée est qu’il y a un cercle circulaire en deux dimensions avec des tracés tout autour du graphique. Plutôt que de simples axes X et Y, chaque tracé sur une carte radar est son propre axe, marquant un point entre le bord extérieur du cercle et le centre même de celui-ci. Les tracés représentent une sorte de catégorie, et lorsqu’ils sont connectés entre eux, ils sont comme des sommets qui forment des formes pour aider à voir la relation entre les valeurs de catégorie, un peu comme les vecteurs d’un SVG.

Tableau comparatif des supercondensateurs.
Tableau comparatif des supercondensateurs. (Source de l’image : NASA) (Grand aperçu)

Parfois, la carte radar est appelée un carte en araignée, et il est facile de comprendre pourquoi. Les axes qui s’étendent vers l’extérieur se croisent avec les parcelles connectées et forment une apparence en forme de toile. Donc, si votre Les sens de l’araignée des picotements au premier coup d’œil, vous savez pourquoi.

Vous savez déjà où nous voulons en venir : Nous allons construire ensemble une carte radar ! Nous travaillerons à partir de zéro avec uniquement du HTML, du CSS et du JavaScript. Mais avant d’y aller, il convient de noter quelques points concernant les cartes radar.

D’abord, tu ne le fais pas avoir pour les construire à partir de zéro. Graphique.js et D3.js sont facilement disponibles avec des approches pratiques qui simplifient considérablement le processus. Considérant que je n’avais besoin que d’un seul graphique pour le projet, j’ai décidé de ne pas utiliser de bibliothèque et j’ai relevé le défi de le créer moi-même. J’ai appris quelque chose de nouveau et j’espère que vous aussi !

Deuxièmement, il y a mises en garde concernant l’utilisation de cartes radar pour la visualisation des données. Bien qu’ils soient effectivement efficaces, ils peuvent également être difficiles à lire lorsque plusieurs séries s’empilent. Les relations entre les graphiques ne sont pas aussi déchiffrables que, par exemple, les graphiques à barres. L’ordre des catégories autour du cercle affecte la forme globale, et l’échelle entre les séries doit être cohérente pour tirer des conclusions.

Cela dit, plongeons-nous dans le vif du sujet et mettons la main à la pâte avec les tracés de données.

Les composants

Ce que j’aime immédiatement dans les cartes radar, c’est qu’elles sont intrinsèquement géométriques. La connexion des tracés produit une série d’angles qui forment des formes de polygones. Les côtés sont des lignes droites. Et CSS est absolument merveilleux pour travailler avec des polygones étant donné que nous avons le CSS polygon() fonction pour les dessiner en déclarant autant de points que nécessaire dans les arguments de la fonction.

Nous commencerons par un graphique de forme pentagonale avec cinq catégories de données.

Voir le stylo [Radar chart (Pentagon) [forked]](https://codepen.io/smashingmag/pen/abMaEyo) par Sam Preethi.

Voir le stylo Carte radar (Pentagone) [forked] par Sam Preethi.

Il y a trois composants nous devons établir en HTML avant de travailler sur le style. Ce seraient :

  1. Grilles: Ceux-ci fournissent les axes sur lesquels les diagrammes sont dessinés. C’est la toile d’araignée du groupe.
  2. Graphiques: Ce sont les polygones que nous dessinons avec les coordonnées de chaque tracé de données avant de les colorer.
  3. Étiquettes: Le texte qui identifie les catégories le long des axes des graphiques.

Voici comment j’ai décidé de supprimer cela en HTML :

<!-- GRIDS -->
<div class="wrapper">
  <div class="grids polygons">
    <div></div>
  </div>
  <div class="grids polygons">
    <div></div>
  </div>
  <div class="grids polygons">
    <div></div>
  </div>
</div>

<!-- GRAPHS -->
<div class="wrapper">
  <div class="graphs polygons">
    <div><!-- Set 1 --></div>
  </div>
  <div class="graphs polygons">
    <div><!-- Set 2 --></div>
  </div>
  <div class="graphs polygons">
    <div><!-- Set 3 --></div>
  </div>
  <!-- etc. -->
</div>

<!-- LABELS -->
<div class="wrapper">
  <div class="labels">Data A</div>
  <div class="labels">Data B</div>
  <div class="labels">Data C</div>
  <div class="labels">Data D</div>
  <div class="labels">Data E</div>
  <!-- etc. -->
</div>

Je suis sûr que vous pouvez lire le balisage et voir ce qui se passe, mais nous avons trois éléments parents (.wrapper) qui contiennent chacun l’un des composants principaux. Le premier parent contient le .gridsle deuxième parent contient le .graphset le troisième parent contient le .labels.

Styles de base

Nous allons commencer par configurer quelques variables de couleur que nous pouvons utiliser pour remplir les éléments au fur et à mesure :

:root {
  --color1: rgba(78, 36, 221, 0.6); /* graph set 1 */
  --color2: rgba(236, 19, 154, 0.6); /* graph set 2 */
  --color3: rgba(156, 4, 223, 0.6); /* graph set 3 */
  --colorS: rgba(255, 0, 95, 0.1); /* graph shadow */
}

Notre prochaine tâche consiste à établir la mise en page. CSS Grid est une approche solide pour cela car nous pouvons placer les trois éléments de la grille ensemble sur la grille en seulement quelques lignes :

/* Parent container */
.wrapper { display: grid; }

/* Placing elements on the grid */
.wrapper > div {
  grid-area: 1 / 1; /* There's only one grid area to cover */
}

Allons-y et définissons une taille pour les éléments de la grille. J’utilise une valeur de longueur fixe de 300px, mais vous pouvez utiliser n’importe quelle valeur dont vous avez besoin et la varier si vous prévoyez de l’utiliser à d’autres endroits. Et plutôt que de déclarer une hauteur explicite, chargeons CSS de calculer une hauteur en utilisant aspect-ratio pour former des carrés parfaits.

/* Placing elements on the grid */
.wrapper div {
  aspect-ratio: 1 / 1;
  grid-area: 1 / 1;
  width: 300px;
}

Nous ne pouvons encore rien voir. Nous devrons colorier les choses en :

/* ----------
Graphs
---------- */
.graphs:nth-of-type(1) > div { background: var(--color1); }
.graphs:nth-of-type(2) > div { background: var(--color2); }
.graphs:nth-of-type(3) > div { background: var(--color3); }

.graphs {
  filter: 
    drop-shadow(1px 1px 10px var(--colorS))
    drop-shadow(-1px -1px 10px var(--colorS))
    drop-shadow(-1px 1px 10px var(--colorS))
    drop-shadow(1px -1px 10px var(--colorS));
}

/* --------------
Grids 
-------------- */
.grids {
  filter: 
    drop-shadow(1px 1px 1px #ddd)
    drop-shadow(-1px -1px 1px #ddd)
    drop-shadow(-1px 1px 1px #ddd)
    drop-shadow(1px -1px 1px #ddd);
    mix-blend-mode: multiply;
}

.grids > div { background: white; }

Oh, attendez! Nous devons définir des largeurs sur les grilles et les polygones pour qu’ils prennent forme :

.grids:nth-of-type(2) { width: 66%; }
.grids:nth-of-type(3) { width: 33%; }

/* --------------
Polygons 
-------------- */
.polygons { place-self: center; }
.polygons > div { width: 100%; }

Puisque nous y sommes déjà, je vais positionner légèrement les étiquettes et leur donner de la largeur :

/* --------------
Labels
-------------- */
.labels:first-of-type { inset-block-sptart: -10%; }

.labels {
  height: 1lh;
  position: relative;
  width: max-content;
}

Nous ne pouvons toujours pas voir ce qui se passe, mais nous le pouvons si nous dessinons temporairement des bordures autour des éléments.

Voir le stylo [Radar chart layout [forked]](https://codepen.io/smashingmag/pen/QWoVamB) par Sam Preethi.

Voir le stylo Disposition de la carte radar [forked] par Sam Preethi.

Tous combinés, cela n’a pas l’air si génial pour l’instant. Fondamentalement, nous avons une série de grilles qui se chevauchent suivies de graphiques parfaitement carrés empilés les uns sur les autres. Les étiquettes sont également retirées dans le coin. Nous n’avons encore rien dessiné, donc cela ne me dérange pas pour l’instant car nous avons les éléments HTML dont nous avons besoin, et CSS est techniquement en train d’établir une mise en page qui devrait s’assembler lorsque nous commençons à tracer des points et à dessiner des polygones.

Plus précisement:

  • Le .wrapper les éléments sont affichés sous forme de conteneurs CSS Grid.
  • Les enfants directs du .wrapper les éléments sont divest placé exactement au même endroit grid-area. Cela les amène à s’empiler les uns sur les autres.
  • Le .polygons sont centrés (place-self: center).
  • L’enfant divs dans le .polygons occuper toute la largeur (width:100%).
  • Chacun div est 300px large et carré avec un un à un aspect-ratio.
  • Nous déclarons explicitement une position relative sur le .labels. De cette façon, ils peuvent être automatiquement positionnés lorsque nous commençons à travailler en JavaScript.

Le reste? Appliquez simplement quelques couleurs comme arrière-plans et déposez des ombres.

Calcul des coordonnées du tracé

Ne t’inquiète pas. Nous n’entrons pas dans le détail de la géométrie des polygones. Jetons plutôt un coup d’œil rapide aux équations que nous utilisons pour calculer les coordonnées des sommets de chaque polygone. Vous n’avez pas besoin de connaître ces équations pour utiliser le code que nous allons écrire, mais cela ne fait jamais de mal de jeter un coup d’œil sous le capot pour voir comment tout cela se déroule.

x1 = x + cosθ1 = cosθ1 if x=0
y1 = y + sinθ1 = sinθ1 if y=0
x2 = x + cosθ2 = cosθ2 if x=0
y2 = y + sinθ2 = sinθ2 if y=0
etc.

x, y = center of the polygon (assigned (0, 0) in our examples)

x1, x2… = x coordinates of each vertex (vertex 1, 2, and so on)
y1, y2… = y coordinates of each vertex
θ1, θ2… = angle each vertex makes to the x-axis

On peut supposer que 𝜃 est 90deg (c’est à dire, 𝜋/2) puisqu’un sommet peut toujours être placé juste au-dessus ou en dessous du centre (c’est-à-dire Données A dans cet exemple). Le reste des angles peut être calculé comme ceci :

n = number of sides of the polygon

𝜃1 = 𝜃0 + 2𝜋/𝑛 = 𝜋/2 + 2𝜋/𝑛
𝜃2 = 𝜃0 + 4𝜋/𝑛 = 𝜋/2 + 4𝜋/𝑛
𝜃3 = 𝜃0 + 6𝜋/𝑛 = 𝜋/2 + 6𝜋/𝑛
𝜃3 = 𝜃0 + 8𝜋/𝑛 = 𝜋/2 + 8𝜋/𝑛
𝜃3 = 𝜃0 + 10𝜋/𝑛 = 𝜋/2 + 10𝜋/𝑛

Armés de ce contexte, nous pouvons résoudre pour notre x et y valeurs:

x1 = cos(𝜋/2 + 2𝜋/# sides)
y1 = sin(𝜋/2 + 2𝜋/# sides)
x2 = cos(𝜋/2 + 4𝜋/# sides)
y2 = sin(𝜋/2 + 4𝜋/# sides)
etc.

Le nombre de côtés dépend du nombre de parcelles dont nous avons besoin. Nous avons dit d’emblée qu’il s’agissait d’une forme pentagonale, nous travaillons donc avec cinq côtés dans cet exemple particulier.

x1 = cos(𝜋/2 + 2𝜋/5)
y1 = sin(𝜋/2 + 2𝜋/5)
x2 = cos(𝜋/2 + 4𝜋/5)
y2 = sin(𝜋/2 + 4𝜋/5)
etc.

Dessiner des polygones avec JavaScript

Maintenant que les calculs sont pris en compte, nous avons ce dont nous avons besoin pour commencer à travailler en JavaScript dans le but de tracer les coordonnées, de les relier entre elles et de peindre les polygones résultants.

Par souci de simplicité, nous laisserons le API de canevas à partir de cela et utilisez à la place des éléments HTML normaux pour dessiner le graphique. Vous pouvez cependant utiliser les mathématiques décrites ci-dessus et la logique suivante comme base pour dessiner des polygones dans le langage, le framework ou l’API de votre choix.

OK, nous avons donc trois types de composants sur lesquels travailler : les grilles, les graphiques et les étiquettes. Nous commençons par la grille et progressons à partir de là. Dans chaque cas, je vais simplement insérer le code et expliquer ce qui se passe.

Dessiner la grille

// Variables
let sides = 5; // # of data points
let units = 1; // # of graphs + 1
let vertices = (new Array(units)).fill(""); 
let percents = new Array(units);
percents[0] = (new Array(sides)).fill(100); // for the polygon's grid component
let gradient = "conic-gradient(";
let angle = 360/sides;

// Calculate vertices
with(Math) { 
  for(i=0, n = 2 * PI; i < sides; i++, n += 2 * PI) {
    for(j=0; j < units; j++) {
      let x = ( round(cos(-1 * PI/2 + n/sides) * percents[j][i]) + 100 ) / 2; 
      let y = ( round(sin(-1 * PI/2 + n/sides) * percents[j][i]) + 100 ) / 2; 
      vertices[j] += `${x}% ${y} ${i == sides - 1 ? '%':'%, '}`;
  }
  gradient += `white ${
    (angle * (i+1)) - 1}deg,
    #ddd ${ (angle * (i+1)) - 1 }deg,
    #ddd ${ (angle * (i+1)) + 1 }deg,
    white ${ (angle * (i+1)) + 1 }deg,
  `;}
}

// Draw the grids
document.querySelectorAll('.grids>div').forEach((grid,i) => {
  grid.style.clipPath =`polygon(${ vertices[0] })`;
});
document.querySelector('.grids:nth-of-type(1) > div').style.background =`${gradient.slice(0, -1)} )`;

Vérifiez-le! Nous avons déjà une toile d’araignée.

Voir le stylo [Radar chart (Grid) [forked]](https://codepen.io/smashingmag/pen/poYOpOG) par Sam Preethi.

Voir le stylo Carte radar (Grille) [forked] par Sam Preethi.

Voici ce qui se passe dans le code :

  1. sides est le nombre de côtés du graphique. Encore une fois, nous travaillons avec cinq parties.
  2. vertices est un tableau qui stocke les coordonnées de chaque sommet.
  3. Puisque nous ne construisons pas encore de graphiques — seulement la grille — le nombre de units est réglé sur 1et un seul élément est ajouté au percents tableau à percents[0]. Pour les polygones de grille, les valeurs des données sont 100.
  4. gradient est une chaîne pour construire le conic-gradient() qui établit les lignes du quadrillage.
  5. angle est un calcul de 360deg divisé par le nombre total de sides.

A partir de là, on calcule les sommets :

  1. i est un itérateur qui parcourt le nombre total de sides (c’est à dire, 5).
  2. j est un itérateur qui parcourt le nombre total de units (c’est à dire, 1).
  3. n est un compteur qui compte par incréments de 2*PI (c’est à dire, 2𝜋, 4𝜋, 6𝜋et ainsi de suite).

Le x et y les valeurs de chaque sommet sont calculées comme suit, sur la base des équations géométriques dont nous avons discuté précédemment. Notez que nous multiplions 𝜋 par -1 pour piloter la rotation.

cos(-1 * PI/2 + n/sides) // cos(𝜋/2 + 2𝜋/sides), cos(𝜋/2 + 4𝜋/sides)...
sin(-1 * PI/2 + n/sides) // sin(𝜋/2 + 2𝜋/sides), sin(𝜋/2 + 4𝜋/sides)...

Nous convertissons le x et y valeurs en pourcentages (puisque c’est ainsi que les points de données sont formatés), puis placez-les sur le graphique.

let x = (round(cos(-1 * PI/2 + n/sides) * percents[j][i]) + 100) / 2;
let y = (round(sin(-1 * PI/2 + n/sides) * percents[j][i]) + 100) / 2;

Nous construisons également le conic-gradient(), qui fait partie de la grille. Chaque arrêt de couleur correspond à l’angle de chaque sommet — à chacun des incréments d’angle, un gris (#ddd) la ligne est tracée.

gradient += 
  `white ${ (angle * (i+1)) - 1 }deg,
   #ddd ${ (angle * (i+1)) - 1 }deg,
   #ddd ${ (angle * (i+1)) + 1 }deg,
   white ${ (angle * (i+1)) + 1 }deg,`

Si nous imprimons les variables calculées après le for boucle, ce seront les résultats pour la grille vertices et gradient:

console.log(`polygon( ${vertices[0]} )`); /* grid’s polygon */
// polygon(97.5% 34.5%, 79.5% 90.5%, 20.5% 90.5%, 2.5% 34.5%, 50% 0%)

console.log(gradient.slice(0, -1)); /* grid’s gradient */
// conic-gradient(white 71deg, #ddd 71deg,# ddd 73deg, white 73deg, white 143deg, #ddd 143deg, #ddd 145deg, white 145deg, white 215deg, #ddd 215deg, #ddd 217deg, white 217deg, white 287deg, #ddd 287deg, #ddd 289deg, white 289deg, white 359deg, #ddd 359deg, #ddd 361deg, white 361deg

Ces valeurs sont affectées aux éléments de la grille clipPath et backgroundrespectivement, et ainsi la grille apparaît sur la page.

Le graphique

// Following the other variable declarations 
// Each graph's data points in the order [B, C, D... A] 
percents[1] = [100, 50, 60, 50, 90]; 
percents[2] = [100, 80, 30, 90, 40];
percents[3] = [100, 10, 60, 60, 80];

// Next to drawing grids
document.querySelectorAll('.graphs > div').forEach((graph,i) => {
  graph.style.clipPath =`polygon( ${vertices[i+1]} )`;
});

Voir le stylo [Radar chart (Graph) [forked]](https://codepen.io/smashingmag/pen/KKExZYE) par Sam Preethi.

Voir le stylo Carte radar (graphique) [forked] par Sam Preethi.

Maintenant, on dirait que nous arrivons à quelque chose ! Pour chaque graphique, nous ajoutons son ensemble de points de données au percents tableau après avoir incrémenté la valeur de units pour correspondre au nombre de graphiques. Et c’est tout ce dont nous avons besoin pour dessiner des graphiques sur le graphique. Tournons pour le moment notre attention vers les étiquettes.

Les étiquettes

// Positioning labels

// First label is always set in the top middle
let firstLabel = document.querySelector('.labels:first-of-type');
firstLabel.style.insetInlineStart =`calc(50% - ${firstLabel.offsetWidth / 2}px)`;

// Setting labels for the rest of the vertices (data points). 
let v = Array.from(vertices[0].split(' ').splice(0, (2 * sides) - 2), (n)=> parseInt(n)); 

document.querySelectorAll('.labels:not(:first-of-type)').forEach((label, i) => {
  let width = label.offsetWidth / 2; 
  let height = label.offsetHeight;
  label.style.insetInlineStart = `calc( ${ v[i*2] }% + ${ v[i*2] < 50 ? - 3*width : v[i*2] == 50 ? - width: width}px )`;
  label.style.insetBlockStart = `calc( ${ v[(i*2) + 1] }% - ${ v[(i * 2) + 1] == 100 ? - height: height / 2 }px )`;
});

Le positionnement des étiquettes est déterminé par trois éléments :

  1. Les coordonnées des sommets (c’est-à-dire les points de données) à côté desquels ils doivent se trouver,
  2. La largeur et la hauteur de leur texte, et
  3. Tout espace vide nécessaire autour des étiquettes afin qu’elles ne chevauchent pas le graphique.

Toutes les étiquettes sont positionnées relative en CSS. En ajoutant le inset-inline-start et inset-block-start valeurs dans le script, nous pouvons repositionner les étiquettes en utilisant les valeurs comme coordonnées. La première étiquette est toujours définie en haut et au milieu. Les coordonnées du reste des étiquettes sont les mêmes que celles de leurs sommets respectifs, plus un décalage. Le décalage est déterminé comme ceci :

  1. axe x/horizontal
    Si l’étiquette est à gauche (c’est-à-dire x est inférieur à 50%), puis il est déplacé vers la gauche en fonction de son width. Sinon, il est déplacé vers la droite. Ainsi, les bords droit ou gauche des étiquettes, selon le côté du graphique sur lequel elles se trouvent, sont uniformément alignés sur leurs sommets.
  2. axe y/vertical
    La hauteur de chaque étiquette est fixe. Il n’y a pas grand chose de décalage à ajouter, à part peut-être les déplacer vers le bas de la moitié de leur hauteur. Toute étiquette en bas (c’est-à-dire lorsque y est de 100 %), cependant, vous pourriez utiliser un espace supplémentaire au-dessus pour respirer.

Et devine quoi…

Avaient fini!

Voir le stylo [Radar chart (Pentagon) [forked]](https://codepen.io/smashingmag/pen/XWGPVLJ) par Sam Preethi.

Voir le stylo Carte radar (Pentagone) [forked] par Sam Preethi.

Pas trop mal, non ? La partie la plus compliquée, je pense, ce sont les mathématiques. Mais puisque nous avons compris cela, nous pouvons pratiquement l’intégrer à toute autre situation où une carte radar est nécessaire. Avez-vous plutôt besoin d’un graphique en quatre points ? Mettez à jour le nombre de sommets dans le script et tenez compte de moins d’éléments dans le balisage et les styles.

En fait, voici deux autres exemples montrant différentes configurations. Dans chaque cas, j’augmente ou diminue simplement le nombre de sommets que le script utilise pour produire différents ensembles de coordonnées qui aident à positionner les points le long de la grille.

Besoin de seulement trois côtés ? Cela signifie simplement deux jeux de coordonnées en moins :

Voir le stylo [Radar chart (Triangle) [forked]](https://codepen.io/smashingmag/pen/vYPzpqJ) par Sam Preethi.

Voir le stylo Carte radar (Triangle) [forked] par Sam Preethi.

Besoin de sept côtés ? Nous allons plutôt produire davantage de jeux de coordonnées :

Voir le stylo [Radar chart (Heptagon) [forked]](https://codepen.io/smashingmag/pen/WNmgdqY) par Sam Preethi.

Voir le stylo Carte radar (Heptagone) [forked] par Sam Preethi.
Éditorial fracassant
(gg, ouais)




Source link