Graphiques animés avec D3 🧑‍🎨

Graphiques animés avec D3.js 🧑‍🎨

D3.js est la librairie de visualisation de données la plus célèbre, et pour une bonne raison : c’est une collection ingénieusement conçue de fonctions et de méthodes qui permettent de créer des visualisations de données entièrement personnalisables sur le Web sous forme d’images SVG. Elle a été créée vers 2011 par Mike Bostock et d’autres informaticiens. Plus récemment, Philippe Rivière est devenu l’un des principaux mainteneurs et contributeurs.

Dans une leçon précédente, nous avons utilisé la librairie Plot pour nos visualisations. En coulisses, Plot utilise… D3 ! Et elle est principalement maintenue par Bostock et Rivière ! Surprise ! 😁 Plot est formidable et je l’utilise la majorité du temps pour mes dataviz. Mais lorsque je veux créer quelque chose de très personnalisé, en particulier avec des animations, D3 reste ma préférence.

Dans ce projet, nous allons utiliser des données sur les tremblements de terre pour créer un nuage de points animés avec D3 et Svelte, comme montré ci-dessous. Je vous recommande fortement de compléter les sections précédentes du cours avant de vous lancer. Je ne passerai pas beaucoup de temps sur la configuration, l’utilisation de la librairie Simple Data Analysis ou du framework Svelte.

Un nuage de points animés.

Vous voulez être prévenu quand de nouvelles leçons sont publiées ? Abonnez-vous à l'infolettre ✉️ et donnez une ⭐ au cours sur GitHub pour me garder motivé ! Cliquez ici pour me contacter.

Configuration

Nous allons utiliser setup-sda pour tout configurer et installer ce dont nous avons besoin.

Ouvrez un nouveau dossier avec VS Code et exécutez deno -A jsr:@nshiab/setup-sda --svelte.

Une capture d’écran de VS Code après avoir exécuté la librairie setup-sda.

Données sur les tremblements de terre

Pour récupérer les données sur les tremblements de terre, j’ai utilisé le catalogue USGS Earthquake. Comme l’année 2021 semblait particulièrement active, j’ai téléchargé deux fichiers CSV pour cette année et je les ai mis en ligne sur GitHub.

En utilisant la librairie Simple Data Analysis dans le dossier sda, nous pouvons facilement les récupérer et les mettre en cache. Mettez à jour sda/main.ts, puis exécutez deno task sda pour l’exécuter et l’observer.

sda/main.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
 
const sdb = new SimpleDB();
 
const earthquakes = sdb.newTable("earthquakes");
await earthquakes.cache(async () => {
  await earthquakes.loadData([
    "https://raw.githubusercontent.com/nshiab/data-fetch-lesson/refs/heads/main/earthquakes-2021-1.csv",
    "https://raw.githubusercontent.com/nshiab/data-fetch-lesson/refs/heads/main/earthquakes-2021-2.csv",
  ]);
});
 
await earthquakes.logTable();
 
await sdb.done();

Une capture d’écran de VS Code après avoir exécuté la librairie Simple Data Analysis.

Comme vous pouvez le voir, notre jeu de données contient plus de 28 000 tremblements de terre et de nombreuses colonnes. Nous pouvons le filtrer et ne garder que ce qui nous intéresse :

  • Nous ne voulons que earthquake dans la colonne type et reviewed dans la colonne status.
  • Nous filtrons pour ne garder que les tremblements de terre qui pourraient causer des dommages, avec une magnitude de 5 ou plus.
  • Nous renommons les colonnes latitude et longitude avec des noms plus courts.
  • Nous ne gardons que les colonnes time, lat, lon, depth et mag.
  • Nous arrondissons les valeurs numériques à 3 décimales.
  • Pour que ce soit plus logique, nous rendons les valeurs de depth négatives.
  • Et enfin, nous supprimons les doublons.

Nous obtenons environ 2 000 tremblements de terre.

sda/main.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
 
const sdb = new SimpleDB();
 
const earthquakes = sdb.newTable("earthquakes");
await earthquakes.cache(async () => {
  await earthquakes.loadData([
    "https://raw.githubusercontent.com/nshiab/data-fetch-lesson/refs/heads/main/earthquakes-2021-1.csv",
    "https://raw.githubusercontent.com/nshiab/data-fetch-lesson/refs/heads/main/earthquakes-2021-2.csv",
  ]);
});
 
await earthquakes.sort({ time: "asc" });
await earthquakes.keep({
  "type": "earthquake",
  "status": "reviewed",
});
await earthquakes.filter(`mag >= 5`);
await earthquakes.renameColumns({ latitude: "lat", longitude: "lon" });
await earthquakes.selectColumns([
  "time",
  "lat",
  "lon",
  "depth",
  "mag",
]);
await earthquakes.round(["lat", "lon", "depth", "mag"], {
  decimals: 3,
});
await earthquakes.updateColumn("depth", `depth * -1`);
await earthquakes.removeDuplicates();
await earthquakes.logTable();
 
await sdb.done();

Les données sur les tremblements de terre nettoyées dans le terminal de VS Code.

Exploration des données

Avant de plonger dans des visualisations personnalisées, il est important d’explorer un peu les données. Nous pouvons utiliser la fonction writeChart avec Plot pour dessiner rapidement quelques graphiques et avoir une idée de ce avec quoi nous travaillons :

  • sda/output/earthquakes-lat-lon.png nous montre où se produisent la plupart des tremblements de terre — le long des failles sismiques. Toutes les coordonnées semblent correctes.
  • Avec sda/output/earthquakes-time-mag.png, on peut clairement voir les tremblements de terre les plus puissants. Les trois de magnitude supérieure à 8 correspondent à la Liste des tremblements de terre en 2021 sur Wikipédia.
  • sda/output/earthquakes-mag-depth.png suggère que la plupart des séismes se produisent à moins de 250 km de profondeur. Les quatre plus puissants étaient proches de la surface.
sda/main.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
import { dot, plot } from "@observablehq/plot";
 
const sdb = new SimpleDB();
 
const earthquakes = sdb.newTable("earthquakes");
await earthquakes.cache(async () => {
  await earthquakes.loadData([
    "https://raw.githubusercontent.com/nshiab/data-fetch-lesson/refs/heads/main/earthquakes-2021-1.csv",
    "https://raw.githubusercontent.com/nshiab/data-fetch-lesson/refs/heads/main/earthquakes-2021-2.csv",
  ]);
});
 
await earthquakes.sort({ time: "asc" });
await earthquakes.keep({
  "type": "earthquake",
  "status": "reviewed",
});
await earthquakes.filter(`mag >= 5`);
await earthquakes.renameColumns({ latitude: "lat", longitude: "lon" });
await earthquakes.selectColumns([
  "time",
  "lat",
  "lon",
  "depth",
  "mag",
]);
await earthquakes.round(["lat", "lon", "depth", "mag"], {
  decimals: 3,
});
await earthquakes.updateColumn("depth", `depth * -1`);
await earthquakes.removeDuplicates();
await earthquakes.logTable();
await earthquakes.writeChart(
  (data) =>
    plot({
      marks: [
        dot(data, {
          x: "lon",
          y: "lat",
        }),
      ],
    }),
  "sda/output/earthquakes-lat-lon.png",
);
await earthquakes.writeChart(
  (data) =>
    plot({
      marks: [
        dot(data, {
          x: "time",
          y: "mag",
        }),
      ],
    }),
  "sda/output/earthquakes-time-mag.png",
);
await earthquakes.writeChart(
  (data) =>
    plot({
      y: { labelArrow: "down" },
      marks: [
        dot(data, {
          x: "mag",
          y: "depth",
        }),
      ],
    }),
  "sda/output/earthquakes-mag-depth.png",
);
 
await sdb.done();

Trois graphiques générés avec Simple Data Analysis et Plot dans VS Code.

💡

Pour ouvrir deux onglets l’un au-dessus de l’autre, faites un clic droit sur l’onglet que vous voulez en bas et cliquez sur Split down. Vous pouvez aussi utiliser Split left, Split right ou Split top. Vous pouvez également faire glisser un onglet vers un autre écran, si vous en avez plusieurs.

Écriture des données pour le web

Comme nos données sont prêtes, nous pouvons maintenant les écrire dans un fichier JSON que Svelte — et tout navigateur web — pourra lire. Il suffit d’ajouter une ligne avec la méthode writeData et de s’assurer d’écrire le fichier dans le dossier src (au lieu de sda, où nous avons travaillé jusqu’ici).

Si vous vous souvenez des leçons précédentes, writeData crée une liste d’objets. C’est exactement ce dont D3 a besoin. 😉

Notez que les fichiers JSON ne peuvent pas stocker d’objets Date, donc nos dates ont été sérialisées. Elles sont enregistrées en tant que chaînes de caractères au format ISO 8601. Elles seront faciles à reconvertir en dates.

sda/main.ts
import { SimpleDB } from "@nshiab/simple-data-analysis";
import { dot, plot } from "@observablehq/plot";
 
const sdb = new SimpleDB();
 
const earthquakes = sdb.newTable("earthquakes");
await earthquakes.cache(async () => {
  await earthquakes.loadData([
    "https://raw.githubusercontent.com/nshiab/data-fetch-lesson/refs/heads/main/earthquakes-2021-1.csv",
    "https://raw.githubusercontent.com/nshiab/data-fetch-lesson/refs/heads/main/earthquakes-2021-2.csv",
  ]);
});
 
await earthquakes.sort({ time: "asc" });
await earthquakes.keep({
  "type": "earthquake",
  "status": "reviewed",
});
await earthquakes.filter(`mag >= 5`);
await earthquakes.renameColumns({ latitude: "lat", longitude: "lon" });
await earthquakes.selectColumns([
  "time",
  "lat",
  "lon",
  "depth",
  "mag",
]);
await earthquakes.round(["lat", "lon", "depth", "mag"], {
  decimals: 3,
});
await earthquakes.updateColumn("depth", `depth * -1`);
await earthquakes.removeDuplicates();
await earthquakes.logTable();
await earthquakes.writeChart(
  (data) =>
    plot({
      marks: [
        dot(data, {
          x: "lon",
          y: "lat",
        }),
      ],
    }),
  "sda/output/earthquakes-lat-lon.png",
);
await earthquakes.writeChart(
  (data) =>
    plot({
      marks: [
        dot(data, {
          x: "time",
          y: "mag",
        }),
      ],
    }),
  "sda/output/earthquakes-time-mag.png",
);
await earthquakes.writeChart(
  (data) =>
    plot({
      y: { labelArrow: "down" },
      marks: [
        dot(data, {
          x: "mag",
          y: "depth",
        }),
      ],
    }),
  "sda/output/earthquakes-mag-depth.png",
);
await earthquakes.writeData("src/data/earthquakes.json");
 
await sdb.done();

Un fichier JSON écrit par la librairie Simple Data Analysis.

Composant de graphique

Créons un nouveau composant Svelte avec une fonction utilitaire pour notre nuage de points.

Mais avant cela, il est toujours utile de définir quelques types que nous allons utiliser à plusieurs reprises. Dans src/lib/index.ts, nous pouvons placer les types et variables qui seront facilement accessibles dans tout notre projet Svelte.

Nous pouvons créer les types earthquake et variable, et les exporter. Ils seront très pratiques par la suite.

src/lib/index.ts
type earthquake = {
  time: Date;
  lat: number;
  lon: number;
  depth: number;
  mag: number;
};
 
type variable = keyof earthquake;
 
export type { earthquake, variable };

Créons maintenant la fonction utilitaire drawChart.ts dans le dossier src/helpers (et non dans sda, encore une fois). C’est là que notre code D3 va vivre. Cette fonction aura besoin de quelques éléments :

  • Un id, qui sera l’identifiant de l’élément svg dans lequel nous allons dessiner notre graphique. Plus d’informations sur svg ci-dessous.
  • Les données earthquakes.
  • Les variables x, y et r (le rayon de nos points).
  • La width et la height du graphique.

Pour l’instant, contentons-nous d’afficher ces paramètres dans la console.

Notez que comme nous utilisons src/lib/index.ts, nous pouvons facilement importer nos types (et tout ce que nous voulons y mettre) avec from $lib. C’est un raccourci bien pratique !

src/helpers/drawChart.ts
import type { earthquake, variable } from "$lib";
 
export default function drawChart(
  id: string,
  earthquakes: earthquake[],
  x: variable,
  y: variable,
  r: variable,
  width: number,
  height: number,
) {
  console.log({ id, earthquakes, x, y, r, width, height });
}

Nous pouvons maintenant créer un nouveau composant Chart.svelte qui :

  • Importe les données de earthquakes.json sous le nom earthquakesRaw (ligne 3), puis les transforme pour convertir les valeurs time en objets Date (lignes 13–16).
  • Récupère id, x, y et r comme props (lignes 6–11).
  • Crée les états width et height (lignes 18–19) et les lie à clientWidth et clientHeight de l’élément svg dans lequel nous allons dessiner notre graphique (ligne 26). Nous parlerons plus en détail des éléments svg plus tard.
  • Utilise la rune $effect pour appeler drawChart avec toutes les props et les états. Cela signifie que Svelte rappellera drawChart si un des arguments change, y compris width et height, rendant ainsi le graphique réactif.
  • Définit une margin-top, une width et une height pour le svg dans les balises style.
src/components/Chart.svelte
<script lang="ts">
    import type { variable } from "$lib";
    import earthquakesRaw from "../data/earthquakes.json";
    import drawChart from "../helpers/drawChart";
 
    const { id, x, y, r }: {
      id: string;
      x: variable;
      y: variable;
      r: variable
    } = $props();
 
    const earthquakes = earthquakesRaw.map((d) => ({
        ...d,
        time: new Date(d.time),
    }));
 
    let width = $state(0);
    let height = $state(0);
 
    $effect(() => {
        drawChart(id, earthquakes, x, y, r, width, height);
    });
</script>
 
<svg {id} bind:clientWidth={width} bind:clientHeight={height}></svg>
 
<style>
    svg {
        margin-top: 2rem;
        width: 100%;
        height: 400px;
    }
</style>

Et enfin, nous pouvons importer notre nouveau composant <Chart /> dans notre page, qui se trouve dans src/routes/+page.svelte. Nous définissons les props appropriés : id, x, y et r. Pour commencer, dessinons un graphique des tremblements de terre et de leur magnitude au fil du temps. Au passage, nous pouvons ajouter des titres et un peu de texte.

Si vous surveillez encore sda/main.ts, vous pouvez l’arrêter (CTRL + C) et lancer deno task dev à la place pour démarrer un serveur local. Ouvrez l’URL affichée dans votre terminal dans votre navigateur préféré.

Dans la console de votre navigateur, vous devriez voir le log provenant de drawChart.ts. Nous sommes prêts à coder notre graphique !

src/routes/+page.svelte
<script lang="ts">
    import Chart from "../components/Chart.svelte";
</script>
 
<h1>Earthquakes</h1>
<p>
    The data used below includes only earthquakes with a magnitude of 5 or more
    that occurred in 2021.
</p>
 
<h2>Scatter plot</h2>
<Chart id="scatterplot" x="time" y="mag" r="mag" />

VS Code et Code avec un projet Svelte lancé en local.

Dessiner avec D3

Toute cette configuration peut sembler compliquée… mais il y a en réalité beaucoup d’avantages à compartimenter votre code. Quand chaque fichier est dédié à une seule tâche, il est plus facile à déboguer. Il y a moins de répétition dans votre projet. De plus, de petits morceaux de code qui ne font des choses précises sont plus faciles à retravailler qu’un gros fichier qui fait tout. Cela deviendra particulièrement évident quand nous ajouterons des animations.

Mais nous avons assez attendu — jouons enfin avec D3 !

Arrêtez votre serveur local (CTRL + C) et installez D3 avec deno add npm:d3. Puis relancez votre serveur local avec deno task dev.

Commençons doucement en dessinant un grand cercle bleu dans notre svg avec la fonction drawChart. Dans le code ci-dessous, D3 :

  • Sélectionne l’élément svg avec l’id donné.
  • Ajoute un élément circle au svg.
  • Spécifie plusieurs attributs pour le cercle en enchaînant les méthodes.
  • cx et cy sont les coordonnées du centre du cercle. Nous le plaçons au centre du svg en utilisant width / 2 et height / 2.
  • r est le rayon du cercle — ici, 50 pixels.
  • fill est la couleur à l’intérieur du cercle — ici, bleu.
src/helpers/drawChart.ts
import type { earthquake, variable } from "$lib";
import { select } from "d3";
 
export default function drawChart(
  id: string,
  earthquakes: earthquake[],
  x: variable,
  y: variable,
  r: variable,
  width: number,
  height: number,
) {
  const svg = select(`#${id}`);
 
  svg.append("circle")
    .attr("cx", width / 2)
    .attr("cy", height / 2)
    .attr("r", 50)
    .attr("fill", "blue");
}

Si vous faites un clic droit sur votre cercle bleu et que vous l’inspectez dans votre navigateur, vous verrez votre svg avec le circle ajouté au code de votre page. C’est ainsi que l’on dessine des éléments SVG avec D3 : en lui indiquant exactement ce que l’on veut et où le placer.

Du code D3 créant un cercle bleu affiché dans Google Chrome.

Pour l’instant, ce cercle n’est lié à aucune donnée. Et nous avons plus de deux mille tremblements de terre. Comment les afficher tous ?

D’abord, nous avons besoin d’échelles (scales) pour convertir les valeurs des tremblements de terre en pixels, rayons et couleurs. Les échelles D3 (il y en a beaucoup) ont besoin de deux choses : un domain et un range.

Par exemple, dans le code ci-dessous, nous utilisons la fonction extent pour récupérer les valeurs minimale et maximale des données x. Cette fonction retourne une liste du type [min, max]. Au cas où vous ne vous en souvenez pas, x est défini comme time dans src/routes/+page.svelte.

Nous créons ensuite une scaleTime (car x contient des dates) avec :

  • les valeurs minimales et maximales de temps comme domain
  • [0, width] comme range

Désormais, cette échelle peut convertir des objets Date en valeurs de pixels. Au lieu de width / 2 pour cx, nous pouvons maintenant utiliser une date en 2021 ! xScale la convertira automatiquement en la bonne position en pixels.

src/helpers/drawChart.ts
import type { earthquake, variable } from "$lib";
import { extent, scaleTime, select } from "d3";
 
export default function drawChart(
  id: string,
  earthquakes: earthquake[],
  x: variable,
  y: variable,
  r: variable,
  width: number,
  height: number,
) {
  const svg = select(`#${id}`);
 
  const xDomain = extent(earthquakes, (d: earthquake) => d[x]);
  const xScale = scaleTime().domain(xDomain).range([0, width]);
 
  svg.append("circle")
    .attr("cx", xScale(new Date("2021-06-01T00:00:00Z")))
    .attr("cy", height / 2)
    .attr("r", 50)
    .attr("fill", "blue");
}

Nous pouvons faire la même chose pour y (qui est actuellement défini sur mag) et cy en utilisant une scaleLinear.

Encore une fois, modifiez la valeur de magnitude à la ligne 23 pour voir yScale en action. Rappelez-vous que les magnitudes dans nos données varient entre 5 et 8.

src/helpers/drawChart.ts
import type { earthquake, variable } from "$lib";
import { extent, scaleLinear, scaleTime, select } from "d3";
 
export default function drawChart(
  id: string,
  earthquakes: earthquake[],
  x: variable,
  y: variable,
  r: variable,
  width: number,
  height: number,
) {
  const svg = select(`#${id}`);
 
  const xDomain = extent(earthquakes, (d: earthquake) => d[x]);
  const xScale = scaleTime().domain(xDomain).range([0, width]);
 
  const yDomain = extent(earthquakes, (d: earthquake) => d[y]);
  const yScale = scaleLinear().domain(yDomain).range([height, 0]);
 
  svg.append("circle")
    .attr("cx", xScale(new Date("2021-06-01T00:00:00Z")))
    .attr("cy", yScale(7))
    .attr("r", 50)
    .attr("fill", "blue");
}

Nous pouvons aussi ajouter une échelle pour l’attribut r — cette fois en utilisant une échelle racine carrée (scaleSqrt), car nous voulons que la surface du cercle soit proportionnelle aux données.

Et ajoutons une autre échelle pour la couleur, qui pourrait également être liée à rDomain.

Oui, je sais — les échelles D3 sont incroyables ! Et comme ce sont simplement des fonctions, vous pouvez les utiliser pour tout ce que vous voulez, que ce soit dans des graphiques D3 ou ailleurs !

src/helpers/drawChart.ts
import type { earthquake, variable } from "$lib";
import { extent, scaleLinear, scaleSqrt, scaleTime, select } from "d3";
 
export default function drawChart(
  id: string,
  earthquakes: earthquake[],
  x: variable,
  y: variable,
  r: variable,
  width: number,
  height: number,
) {
  const svg = select(`#${id}`);
 
  const xDomain = extent(earthquakes, (d: earthquake) => d[x]);
  const xScale = scaleTime().domain(xDomain).range([0, width]);
 
  const yDomain = extent(earthquakes, (d: earthquake) => d[y]);
  const yScale = scaleLinear().domain(yDomain).range([height, 0]);
 
  const rDomain = extent(earthquakes, (d: earthquake) => d[r]);
  const rScale = scaleSqrt().domain(rDomain).range([0, 20]);
  const fillScale = scaleLinear().domain(rDomain).range(["yellow", "red"]);
 
  svg.append("circle")
    .attr("cx", xScale(new Date("2021-06-01T00:00:00Z")))
    .attr("cy", yScale(7))
    .attr("r", rScale(7))
    .attr("fill", fillScale(7));
}

Maintenant que nous avons nos échelles, nous pouvons exploiter tout le potentiel de D3 ! Au lieu d’ajouter un seul cercle, attachons des éléments SVG à nos données.

Voici une explication étape par étape du nouveau code ci-dessous :

  • D’abord, nous sélectionnons tous les cercles dans le SVG (ligne 25). Lors du premier rendu, il n’y en a aucun — et c’est normal.
  • Ensuite, nous lions les données (ligne 26). Ici, nous avons plus de 2 000 tremblements de terre. Cela signifie que nous disons à D3 : « Hé, je vais te demander de dessiner quelque chose plus de 2 000 fois. »
  • Nous lions les données aux éléments SVG (ligne 27). Pour chaque tremblement de terre, nous allons dessiner un circle.
  • Nous pouvons maintenant utiliser des fonctions pour indiquer à D3 quels attributs nous voulons pour chaque tremblement de terre. Ici, nous utilisons x, y et r avec les échelles appropriées.

Comme certains tremblements de terre se chevauchent, j’ai également défini l’opacity à 0.5.

src/helpers/drawChart.ts
import type { earthquake, variable } from "$lib";
import { extent, scaleLinear, scaleSqrt, scaleTime, select } from "d3";
 
export default function drawChart(
  id: string,
  earthquakes: earthquake[],
  x: variable,
  y: variable,
  r: variable,
  width: number,
  height: number,
) {
  const svg = select(`#${id}`);
 
  const xDomain = extent(earthquakes, (d: earthquake) => d[x]);
  const xScale = scaleTime().domain(xDomain).range([0, width]);
 
  const yDomain = extent(earthquakes, (d: earthquake) => d[y]);
  const yScale = scaleLinear().domain(yDomain).range([height, 0]);
 
  const rDomain = extent(earthquakes, (d: earthquake) => d[r]);
  const rScale = scaleSqrt().domain(rDomain).range([0, 20]);
  const fillScale = scaleLinear().domain(rDomain).range(["yellow", "red"]);
 
  svg.selectAll("circle")
    .data(earthquakes)
    .join("circle")
    .attr("cx", (d: earthquake) => xScale(d[x]))
    .attr("cy", (d: earthquake) => yScale(d[y]))
    .attr("r", (d: earthquake) => rScale(d[r]))
    .attr("fill", (d: earthquake) => fillScale(d[r]))
    .attr("opacity", 0.5);
}

Et regardez-moi ça ! Vous avez maintenant tous vos tremblements de terre ajoutés à votre svg ! Et si vous inspectez un cercle et consultez ses Properties, vous verrez les données du tremblement de terre. Elles sont réellement liées à l’élément SVG.

Deux mille cercles dessinés dans un élément SVG avec D3.

Axes

Nous avons dessiné nos tremblements de terre, mais ce serait bien d’ajouter des axes x et y. Pour nous assurer qu’il y ait suffisamment d’espace autour, nous devons aussi définir des marges.

Créons un objet margins pour ajouter de l’espace autour du graphique et pour positionner correctement nos axes.

Pour dessiner les axes, nous pouvons utiliser les fonctions axisLeft et axisBottom avec nos échelles. En général, on les place dans un élément g (pour group) afin de pouvoir facilement les identifier (avec une class ou un id) ou les déplacer si nécessaire.

Les axes D3 sont assez intelligents et essaient automatiquement de créer des étiquettes de graduation pertinentes. Ici, comme nous utilisons une scaleTime pour l’axe du bas (axisBottom), et que son domaine est limité à 2021, la fonction affiche 2021 au début, puis uniquement les mois. Toutefois, pour éviter le chevauchement des étiquettes, j’ai défini le nombre de ticks à 3.

Aussi, pour éviter de redessiner les axes encore et encore lorsqu’une prop ou un état change (comme lors du redimensionnement de la fenêtre), je leur ai donné une class appelée axis et je l’utilise pour les sélectionner et les supprimer avant de les redessiner (ligne 55). Notez que nous n’avons pas besoin de faire cela pour les cercles car ils sont liés à leurs données, même entre les rendus.

src/helpers/drawChart.ts
import type { earthquake, variable } from "$lib";
import {
  axisBottom,
  axisLeft,
  extent,
  scaleLinear,
  scaleSqrt,
  scaleTime,
  select,
} from "d3";
 
export default function drawChart(
  id: string,
  earthquakes: earthquake[],
  x: variable,
  y: variable,
  r: variable,
  width: number,
  height: number,
) {
  const svg = select(`#${id}`);
 
  const margins = {
    top: 20,
    right: 20,
    bottom: 35,
    left: 80,
  };
 
  const xDomain = extent(earthquakes, (d: earthquake) => d[x]);
  const xScale = scaleTime().domain(xDomain).range([
    margins.left,
    width - margins.right,
  ]);
 
  const yDomain = extent(earthquakes, (d: earthquake) => d[y]);
  const yScale = scaleLinear().domain(yDomain).range([
    height - margins.bottom,
    margins.top,
  ]);
 
  const rDomain = extent(earthquakes, (d: earthquake) => d[r]);
  const rScale = scaleSqrt().domain(rDomain).range([0, 20]);
  const fillScale = scaleLinear().domain(rDomain).range(["yellow", "red"]);
 
  svg.selectAll("circle")
    .data(earthquakes)
    .join("circle")
    .attr("cx", (d: earthquake) => xScale(d[x]))
    .attr("cy", (d: earthquake) => yScale(d[y]))
    .attr("r", (d: earthquake) => rScale(d[r]))
    .attr("fill", (d: earthquake) => fillScale(d[r]))
    .attr("opacity", 0.5);
 
  svg.selectAll(".axis").remove();
 
  svg
    .append("g")
    .attr("class", "axis")
    .attr(
      "transform",
      `translate(0, ${height - margins.bottom})`,
    )
    .call(axisBottom(xScale).ticks(3));
 
  svg
    .append("g")
    .attr("class", "axis")
    .attr("transform", `translate(${margins.left}, 0)`)
    .call(axisLeft(yScale));
}

Enfin, ce serait une bonne idée d’ajouter des étiquettes aux axes également — et peut-être un peu de marge intérieure pour éviter que les cercles ne chevauchent les axes.

Pour avoir de vraies étiquettes au lieu des abréviations utilisées dans nos données, j’ai créé un objet labels que nous pouvons utiliser pour le texte.

src/helpers/drawChart.ts
import type { earthquake, variable } from "$lib";
import {
  axisBottom,
  axisLeft,
  extent,
  scaleLinear,
  scaleSqrt,
  scaleTime,
  select,
} from "d3";
 
export default function drawChart(
  id: string,
  earthquakes: earthquake[],
  x: variable,
  y: variable,
  r: variable,
  width: number,
  height: number,
) {
  const svg = select(`#${id}`);
 
  const margins = {
    top: 20,
    right: 20,
    bottom: 35,
    left: 80,
  };
  const inset = 10;
 
  const xDomain = extent(earthquakes, (d: earthquake) => d[x]);
  const xScale = scaleTime().domain(xDomain).range([
    margins.left,
    width - margins.right,
  ]);
 
  const yDomain = extent(earthquakes, (d: earthquake) => d[y]);
  const yScale = scaleLinear().domain(yDomain).range([
    height - margins.bottom,
    margins.top,
  ]);
 
  const rDomain = extent(earthquakes, (d: earthquake) => d[r]);
  const rScale = scaleSqrt().domain(rDomain).range([0, 20]);
  const fillScale = scaleLinear().domain(rDomain).range(["yellow", "red"]);
 
  svg.selectAll("circle")
    .data(earthquakes)
    .join("circle")
    .attr("cx", (d: earthquake) => xScale(d[x]))
    .attr("cy", (d: earthquake) => yScale(d[y]))
    .attr("r", (d: earthquake) => rScale(d[r]))
    .attr("fill", (d: earthquake) => fillScale(d[r]))
    .attr("opacity", 0.5);
 
  svg.selectAll(".axis").remove();
 
  svg
    .append("g")
    .attr("class", "axis")
    .attr(
      "transform",
      `translate(0, ${height - margins.bottom + inset})`,
    )
    .call(axisBottom(xScale).ticks(3));
 
  svg
    .append("g")
    .attr("class", "axis")
    .attr("transform", `translate(${margins.left - inset}, 0)`)
    .call(axisLeft(yScale));
 
  svg.selectAll(".labels").remove();
 
  const labels: { [key: string]: string } = {
    "time": "Time",
    "mag": "Magnitude",
  };
 
  svg.append("text")
    .attr("class", "labels")
    .attr("x", width - margins.right)
    .attr("y", height)
    .attr("font-size", 12)
    .attr("text-anchor", "end")
    .text(`${labels[x]} →`);
 
  svg.append("text")
    .attr("class", "labels")
    .attr("x", margins.left - inset / 2)
    .attr("y", margins.top - inset)
    .attr("font-size", 12)
    .attr("text-anchor", "end")
    .text(`${labels[y]} ↑`);
}

Ce rendu est vraiment chouette pour un premier graphique D3 ! Et en plus, il est entièrement réactif ! Essayez de redimensionner votre navigateur et admirez la magie !

Deux mille cercles dessinés dans un élément SVG avec D3.

Animations

Notre graphique est super, mais soyons honnêtes : on pourrait créer la même chose beaucoup plus rapidement et facilement avec Plot…

Cependant, il y a une chose qui est difficile à faire avec Plot : les animations.

Mettons à jour notre fichier src/routes/+page.svelte pour offrir à l’utilisateur deux types de graphiques. Nous pouvons utiliser le composant pré-codé <Radio />, qui crée des boutons radio, et lier (bind) la variable chartType à l’option sélectionnée.

En fonction de l’état de chartType, nous pouvons mettre à jour les états x et y nouvellement créés, qui seront ensuite transmis à notre composant <Chart />.

src/routes/+pages.svelte
<script lang="ts">
    import type { variable } from "$lib";
    import Chart from "../components/Chart.svelte";
    import Radio from "../components/Radio.svelte";
 
    let chartType = $state("Time/Magnitude");
 
    let x = $state<variable>("time");
    let y = $state<variable>("mag");
 
    $effect(() => {
        if (chartType === "Time/Magnitude") {
            x = "time";
            y = "mag";
        } else {
            x = "mag";
            y = "depth";
        }
    });
</script>
 
<h1>Earthquakes</h1>
<p>
    The data used below includes only earthquakes with a magnitude of 5 or more
    that occurred in 2021.
</p>
 
<h2>Scatter plot</h2>
<Radio
    bind:value={chartType}
    values={["Time/Magnitude", "Magnitude/Depth"]}
    label="Pick a chart:"
/>
<Chart id="scatterplot" {x} {y} r="mag" />
💡

Vous vous demandez peut-être ce que signifie <variable> aux lignes 8 et 9. Tout comme vous pouvez passer des arguments à certaines fonctions, vous pouvez aussi passer des types. C’est la syntaxe prévue pour cela. Ici, cela indique à $state que l’état créé devra être du type variable.

Maintenant, chaque fois que nous changeons le type de graphique, notre composant <Chart /> est re-rendu avec de nouvelles valeurs x et y, qui sont utilisées par notre fonction drawChart !

Des boutons radio mettant à jour un graphique D3.

Il y a quelques petits bugs visuels que nous pouvons corriger tout de suite dans src/helpers/drawChart.ts :

  • Si x est time, nous devons utiliser une scaleTime, mais si c’est mag, nous devons utiliser scaleLinear.
  • Si x est time, nous pouvons garder 3 ticks, mais si c’est mag, on peut laisser D3 décider.
  • Nous pouvons ajouter Depth à notre objet labels et inverser la flèche de l’axe Y si c’est depth.
  • Nous pouvons ajuster les margins et la position des étiquettes pour éviter que notre nouveau texte soit coupé.
src/helpers/drawChart.ts
import type { earthquake, variable } from "$lib";
import {
  axisBottom,
  axisLeft,
  extent,
  scaleLinear,
  scaleSqrt,
  scaleTime,
  select,
} from "d3";
 
export default function drawChart(
  id: string,
  earthquakes: earthquake[],
  x: variable,
  y: variable,
  r: variable,
  width: number,
  height: number,
) {
  const svg = select(`#${id}`);
 
  const margins = {
    top: 20,
    right: 20,
    bottom: 45,
    left: 85,
  };
  const inset = 10;
 
  const xDomain = extent(earthquakes, (d: earthquake) => d[x]);
  const xRange = [
    margins.left,
    width - margins.right,
  ];
  const xScale = x === "time"
    ? scaleTime().domain(xDomain).range(xRange)
    : scaleLinear().domain(xDomain).range(xRange);
 
  const yDomain = extent(earthquakes, (d: earthquake) => d[y]);
  const yScale = scaleLinear().domain(yDomain).range([
    height - margins.bottom,
    margins.top,
  ]);
 
  const rDomain = extent(earthquakes, (d: earthquake) => d[r]);
  const rScale = scaleSqrt().domain(rDomain).range([0, 20]);
  const fillScale = scaleLinear().domain(rDomain).range(["yellow", "red"]);
 
  svg.selectAll("circle")
    .data(earthquakes)
    .join("circle")
    .attr("cx", (d: earthquake) => xScale(d[x]))
    .attr("cy", (d: earthquake) => yScale(d[y]))
    .attr("r", (d: earthquake) => rScale(d[r]))
    .attr("fill", (d: earthquake) => fillScale(d[r]))
    .attr("opacity", 0.5);
 
  svg.selectAll(".axis").remove();
 
  svg
    .append("g")
    .attr("class", "axis")
    .attr(
      "transform",
      `translate(0, ${height - margins.bottom + inset})`,
    )
    .call(x === "time" ? axisBottom(xScale).ticks(3) : axisBottom(xScale));
 
  svg
    .append("g")
    .attr("class", "axis")
    .attr("transform", `translate(${margins.left - inset}, 0)`)
    .call(axisLeft(yScale));
 
  svg.selectAll(".labels").remove();
 
  const labels: { [key: string]: string } = {
    "time": "Time",
    "mag": "Magnitude",
    "depth": "Depth (km)",
  };
 
  svg.append("text")
    .attr("class", "labels")
    .attr("x", width - margins.right)
    .attr("y", height - 3)
    .attr("font-size", 12)
    .attr("text-anchor", "end")
    .text(`${labels[x]} →`);
 
  svg.append("text")
    .attr("class", "labels")
    .attr("x", margins.left - inset / 2)
    .attr("y", margins.top - inset)
    .attr("font-size", 12)
    .attr("text-anchor", "end")
    .text(y === "depth" ? `${labels[y]} ↓` : `${labels[y]} ↑`);
}

Un nuage de points redessiné avec différentes variables.

C’est sympa, mais ce n’est pas animé. Le graphique est simplement redessiné à chaque fois, sans aucune transition. Reprenons src/helpers/drawChart.ts pour créer une transition fluide des cercles.

Créer des animations avec D3 est très simple. Il suffit d’abord de sélectionner les éléments que vous voulez animer, d’appeler .transition(), puis d’enchaîner les attributs que vous souhaitez modifier.

Dans notre cas, nous devons d’abord vérifier s’il faut dessiner ou animer. Pour cela, on regarde s’il y a déjà quelque chose dans le svg (ligne 23). S’il n’y a rien, cela signifie qu’on doit dessiner (lignes 53–60). Sinon, on veut animer les éléments déjà présents (lignes 62–65).

Pour animer, on sélectionne tous les cercles, on appelle .transition() pour dire à D3 d’interpoler entre leurs attributs actuels et les nouveaux. Ici, on met simplement à jour cx et cy à l’aide du domain et range mis à jour de xScale et yScale.

Facile !

src/helpers/drawChart.ts
import type { earthquake, variable } from "$lib";
import {
  axisBottom,
  axisLeft,
  extent,
  scaleLinear,
  scaleSqrt,
  scaleTime,
  select,
} from "d3";
 
export default function drawChart(
  id: string,
  earthquakes: earthquake[],
  x: variable,
  y: variable,
  r: variable,
  width: number,
  height: number,
) {
  const svg = select(`#${id}`);
 
  const animate = svg.selectAll("*").nodes().length > 0;
 
  const margins = {
    top: 20,
    right: 20,
    bottom: 45,
    left: 85,
  };
  const inset = 10;
 
  const xDomain = extent(earthquakes, (d: earthquake) => d[x]);
  const xRange = [
    margins.left,
    width - margins.right,
  ];
  const xScale = x === "time"
    ? scaleTime().domain(xDomain).range(xRange)
    : scaleLinear().domain(xDomain).range(xRange);
 
  const yDomain = extent(earthquakes, (d: earthquake) => d[y]);
  const yScale = scaleLinear().domain(yDomain).range([
    height - margins.bottom,
    margins.top,
  ]);
 
  const rDomain = extent(earthquakes, (d: earthquake) => d[r]);
  const rScale = scaleSqrt().domain(rDomain).range([0, 20]);
  const fillScale = scaleLinear().domain(rDomain).range(["yellow", "red"]);
 
  if (!animate) {
    svg.selectAll("circle")
      .data(earthquakes)
      .join("circle")
      .attr("cx", (d: earthquake) => xScale(d[x]))
      .attr("cy", (d: earthquake) => yScale(d[y]))
      .attr("r", (d: earthquake) => rScale(d[r]))
      .attr("fill", (d: earthquake) => fillScale(d[r]))
      .attr("opacity", 0.5);
  } else {
    svg.selectAll("circle")
      .transition()
      .attr("cx", (d: earthquake) => xScale(d[x]))
      .attr("cy", (d: earthquake) => yScale(d[y]));
  }
 
  svg.selectAll(".axis").remove();
 
  svg
    .append("g")
    .attr("class", "axis")
    .attr(
      "transform",
      `translate(0, ${height - margins.bottom + inset})`,
    )
    .call(x === "time" ? axisBottom(xScale).ticks(3) : axisBottom(xScale));
 
  svg
    .append("g")
    .attr("class", "axis")
    .attr("transform", `translate(${margins.left - inset}, 0)`)
    .call(axisLeft(yScale));
 
  svg.selectAll(".labels").remove();
 
  const labels: { [key: string]: string } = {
    "time": "Time",
    "mag": "Magnitude",
    "depth": "Depth (km)",
  };
 
  svg.append("text")
    .attr("class", "labels")
    .attr("x", width - margins.right)
    .attr("y", height - 3)
    .attr("font-size", 12)
    .attr("text-anchor", "end")
    .text(`${labels[x]} →`);
 
  svg.append("text")
    .attr("class", "labels")
    .attr("x", margins.left - inset / 2)
    .attr("y", margins.top - inset)
    .attr("font-size", 12)
    .attr("text-anchor", "end")
    .text(y === "depth" ? `${labels[y]} ↓` : `${labels[y]} ↑`);
}

Mais si vous testez l’animation maintenant, ce n’est pas très intéressant. Elle est trop rapide, et tous les points bougent en même temps. On peut rendre ça plus sympa en ajoutant une duration, une ease et un delay.

Une animation simple d’un nuage de points.

Pour la durée, j’ai choisi 1 000 millisecondes. Pour l’easing, j’aime bien easeCubicInOut, mais vous avez plein d’options avec D3. Et pour le délai, j’utilise simplement la durée multipliée par un nombre aléatoire entre 0 et 1. Ainsi, chaque point aura un délai différent — compris entre 0 et la durée choisie.

Maintenant, l’ensemble paraît plus organique et agréable à regarder !

src/helpers/drawChart.ts
import type { earthquake, variable } from "$lib";
import {
  axisBottom,
  axisLeft,
  easeCubicInOut,
  extent,
  scaleLinear,
  scaleSqrt,
  scaleTime,
  select,
} from "d3";
 
export default function drawChart(
  id: string,
  earthquakes: earthquake[],
  x: variable,
  y: variable,
  r: variable,
  width: number,
  height: number,
) {
  const svg = select(`#${id}`);
 
  const animate = svg.selectAll("*").nodes().length > 0;
  const duration = 1000;
 
  const margins = {
    top: 20,
    right: 20,
    bottom: 45,
    left: 85,
  };
  const inset = 10;
 
  const xDomain = extent(earthquakes, (d: earthquake) => d[x]);
  const xRange = [
    margins.left,
    width - margins.right,
  ];
  const xScale = x === "time"
    ? scaleTime().domain(xDomain).range(xRange)
    : scaleLinear().domain(xDomain).range(xRange);
 
  const yDomain = extent(earthquakes, (d: earthquake) => d[y]);
  const yScale = scaleLinear().domain(yDomain).range([
    height - margins.bottom,
    margins.top,
  ]);
 
  const rDomain = extent(earthquakes, (d: earthquake) => d[r]);
  const rScale = scaleSqrt().domain(rDomain).range([0, 20]);
  const fillScale = scaleLinear().domain(rDomain).range(["yellow", "red"]);
 
  if (!animate) {
    svg.selectAll("circle")
      .data(earthquakes)
      .join("circle")
      .attr("cx", (d: earthquake) => xScale(d[x]))
      .attr("cy", (d: earthquake) => yScale(d[y]))
      .attr("r", (d: earthquake) => rScale(d[r]))
      .attr("fill", (d: earthquake) => fillScale(d[r]))
      .attr("opacity", 0.5);
  } else {
    svg.selectAll("circle")
      .transition()
      .duration(duration)
      .ease(easeCubicInOut)
      .delay(() => Math.random() * duration)
      .attr("cx", (d: earthquake) => xScale(d[x]))
      .attr("cy", (d: earthquake) => yScale(d[y]));
  }
 
  svg.selectAll(".axis").remove();
 
  svg
    .append("g")
    .attr("class", "axis")
    .attr(
      "transform",
      `translate(0, ${height - margins.bottom + inset})`,
    )
    .call(x === "time" ? axisBottom(xScale).ticks(3) : axisBottom(xScale));
 
  svg
    .append("g")
    .attr("class", "axis")
    .attr("transform", `translate(${margins.left - inset}, 0)`)
    .call(axisLeft(yScale));
 
  svg.selectAll(".labels").remove();
 
  const labels: { [key: string]: string } = {
    "time": "Time",
    "mag": "Magnitude",
    "depth": "Depth (km)",
  };
 
  svg.append("text")
    .attr("class", "labels")
    .attr("x", width - margins.right)
    .attr("y", height - 3)
    .attr("font-size", 12)
    .attr("text-anchor", "end")
    .text(`${labels[x]} →`);
 
  svg.append("text")
    .attr("class", "labels")
    .attr("x", margins.left - inset / 2)
    .attr("y", margins.top - inset)
    .attr("font-size", 12)
    .attr("text-anchor", "end")
    .text(y === "depth" ? `${labels[y]} ↓` : `${labels[y]} ↑`);
}

Nous pourrions aussi animer les axes et les étiquettes.

Pour l’axe des x, comme il passe de dates à des nombres et inversement, il n’y a pas de manière fluide de faire la transition des valeurs comme on le fait avec l’axe des y. À la place, j’ai enchaîné des transitions pour le faire disparaître, le mettre à jour, puis le faire réapparaître. C’est le même principe pour les étiquettes de texte.

À noter également : nous n’avons plus besoin de supprimer les axes ou les étiquettes, puisqu’on les met désormais à jour directement.

src/helpers/drawChart.ts
import type { earthquake, variable } from "$lib";
import {
  axisBottom,
  axisLeft,
  easeCubicInOut,
  extent,
  scaleLinear,
  scaleSqrt,
  scaleTime,
  select,
} from "d3";
 
export default function drawChart(
  id: string,
  earthquakes: earthquake[],
  x: variable,
  y: variable,
  r: variable,
  width: number,
  height: number,
) {
  const svg = select(`#${id}`);
 
  const animate = svg.selectAll("*").nodes().length > 0;
  const duration = 1000;
 
  const margins = {
    top: 20,
    right: 20,
    bottom: 45,
    left: 85,
  };
  const inset = 10;
 
  const xDomain = extent(earthquakes, (d: earthquake) => d[x]);
  const xRange = [
    margins.left,
    width - margins.right,
  ];
  const xScale = x === "time"
    ? scaleTime().domain(xDomain).range(xRange)
    : scaleLinear().domain(xDomain).range(xRange);
 
  const yDomain = extent(earthquakes, (d: earthquake) => d[y]);
  const yScale = scaleLinear().domain(yDomain).range([
    height - margins.bottom,
    margins.top,
  ]);
 
  const rDomain = extent(earthquakes, (d: earthquake) => d[r]);
  const rScale = scaleSqrt().domain(rDomain).range([0, 20]);
  const fillScale = scaleLinear().domain(rDomain).range(["yellow", "red"]);
 
  if (!animate) {
    svg.selectAll("circle")
      .data(earthquakes)
      .join("circle")
      .attr("cx", (d: earthquake) => xScale(d[x]))
      .attr("cy", (d: earthquake) => yScale(d[y]))
      .attr("r", (d: earthquake) => rScale(d[r]))
      .attr("fill", (d: earthquake) => fillScale(d[r]))
      .attr("opacity", 0.5);
  } else {
    svg.selectAll("circle")
      .transition()
      .duration(duration)
      .ease(easeCubicInOut)
      .delay(() => Math.random() * duration)
      .attr("cx", (d: earthquake) => xScale(d[x]))
      .attr("cy", (d: earthquake) => yScale(d[y]));
  }
 
  if (!animate) {
    svg
      .append("g")
      .attr("id", "x-axis")
      .attr(
        "transform",
        `translate(0, ${height - margins.bottom + inset})`,
      )
      .call(x === "time" ? axisBottom(xScale).ticks(3) : axisBottom(xScale));
  } else {
    svg.select("#x-axis")
      .attr("opacity", 1)
      .transition()
      .duration(duration / 2)
      .attr("opacity", 0)
      .transition()
      .duration(0)
      .call(x === "time" ? axisBottom(xScale).ticks(3) : axisBottom(xScale))
      .transition()
      .duration(duration / 2)
      .attr("opacity", 1);
  }
 
  if (!animate) {
    svg
      .append("g")
      .attr("id", "y-axis")
      .attr("transform", `translate(${margins.left - inset}, 0)`)
      .call(axisLeft(yScale));
  } else {
    svg.select("#y-axis")
      .transition()
      .duration(duration).call(axisLeft(yScale));
  }
 
  const labels: { [key: string]: string } = {
    "time": "Time",
    "mag": "Magnitude",
    "depth": "Depth (km)",
  };
 
  if (!animate) {
    svg.append("text")
      .attr("id", "label-x")
      .attr("x", width - margins.right)
      .attr("y", height - 3)
      .attr("font-size", 12)
      .attr("text-anchor", "end")
      .text(`${labels[x]} →`);
 
    svg.append("text")
      .attr("id", "label-y")
      .attr("x", margins.left - inset / 2)
      .attr("y", margins.top - inset)
      .attr("font-size", 12)
      .attr("text-anchor", "end")
      .text(y === "depth" ? `${labels[y]} ↓` : `${labels[y]} ↑`);
  } else {
    svg.select("#label-x")
      .attr("opacity", 1)
      .transition()
      .duration(duration / 2)
      .attr("opacity", 0)
      .transition()
      .duration(0)
      .text(`${labels[x]} →`)
      .transition()
      .duration(duration / 2)
      .attr("opacity", 1);
 
    svg.select("#label-y")
      .attr("opacity", 1)
      .transition()
      .duration(duration / 2)
      .attr("opacity", 0)
      .transition()
      .duration(0)
      .text(y === "depth" ? `${labels[y]} ↓` : `${labels[y]} ↑`)
      .transition()
      .duration(duration / 2)
      .attr("opacity", 1);
  }
}

Un nuage de points animés.

Construction de la page

Jusqu’à présent, nous avons exécuté notre page avec un serveur local. Si vous souhaitez construire votre site web, exécutez la commande deno task build. Svelte minimisera et optimisera votre code et créera les fichiers de votre site web dans le dossier build. Vous pourrez ensuite héberger ces fichiers sur un serveur pour partager votre travail avec le monde entier !

Conclusion

Notre graphique est magniiifiiiique ! Nous pourrions continuer à itérer pour l’améliorer encore plus, mais je pense que nous en avons déjà fait beaucoup. Vous pouvez être fier de vous.

Si vous souhaitez explorer davantage d’exemples D3, n’oubliez pas de consulter la galerie D3. Tout le code est en code ouvert !

Si vous êtes prêt à relever un défi supplémentaire, nous n’avons pas utilisé les valeurs lat et lon. Essayez d’ajouter une autre option aux boutons radio qui met à jour x vers lon et y vers lat, puis ajustez la fonction drawChart pour vous assurer que tout fonctionne correctement avec cette nouvelle option.

D3 n’est pas seulement bon pour les graphiques. C’est aussi G-É-N-I-A-L pour les cartes ! Et c’est ce dont traitera la prochaine leçon. À bientôt !

Vous avez aimé ? Vous voulez être prévenu quand de nouvelles leçons sont publiées ? Abonnez-vous à l'infolettre ✉️ et donnez une ⭐ au cours sur GitHub pour me garder motivé ! Contactez-moi si vous avez des questions.