Comment faire cohabiter D3 et Vue ?
Pourquoi vouloir faire cohabiter Vue et D3 ?
Vue est un excellent framework pour construire des applications web complexes. Son coté minimaliste lui permet de rester léger mais si on veut créer une application riche en visualisations, Vue ne suffit pas. Il faut alors se tourner vers d’autres outils comme la librairie D3 de Mike Bostock.
D3 permet de créer des visualisations complexes, dynamiques et adaptées aux besoins les plus fins, mais son fonctionnement est indépendant de tout framework, ce qui rend l’intégration d’une visualisation D3 à un composant Vue à la charge du développeur.
Dans cet article nous verrons comment faire cohabiter ces deux outils pour créer un composant Vue qui contient une visualisation D3.
Fonctionnement de Vue
Vue est un framework qui s’appuie sur le principe de composants réutilisables. En découpant notre application dans plusieurs composants à responsabilité réduite, on crée facilement une application puissante et maintenable.
Un point très intéressant de Vue est la réactivité de son modèle de données. Les changements de données sont détectés automatiquement et les composants qui en dépendent se mettent à jour immédiatement.
Fonctionnement de D3
D3 est une librairie de visualisation plutôt bas-niveau. Il n’y a pas de graphiques tout fait, mais plutôt une collection de fonctions qui permettent de faire des visualisations adaptées aux besoins les plus fins.
D3 est composé de plusieurs modules, qui vont des fonctions de mathématiques et d’analyse, à des outils pour modifier directement le DOM.
Dans cet article nous utiliserons qu’une petite sous partie de l’ensemble des modules de D3 :
- d3-array Des fonctions sur les listes
- d3-scale Des fonctions pour faire correspondre des données vers un espace différent, comme un nombre de pixels
- d3-selection Des fonctions pour modifier le DOM
Outre l’absence totale de composants (D3 n’est pas un framework) une grosse différence entre D3 et Vue est la façon de modifier le DOM à partir des données.
Vue utilise un DOM virtuel qui est créé à partir de l’architecture des composants, et qui se met à jour avec son modèle de données réactif.
D3 modifie directement le DOM et se met à jour manuellement avec un pattern appelé “General Update Pattern”
Un composant histogramme avec D3 dans une application Vue
Pour le besoin de cet article nous allons créer un composant réutilisable qui utilise D3 pour afficher un histogramme.
Ce composant aura 4 propriétés :
- un tableau de valeurs
- le nombre de barres qui composent l’histogramme
- la hauteur du graphique
- la largeur du graphique
Pour l’affichage de l’histogramme dans le DOM, on va utiliser un svg inline
.
Le composant a donc pour structure :
<template>
<svg :width="width" :height="height"></svg>
</template>
<script>
import * as d3 from "d3";
export default {
name: "Histogram",
props: {
// taille en pixel du composant
width: { type: Number },
height: { type: Number },
// Une liste de nombres
data: { type: Array },
// Combien de Bins (combien de barres)
numBins: { type: Number, default: 40 }
},
}
</script>
Histogramme des données
D3 a une fonction qui permet de facilement compter la distribution d’un tableau de données dans plusieurs bins. Chaque bin représente le nombre de valeurs entre deux bornes. Par exemple la bin 0-10
contient l’ensemble des points compris entre 0 et 10.
Nous allons utiliser une computed prop
de Vue pour créer la distribution qui nous servira ensuite pour l’affichage de l’histogramme. Mettre cette fonction dans une computed nous permet de s’assurer qu’elle est toujours à jour par rapport à notre propriété this.data
histogram() {
const countHistogram = d3
.histogram()
.domain(d3.extent(this.data)) // récupère le min et le max de this.data dans un tableau
.thresholds(this.numBins); // on assigne le nombre de bins
return countHistogram(this.data) // on appelle la fonction sur nos nouvelles données
.map(bins => ({ x: bins.x0, y: bins.length })); // on garde uniquement le nombre d'éléments et la valeur de départ de la bin
}
Render
Pour créer le svg de l’histogramme nous avons besoin de créer un rect
pour chaque bin. Contrairement aux composants classiques, le template Vue ne contient pas ce qui va être affiché, c’est D3 qui va s’en charger.
On va donc créer une fonction renderSvg
qui peut accéder au DOM directement avec this.$el
d3-scale
Premièrement nous avons besoin de créer des scales
. Ces scales
nous permettent d’associer des valeurs sur des positions en pixels. Par exemple, nous avons une première bin qui contient 10 éléments, la question est de savoir quelle est la taille et la position du rectangle associé. Nous avons 2 scales
:
La scale x
qui associe une valeur à l’axe horizontal du graphique. Il y a les valeurs de l’histogramme (le domain
pour d3) et un axe qui fait 300 pixels, et nous voulons une marge entre chaque barre de 20%.
const x = d3
.scaleBand()
.padding(0.2)
.domain(this.histogram.map(d => d.x))
.rangeRound([0, this.width]);
Et l’autre scale
, y
qui associe le nombre de points dans chaque bin à une hauteur du rectangle. Le domain
contient les valeurs extrêmes, c’est à dire 0
et la taille de la plus grande bin.
const y = d3
.scaleLinear()
.domain([0, d3.max(this.histogram, d => d.y)])
.range([this.height, 0]);
D3 General update pattern
Enfin il faut afficher les points, je ne rentrerai pas trop dans les détails, mais le code suivant parcourt en même temps this.histogram
et le selecteur 'rect'
et créé un rect
pour chaque data supplémentaire. Si vous voulez plus de détails sur les sélections d3, je vous conseille les exemples de General Update Pattern par Mike Bostock
this.svg.selectAll("rect").data(this.histogram)
.enter() // quand il y a plus de données que d'élements rect
.append("rect") // on rajoute un élément rect avec la class "bar"
.attr("class", "bar")
.merge(selection) // on fait un update et un enter
.attr(
"transform",
d => `translate(${x(d.x)} ${this.height}) scale(1 -1)`
) // on positionne la barre horizontalement
.attr("width", x.bandwidth()) // largeur de la barre est définie par la scale x
.attr("height", d => y(d.y)); // on change la hauteur de la barre
Watchers
Comme D3 modifie directement le DOM, il n’est pas lié à la réactivité des données Vue. C’est donc à nous de décider quand et pourquoi il faut appeler renderSvg()
.
La méthode renderSvg()
doit être appelée à chaque fois que l’on veut modifier l’intérieur du svg. C’est donc quand les props data
et numBins
changent que nous devons modifier le DOM à nouveau.
Il suffit d’ajouter un champ watch
à notre composant
watch: {
data: "renderSvg",
numBins: "renderSvg"
}
De plus au moment de la création de notre histogramme nous devons aussi afficher le svg dans la fonction mounted()
Démo
Inconvénients de cette méthode
Intégrer D3 dans Vue de cette façon apporte son lot de difficultés supplémentaires, qui peuvent être contournées plus ou moins facilement.
Mise à jour asynchrone de Vue
Vue modifie le DOM de façon asynchrone, c’est à dire qu’un changement de donnée ne modifie pas immédiatement le DOM. Cependant dans notre exemple nous utilisons d3 pour modifier le DOM immédiatement (avec le watcher
), ce qui peut compliquer certains évenements.
Par exemple :
Si une partie du composant fonctionne avec Vue et l’autre avec D3 et que nous avons besoin de calculer la taille d’une div depuis le DOM, la div risque de ne pas exister au moment du watch.
La solution classique est d’utiliser la fonction de Vue $nextTick()
qui permet d’attendre l’update de Vue avant de faire nos propres changements au DOM
Si vous voulez plus d’informations sur le comportement de Vue je vous conseille la très bonne documentation de Vue - Async Update Queue
Scoped style
Vue permet d’appliquer un style uniquement au niveau d’un composant, avec le scoped CSS. Ce CSS ne sera pas appliqué aux éléments créés par D3.
Pour contourner ce problème Vue propose une syntaxe spéciale, le Deep Selector :
.histogram >>> rect.bar {
fill: #009688;
}
Ici .histogram
est appliqué par Vue et rect.bar par D3. Le selector >>>
permet d’appliquer le style même dans le cas d’un style de type scoped
Conclusion
Nous avons pu voir qu’il est relativement simple de faire co-exister D3 dans un composant Vue. En prenant bien en compte les particularités des deux librairies on peut facilement créer des graphiques réutilisables, dynamiques et intégrés dans une application Vue.
Cette entrée a été publiée dans programmation avec comme mot(s)-clef(s) frontend, javascript, dataviz, vuejs, d3js
Les articles suivant pourraient également vous intéresser :
- La bonne et la mauvaise review par Sébastien Bizet
- Dark mode vs Light mode : accessibilité et éco-conception par Jean-Baptiste Bergy
- Principes SOLID et comment les appliquer en Python par Mariana ROLDAN VELEZ
- Pydantic, la révolution de python ? par Pablo Abril
- Comment utiliser les fixtures pytest en autouse tout en maîtrisant ses effets de bord ? par Amaury Boin
Vos commentaires
Cela m’a beaucoup aidé à orienter mes efforts lors d’une app que j’essaie de construire, très bien rédigé et en plus les bons liens aux documentations pertinentes. Merci!
Postez votre commentaire :
Votre commentaire a bien été envoyé ! Il sera affiché une fois que nous l'aurons validé.
Vous devez activer le javascript ou avoir un navigateur récent pour pouvoir commenter sur ce site.