TP2 - Réaliser une single page application avec Quarto et bertin

L’objectif de ce TP est d’apprendre à créer des cartes interactives avec Quarto et la bibliothèque JavaScript bertin.

Responsables pédagogiques

Manhamady OUEDRAOGO (Burkina Faso) & Nicolas LAMBERT (France)

Ont participé à l’élaboration de ce module

Claude GRASLAND (France), Souleymane Sidi TRAORE (Mali), Malika MADELIN (France), Sébastien REY-COYREHOURCQ (France), Vakaramoko BAMBA (Côte d’Ivoire), Hugues PECOUT (France), Yentougle MOUTORE (Togo), Bénédicte GARNIER (France), Côomlan Charles HOUNTON (Bénin), Pauline GLUSKI (France)

Introduction

Objectifs

Le but de ce TP est de réaliser une application de cartographie à l’échelle mondiale sur une page web pleine page. d’un point de vue technique, on parlera de single page application (SPA).

Les bases et prérequis nécessaires à la bonne compréhension de ce TP sont disponibles via ce cours introductif et ce premier TP. Merci de procéder dans cet ordre.

Données

Pour cet exercice, nous nous basons sur un tableau de données contenant 6 variables issues de la banque mondiale, disponibles de 1990 à 2021. Elles ont été harmonisées par Claude Grasland. Les voici ci-dessous.

1. Créer un document Quarto

Dans Rstudio, créez un document Quarto avec l’entête suivante :

---
title: "World Explorer"
format:
  html:
    echo: false
    code-tools: true
    page-layout: full
---

Le format de sortie est le format html.

  • echo: false permet de ne pas afficher le code dans le document final
  • code-tools: true permet de donner accès au code source en cliquant sur </> Code en haut à droite de la page.
  • page-layout: full permet de définir une disposition pleine page

À présent, copiez et collez en dessous l’ensemble du bloc suivant :

```{ojs}
//| panel: sidebar
"Le menu"
```
::: {.panel-tabset}

## Carte
```{ojs}
"La carte"
```
## Données
```{ojs}
"Les données"
```
## Top 10
```{ojs}
"le graphique"
```
:::
```{ojs}
"Annexe technique"
```

Cliquez sur Render (ou Ctrl+Shift+K) pour voir apparaitre la structure de votre application. Elle est composée de 6 parties :

  • Le titre (défini dans l’entête de votre document title: "World Explorer" ). À vous de remplacer le titre par la chaîne de caractère de votre choix.
  • Le menu (sur la gauche). C’est dans ce chunk que nous allons placer des Inputs permettant à l’utilisateur d’interagir avec la carte.
  • La carte (sur la droite). C’est dans ce chunk que nous allons dessiner une carte avec la bibliothèque bertin.
  • Les données (sur la droite). C’est dans ce chunk que nous allons afficher le tableau de données derrière la carte.
  • Un graphique (sur la droite). C’est dans ce chunk que nous allons afficher un bar plot.
  • Les annexes techniques (en bas). Dans ce chunk, on écrira tout ce qui est nécéssaire à l’élaboration de l’appli (import des données, etc, mais qu’on ne souhaite pas afficher).

Rappelez-vous qu’avec Observable JavaScript, l’ordre d’écriture n’a pas d’importance. On peut donc écrire du code à la fin du document, comme une annexe technique.

La classe {.panel-tabset} permet de positionner la carte, les données et le graphique dans 3 onglets différents.

  • Sauvegardez.

2. Les données

Téléchargez les données et mettez-les dans un repertoire data.

Dans le chunck de l’annexe technique, importez les données géométriques et attributaires qui se trouvent dans le répertoire data.

world = FileAttachment("data/world.json").json()
stats = FileAttachment("data/worldbank_data.csv").csv()

Comme dit précédement, les données contiennent des indicateurs à plusieurs dates.

3. Le menu

Ici, on définit quelles interactions ont souhaite proposer à l’utilisateur.

Copiez et collez les lignes de code suivantes dans votre chunck menu.

  • Un slider pour choisir l’année
viewof year =  Inputs.range(
  [1990, 2019], 
  {value: 2019, step: 1, label: "Année"}
)
  • Un slider pour configurer la taille des cercles
viewof k =  Inputs.range(
  [20, 100], 
  {value: 50, step: 1, label: "Rayon max"}
)
  • Une liste déroulante pour choisir l’indicateur
meta = FileAttachment("data/worldbank_meta.csv").csv()
viewof indicator = Inputs.select(
  new Map(meta.map((d) => [d.indicator, d.shortcode])),
  { label: "Indicateur" }
)
  • Une liste déroulante pour choisir la projection cartographique
projections = ["Patterson", "NaturalEarth1", "Bertin1953", "InterruptedSinusoidal", "Armadillo", "Baker", "Gingery", "Berghaus", "Loximuthal", "Healpix", "InterruptedMollweideHemispheres", "Miller", "Aitoff", "ConicEqualArea", "Eckert3", "Hill"]
viewof proj = Inputs.select(projections, {label: "Projection", width: 350})
  • Deux sliders pour définir le centre de projection. De -180 à +180 en longitude. De -90 à +90 en latitude.
viewof x =  Inputs.range( [-180, 180], {value: 0, step: 1, label: "Rotation (x)"} )
viewof y =  Inputs.range( [-90, 90], {value: 0, step: 1, label: "Rotation (y)"} )
  • La couleur des symboles
viewof color = Inputs.color({label: "couleur", value: "#4682b4"})
  • Un slider pour définir le niveau de généralisation du fond de carte
viewof simpl =  Inputs.range(
  [0, 1], 
  {value: 1, step: 0.1, label: "Simplification"}
)

On obtient le menu suivant où year correspond à l’année, k correspond à la taille des cercles, indicator correspond au nom de la variable, proj correspond à la projection cartographique centrée sur x et y, color correspond à la couleur des symboles et simpl correspond au niveau de généralisation du fond de carte.

4. Mise en forme des données

Avant de construire la carte, nous avons besoin de manipuler un peu les données. On effectue ces opérations dans la partie annexe technique. Plusieurs opérations son nécessaires:

Tout d’abord, on réalise une généralisation du fond de carte avec la fonction simplify de la bibliothèque geotoolbox qui permet de faire simplement la plupart des opérations SIG utiles en cartographie (voir). On peut la charger directement ou la télécharger ici pour travailler sans connexion internet. Le niveau de simplification sera déterminé par l’utilisateur dans le slider simpl.

geo = require("geotoolbox@latest")

ou (si vous souhaitez travailler sans connexion internet)

geo = require("./lib/geotoolbox.js")

Puis

world2 = geo.simplify(world, {k: simpl})

Puis, avec l’instruction JavaScript filter, on crée un tableau contenant uniquement l’année sélectionnée et on effectue une jointure entre les données et le fond de carte grâce à la bibliothèque bertin (on peut aussi télécharger la bibliothèque ici).

bertin = require("bertin@latest")

ou (si vous souhaitez travailler sans connexion internet)

bertin = require("./lib/bertin.js")
statsyear = stats.filter(d => d.date == year)
data = bertin.merge(world2, "id", statsyear, "iso3c")

On a aussi besoin de récupérer une valeur de référence pour chaque indicateur pour rendre comparables la taille des symboles d’une année à l’année sur l’autre. On récupère la valeur maximale de l’année 2019.

varmax = d3.max(stats.filter(d => d.date == 2019), d => +d[indicator])

Après ces opérations, l’objet data contient les géométries généralisées et les données pour l’année sélectionnée

5. Réalisation de la carte

Ici, on réalise la carte dans la chuck carte avec la bibliothèque bertin.

Tout d’abord, on fabrique le titre en concaténant le nom de la variable et l’année.

title = meta.map((d) => [d.indicator, d.shortcode]).find((d) => d[1] == indicator)[0] + " in " + year

Puis, on dessine la carte avec la fonction draw de la bibliothèque bertin. Libre à vous de personnaliser la mise en page et les couleurs en modifiant/ajoutant quelques paramètres.

bertin.draw({
params: {projection: proj + `.rotate([${x}, ${y}])`, clip: true },
layers:[
  {type: "header", text: title},
  {type: "bubble", 
    geojson: data,
    values: indicator, 
    fill: color,
    fixmax: varmax,
    k, 
    tooltip: ["$name",d => d.properties[indicator]]
  },
  {geojson: world, fill: "#CCC"},
  {type: "graticule"},  
  {type: "outline"}
]})

6. Affichage des données

Ici, on ajoute un tableau de données dans la chuck données. On sélectionne les colonnes à afficher.

Inputs.table(statsyear, {  columns: [
    "country",
    "capital_city",
    "region",
    indicator
  ]})

7. Réalisation du graphique

On souhaite réaliser un diagramme en barres avec les N pays qui ont les plus fortes valeurs sur l’indicateur séléctionné. Nous récupérons donc l’objet stats qui contient les données, nous le trions, et ne gardons que les N premières valeurs. N est défini dans un slider. Le graphique est réalisé avec la bibliothèque Plot.

Ajoutez le code ci-dessous chuck graphique.

viewof topnb = Inputs.range([5, 30], {label: "Nombre de pays représentés", step: 1})
top = statsyear.sort((a, b) => d3.descending(+a[indicator], +b[indicator]))
  .slice(0, topnb)
Plot.plot({
    marginLeft: 60,
  marks: [
    Plot.barY(top, {
      x: "iso3c",
      y: indicator,
      sort: { x: "y", reverse: true },
      fill: color
    }),
    Plot.ruleY([0])
  ]
})

7. C’est fini !

Appuyez sur Render pour voir le résultat. La solution est disponible ici.

8. Aller plus loin

Cette application est largement perfectible. Une piste possible d’amélioration serait de remplacer le slider des années par un bouton play pour faire une vraie carte animée. Une solution consiste à utiliser l’input scrubber dévelopé par Mike Bostock : https://observablehq.com/@mbostock/scrubber.

Deux options sont possibles. Vous pouvez importer la fonction depuis Observable.

import {Scrubber} from "@mbostock/scrubber"

Ou copier le code de la fonction qui se trouve sur cette page.

Puis, remplacez le slider des années par :

viewof year = Scrubber(d3.range(1990, 2019), { autoplay: false })

Et pour encore plus de fun 🥳, vous pouvez vous amusez à faire varier le centre de projection automatiquement. Pour cela, vous pouvez ajouter la fonction suivante dans la partie Annexe technique.

function* timer({ speed = 1000, step = 1, interval = [-180, 180] } = {}) {
  let i = interval[0];
  while (true) {
    yield Promises.delay(speed, (i = i + step));
    if (i >= interval[1]) {
      i = interval[0];
    }
  }
}

Puis, dans la partie menu, remplacer

viewof y =  Inputs.range( [-90, 90], {value: 0, step: 1, label: "Rotation (y)"} )

par

y = timer({ speed: 10, step: 9, interval: [-90, 90] })

Vous pouvez faire varier les paramètres speed et step pour changer la vitesse (qui dépend des capacités de votre ordinateur). Vous pouvez aussi remplacer le slider Rotation (x) en utilisant interval: [-180, 180]