Makina Blog

Le blog Makina-corpus

Vue.js & D3.js : un bon combo pour une data visualisation dynamique


Pour faire des indicateurs dynamiques avec D3.js, l'utilisation d'un framework front comme Vue.js peut simplifier la vie.

Sommaire

En tirant partie de la réactivité de Vue.js, nous pouvons nous passer d'une bonne partie du script D3.js. Ce qui a l'avantage de produire un code mieux organisé, plus lisible et donc plus maintenable.

Dans cet article, nous considérons que l'objet de notre composant est de présenter un graphique, accompagné d'un formulaire.

Le composant sera autonome, c'est à dire qu'il n'aura pas de propriété en entrée, c'est lui qui ira chercher les données à utiliser via une API par exemple.

Le composant devra réagir à la mise à jour du formulaire :

  • Aller chercher les données correspondantes à la nouvelle sélection : les entrées de notre formulaires
  • Rafraîchir le graphique avec ces nouvelles données

Un cas d'usage assez classique que nous avons, par exemple, rencontré sur le site d'Air Pays de la Loire.

Un squelette de composant pour démarrer rapidement

Tout au long de cette partie, nous allons voir quelques principes à avoir en tête lorsqu'on utilise D3.js dans un composant Vue.js. Ensuite, en prenant en compte ces quelques principes, je vous proposerai un squelette de composant pour bien démarrer une data visualisation "D3.js + Vue.js".

Pour illustrer, nous allons partir d'un exemple d'histogramme de la bibliothèque de D3.js. J'ai collé cet exemple de manière naïve dans un composant Vue.js : jusqu'ici l'utilisation du framework n'apporte rien.

Pour simplifier, dans cet exemple, la partie "récupération des données via une API" n'est pas incluse, les données sont inscrites en dur dans le composant.

<template>
  <div id="ma-dataviz">
  </div>
</template>

<script>
import * as d3 from 'd3';
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'MaDativiz',
  data() {
    return {
      dataX: [
        'catégorie A',
        'catégorie B',
        'catégorie C',
        'catégorie D',
      ],
      /** les données pour notre dataviz, variables dans le temps */
      dataY: [5, 8, 9, 2],
      width: 300,
      height: 150,
      margin: 50,
      rectColor: '#4545FF',
      axeColor: "#888888",
      gridColor: "#AAAAAA",
    }
  },
  mounted() {
    this.buildDataviz();
  },
  methods: {
    buildDataviz() {
      const xRange = [this.margin, this.width - this.margin];
      const yType = d3.scaleLinear;
      const yDomain = [d3.min(this.dataY), d3.max(this.dataY)];
      const yRange = [this.height - this.margin, this.margin];
      const datavizElement = d3.select('#ma-dataviz');

      // Construct scales and axes.
      const xScale = d3.scaleBand(this.dataX, xRange);
      const yScale = yType(yDomain, yRange);
      const xAxis = d3.axisBottom(xScale).ticks(this.width / 80).tickSizeOuter(0);
      const yAxis = d3.axisLeft(yScale).ticks(this.height / 40);

      const svg = datavizElement.append("svg")
        .attr("width", this.width)
        .attr("height", this.height)
        .attr("viewBox", [0, 0, this.width, this.height]);

      yAxisElement = svg.append("g")
          .attr("transform", `translate(${this.margin},0)`)
          .call(yAxis)
          .call(g => g.select(".domain").remove())
          .call(g => g.selectAll(".tick line").clone()
              .attr("x2", this.width - this.margin - this.margin)
              .attr("stroke", this.gridColor));

      yAxisElement.select(".domain")
        .style('stroke', this.axeColor);

      yAxisElement.selectAll(".tick text")
        .style('fill', this.axeColor);

      svg.append("g")
          .attr("fill", this.color)
        .selectAll("rect")
        .data(this.dataX)
        .join("rect")
          .attr("x", d => xScale(d))
          .attr("width", xScale.bandwidth())
          .attr("y", (d, i) => yScale(this.dataY[i]))
          .attr("height", (d, i) => yScale(0) - yScale(this.dataY[i]))
        .append("title")
          .text((d, i) => [d, this.dataY[i]].join("\n"));

      xAxisElement = svg.append("g")
          .attr("transform", `translate(0,${this.height - this.margin})`)
          .call(xAxis);

      xAxisElement.select(".domain")
        .style('stroke', this.axeColor);

      xAxisElement.selectAll(".tick text")
        .style('fill', this.axeColor);

      xAxisElement.selectAll(".tick line")
        .attr('stroke-width', '0.5px')
        .attr('stroke', this.gridColor);
    },
  },
});
</script>

(Ce code n'est pas fonctionnel, il n'est là que pour illustrer)

1. Éviter d'écrire du code D3.js pour construire du HTML connu

Dans ce que nous propose la bibliothèque d'exemples de D3.js, souvent, l'ensemble du graphique est dessiné avec D3.js. Le code part d'un div vide et vient y insérer, via D3.js, un svg, puis, à l'intérieur de cet élément, divers autres éléments (g, path, rect, text) qui viendront composer le graphique.

Cette partie du script génère un SVG à partir de code verbeux et pas forcément évident à comprendre. Dans notre composant Vue.js, nous avons la possibilité de déclarer un template dans lequel nous pouvons mettre ce que nous voulons. Profitons-en pour y renseigner tout ce que nous connaissons déjà de notre SVG.

C'est le premier conseil que nous pouvons donner : déclarez votre SVG directement dans le template Vue.js plutôt que de la générer avec D3.js, et mettez-y tout ce que vous connaissez, à priori, sur votre graphique.

Pour notre exemple cela donne :

<template>
  <div id="ma-dataviz">
    <svg
      :viewBox="'[0, 0, ' + width +', ' + height + ']'"
      :width="width"
      :height="height"
    >
      <g class="axis-y"></g>
      <g class="axis-x"></g>
      <g class="data"></g>
    </svg>
  </div>
</template>

<script>
import * as d3 from 'd3';
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'MaDativiz',
  data() {
    return {
      dataX: [
        'catégorie A',
        'catégorie B',
        'catégorie C',
        'catégorie D',
      ],
      /** les données pour notre dataviz, variables dans le temps */
      dataY: [5, 8, 9, 2],
      width: 300,
      height: 150,
      margin: 50,
      rectColor: '#4545FF',
      axeColor: "#888888",
      gridColor: "#AAAAAA",
    }
  },
  mounted() {
    this.buildDataviz();
  },
  methods: {
    buildDataviz() {
      const xRange = [this.margin, this.width - this.margin];
      const yDomain = [d3.min(this.dataY), d3.max(this.dataY)];
      const yRange = [this.height - this.margin, this.margin];

      // Construct scales and axes.
      const xScale = d3.scaleBand(this.dataX, xRange);
      const yScale = d3.scaleLinear(yDomain, yRange);
      const xAxis = d3.axisBottom(xScale).ticks(this.width / 80).tickSizeOuter(0);
      const yAxis = d3.axisLeft(yScale).ticks(this.height / 40);

      yAxisElement = d3.select('#ma-dataviz .axis-y')
          .attr("transform", `translate(${this.margin},0)`)
          .call(yAxis)
          .call(g => g.select(".domain").remove())
          .call(g => g.selectAll(".tick line").clone()
              .attr("x2", this.width - this.margin - this.margin)
              .attr("stroke", this.gridColor));

      yAxisElement.select(".domain")
        .style('stroke', this.axeColor);

      yAxisElement.selectAll(".tick text")
        .style('fill', this.axeColor);

      d3.select('#ma-dataviz .data')
          .attr("fill", this.color)
        .selectAll("rect")
        .data(this.dataX)
        .join("rect")
          .attr("x", d => xScale(d))
          .attr("width", xScale.bandwidth())
          .attr("y", (d, i) => yScale(this.dataY[i]))
          .attr("height", (d, i) => yScale(0) - yScale(this.dataY[i]))
        .append("title")
          .text((d, i) => [d, this.dataY[i]].join("\n"));

      xAxisElement = d3.select('#ma-dataviz .axis-x')
        .attr("transform", `translate(0,${this.height - this.margin})`)
        .call(xAxis);

      xAxisElement.select(".domain")
        .style('stroke', this.axeColor);

      xAxisElement.selectAll(".tick text")
        .style('fill', this.axeColor);

      xAxisElement.selectAll(".tick line")
        .attr('stroke-width', '0.5px')
        .attr('stroke', this.gridColor);
    },
  },
});
</script>

2. Tirer profit au maximum de la réactivité de Vue.js

Dans le cas d'une data visualisation qui peut être rafraîchie en fonction d'une sélection, l'utilisation de Vue.js peut avoir beaucoup d'intérêt notamment grâce au principe de réactivité du framework : ne nous en privons pas !

Ce sera mon second conseil, puisque nous sommes dans un composant Vue.js : utilisez les propriétés (calculées ou non) de Vue.js et adaptez votre template avec ces propriétés.

Dans notre exemple, un grand nombre de variables sont déclarées dans le script D3.js. La plupart de ces variables peuvent être déclarées comme des propriétés calculées. Cela allège le script D3.js et le rend plus lisible.

En regardant les propriétés créées, on se rend compte que nous avons toutes les informations disponibles pour dessiner les rectangles de notre histogramme. Nous n'avons plus besoin de passer par un script D3.js pour les générer.

Cela donne le code suivant :

<template>
  <div id="ma-dataviz">
    <svg
      :viewBox="viewBox"
      :width="width"
      :height="height"
    >
      <g class="axis-y"></g>
      <g class="axis-x"></g>
      <g class="data" :fill="color">
        <rect
          v-for="(category, i) in dataX" :key="i"
          :x="xScale(category)"
          :width="xScale.bandwidth()"
          :y="yScale(dataY[i])"
          :height="yScale(0) - yScale(dataY[i])"
          :title="[category, this.dataY[i]].join('\n')"
        >
        </rect>
      </g>
    </svg>
  </div>
</template>

<script>
import * as d3 from 'd3';
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'MaDativiz',
  data() {
    return {
      dataX: [
        'catégorie A',
        'catégorie B',
        'catégorie C',
        'catégorie D',
      ],
      /** les données pour notre dataviz, variables dans le temps */
      dataY: [5, 8, 9, 2],
      width: 300,
      height: 150,
      margin: 50,
      rectColor: '#4545FF',
      axeColor: "#888888",
      gridColor: "#AAAAAA",
    }
  },
  computed: {
    viewBox() { return `[0, 0, ${this.width}, ${this.height}]`; },
    xRange() { return [this.margin, this.width - this.margin]; },
    yDomain() { return [d3.min(this.dataY), d3.max(this.dataY)]; },
    yRange() { return [this.height - this.margin, this.margin]; },
    xScale() { return d3.scaleBand(this.dataX, this.xRange); },
    yScale() { return d3.scaleLinear(yDomain, this.yRange); },
    xAxis() { return d3.axisBottom(this.xScale).ticks(this.width / 80).tickSizeOuter(0); },
    yAxis() { return d3.axisLeft(this.yScale).ticks(this.height / 40); },
  },
  mounted() {
    this.buildDataviz();
  },
  methods: {
    buildDataviz() {
      yAxisElement = d3.select('#ma-dataviz .axis-y')
          .attr("transform", `translate(${this.margin},0)`)
          .call(yAxis)
          .call(g => g.select(".domain").remove())
          .call(g => g.selectAll(".tick line").clone()
              .attr("x2", this.width - this.margin - this.margin)
              .attr("stroke", this.gridColor));

      yAxisElement.select(".domain")
        .style('stroke', this.axeColor);

      yAxisElement.selectAll(".tick text")
        .style('fill', this.axeColor);

      xAxisElement = d3.select('#ma-dataviz .axis-x')
        .attr("transform", `translate(0,${this.height - this.margin})`)
        .call(xAxis);

      xAxisElement.select(".domain")
        .style('stroke', this.axeColor);

      xAxisElement.selectAll(".tick text")
        .style('fill', this.axeColor);

      xAxisElement.selectAll(".tick line")
        .attr('stroke-width', '0.5px')
        .attr('stroke', this.gridColor);
    },
  },
});
</script>

À noter qu'en appliquant les remarques du point 1 et 2, nous avons en fait transféré la responsabilité du DOM de D3.js à Vue.js. Et comme Vue.js sait très bien gérer le DOM, c'est plutôt un bon point.

3. Séparer les parties dynamiques des parties statiques

Toujours pour gagner en lisibilité, et pour éviter d’exécuter 2 fois un script qui fait la même chose, il peut être intéressant de séparer les parties de la data visualisation qui ne bougeront pas de celles qui sont dynamiques.

Dans notre exemple, le nombre de catégories ne bougera pas, par contre les valeurs pour chaque catégorie sont variables. Nous pouvons donc construire l'axe des abscisses dans un script d'initialisation mais pas l'axe des ordonnées :

<template>
  <div id="ma-dataviz">
    <svg
      :viewBox="viewBox"
      :width="width"
      :height="height"
    >
      <g class="axis-y"></g>
      <g class="axis-x"></g>
      <g class="data" :fill="color">
        <rect
          v-for="(category, i) in dataX" :key="i"
          :x="xScale(category)"
          :width="xScale.bandwidth()"
          :y="yScale(dataY[i])"
          :height="yScale(0) - yScale(dataY[i])"
          :title="[category, this.dataY[i]].join('\n')"
        >
        </rect>
      </g>
    </svg>
  </div>
</template>

<script>
import * as d3 from 'd3';
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'MaDativiz',
  data() {
    return {
      dataX: [
        'catégorie A',
        'catégorie B',
        'catégorie C',
        'catégorie D',
      ],
      /** les données pour notre dataviz, variables dans le temps */
      dataY: [5, 8, 9, 2],
      width: 300,
      height: 150,
      margin: 50,
      rectColor: '#4545FF',
      axeColor: "#888888",
      gridColor: "#AAAAAA",
    }
  },
  computed: {
    viewBox() { return `[0, 0, ${this.width}, ${this.height}]`; },
    xRange() { return [this.margin, this.width - this.margin]; },
    yDomain() { return [d3.min(this.dataY), d3.max(this.dataY)]; },
    yRange() { return [this.height - this.margin, this.margin]; },
    xScale() { return d3.scaleBand(this.dataX, this.xRange); },
    yScale() { return d3.scaleLinear(yDomain, this.yRange); },
    xAxis() { return d3.axisBottom(this.xScale).ticks(this.width / 80).tickSizeOuter(0); },
    yAxis() { return d3.axisLeft(this.yScale).ticks(this.height / 40); },
  },
  mounted() {
    this.initDataviz()
    this.refreshDataviz();
  },
  methods: {
    initDataviz() {
      yAxisElement = d3.select('#ma-dataviz .axis-y')
        .attr("transform", `translate(${this.margin},0)`)
        .call(yAxis)
        .call(g => g.select(".domain").remove())
        .call(g => g.selectAll(".tick line").clone()
          .attr("x2", this.width - this.margin - this.margin)
          .attr("stroke", this.gridColor));

      yAxisElement.select(".domain")
        .style('stroke', this.axeColor);

      yAxisElement.selectAll(".tick text")
        .style('fill', this.axeColor);
    },
    refreshDataviz() {
      d3.select('#ma-dataviz .axis-x')
        .attr("transform", `translate(0,${this.height - this.margin})`)
        .call(xAxis);

      xAxisElement.select(".domain")
        .style('stroke', this.axeColor);

      xAxisElement.selectAll(".tick text")
        .style('fill', this.axeColor);

      xAxisElement.selectAll(".tick line")
        .attr('stroke-width', '0.5px')
        .attr('stroke', this.gridColor);
    },
  },
});
</script>

4. Utiliser la balise <style> du composant

Comme pour notre premier point, il n'est pas rare de voir des scripts D3.js entièrement déclarer le style des différents éléments qui le composent. Si certains paramètres peuvent paraître légitimes (comme une couleur qui varie en fonction d'une donnée) d'autres encombrent inutilement le script :

  • Style des axes et des grilles
  • Police et taille des libellés
  • Remplissage de certains éléments…

Ce sera mon dernier conseil : déclarez ce que vous pouvez dans la balise <style> de votre composant Vue.js, c'est finalement plus logique et c'est bien moins verbeux !

<template>
  <div id="ma-dataviz">
    <svg
      :viewBox="viewBox"
      :width="width"
      :height="height"
    >
      <g class="axis-y"></g>
      <g class="axis-x"></g>
      <g class="data">
        <rect
          v-for="(category, i) in dataX" :key="i"
          :x="xScale(category)"
          :width="xScale.bandwidth()"
          :y="yScale(dataY[i])"
          :height="yScale(0) - yScale(dataY[i])"
          :title="[category, this.dataY[i]].join('\n')"
        >
        </rect>
      </g>
    </svg>
  </div>
</template>

<style>
#ma-dataviz {
  --rect-color: #4545FF;
  --axe-color: #888888;
  --grid-color: #AAAAAA;
}
#ma-dataviz.domain {
  stroke: --var(--axe-color);
}
#ma-dataviz .tick text {
  stroke: --var(--axe-color);
}
#ma-dataviz .tick line {
  stroke: --var(--axe-grid);
}
#ma-dataviz .data rect {
  fill: --var(--rect-color);
}
</style>

<script>
import * as d3 from 'd3';
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'MaDativiz',
  data() {
    return {
      dataX: [
        'catégorie A',
        'catégorie B',
        'catégorie C',
        'catégorie D',
      ],
      /** les données pour notre dataviz, variables dans le temps */
      dataY: [5, 8, 9, 2],
      width: 300,
      height: 150,
      margin: 50,
    }
  },
  computed: {
    viewBox() { return `[0, 0, ${this.width}, ${this.height}]`; },
    xRange() { return [this.margin, this.width - this.margin]; },
    yDomain() { return [d3.min(this.dataY), d3.max(this.dataY)]; },
    yRange() { return [this.height - this.margin, this.margin]; },
    xScale() { return d3.scaleBand(this.dataX, this.xRange); },
    yScale() { return d3.scaleLinear(yDomain, this.yRange); },
    xAxis() { return d3.axisBottom(this.xScale).ticks(this.width / 80).tickSizeOuter(0); },
    yAxis() { return d3.axisLeft(this.yScale).ticks(this.height / 40); },
  },
  mounted() {
    this.initDataviz()
    this.refreshDataviz();
  },
  methods: {
    initDataviz() {
      yAxisElement = d3.select('#ma-dataviz .axis-y')
        .attr("transform", `translate(${this.margin},0)`)
        .call(yAxis)
        .call(g => g.select(".domain").remove())
        .call(g => g.selectAll(".tick line").clone())
          .attr("x2", this.width - this.margin - this.margin);
    },
    refreshDataviz() {
      d3.select('#ma-dataviz .axis-x')
        .attr("transform", `translate(0,${this.height - this.margin})`)
        .call(xAxis);
    },
  },
});
</script>

Pour n'appliquer notre style qu'à notre composant, on pourrait ici utiliser l'attribut scoped sur la balise <style>. Mais attention, quand nous utilisons cet attribut Vue.js ne traque que les éléments du DOM dont il a la charge. Le style ne sera pas appliqué aux éléments créés par D3.js. Il est cependant possible de contourner cette limitation en utilisant le sélecteur :deep() de Vue.js.

5. Notre squelette de composant

Les différents points vus jusqu'ici concernaient le "mariage" de Vue.js et D3.js. Si nous ajoutons maintenant les éléments suivants :

  • Entrées de formulaire pour modifier notre graphique à la volée
  • Gestion de la récupération de ces données et de la mise à jour du graphique

Nous obtenons la proposition de squelette ci-dessous. Elle n'est pas adaptée à toutes les situations (nous verrons d'ailleurs, dans la mise en application, que nous ne l'utilisons pas entièrement) mais c'est un bon démarrage.

Les commentaires à l'intérieur du code devraient suffire à comprendre son fonctionnement.

import * as d3 from 'd3';
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'DatavizSkeleton',
  props: {
    width: {
      default: 300,
      type: Number,
    },
    height: {
      default: 200,
      type: Number,
    }
  },
  data() {
    return {
      // inputs
      someInput: undefined,
      someInputOptionList: [],
      anotherInput: 10,

      // loading and error handling
      loading: true,
      error: undefined,

      // actual data for the dataviz
      data: [],

      // d3 dataviz
      svgElementId: 'dataviz-' + Math.random().toString(36).substring(2),
      padding: 40,
    }
  },
  computed: {
    viewBox() { return `0 0 ${this.width} ${this.height}`},
    // Examples of computed values you don't want to calculate on your actual D3.js script
    dataMinValue() { return this.data.length ? d3.min(this.data) : 0; },
    dataMaxValue() { return this.data.length ? d3.max(this.data) : 0; },
    rangeValue() {
      return d3.scaleSequential()
        .domain(this.dataMinValue, this.dataMaxValue)
        .nice()
        .range([this.height - this.padding, this.padding])
      ;
    },
  },
  mounted() {
    this.initInputOptions();
    this.refresh();
  },
  methods: {
    /**
     * Refresh data and dataviz.
     *
     * This method will be launch each time an input is updated.
     * It will fetch new data according to input values and then
     * refresh the right part of the dataviz.
     */
    refresh() {
      this.fetchData()
      .then(() => this.refreshDataviz())
    },
    /**
     * Use this one if you need to fetch data from elsewhere to
     * initialize your inputs.
     */
    initInputOptions() {
      // A dumb example :
      (new Promise((resolve, reject) => {
        this.someInputOptionList = [
          {id: 1, label: 'option1'},
          {id: 2, label: 'option2'},
          {id: 3, label: 'option3'},
        ];

        resolve();
      }))
      .catch(e => {
        this.error = true
        console.log(e)
      })
    },
    /**
     * Fetch data needed to build your dataviz.
     *
     * Here, call your external API to get your data.
     *
     * @returns
     */
    fetchData() {
      // A dumb example :
      return (new Promise((resolve, reject) => {
        const values = Array.from(
          {length: 4000},
          d3.randomNormal(0.5, 0.08)
        );
        const frequencies = d3.rollups(values, v => v.length, value => value.toFixed(2))

        this.data = d3.sort(frequencies, ([i,v]) => i);

        resolve();
      }))
      .catch(e => {
        this.error = true;
        console.log(e);
      })
      .finally(() => {
        this.loading = false;
      })
    },
    /**
     * Refresh the data dependent part of the dataviz.
     */
    refreshDataviz() {
      d3.select(`#${this.svgElementId} .data`)

      // ...
    },
  },
});
<template>
  <div class="dataviz-shell">
    <form class="form">
      <div>
        <label for="some-input">Some input&nbsp;: </label>
        <select
          id="some-input"
          name="some-input"
          v-model="someInput"
          @change="refresh"
        >
          <option v-for="option in someInputOptionList" :value="option.id" :key="option.id">
            {{ option.label }}
          </option>
        </select>
      </div>
      <div>
        <label for="another-input">Another input&nbsp;: </label>
        <input
          id="another-input"
          name="another-input"
          type="range" min="1" max="100"
          v-model="anotherInput"
          @change="refresh"
        />
      </div>
    </form>

    <p v-if="loading">Loading data.</p>
    <p v-else-if="error">An error appeared while loading data.</p>
    <p v-else-if="data.length == 0">There is no data available for your selection.</p>
    <svg
      v-show="data.length != 0"
      :id="svgElementId"
      :viewBox="viewBox"
    >
      <g class="axe-x"></g>
      <g class="axe-y"></g>
      <g class="data"></g>
    </svg>
  </div>
</template>

<style>
  .dataviz-shell {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
  }
</style>

<script src="./DatavizSkeleton.js"/>

À noter :

  • Les inputs rattachés à des variables via v-model et la gestion des événements pour un rafraîchissement du graphique à chaque mouvement sur le formulaire
  • La gestion du chargement des données et des erreurs intégrées dans le template

Mise en application avec l'open data de Nantes Métropole

Je passe tous les matins devant un compteur de cyclistes dans le centre de Nantes, après recherche, il se trouve que Nantes Métropole propose les données de ces compteurs via son opendata : cela fera une très bonne illustration pour réaliser un graphique utilisant notre squelette.

Le graphique suivant a pour but de représenter le trafic moyen (en cyclistes), sur la semaine et par tranche horaire, pour une borne de comptage particulière. Une liste déroulante permet de sélectionner une borne ainsi qu'une carte qui présente la répartition des bornes à Nantes Métropole.

La dataviz ici

Sur cet exemple, nous pouvons déjà construire pas mal d'éléments du graphique avant même de commencer à utiliser D3.js.

  • Nous savons qu'il sera composé de 7*24 pastilles représentant les 24 tanches horaires des 7 jours de la semaine
  • Nous pouvons placer les libellés des jours et des heures qui ne bougeront pas non plus

La seule inconnue - variable en fonction des données - est la couleur de ces pastilles. En utilisant une propriété calculée pour cette couleur, on se rend compte que nous pouvons faire ce graphique presque sans utiliser D3.js.

En fait, ici, D3.js n'est utilisé que pour retraiter facilement les données en entrée et pour créer une échelle de couleur. Les scripts d'initialisation et de rafraîchissement du graphique sont entièrement vides !

Un dernier mot

Pour conclure, je pense qu'au delà du squelette, il faut surtout retenir les 4 principes qui ont aidé à la construire :

  1. Construisez directement, dans la balise <template>, le code HTML que vous connaissez
  2. Utilisez les propriétés calculées de Vue.js et servez-vous en directement dans la balise <template>
  3. Séparez les parties dynamiques des parties statiques
  4. Utilisez la balise <style> de votre composant !

À noter aussi, les exemples dans cet article sont relativement simples et à la fin, nous pouvons nous demander : est-il vraiment nécessaire d'utiliser D3.js pour ça ? C'est une très bonne question à se poser. Finalement, si notre visualisation est simple et que nous n'avons pas besoin des fonctions mathématiques de D3.js pour la construire, eh bien, autant se passer complètement de D3.js : parfois, moins c'est mieux !

Ressources

Vous retrouverez l'ensemble du code présent dans cet article dans sur ce dépôt Github, notamment :

Formations associées

Formation Dataviz

Formation à la data visualisation

Toulouse Le 18 avril 2023

Voir la formation

Formations Front end

Formation Développement d'applications JavaScript

Toulouse Du 18 au 20 juin 2024

Voir la formation

Formations Front end

Formation VueJS

Nantes Du 8 au 10 juillet 2024

Voir la formation

Actualités en lien

Image
Symfony + Vue.js
21/06/2022

Créer une application Symfony/Vue.js

Pour faire des essais ou bien démarrer un nouveau projet, vous avez besoin de créer rapidement une application Symfony couplée avec un front Vue.js ? Suivez le guide !

Voir l'article
Image
Makina Corpus Blog Scrollytelling
15/11/2021

Le Scrollytelling pour raconter une histoire avec vos données !

Le Scrollytelling, contraction de "scroll" et "storytelling", c'est l'art de raconter une histoire sur le web. Les éléments graphiques évoluent au fur et à mesure que l'on  fait défiler la page, et l'histoire se raconte… 

Voir l'article
Image
IA / DataScience : DataViz les règles visualisation
28/10/2021

[Conférence] Les règles de la visualisation de données 1/4 : Manuel Lima (Google)

Le 27 et 28 novembre 2020 se tenait la conférence S-H-O-W qui portait sur l'usage des règles dans la visualisation de données.

Voir l'article

Inscription à la newsletter

Nous vous avons convaincus