Fermer

juin 12, 2018

Visualisation interactive de données avec JavaScript moderne et D3 –


Dans cet article, je veux vous présenter un exemple de projet que j'ai récemment construit – un type de visualisation totalement original en utilisant la bibliothèque D3, qui montre comment chacun de ces composants s'additionne pour faire D3 une grande bibliothèque à apprendre.

D3 signifie Data Driven Documents. C'est une bibliothèque JavaScript qui peut être utilisée pour faire toutes sortes de merveilleuses visualisations de données et graphiques.

Si vous avez déjà vu l'un des fabuleux récits interactifs du New York Times, vous aurez déjà ont vu D3 en action. Vous pouvez également voir des exemples sympas de grands projets qui ont été construits avec D3 ici

La courbe d'apprentissage est assez raide pour commencer avec la bibliothèque, puisque D3 a quelques bizarreries spéciales que vous n'auriez probablement jamais vu auparavant. Cependant, si vous pouvez passer la première phase de l'apprentissage de D3 assez dangereux, alors vous serez bientôt capable de construire des choses vraiment cool pour vous.

Il y a trois facteurs principaux qui font que D3 se démarque de tout autres bibliothèques:

  1. Flexibilité . D3 vous permet de prendre n'importe quel type de données et de l'associer directement à des formes dans la fenêtre du navigateur. Ces données peuvent être absolument n'importe quoi permettant un large éventail de cas d'utilisation intéressants pour créer des visualisations complètement originales.
  2. Elegance . Il est facile d'ajouter des éléments interactifs avec des transitions douces entre les mises à jour. La bibliothèque est magnifiquement écrite et une fois que vous avez compris la syntaxe, il est facile de garder votre code propre et bien rangé.
  3. Communauté . Il existe déjà un vaste écosystème de développeurs fantastiques utilisant D3, qui partagent volontiers leur code en ligne. Vous pouvez utiliser des sites comme bl.ocks.org et blockbuilder.org pour trouver rapidement du code pré-écrit par d'autres, et copier ces extraits directement dans vos propres projets.

Projet

En tant que professeur d'économie à l'université, j'ai toujours été intéressé par l'inégalité des revenus. J'ai pris quelques cours sur le sujet, et cela m'a semblé quelque chose qui n'était pas entièrement compris au degré qu'il devrait être.

J'ai commencé à explorer l'inégalité des revenus en utilisant Google Public Data Explorer … [19659003]  Graphique linéaire illustrant l'évolution des revenus au fil du temps aux États-Unis

Lorsque l'on rajuste l'inflation, le revenu des ménages reste stable pour les 40% de la population les plus pauvres , bien que la productivité par travailleur ait monté en flèche. C'est seulement que les 20% qui ont récolté le plus de bénéfices (et dans cette tranche, la différence est encore plus choquante si vous regardez le top 5%).

Voici un message que je voulais traverser d'une manière convaincante, ce qui me donnait l'occasion parfaite d'utiliser quelques D3.js, alors j'ai commencé à dessiner quelques idées

Esquisse

Parce que nous travaillons avec D3, je pourrais plus ou moins juste commencer à esquisser absolument rien que je pouvais penser. Faire un simple graphique linéaire, graphique à barres ou graphique à bulles aurait été assez facile, mais je voulais faire quelque chose de différent.

Je trouve que l'analogie la plus courante que les gens avaient tendance à utiliser comme contre-argument "Si la tarte grossit alors il y a plus à faire". L'intuition est que, si la part totale du PIB parvient à augmenter dans une large mesure, alors même si certaines personnes obtiennent une tranche plus petite de la tarte, alors ils seront encore mieux lotis . Cependant, comme nous pouvons le voir, il est tout à fait possible que le gâteau soit plus gros et pour que les gens en reçoivent moins.

Ma première idée pour visualiser ces données ressemblait à ceci: [19659003]  Ma première esquisse représentant «la tarte qui s'agrandit»

L'idée serait que nous ayons ce graphique à secteurs, chaque tranche représentant un cinquième de la distribution des revenus aux États-Unis. La superficie de chaque tranche de tarte se rapportait au revenu de ce segment de la population, et la superficie totale du graphique représenterait son PIB total.

Cependant, j'ai rapidement rencontré un petit problème. Il s'avère que le cerveau humain est exceptionnellement pauvre pour distinguer la taille des différentes zones . Quand j'ai cartographié cela de manière plus concrète, le message n'était pas aussi évident qu'il aurait dû l'être:

 À quoi ressemblait le dessin ...

Ici, il semble vraiment être le plus pauvre Les Américains deviennent plus riches au fil du temps, ce qui confirme ce qui semble être intuitivement vrai. Je pensais à ce problème un peu plus, et ma solution impliquait de garder l'angle de chaque arc constant, le rayon de chaque arc changeant dynamiquement.

 Ma première esquisse représentant "la tarte qui s'agrandit" [19659003] Voici comment cela a fini par ressembler à la pratique:

 A quoi ressemblait la nouvelle esquisse

Je tiens à souligner que cette image tend encore à sous-estimer l'effet ici. L'effet aurait été plus évident si nous avions utilisé un simple graphique à barres:

 Comment cela ressemblerait à un graphique à barres

Cependant, je m'étais engagé à faire une visualisation unique, et je voulais marteler à la maison ce message que le tarte peut obtenir plus grand tandis qu'une part peut obtenir plus petit . Maintenant que j'ai eu mon idée, il était temps de la construire avec D3

Code d'emprunt

Donc, maintenant que je sais ce que je vais construire, il est temps de se plonger dans la vraie vie de ce projet, et commencer à écrire du code .

Vous pourriez penser que je commencerais par écrire mes premières lignes de code à partir de zéro, mais vous auriez tort. Ceci est D3, et puisque nous travaillons avec D3, nous pouvons toujours trouver du code pré-écrit de la communauté pour commencer.

Nous créons quelque chose de complètement nouveau, mais il a beaucoup en commun avec un J'ai donc jeté un rapide coup d'oeil sur bl.ocks.org et j'ai décidé de suivre cette mise en œuvre classique de Mike Bostock, l'un des créateurs de D3. Ce fichier a probablement déjà été copié des milliers de fois, et le gars qui l'a écrit est un vrai assistant avec JavaScript, donc nous pouvons être sûrs que nous commençons avec un bon bloc de code.

Ce fichier est écrit en D3 V3, qui est maintenant deux versions périmées, depuis la version 5 a été finalement publié le mois dernier. Un grand changement dans D3 V4 était que la bibliothèque passait à l'utilisation d'un espace de noms plat, de sorte que les fonctions d'échelle comme d3.scale.ordinal () sont écrites comme d3.scaleOrdinal () à la place . Dans la version 5, le plus gros changement était que les fonctions de chargement des données sont maintenant structurées comme Promises ce qui facilite le traitement simultané de plusieurs jeux de données.

Pour éviter toute confusion, j'ai déjà connu de créer une version V5 mise à jour de ce code, que j'ai enregistré sur blockbuilder.org . J'ai également converti la syntaxe pour qu'elle corresponde aux conventions ES6, comme la commutation de fonctions anonymes de ES5 en fonctions de flèches.

Voici ce que nous commençons déjà:

 Le camembert que nous commençons avec

J'ai ensuite copié ces fichiers dans mon répertoire de travail, et fait en sorte que je puisse tout répliquer sur ma propre machine. Si vous voulez suivre vous-même ce tutoriel, vous pouvez cloner ce projet à partir de notre repo GitHub . Vous pouvez commencer avec le code dans le fichier starter.html . S'il vous plaît noter que vous aurez besoin d'un serveur (tel que celui-ci ) pour exécuter ce code, car sous le capot, il s'appuie sur l'API Fetch pour récupérer les données. Je vous donne un rapide aperçu de la façon dont ce code fonctionne

Marcher dans notre code

Tout d'abord, nous déclarons quelques constantes au début de notre fichier, que nous utiliserons pour définir la taille. de notre camembert:

 const largeur = 540;
hauteur constante = 540;
rayon constant = Math.min (largeur, hauteur) / 2;

Cela rend notre code super réutilisable, car si nous voulons toujours le rendre plus grand ou plus petit, nous n'avons qu'à nous soucier de changer ces valeurs ici.

Ensuite, nous ajoutons un canevas SVG à l'écran . Si vous ne connaissez pas beaucoup les SVG, alors vous pouvez considérer le canevas comme l'espace sur la page sur lequel nous pouvons dessiner des formes. Si nous essayons de dessiner un SVG en dehors de cette zone, il ne s'affichera simplement pas à l'écran:

 const svg = d3.select ("# chart-area")
  .append ("svg")
    .attr ("largeur", largeur)
    .attr ("hauteur", hauteur)
  .append ("g")
    .attr ("transformer", `traduire ($ {largeur / 2}, $ {hauteur / 2})`);

Nous attrapons une div vide avec l'ID de chart-area avec un appel à d3.select () . Nous attachons également un canevas SVG avec la méthode d3.append () et nous définissons des dimensions pour sa largeur et sa hauteur en utilisant la méthode d3.attr () .

Nous attachons également un élément de groupe SVG à ce canevas, qui est un type spécial d'élément que nous pouvons utiliser pour structurer les éléments ensemble. Cela nous permet de déplacer toute notre visualisation au centre de l'écran, en utilisant l'attribut de transformation de l'élément de groupe .

Après cela, nous mettons en place une échelle par défaut que nous allons utiliser pour assigner une nouvelle couleur pour chaque tranche de notre tarte:

 const color = d3.scaleOrdinal (["#66c2a5", "#fc8d62", "#8da0cb","#e78ac3", "#a6d854", "#ffd92f"]);

Ensuite, nous avons quelques lignes qui configurent la disposition des secteurs de D3:

 const pie = d3.pie ()
  .value (d => d.count)
  .sort (null);

Dans D3, les mises en page sont des fonctions spéciales que nous pouvons appeler sur un ensemble de données. Une fonction de mise en page prend un tableau de données dans un format particulier, et crache un tableau transformé avec des valeurs générées automatiquement, avec lesquelles nous pouvons faire quelque chose.

Nous devons ensuite définir un Générateur de chemins que nous pouvons utiliser pour dessiner nos arcs. Les générateurs de chemins nous permettent de tracer des chemins SVG dans un navigateur Web. Tout ce que fait vraiment D3 est d'associer des morceaux de données avec des formes sur l'écran, mais dans ce cas, nous voulons définir une forme plus compliquée qu'un simple cercle ou carré. Les chemins SVG fonctionnent en définissant une route entre laquelle dessiner une ligne, que nous pouvons définir avec son attribut d

Voici à quoi cela pourrait ressembler:

  
  

 La sortie de ce chemin code SVG

L'attribut d contient un encodage spécial qui permet au navigateur de dessiner le chemin que nous voulons. Si vous voulez vraiment savoir ce que cette chaîne signifie, vous pouvez le découvrir dans la documentation SVG de MDN . Pour la programmation en D3, nous n'avons pas vraiment besoin de savoir quoi que ce soit sur cet encodage spécial, puisque nous avons des générateurs qui vont cracher pour nous les attributs d que nous avons juste besoin d'initialiser avec quelques paramètres simples.

Pour un arc, nous devons donner à notre générateur de chemin une valeur innerRadius et une valeur outerRadius en pixels, et le générateur va trier les calculs complexes qui entrent dans le calcul de chacun les angles pour nous:

 const arc = d3.arc ()
  .innerRadius (0)
  .outerRadius (rayon);

Pour notre tableau, nous utilisons une valeur de zéro pour notre innerRadius ce qui nous donne un graphique à secteurs standard. Cependant, si nous voulions dessiner un graphique donut à la place, alors tout ce que nous aurions à faire est de brancher une valeur qui est plus petite que notre valeur outerRadius . déclarations de fonction, nous chargeons dans nos données avec la fonction d3.json () :

 d3.json ("data.json", type) .then (data => {
  // Fait quelque chose avec nos données
});

Dans la version 5.x de D3, un appel à d3.json () renvoie une promesse ce qui signifie que D3 récupère le contenu du fichier JSON qu'il trouve au chemin relatif que nous lui donnons, et exécuter la fonction que nous appelons dans la méthode then () une fois qu'elle a été chargée. Nous avons alors accès à l'objet que nous regardons dans data argument de notre callback.

Nous passons aussi dans une référence de fonction ici – type – qui va convertir toutes les valeurs que nous chargeons en nombres , avec lequel on pourra travailler plus tard:

 function type (d) {
  d.apples = nombre (d.apples);
  d.oranges = Nombre (d.oranges);
  retour d;
}

Si nous ajoutons une instruction console.log (data); en haut de notre rappel d3.json nous pouvons regarder les données que nous travaillons actuellement avec:

 {pommes: Array (5), oranges: Array (5)}
  pommes: Array (5)
    0: {region: "Nord", compte: "53245"}
    1: {région: "Sud", compte: "28479"}
    2: {région: "Est", compte: "19697"}
    3: {region: "West", comptage: "24037"}
    4: {region: "Central", comptage: "40245"}
  oranges: Tableau (5)
    0: {région: "Nord", compte: "200"}
    1: {région: "Sud", compte: "200"}
    2: {région: "Est", compte: "200"}
    3: {région: "Ouest", compte: "200"}
    4: {region: "Central", compte: "200"}

Nos données sont divisées ici en deux tableaux différents, représentant nos données pour pommes et oranges respectivement.

Avec cette ligne, nous allons changer les données que nous regardons chaque fois que l'un de nos boutons radio est cliqué:

 d3.selectAll ("entrée")
  .on ("change", mise à jour);

Nous devrons aussi appeler la fonction update () à la première exécution de notre visualisation, en passant une valeur initiale (avec notre tableau "pommes").

 update ("pommes ");

Jetons un coup d'œil à ce que fait notre fonction update () . Si vous êtes nouveau sur D3, cela peut causer une certaine confusion, car c'est l'une des parties les plus difficiles à comprendre de D3 …

 function update (value = this.value) {
  // Joindre de nouvelles données
  const path = svg.selectAll ("chemin")
    .data (tarte (données [value]));

  // Mettre à jour les arcs existants
  path.transition (). duration (200) .attrTween ("d", arcTween);

  // Entrer de nouveaux arcs
  path.enter (). append ("chemin")
    .attr ("fill", (d, i) => couleur (i))
    .attr ("d", arc)
    .attr ("coup", "blanc")
    .attr ("stroke-width", "6px")
    .each (fonction (d) {this._current = d;});
}

Tout d'abord, nous utilisons un paramètre de fonction par défaut pour la valeur . Si nous passons un argument à notre fonction update () (lorsque nous l'exécutons pour la première fois), nous utiliserons cette chaîne, sinon nous aurons la valeur que nous avons désirer de l'événement cliquez sur de nos entrées radio

Nous utilisons ensuite le General Update Pattern en D3 pour gérer le comportement de nos arcs. Cela implique généralement d'effectuer une jointure de données, de quitter les anciens éléments, de mettre à jour les éléments existants à l'écran et d'ajouter de nouveaux éléments qui ont été ajoutés à nos données. Dans cet exemple, nous n'avons pas besoin de nous soucier des éléments qui sortent, puisque nous avons toujours le même nombre de tranches sur l'écran.

Tout d'abord, il y a notre jointure de données:

 // JOIN
const path = svg.selectAll ("chemin")
  .data (tarte (données [val]));

Chaque fois que notre visualisation est mise à jour, cela associe un nouveau tableau de données avec nos SVG à l'écran. Nous passons nos données (soit le tableau pour "pommes" ou "oranges") dans notre fonction de mise en page pie () qui calcule certains angles de début et de fin, qui peuvent être utilisés pour dessiner nos arcs . Cette variable path contient maintenant une sélection virtuelle spéciale de tous les arcs sur l'écran

Ensuite, nous mettons à jour tous les SVG sur l'écran qui existent encore dans notre tableau de données. Nous ajoutons ici une transition ici – une fonctionnalité fantastique de la bibliothèque D3 – pour diffuser ces mises à jour sur 200 millisecondes:

 // MISE À JOUR
path.transition (). durée (200)
  .attrTween ("d", arcTween);

Nous utilisons la méthode attrTween () dans l'appel d3.transition () pour définir une transition personnalisée que D3 doit utiliser pour mettre à jour les positions de chacun de ses arcs. (transition avec l'attribut d ). Nous n'avons pas besoin de faire cela si nous essayons d'ajouter une transition à la plupart de nos attributs, mais nous devons le faire pour la transition entre différents chemins. D3 ne peut pas vraiment comprendre comment faire la transition entre les chemins personnalisés, donc nous utilisons la fonction arcTween () pour indiquer à D3 comment chacun de nos chemins devrait être dessiné à chaque instant. [19659003] Voici à quoi ressemble cette fonction:

 function arcTween (a) {
  const i = d3.interpolate (this._current, a);
  this._current = i (1);
  return t => arc (i (t));
}

Nous utilisons d3.interpolate () ici pour créer ce qu'on appelle un interpolateur . Lorsque nous appelons la fonction que nous stockons dans la variable i avec une valeur comprise entre 0 et 1, nous récupérons une valeur qui se situe quelque part entre this._current et ] a . Dans ce cas, this._current est un objet qui contient l'angle de début et de fin de la tranche de secteur que nous examinons, et a représente le nouveau point de données que nous sommes.

Une fois l'interpolateur configuré, nous mettons à jour la valeur this._current pour contenir la valeur que nous aurons à la fin ( i (a) ), puis nous retournons une fonction qui calculera le chemin que notre arc devrait contenir, basé sur cette valeur t . Notre transition exécutera cette fonction à chaque instant de son horloge (passant dans un argument entre 0 et 1), et ce code signifiera que notre transition saura où nos arcs devraient être dessinés à tout moment.

notre update () fonction doit ajouter de nouveaux éléments qui ne se trouvaient pas dans le tableau de données précédent:

 // ENTER
path.enter (). append ("chemin")
  .attr ("fill", (d, i) => couleur (i))
  .attr ("d", arc)
  .attr ("coup", "blanc")
  .attr ("stroke-width", "6px")
  .each (fonction (d) {this._current = d;});

Ce bloc de code définit les positions initiales de chacun de nos arcs, la première fois que cette fonction de mise à jour est exécutée. La méthode enter () nous donne ici tous les éléments de nos données qui doivent être ajoutés à l'écran, puis nous pouvons boucler chacun de ces éléments avec le attr () méthodes, pour définir le remplissage et la position de chacun de nos arcs. Nous donnons également à chacun de nos arcs une bordure blanche, ce qui rend notre tableau un peu plus net. Enfin, nous définissons la propriété this._current de chacun de ces arcs comme valeur initiale de l'élément dans nos données, que nous utilisons dans la fonction arcTween ()

Ne vous inquiétez pas si vous ne pouvez pas suivre exactement comment cela fonctionne, car c'est un sujet assez avancé dans D3. La grande chose à propos de cette bibliothèque est que vous n'avez pas besoin de connaître tous ses rouages ​​pour créer des choses puissantes avec elle. Tant que vous pouvez comprendre les bits que vous devez modifier, il est bon d'abstraire certains détails qui ne sont pas absolument essentiels.

Cela nous amène à l'étape suivante du processus …

Adapter le code [19659010] Maintenant que nous avons du code dans notre environnement local, et nous comprenons ce qu'il fait, je vais changer les données que nous examinons, afin qu'elles fonctionnent avec les données qui nous intéressent.

J'ai inclus les données avec lesquelles nous travaillerons dans le dossier data / de notre projet. Puisque ce nouveau fichier revenus.csv est au format CSV cette fois (c'est le genre de fichier que vous pouvez ouvrir avec Microsoft Excel), je vais utiliser le d3.csv () fonction, au lieu de la fonction d3.json () :

 d3.csv ("data / revenus.csv"). Then (data => {
  ...
});

Cette fonction fait essentiellement la même chose que d3.json () – convertir nos données dans un format que nous pouvons utiliser. Je supprime également la fonction d'initialisation type () comme deuxième argument ici, car elle était spécifique à nos anciennes données.

Si vous ajoutez un console.log (data) déclaration en haut du rappel d3.csv vous pourrez voir la forme des données avec lesquelles nous travaillons:

 (50) [{…}, {…}, {…}, {…}, {…}, {…}, {…} ... columns: Array(9)]
  0:
    1: "12457"
    2: "32631"
    3: "56832"
    4: "92031"
    5: "202366"
    moyenne: "79263"
    haut: "350870"
    total: "396317"
    année: "2015"
  1: {1: "11690", 2: "31123", 3: "54104", 4: "87935", 5: "194277", année: "2014", en haut: "332729", moyenne: "75826" , total: "379129"}
  2: {1: "11797", 2: "31353", 3: "54683", 4: "87989", 5: "196742", année: "2013", en haut: "340329", moyenne: "76513" , total: "382564"}
  ...

Nous avons un tableau de 50 items, chaque item représentant une année dans nos données. Pour chaque année, nous avons alors un objet, avec des données pour chacun des cinq groupes de revenu, ainsi que quelques autres champs. Nous pourrions créer un diagramme à secteurs pour l'une de ces années, mais nous devrons d'abord mélanger un peu nos données afin qu'elles soient dans le bon format. Lorsque nous voulons écrire une jointure de données avec D3, nous devons passer dans un tableau, où chaque élément sera lié à un SVG.

Rappelons que, dans notre dernier exemple, nous avions un tableau avec un élément pour chaque tarte tranche que nous voulions afficher sur l'écran. Comparez cela à ce que nous avons en ce moment, qui est un objet avec les clés de 1 à 5 représentant chaque tranche de tarte que nous voulons dessiner.

Pour résoudre ce problème, je vais ajouter une nouvelle fonction appelée ] prepareData () pour remplacer la fonction type () que nous avions précédemment, qui va parcourir chaque élément de nos données quand il est chargé:

 function prepareData (d) {
  revenir {
    nom: d.year,
    moyenne: parseInt (d.average),
    valeurs: [
      {
        name: "first",
        value: parseInt(d["1"])
      },
      {
        nom: "deuxième",
        valeur: parseInt (d ["2"])
      },
      {
        nom: "troisième",
        valeur: parseInt (d ["3"])
      },
      {
        nom: "quatrième",
        valeur: parseInt (d ["4"])
      },
      {
        nom: "cinquième",
        valeur: parseInt (d ["5"])
      }
    ]
  }
}

d3.csv ("data / revenus.csv", prepareData) .then (data => {
    ...
});

Pour chaque année, cette fonction retournera un objet avec un tableau de valeurs que nous transmettrons à notre jointure de données. Nous étiquetons chacune de ces valeurs avec un champ et nous leur donnons une valeur numérique basée sur les valeurs de revenu que nous avions déjà. Nous suivons également le revenu moyen de chaque année à des fins de comparaison.

À ce stade, nous avons nos données dans un format avec lequel nous pouvons travailler:

 (50) [{…}, {…}, {…}, {…}, {…}, {…}, {…} ... columns: Array(9)]
  0:
  moyenne: 79263
  nom: "2015"
  values: Array (5)
    0: {nom: "premier", valeur: 12457}
    1: {name: "second", valeur: 32631}
    2: {name: "third", valeur: 56832}
    3: {nom: "quatrième", valeur: 92031}
    4: {nom: "cinquième", valeur: 202366}
  1: {name: "2014", moyenne: 75826, valeurs: Array (5)}
  2: {name: "2013", moyenne: 76513, valeurs: Array (5)}
  ...

Je vais commencer par générer un graphique pour la première année de nos données, puis je vais m'inquiéter de la mettre à jour pour le reste des années.

En ce moment, nos données commencent en l'année 2015 et se termine en 1967, nous devrons donc inverser ce tableau avant de faire quoi que ce soit d'autre:

 d3.csv ("data / revenus.csv", prepareData) .then (data => {
  data = data.reverse ();
  ...
});

Contrairement à un graphique à secteurs normal, pour notre graphique, nous voulons corriger les angles de chacun de nos arcs, et avoir juste le rayon qui change lorsque notre visualisation se met à jour. Pour ce faire, nous allons changer la méthode value () sur notre disposition de tarte, de sorte que chaque tranche de tarte reçoive toujours les mêmes angles:

 const pie = d3.pie ()
  .value (1)
  .sort (null);

Ensuite, nous aurons besoin de mettre à jour notre rayon chaque fois que notre visualisation sera mise à jour. Pour ce faire, nous devrons trouver une échelle que nous pouvons utiliser. Une échelle est une fonction dans D3 qui prend une entrée entre deux valeurs, que nous passons dans le domaine puis crache une sortie entre deux valeurs différentes, que nous passons dans la gamme . Voici l'échelle que nous utiliserons:

 d3.csv ("data / revenus.csv", prepareData) .then (data => {
  data = data.reverse ();
  const radiusScale = d3.scaleSqrt ()
    .domaine ([0, data[49] .values ​​[4] .value])
    .range ([0, Math.min(width, height) / 2]);
  ...
});

Nous ajoutons cette échelle dès que nous avons accès à nos données et nous disons que notre contribution devrait être comprise entre 0 et la plus grande valeur de notre ensemble de données, qui est le revenu du groupe le plus riche de l'année dernière dans nos données ( données [49] .values ​​[4] .value ). Pour le domaine, nous définissons l'intervalle entre deux valeurs de sortie.

Cela signifie qu'une entrée de zéro doit nous donner une valeur de pixel de zéro, et une entrée de la plus grande valeur de nos données devrait nous donner une valeur de la moitié de la valeur de notre largeur ou de notre hauteur – la valeur la plus petite.

Notez que nous utilisons également une échelle de racine carrée ici. La raison pour laquelle nous faisons cela est que nous voulons que la superficie de nos tranches de tarte soit proportionnelle au revenu de chacun de nos groupes, plutôt que le rayon. Puisque area = πr 2 nous devons utiliser une échelle de racine carrée pour prendre en compte ceci.

Nous pouvons ensuite utiliser cette échelle pour mettre à jour la valeur outerRadius de notre générateur d'arc notre update () fonction:

 fonction mise à jour (value = this.value) {
  arc.outerRadius (d => radiusScale (d.data.value));
  ...
});

Chaque fois que nos données changent, cela modifie la valeur de rayon que nous voulons utiliser pour chacun de nos arcs.

Nous devrions également retirer notre appel à outerRadius lorsque nous avons initialement configuré notre générateur d'arc , de sorte que nous avons juste ceci en haut de notre fichier:

 const arc = d3.arc ()
  .innerRadius (0);

Enfin, nous devons apporter quelques modifications à cette fonction update () pour que tout corresponde à nos nouvelles données:

 function update (data) {
  arc.outerRadius (d => radiusScale (d.data.value));

  // JOIN
  const path = svg.selectAll ("chemin")
    .data (tarte (données.values));

  // METTRE À JOUR
  path.transition (). duration (200) .attrTween ("d", arcTween);

  // ENTRER
  path.enter (). append ("chemin")
    .attr ("fill", (d, i) => couleur (i))
    .attr ("d", arc)
    .attr ("coup", "blanc")
    .attr ("stroke-width", "2px")
    .each (fonction (d) {this._current = d;});
}

Comme nous n'allons plus utiliser nos boutons radio, je ne fais que passer l'objet-année que nous voulons utiliser en appelant:

 // Rendre la première année dans nos données
mise à jour (données [0]);

Enfin, je vais supprimer l'écouteur d'événement que nous avons défini pour nos entrées de formulaire. Si tout est allé au plan, nous devrions avoir un beau tableau pour la première année dans nos données:

 Notre carte rendue pour la première année de nos données

Making it Dynamic

La prochaine étape est d'avoir notre cycle de visualisation entre les différentes années, montrant comment les revenus ont changé au fil du temps. Nous ferons cela en ajoutant un appel à la fonction setInterval () de JavaScript, que nous pouvons utiliser pour exécuter du code à plusieurs reprises:

 d3.csv ("data / revenus.csv", prepareData). alors (data => {
  ...

  mise à jour de la fonction (données) {
    ...
  }

  laisser le temps = 0;
  laissez interval = setInterval (étape, 200);

  function step () {
    mise à jour (données [time]);
    temps = (temps == 49)? 0: heure + 1;
  }

  mise à jour (données [0]);
});

Nous configurons une minuterie dans cette variable time et toutes les 200ms, ce code exécutera la fonction step () qui mettra à jour notre graphique pour l'année suivante données, et incrémenter le temporisateur de 1. Si le temporisateur est à une valeur de 49 (la dernière année de nos données), il se réinitialisera lui-même. Cela nous donne maintenant une belle boucle qui fonctionnera continuellement:

 Notre mise à jour graphique entre chaque année dans nos données

Pour rendre les choses un peu plus utiles. Je vais aussi ajouter quelques étiquettes qui nous donnent les chiffres bruts. Je remplacerai tout le code HTML dans le corps de notre fichier avec ceci:

Année:

                                       
Support de revenu Revenu du ménage (dollars de 2015)
Le plus haut 20%
Deuxième plus haut 20 %
Milieu 20%
Deuxième plus bas 20%
Le plus bas 20%
Moyenne
    
  

Nous structurons notre page ici en utilisant le système de grille de Bootstrap ce qui nous permet de mettre en forme nos éléments de page dans des boîtes.

Je vais mettre à jour tout cela avec jQuery ] chaque fois que nos données changent:

 function updateHTML (data) {
  // Mettre à jour le titre
  $ ("# année"). text (data.name);

  // Mettre à jour les valeurs de table
  $ ("# fig1"). html (données.values ​​[0] .value.toLocaleString ());
  $ ("# fig2"). html (données.values ​​[1] .value.toLocaleString ());
  $ ("# fig3"). html (données.values ​​[2] .value.toLocaleString ());
  $ ("# fig4"). html (données.values ​​[3] .value.toLocaleString ());
  $ ("# fig5"). html (data.values ​​[4] .value.toLocaleString ());
  $ ("# avFig"). html (data.average.toLocaleString ());
}

d3.csv ("data / revenus.csv", prepareData) .then (data => {
  ...
  mise à jour de la fonction (données) {
    updateHTML (données);
    ...
  }
  ...
}

Je vais également apporter quelques modifications au CSS en haut de notre fichier, ce qui nous donnera une légende pour chacun de nos arcs, et aussi centrer notre entête:

Ce que nous finissons par quelque chose est plutôt présentable:

 Notre tableau après avoir ajouté une table et un style

Puisqu'il est assez difficile de voir comment ces arcs ont changé au fil du temps ici, Je veux ajouter dans quelques lignes de grille pour montrer à quoi ressemblait la répartition des revenus dans la première année de nos données:

 d3.csv ("data / revenus.csv", prepareData) .then (data => {
  ...
  mise à jour (données [0]);

  données [0] .values.forEach ((d, i) => {
    svg.append ("cercle")
      .attr ("fill", "none")
      .attr ("cx", 0)
      .attr ("cy", 0)
      .attr ("r", radiusScale (d.value))
      .attr ("coup", couleur (i))
      .attr ("stroke-dasharray", "4,4");
  });
});

I’m using the Array.forEach() method to accomplish this, although I could have also gone with D3’s usual General Update Pattern again (JOIN/EXIT/UPDATE/ENTER).

I also want to add in a line to show the average income in the US, which I’ll update every year. First, I’ll add the average line for the first time:

d3.csv("data/incomes.csv", prepareData).then(data => {
  ...

  data[0].values.forEach((d, i) => {
    svg.append("circle")
      .attr("fill", "none")
      .attr("cx", 0)
      .attr("cy", 0)
      .attr("r", radiusScale(d.value))
      .attr("stroke", color(i))
      .attr("stroke-dasharray", "4,4");
  });

  svg.append("circle")
    .attr("class", "averageLine")
    .attr("fill", "none")
    .attr("cx", 0)
    .attr("cy", 0)
    .attr("stroke", "grey")
    .attr("stroke-width", "2px");
});

Then I’ll update this at the end of our update() function whenever the year changes:

function update(data) {
  ...
  svg.select(".averageLine").transition().duration(200)
    .attr("r", radiusScale(data.average));
}

I should note that it’s important for us to add each of these circles after our first call to update()because otherwise they’ll end up being rendered behind each of our arc paths (SVG layers are determined by the order in which they’re added to the screen, rather than by their z-index).

At this point, we have something that conveys the data that we’re working with a bit more clearly:

Our chart after adding in some grid lines for the first year of our data

Making it Interactive

As a last step, I want us to add in some controls to let the user dig down into a particular year. I want to add in a Play/Pause button, as well as a year slider, allowing the user to pick a particular date to look at.

Here’s the HTML that I’ll use to add these elements onto the screen:

...

We’ll need to add some event listeners to both of these elements, to engineer the behavior that we’re looking for.

First off, I want to define the behavior of our Play/Pause button. We’ll need to replace the code that we wrote for our interval earlier to allow us to stop and start the timer with the button. I’ll assume that the visualization starts in a “Paused” state, and that we need to press this button to kick things off.

function update(data) {
  ...

  let time = 0;
  let interval;

  function step() {
    update(data[time]);
    time = (time == 49) ? 0 : time + 1;
  }

  $("#play-button").on("click", function() {
    const button = $(this);
    if (button.text() === "Play"){
      button.text("Pause");
      interval = setInterval(step, 200);
    } autre {
      button.text("Play");
      clearInterval(interval);
    }
  });
  ...
}

Whenever our button gets clicked, our if/else block here is going to define a different behavior, depending on whether our button is a “Play” button or a “Pause” button. If the button that we’re clicking says “Play”, we’ll change the button to a “Pause” button, and start our interval loop going. Alternatively, if the button is a “Pause” button, we’ll change its text to “Play”, and we’ll use the clearInterval() function to stop the loop from running.

For our slider, I want to use the slider that comes with the jQuery UI library. I’m including this in our HTML, and I’m going to write a few lines to add this to the screen:

function update(data) {
  ...
  $("#date-slider").slider({
    max: 49,
    min: 0,
    step: 1,
    slide: (event, ui) => {
      time = ui.value;
      update(data[time]);
    }
  });

  update(data[0]);
  ...
}

Here, we’re using the slide option to attach an event listener to the slider. Whenever our slider gets moved to another value, we’re updating our timer to this new value, and we’re running our update() function at that year in our data.

We can add this line at the end of our update() function so that our slider moves along to the right year when our loop is running:

function update(data) {
  ...

  // Update slider position
  $("#date-slider").slider("value", time);
}

I’ll also add in a line to our updateHTML() function (which runs whenever our visualization changes), which can adjust the value of the label based on the current year in the data:

function updateHTML(data) {
  // Update title
  $("#year").text(data.name);

  // Update slider label
  $("#year-label").text(data.name);

  // Update table values
  $("#fig1").html(data.values[0].value.toLocaleString());
  ...
}

I’ll throw in a few more lines to our CSS to make everything look a little neater:

And there we have it — our finished product — a fully functioning interactive data visualization, with everything working as expected.

Our chart after adding in some interactive elements

Hopefully, this tutorial demonstrated the real power of D3, letting you create absolutely anything you can imagine.

Getting started with D3 from scratch is always a tough process, but the rewards are worth it. If you want to learn how to create custom visualizations of your own, here are a few online resources that you might find helpful:

  • An overview of SitePoint’s D3.js content.
  • The introduction to the library on D3’s homepage. This runs through some of the most basic commands, showing you how to make your first few steps in D3.
  • Let’s Make a Bar Chart” by Mike Bostock — the creator of D3 — showing beginners how to make one of the simplest graphs in the library.
  • D3.js in Action by Elijah Meeks ($35), which is a solid introductory textbook that goes into a lot of detail.
  • D3’s Slack channel is very welcoming to newcomers to D3. It also has a “learning materials” section with a collection of great resources.
  • This online Udemy course ($20), which covers everything in the library in a series of video lectures. This is aimed at JavaScript developers, and includes four cool projects.
  • The multitude of example visualizations that are available at bl.ocks.org and blockbuilder.org.
  • The D3 API Referencewhich gives a thorough technical explanation of everything that D3 has to offer.

And don’t forget, if you want to see the finished version of the code that I was using in the article, then you can find it on our GitHub repo.




Source link