Retour au catalogue

Radar Chart

Graphique radar SVG comparant les features de plusieurs options. Visualisation multi-axes elegante.

comparisoncomplex Both Responsive a11y
corporateminimalsaasuniversalcentered
Theme

Créer un graphique radar SVG animé en React

Un graphique radar SVG animé en React dessine des grilles polygonales concentriques et des axes en SVG pur, puis trace chaque jeu de données comme un élément `<path>` dont le `pathLength` s'anime de 0 à 1 via `whileInView` de Framer Motion. Les points de données apparaissent ensuite en `<circle>` décalés dans le temps.

  • Stack : React + Framer Motion 11 + CSS custom properties, ~135 lignes, zéro bibliothèque de graphiques.
  • Géométrie SVG pure : conversion polaire-cartésienne via Math.cos/sin, pas de canvas, pas de D3.
  • Nécessite au moins 3 axes ; retourne null en dessous pour éviter les formes dégénérées.
  • Accessible : les labels SVG sont de vrais nœuds texte DOM, lisibles par les lecteurs d'écran.
  • Responsive sur mobile via le layout flex colonne, mais le SVG 300×300 fixe ne se redimensionne pas, envisage une approche basée sur viewBox pour les petits écrans.

Comparison Radar Chart affiche un graphique radar SVG pur qui compare deux options ou plus sur plusieurs dimensions notées. Il n'utilise aucune bibliothèque de graphiques : grilles polygonales, axes et polygones de données sont calculés avec de la trigonométrie basique au rendu. Framer Motion gère l'animation de dessin déclenchée au scroll, transformant des chiffres bruts en une section que les visiteurs lisent en quelques secondes.

Anatomie

Le composant enveloppe un SVG 300×300 centré dans une colonne flex qui affiche aussi une ligne de légende en dessous. Dans le SVG, quatre polygones concentriques forment la grille d'arrière-plan, à 25/50/75/100% du rayon. Des lignes radiales fines relient le centre à chaque extrémité d'axe. Par-dessus, un `<motion.path>` par option trace le polygone de données rempli. Des `<motion.circle>` décalés marquent chaque point de données. Les labels texte se positionnent 24px au-delà des extrémités, calculés au même angle polaire que leur axe.

Comment ça marche

Les primitives clés sont `polarToXY` (convertit un angle 0-360° et un rayon en coordonnées x/y SVG, décalé de -90° pour que le premier axe pointe vers le haut) et `buildPath` (mappe chaque valeur sur son point et les joint avec des commandes SVG M/L). Chaque `<motion.path>` anime `pathLength` de 0 à 1 via `whileInView`, avec un décalage de 0.3s entre les options. Les cercles de données utilisent un décalage secondaire : `0.5 + optionIndex * 0.3 + axisIndex * 0.05` secondes, pour qu'ils apparaissent le long du tracé pendant le dessin. La courbe d'accélération `[0.16, 1, 0.3, 1]` est un cubic-bezier personnalisé qui donne une sensation rapide-puis-lente.

Comment le coder en React

  1. Mettre en place les helpers de géométrie polaire

    Écris deux fonctions pures : `polarToXY` convertit un angle en degrés et un rayon en coordonnées SVG x/y (soustrait 90° pour que 0° pointe vers le haut), et `buildPath` transforme un tableau de valeurs 0-100 en chaîne SVG fermée. Toutes deux sont en JavaScript pur, sans bibliothèque.

    function polarToXY(angle: number, r: number): [number, number] {
      const rad = ((angle - 90) * Math.PI) / 180;
      return [CENTER + r * Math.cos(rad), CENTER + r * Math.sin(rad)];
    }
    
    function buildPath(values: number[], count: number): string {
      return values
        .map((v, i) => {
          const angle = (360 / count) * i;
          const [x, y] = polarToXY(angle, (v / 100) * RADIUS);
          return `${i === 0 ? "M" : "L"}${x},${y}`;
        })
        .join(" ") + " Z";
    }
  2. Dessiner la grille et les axes

    Affiche 4 éléments `<polygon>` concentriques en calculant chaque sommet à `(niveau / LEVELS) * RADIUS` selon chaque angle d'axe. Ajoute ensuite une `<line>` par axe du centre à son extrémité. Les deux utilisent la variable CSS `--color-border` pour s'adapter automatiquement à n'importe quel preset de thème.

    {Array.from({ length: LEVELS }).map((_, li) => {
      const r = ((li + 1) / LEVELS) * RADIUS;
      const pts = axes
        .map((_, ai) => polarToXY((360 / count) * ai, r).join(","))
        .join(" ");
      return <polygon key={li} points={pts} fill="none" stroke="var(--color-border)" strokeWidth="1" opacity={0.5} />;
    })}
  3. Animer les polygones de données avec pathLength

    Pour chaque jeu de données, affiche un `<motion.path>` avec le résultat de `buildPath`. Anime `pathLength` de 0 à 1 avec `whileInView` pour déclencher le dessin au scroll. Décale plusieurs options avec `delay: index * 0.3` pour qu'elles apparaissent l'une après l'autre.

    <motion.path
      d={buildPath(opt.values, count)}
      fill="var(--color-accent)"
      fillOpacity={0.15}
      stroke="var(--color-accent)"
      strokeWidth={2}
      initial={{ opacity: 0, pathLength: 0 }}
      whileInView={{ opacity: 1, pathLength: 1 }}
      viewport={{ once: true }}
      transition={{ duration: 1, delay: index * 0.3, ease: [0.16, 1, 0.3, 1] }}
    />
  4. Ajouter les cercles de données décalés et les labels

    Mappe sur chaque valeur de chaque option pour afficher un `<motion.circle>` à la position polaire calculée. Utilise un délai composé `0.5 + optionIndex * 0.3 + axisIndex * 0.05` pour que les cercles semblent parcourir le tracé. Place les éléments `<text>` 24px au-delà de l'extrémité du rayon pour les labels d'axe, avec `textAnchor='middle'` et `dominantBaseline='central'` pour un centrage correct quel que soit l'angle.

Quand l'utiliser

Utilise un graphique radar pour comparer 2-3 options sur 5-8 dimensions qualitatives ou notées simultanément : comparatifs de fonctionnalités sur une page de tarification SaaS, profils de compétences, fiches de spécifications produit. Évite-le quand tes données sont ordinales ou séquentielles (un graphique en barres ou en courbes est plus clair), quand tu as plus de 3 options (les polygones superposés deviennent illisibles), ou quand des valeurs numériques précises sont nécessaires (le radar met en avant la forme, pas les chiffres).

Utilisé par

  • GitHub, Utilise des visualisations de compétences de type radar dans les profils développeurs et les analyses de contributions.
  • Notion, Les dashboards tiers Notion intègrent fréquemment des graphiques radar pour les matrices de compétences d'équipe et le suivi OKR.
  • HubSpot, Les dashboards d'analyse de contacts et de deals utilisent des graphiques radar pour afficher les scores multi-attributs d'un coup d'œil.

FAQ

Pourquoi ne pas utiliser Chart.js ou Recharts plutôt que du SVG brut ?

Chart.js ajoute ~60 Ko et Recharts ~150 Ko au bundle. Pour un seul graphique radar sur une page marketing, les deux fonctions utilitaires présentées ici couvrent 100% des besoins sans ce poids. Utilise une bibliothèque de graphiques si tu as besoin de mises à jour dynamiques, de tooltips, ou d'un tableau de bord avec de nombreux types de graphiques.

Comment rendre le SVG responsive ?

Supprime les attributs `width` et `height` fixes de l'élément `<svg>`, mets `width='100%'` et garde `viewBox='0 0 300 300'`. Le navigateur mettra alors le SVG à l'échelle de son conteneur. Tu devras peut-être légèrement augmenter la taille de police des labels, car elle est relative aux 300 unités du viewBox, pas à la taille rendue.

Puis-je ajouter des tooltips au survol de chaque point ?

Oui. Remplace les éléments `<motion.circle>` par un `<g>` qui encapsule le cercle et un `<foreignObject>` conditionnel contenant un tooltip React, ou utilise un élément enfant `title` pour un tooltip SVG natif. En production, une petite bibliothèque comme Floating UI est plus propre que de gérer les calculs de coordonnées SVG pour la position du tooltip.

L'animation pathLength scintille dans Safari. Comment corriger ça ?

Safari a des problèmes connus avec `pathLength` SVG combiné à `stroke-dasharray`. Ajoute `pathLength={1}` comme prop statique sur le `<path>` sous-jacent pour forcer le navigateur à normaliser les décalages de tirets, puis laisse Framer Motion animer de 0 à 1. Si ça scintille encore, anime `opacity` et `scale` à la place, moins spectaculaire mais universellement supporté.

"use client";

import { motion } from "framer-motion";

interface RadarOption {
  name: string;
  values: number[];
}

interface ComparisonRadarChartProps {
  title?: string;
  subtitle?: string;
  axes?: string[];
  options?: RadarOption[];
}

const E: [number, number, number, number] = [0.16, 1, 0.3, 1];
const SIZE = 300;
const CENTER = SIZE / 2;
const RADIUS = 120;
const LEVELS = 4;

Code complet réservé à Pro

Code source intégral, export multi-framework et playground.

Passer en Pro, 9,99€/mois

Avis

Composant React graphique radar SVG multi-axes, Tutoriel