Mior Agency

Comment créer des graphiques en anneau dynamiques avec TailwindCSS et React

CSS est incroyable – je suis régulièrement étonné de voir à quel point il est venu au cours des années où je l’ai utilisé (~ 2005 – présent). Une de ces surprises est venue quand j’ai remarqué ce tweet de Shruti Balasa qui a montré comment créer un graphique à secteurs en utilisant conic-gradient().

C’est assez simple. Voici un extrait de code :

div {
  background: conic-gradient(red 36deg, orange 36deg 170deg, yellow 170deg);
  border-radius: 50%;
}

En utilisant cette petite quantité de CSS, vous pouvez créer des dégradés qui commencent et se terminent à des angles spécifiques et définir une couleur pour chaque « segment » du graphique à secteurs.

Graphiques de dégradés coniques CSS avec des graphiques en anneau et un graphique à secteurs
(Grand aperçu)

Jours heureux!

Génial, je pensais pouvoir l’utiliser à la place d’une bibliothèque de graphiques pour un projet de tableau de bord sur lequel je travaille pour la nouvelle API CockroachDB Cloud, mais j’ai rencontré un problème. Je ne connaissais pas les valeurs de mon graphique à l’avance et les valeurs que je recevais de l’API n’étaient pas en degrés !

Voici un lien de prévisualisation et un référentiel open source de la façon dont j’ai résolu ces deux problèmes, et dans le reste de cet article, j’expliquerai comment tout cela fonctionne.

Valeurs de données dynamiques

Voici quelques exemples de données d’un typique Réponse API que j’ai triée par value.

const data = [
  {
    name: 'Cluster 1',
    value: 210,
  },
  {
    name: 'Cluster 2',
    value: 30,
  },
  {
    name: 'Cluster 3',
    value: 180,
  },
  {
    name: 'Cluster 4',
    value: 260,
  },
  {
    name: 'Cluster 5',
    value: 60,
  },
].sort((a, b) => a.value - b.value);

Vous pouvez voir que chaque élément du tableau a un name et un value.

Pour convertir le value d’un nombre à un deg valeur à utiliser avec CSS, il y a quelques choses que vous devez faire :

  • Calculez le montant total de toutes les valeurs.
  • Utilisez le montant total pour calculer le pourcentage que chaque valeur représente.
  • Convertissez le pourcentage en degrés.

Note: Le code auquel je ferai référence dans les étapes ci-dessous se trouve dans le référentiel ici : /components/donut-1.js.

Plus après le saut! Continuez à lire ci-dessous ↓

Calculer le montant total

En utilisant JavaScript, vous pouvez utiliser cette petite ligne pour ajout extrait chaque valeur du tableau de données, ce qui donne un seul total.

const total_value = data.reduce((a, b) => a + b.value, 0);

// => 740

Calculer le pourcentage

maintenant que vous avez un total_value, vous pouvez convertir chacune des valeurs de la matrice de données en pourcentage à l’aide d’une fonction JavaScript. j’ai appelé cette fonction covertToPercent.

Note: J’ai utilisé la valeur de 210 du groupe 1 dans cet exemple.

const convertToPercent = (num) => Math.round((num / total_value) * 100);

// convertToPercent(210) => 28

Convertir un pourcentage en degrés

Une fois que vous avez un pourcentage, vous pouvez convertir le pourcentage en degrés en utilisant une autre fonction JavaScript. j’ai appelé cette fonction convertToDegrees.

const convertToDegrees = (num) => Math.round((num / 100) * 360);

// convertToDegrees(28) => 101

Le résultat

À titre de test temporaire, si vous deviez mapper les éléments du tableau de données triées à l’aide des deux fonctions expliquées ci-dessus, vous obtiendriez le résultat suivant :

const test_output = data.map((item) => {
  const percentage = convertToPercent(item.value);
  const degrees = convertToDegrees(percentage);

  return `${degrees}deg`;
});

// => ['14deg', '29deg', '86deg', '101deg', '126deg']

La valeur de retour de test_output est un tableau de value (en degrés) + accord deg.

Cela résout l’un d’un problème en deux parties. Je vais maintenant vous expliquer l’autre partie du problème.

Pour créer un graphique à secteurs à l’aide de conic-gradient()tu as besoin de deux deg valeurs. Le premier est l’angle à partir duquel le dégradé doit commencer et le second est l’angle où le dégradé doit se terminer. Vous aurez également besoin d’une couleur pour chaque segment, mais j’y reviendrai un peu.

 ['red ???? 14deg', 'blue ???? 29deg', 'green ???? 86deg', 'orange ???? 101deg', 'pink ???? 126deg']

En utilisant les valeurs de la test_output, je n’ai que la valeur finale (où le dégradé doit s’arrêter). L’angle de départ de chaque segment est en fait l’angle de fin de l’élément précédent dans le tableau, et l’angle de fin est la valeur cumulée de toutes les valeurs de fin précédentes plus la valeur de fin actuelle. Et pour aggraver les choses, la valeur initiale du premier angle doit être réglée manuellement sur 0 ????.

Voici un schéma pour mieux expliquer ce que cela signifie :

Un diagramme qui explique une fonction.
(Grand aperçu)

Si cela semble déroutant, c’est parce que c’est le cas, mais si vous regardez la sortie d’une fonction qui peut faire tout cela, cela pourrait avoir plus de sens.

"#...", 0, 14,
"#...",, 14, 43,
"#...",, 43, 130,
"#...",, 130, 234,
"#...",, 234, 360,

La fonction qui peut faire tout ça

Et voici la fonction qui peut réellement faire tout cela. Les usages reduce() pour itérer sur la matrice de données, il effectue la somme nécessaire pour calculer les angles et renvoie un nouvel ensemble de nombres qui peuvent être utilisés pour créer les angles de début et de fin corrects à utiliser dans un graphique.

const total_value = data.reduce((a, b) => a + b.value, 0);
const convertToPercent = (num) => Math.round((num / total_value) * 100);
const convertToDegrees = (num) => Math.round((num / 100) * 360);

const css_string = data
  .reduce((items, item, index, array) => {
    items.push(item);

    item.count = item.count || 0;
    item.count += array[index - 1]?.count || item.count;
    item.start_value = array[index - 1]?.count ? array[index - 1].count : 0;
    item.end_value = item.count += item.value;
    item.start_percent = convertToPercent(item.start_value);
    item.end_percent = convertToPercent(item.end_value);
    item.start_degrees = convertToDegrees(item.start_percent);
    item.end_degrees = convertToDegrees(item.end_percent);

    return items;
  }, [])
  .map((chart) => {
    const { color, start_degrees, end_degrees } = chart;
    return ` ${color} ${start_degrees}deg ${end_degrees}deg`;
  })
  .join();

J’ai laissé cela assez verbeux exprès, il est donc plus facile d’ajouter console.log(). J’ai trouvé cela très utile lorsque je développais cette fonctionnalité.

Vous remarquerez peut-être le supplément map enchaîné au bout du reduce. utilisant un map Je peux modifier les valeurs de retour et ajouter degpuis renvoyez-les tous ensemble sous forme de tableau de chaînes.

Résistant join juste à la fin, il reconvertit le tableau en un seul css_stringqui peut être utilisé avec conic-gradient() ????.

"#..." 0deg 14deg,
"#..." 14deg 43deg,
"#..." 43deg 130deg,
"#..." 130deg 234deg,
"#..." 234deg 360deg

En utilisant le css_string avec un SVG foreignObject

Maintenant, malheureusement, vous ne pouvez pas utiliser conic-gradient() avec SVG. Mais vous pouvez envelopper un élément HTML dans un foreignObject et le style de background utilisant un conic-gradient().

<svg viewBox='0 0 100 100' xmlns="http://www.w3.org/2000/svg" style={{ borderRadius: '100%' }}>
  <foreignObject x='0' y='0' width="100" height="100">
    <div
      xmlns="http://www.w3.org/1999/xhtml"
      style={{
        width: '100%',
        height: '100%',
        background: `conic-gradient(${css_string})`, // <- ????
      }}
    />
  </foreignObject>
</svg>

En utilisant ce qui précède, vous devriez regarder un graphique circulaire. Pour faire un diagramme en anneau, je vais devoir expliquer comment faire le trou.

parlons du trou

Il n’y a vraiment qu’une seule façon de « masquer » le centre du camembert pour révéler l’arrière-plan. Cette approche implique l’utilisation d’un clipPath. Cette approche ressemble à l’extrait de code suivant. Je l’ai utilisé pour Donut 1.

Note: Il src pour Donut 1, vous pouvez le voir ici : components/donut-1.js.

<svg viewBox='0 0 100 100' xmlns="http://www.w3.org/2000/svg" style={{ borderRadius: '100%' }}>

  <clipPath id='hole'>
    <path d='M 50 0 a 50 50 0 0 1 0 100 50 50 0 0 1 0 -100 v 18 a 2 2 0 0 0 0 64 2 2 0 0 0 0 -64' />
  </clipPath>

  <foreignObject x='0' y='0' width="100" height="100" clipPath="url(#hole)">
    <div
      xmlns="http://www.w3.org/1999/xhtml"
      style={{
        width: '100%',
        height: '100%',
        background: `conic-gradient(${css_string})`
      }}
    />
  </foreignObject>
</svg>

Cependant, il existe un autre moyen. Cette approche implique l’utilisation d’un <circle /> élément et en le plaçant au centre du graphique à secteurs. Cela fonctionnera si le rembourrage du <circle /> correspond à la couleur d’arrière-plan de tout ce sur quoi le graphique est placé. Dans mon exemple, j’ai utilisé un motif d’arrière-plan et vous remarquerez que si vous regardez attentivement Donut 3, vous ne pouvez pas voir le motif de bulles au centre du graphique.

Note: Il src pour Donut 3, vous pouvez le voir ici : components/donut-3.js.

<svg viewBox='0 0 100 100' xmlns="http://www.w3.org/2000/svg" style={{ borderRadius: '100%' }}>
  <foreignObject x='0' y='0' width="100" height="100">
    <div
      xmlns="http://www.w3.org/1999/xhtml"
      style={{
        width: '100%',
        height: '100%',
        background: `conic-gradient(${css_string})`
      }}
    />
  </foreignObject>
  <circle cx='50' cy='50' r="32" fill="white" />
</svg>

OMI le clipPath L’approche est meilleure, mais il peut être plus difficile de modifier les points du chemin pour obtenir l’épaisseur souhaitée du trou si vous n’avez pas accès à quelque chose comme Figma ou Illustrator.

Enfin, les couleurs !

Les couleurs pour les graphiques sont quelque chose qui me pose toujours problème. La plupart du temps, les couleurs que j’utilise sont définies en CSS, et tout cela se passe en JavaScript, alors comment utiliser les variables CSS en JavaScript ?

Dans mon exemple de site, j’utilise Tailwind pour styliser « tout » et en utilisant cette astuce, je peux exposer les variables CSS afin qu’elles puissent être référencées par leur nom.

Si vous voulez faire la même chose, vous pouvez ajouter un color clé pour le tableau de données :

data={[
  {
    name: 'Cluster 1',
    value: 210,
    color: 'var(--color-fuchsia-400)',
  },
  {
    name: 'Cluster 2',
    value: 30,
    color: 'var(--color-fuchsia-100)',
  },
  {
    name: 'Cluster 3',
    value: 180,
    color: 'var(--color-fuchsia-300)',
  },
  {
    name: 'Cluster 4',
    value: 260,
    color: 'var(--color-fuchsia-500)',
  },
  {
    name: 'Cluster 5',
    value: 60,
    color: 'var(--color-fuchsia-200)',
  },
].sort((a, b) => a.value - b.value)

Et se référer ensuite au color clé dans le tableau map pour le retourner dans le cadre du css_string. J’ai utilisé cette approche dans Donut 2.

Note: tu peux voir le src pour Donut 2 ici : composants/donut-2.js.

.map((chart) => {
  const { color, start_degrees, end_degrees } = chart;
  return ` ${color} ${start_degrees}deg ${end_degrees}deg`;
})
.join();

Vous pouvez même créer dynamiquement le nom de la couleur en utilisant une valeur codée en dur (color-pink-) + le index de la matrice. J’ai utilisé cette approche dans Donut 1.

Note: tu peux voir le src pour Donut 1 ici : composants/donut-1.js.

.map((chart, index) => {
  const { start_degrees, end_degrees } = chart;
  return ` var(--color-pink-${(index + 1) * 100}) ${start_degrees}deg ${end_degrees}deg`;
})
.join();

Si tu es chanceux!

Cependant, vous avez peut-être de la chance et vous travaillez avec une API qui renvoie en fait des valeurs avec une couleur associée. C’est le cas de l’API GitHub GraphQL. Ensuite. Je rassemble un dernier exemple.

API GitHub GraphQL avec graphe Github avec dix langages différents associés à leur propre couleur
(Grand aperçu)

Vous pouvez voir cela fonctionner dans votre navigateur en visitant /github, et le src pour GitHub Donut Chart et Legend peuvent être trouvés ici :

Fin

Vous pensez peut-être que c’est assez compliqué, et qu’il est probablement plus facile d’utiliser une bibliothèque graphique, et vous avez probablement raison. C’est probablement le cas. Mais c’est comme ça super léger. Il n’y a pas de bibliothèques supplémentaires à installer ou à maintenir, et pas de JavaScript lourd qui doit être téléchargé par le navigateur pour qu’ils fonctionnent.

J’ai expérimenté une fois auparavant avec la création de graphiques en anneau en utilisant un SVG et le stroke-dashoffset. Vous pouvez lire à ce sujet dans mon article, « Créer un graphique en anneau SVG à partir de zéro pour votre blog Gatsby ». Cette approche a très bien fonctionné, mais je pense que je préfère l’approche décrite dans ce post. CSS est tout simplement le meilleur !

Si vous souhaitez discuter de l’une des méthodes que j’ai utilisées ici, retrouvez-moi sur Twitter : @PaulieScanlon.

Rendez-vous en ligne !

édito fracassant
(Oui oui)

Laissez un commentaire

Derniers Posts
Une Question ? Un Projet ?
Quel que soit votre projet, MIOR AGENCY vous écoute, analyse vos besoins et propose des pistes de travail en conséquence. Vous avancez avec sérénité et confiance.