Cartographier pour le Web avec quarto {ojs} et geoviz

GEO UNIV’R Tunisie 2024

Author

Nicolas Lambert, Ronan Ysebaert, Elina Marveaux

Published

May 7, 2024

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

1. Introduction

Avant de commencer, merci de regarder cette introduction sur le Web, son histoire, ses langages et l’Observable JavaScript. C’est un prérequis pour comprendre la suite.

2. Démarrer avec Quarto

2.1 Environnement logiciel.

Ce TP s’effectue avec le logiciel Quarto. Pour l’installer, vous pouvez utiliser le lien suivant :

https://quarto.org/docs/get-started

Puis, vous avez besoin d’une interface de développement pour écrire le code et visualiser le résultat. Vous avez le choix.

Dans ce TP, nous privilégions l’utilisation du logiciel Rstudio. Pour le télécharger et l’installer, cliquez sur ce lien.

2.2 Créer un document Quarto

  • Sur votre ordinateur, créez un dossier TP_geoviz à l’emplacement de votre choix.
  • Ouvrez le logiciel RStudio
  • Créez un document Quarto (file > New File > Quarto Document)

  • Cliquez sur “Create Empty Document” en bas à gauche.
  • Mettez-vous en mode source

Vous obtenez un fichier contenant les lignes suivantes :

---
title: "Untitled"
format: html
editor: visual
---
  • Choisissez un titre
  • Sauvegardez le fichier index.qmd dans le dossier TP_geoviz.
  • Dans ce répertoire, créez également un sous répertoire data pour stocker les données.

2.3 Rappel des principes

Dans ce TP, nous allons réaliser des cartes avec Observable JavaScript (ou ojs). Rappelons que l’ojs est un ensemble d’améliorations apportées à JavaScript avec l’objectif d’en faire un langage dédié à la visualisation de données pour le web. Ce langage est complètement intégré dans Quarto.

Les caractéristiques de l’ojs sont les suivantes :

  • Il s’agit de JavaScript + des bibliothèques préchargées comme Plot & d3js 📊
  • Tout est réactif 🔥 et rejoué en temps réel
  • L’ordre des cellules n’a pas d’importance 🤯
  • Chaque début de ligne identifie une cellule ojs. Le nom de ces cellules doit être unique pour l’ensemble du document.

Dans Quarto, toutes les instructions à suivre s’écrivent dans des chunks ojs

```{ojs}
```
Attention

Attention, les chucks et les cellules sont deux concepts différents.

Un chunk contenant une seule cellule

```{ojs}
sum = 10 + 10
```

Un chunk contenant trois cellules

```{ojs}
a = 10
b = 20
c = a * b
```

Un chunk contenant une seule cellule

```{ojs}
{
  // code JavaScript
  let a = 10
  let b = 20
  return a * b
}
```

Pour chaque chunck, vous pouvez définir avec echo si vous souhaitez que le code s’affiche ou non dans votre notebook final. Avec eval, vous choisissez si le code doit s’exécuter ou non.

```{ojs}
//| echo: false
//| eval: true
```

Le code en ligne vous permet également d’exécuter du code à l’intérieur du texte markdown. La syntaxe du code en ligne est similaire à celle des blocs de code, sauf que vous utilisez une seule coche (`) au lieu de trois coche (```)

radius = 5
Le rayon du cercle est égal à **``{ojs} radius``**

donne ceci :

Le rayon du cercle est égal à

Pour générer le document, il faut clicher sur le bouton Render ou utiliser le raccourci clavier Ctrl+Shift+K.

Une fois que vous avez cliqué sur Render, la page web s’affiche dans la panneau Viewer et un dossier Docs est crée. Il contient le site web généré.

Vous pouvez aussi cliquer sur l’icône voir dans une nouvelle fenêtre pour visualiser votre document dans votre navigateur web habituel.

N’oubliez pas de sauvegarder votre document régulièrement (CTRL+S)

2.4 Documentation et exemples

Au fil de ce notebook, vous pourrez vous référer à des éléments de documentation en cliquant sur cet icône.

Vous pourrez également accéder à des exemples pédagogiques et des démos en ligne en cliquant sur celui-là.

3. Les données

Le jeu de données utilisé est issu de la banque mondiale. Les données ont été mises en forme ici.

Téléchargez les données et placez le fichier zip dans votre répertoire data.

3.1 Import des données

Dans {ojs}, on importe les données avec l’instruction FileAttachment() .

Les données étant au format .zip, on écrit :

worldbank = FileAttachment("data/worldbank.zip").zip()

Ce fichier zip contient 3 fichiers.

worldbank.filenames

Il existe plusieurs fonctions disponibles pour interpréter les formats de données. La fonction .csv()permet d’importer des données csv. La fonction .xlsx() permet d’importer des tableurs excel. La fonction .json() permet d’importer des données au format JSON. Ici, on va donc créer 3 jeux de données bien distincts.

  • Les données
data = worldbank.file("data.csv").csv()
  • Les métadonnées
metadata = worldbank.file("metadata.csv").csv()
  • Le fond de carte (pays du monde)
world = worldbank.file("world.json").json()

3.2 Visualiser les données attributaires

Pour visualiser un tableau de données, on peut utiliser l’instruction Inputs.table().

Inputs.table(data)
Inputs.table(metadata)

On peut également combiner cet affichage par table avec la fonction Inputs.search().

viewof search = Inputs.search(data, { query: "Tunisia" })
Inputs.table(search)

Il est possible de combiner des chunks R et des chunks ojs grace à l’instruction ojs_define()

Par exemple :

# chunk r
datafromr <- read.csv("data/data.csv")
ojs_define(ojsdata = datafromr)
// chunk ojs
Inputs.table(transpose(ojsdata))

Ca fonctionne également avec des objets spatiaux. Mais pour cela, il faut procéder de façon un peu différente.

# chunk r
library("sf")
library("geojsonsf")
geomfromr <- st_read("data/world.gpkg")
ojs_define(ojsgeom = sf_geojson(geomfromr))
// chunk ojs
JSON.parse(ojsgeom)

Voir détail : neocarto.github.io/docs/notebooks/ojsdefine/

3.3 Visualiser des géométries

Le fond de carte est au format geoJSON

Pour la visualiser, on a besoin d’importer une bibliothèque de cartographie. Ici, on choisit la bibliothèque geoviz

On l’importe grâce à l’instruction require().

viz = require("geoviz@0.6.1")

Pour visualiser simplement les géométries avec une couleur aléatoire, on tape :

viz.path({data: world})

La carte est un peu grande. Nous pouvons la redimensionner en utilisant le paramètre svg_width.

viz.path({data: world, svg_width: 790})
Attention

L’attribut svg_width modifie les paramètres du containeur SVG contenant la couche. Nous verrons plus tard comment fonctionnement les containers.

3.4 Personnaliser l’affichage

La bibliothèque geoviz n’est pas seulement un viewer de couches SIG. C’est un outil pour réaliser des cartes vectorielles. Avec geoviz, les cartes sont dessinées au format SVG. Il est donc possible d’utiliser tous les attributs SVG pour modifier la carte. A une nuance près. Le JavaScript ne support pas les noms de variable avec un tiret. Il est donc d’usage de convertir ce tiret en camelCase. Par exemple : stroke-width donnera strokeWidth.

Vous pouvez essayer :

  • fill : couleur de fond
  • stroke : couleur de contour
  • strokeWidth : épaisseur des lignes
  • fillOpacity : opacité du fond
  • strokeOpacity : opacité du contour
  • strokeDashArray : pointillés (par exemple [2,3])

Bref, vous pouvez tout personnaliser comme sur une carte Inkscape ou Illustrator.

viz.path({data: world, svg_width: 790, fill: "#d66bb3", strokeWidth:0.5})

Rappelez-vous qu’avec Observable, nous sommes dans un environnement interactif et réactif. On peut donc mettre en place des interactions pour personnaliser la carte.

viewof colorfill = Inputs.color({label: "Fond", value: "#4682b4"})
viewof colorstroke = Inputs.select(["red", "green", "blue"], {label: "Contour"})
viewof thickness = Inputs.range([0, 10], {step: 0.1, label: "Epaisseur", value:1})
viz.path({data: world, svg_width:790, fill: colorfill, stroke: colorstroke, strokeWidth: thickness})

4. La bibliothèque geoviz

4.1 Documentation et exemples

La documentation de geoviz est disponible à l’adresse suivante : riatelab.github.io/geoviz. Cliquez sur ce lien, et conservez précieusement la page pour pouvoir vous y référer à tout moment.

Pour comprendre comment fonctionne cette bibliothèque, de nombreux exemples live sont également disponibles sur la plateforme de notebooks Observable.

4.2. Les marks

Au même titre que l’instruction path, la bibliothèque geoviz met à disposition un certain nombre de marks permettant de constituer une carte.

Par exemple :

  • circle : des cercles
  • square : des carrés
  • halfcircle : des demis cercles
  • spike : des pointes
  • graticule : lignes de latitude et longitude
  • outline : espace terrestre dans une projection donnée
  • tile : tuiles raster
  • header : titre de la carte
  • footer : pied de page (sources)
  • north: fleche nord
  • scalebar : barre d’échelle
  • text : textes et labels

On peut appeler ces marks directement.

viz.circle({ r: 40, fill: "#38896F" })
viz.square({ side: 60, fill: "#38896F", angle: 45 })

Si on utiliser l’attribut data, alors, les marques sont placées au centre des unités géographiques. Par exemple

viz.square({ data: world, svg_width: 790, side: 6, fill: "#38896F", angle: 45 })
viz.text({ data: world, svg_width: 785, text: "ISO3", fill: "#38896F" })

Mais la plupart du temps, on utilisera ces marks à l’intérieur de conteneurs dans lesquels nous pourrons les superposer.

4.3. Les conteneurs

Dans geoviz, pour combiner différentes couches sur une carte, vous devez créer un conteneur SVG . Ce conteneur est créé avec l’instruction create() . Il peut ensuite être affiché à l’aide de la fonction render() .

Astuce

Pour créer la carte dans une seule cellule, on met les instruction entre accolades

{
  let svg = viz.create()
  svg.path({data:world})
  return svg.render()
}

Pour bien fonctionner, le conteneur a besoin que vous définissiez une projection et/ou une emprise géographie (domain).

Note

Notez que dorénavant, c’est au niveau de la fonction create() que nous allons définir la taille de la carte.

Recommençons.

{
  let svg = viz.create({domain: world, width: 790})
  svg.path({data:world, fill :"#38896F"})
  return svg.render()
}

4.4 Les projections

Dans l’écosystème de d3js et geoviz, on utilise des fonctions de projections bien spécifiques dédiées à la représentation de données. elles sont réparties dans 3 bibliothèques : d3-geo , d3-geo-projection et d3-geo-polygon .

On les charge de la façon suivante :

d3 = require("d3", "d3-geo", "d3-geo-projection", "d3-geo-polygon")

Le principe est qu’on utilise en entrée toujours des géométries au format lat/lon qui sont projetées à la volée au moment de l’affichage.

Par exemple :

{
  let svg = viz.create({domain: world, width: 790, projection: d3.geoNaturalEarth1()})
  svg.path({data:world, fill :"#38896F"})
  return svg.render()
}
Note

Notez que vous auriez aussi simplement pu écrire :

viz.path({data:world, fill :"#38896F", svg_width: 790, svg_projection: d3.geoNaturalEarth1()})

Avec les containers, on peut maintenant empiler les couches. En jouant avec les marks et les attributs SVG, on peut réaliser de très beaux templates cartographiques.

{
  let svg = viz.create({width: 790, projection: d3.geoAitoff() })
  svg.outline()
  svg.graticule({stroke: "white", step: 40})
  svg.path({datum:world, fill :"white", fillOpacity:0.3})
  svg.header({text: "Hello World"})
  return svg.render()
}

Grace aux Inputs, vous pouvez vous amuser à visualiser différentes projections.

projections = [
  { name: "Interrupted Sinusoidal", proj: d3.geoInterruptedSinusoidal() },
  { name: "Gingery", proj: d3.geoGingery() },
  { name: "Baker", proj: d3.geoBaker() },
  { name: "PolyhedralWaterman", proj: d3.geoPolyhedralWaterman()  },     
]
viewof projection = Inputs.select(projections, {
  label: "Projection",
  format: (x) => x.name
})
{
  let svg = viz.create({width: 790, projection: projection.proj})
  svg.graticule({stroke :"#38896F", strokeWidth: 1.5, strokeDasharray:null, step:40, clipPath : svg.effect.clipPath()})
  svg.path({datum:world, fill :"#38896F"})
  svg.outline({stroke :"#38896F", fill:"none", strokeWidth: 2})
  return svg.render()
}

4.5 Zoom et Pan

Dans le conteneur, avec l’attribut zoomable , on va aussi pouvoir dire si on veut que la carte soit zoomable.

Par exemple :

{
  let svg = viz.create({width: 790, projection: d3.geoBertin1953(), zoomable:true })
  svg.outline()
  svg.graticule({stroke: "white"})
  svg.path({datum:world, fill :"white", fillOpacity:0.3})
  return svg.render()
}

Avec une projection orthographique et l’attributzoomable = "versor", vous pouvez aussi jouer sur le centre de projection pour faire tourner le globe.

{
  let svg = viz.create({width: 790, projection: d3.geoOrthographic().rotate([-30, -30]), zoomable:"versor" })
  svg.outline()
  svg.graticule({stroke: "white"})
  svg.path({datum:world, fill :"white", fillOpacity:0.3})
  return svg.render()
}

Notez que "versor" s’applique sur n’importe quelle projection, ce qui peut être déroutant, mais aussi bien utile pour comprendre vraiment comment fonctionnent les projections cartographiques.

{
  let svg = viz.create({width: 790, projection: d3.geoEckert3(), zoomable:"versor" })
  svg.outline()
  svg.graticule({stroke: "white"})
  svg.path({datum:world, fill :"white", fillOpacity:0.3})
  return svg.render()
}

4.6 Tuiles raster

Comme les autres marks, les tuiles raster sont également zoomable.

Les styles disponibles par défaut sont : “openstreetmap”, “opentopomap”, “worldterrain”, “worldimagery”, “worldStreet”, “worldphysical”, “shadedrelief”, “stamenterrain”, “cartodbvoyager”, “stamentoner”, “stamentonerbackground”, “stamentonerlite”, “stamenwatercolor”, “hillshade”, “worldocean”, “natgeo” et “worldterrain”

Attention

Pour utiliser la mark tile, vous devez forcément utiliser la projection “mercator”

{
  let svg = viz.create({width: 790, projection: "mercator", zoomable:true })
  svg.tile({url:"natgeo"})
  svg.path({datum:world, fill :"none", stroke:"white"})
  return svg.render()
}

4.7 Infobulles

Avec geoviz, vous pouvez ajouter des infobulles sur n’importe quel objet. En utilisant tip: true, tous les champs sont affichés.

{
  let svg = viz.create({width: 790, projection: d3.geoNaturalEarth1()})
  svg.path({data:world, fill :"#38896F", stroke:"white", strokeWidth:0.3, tip:true})
  return svg.render()
}

Mais tout est personnalisable

{
  let svg = viz.create({width: 790, projection: d3.geoNaturalEarth1()})
  svg.path({data:world, fill :"#38896F", stroke:"white", strokeWidth:0.3, tip: `Ce pays est $NAMEfr et son code est : $ISO3`})
  return svg.render()
}

5 Cartographie statistique

5.1. La jointure

La première chose à faire ici est de réaliser une jointure entre les géométries et les données statistiques importées en haut de ce notebook depuis un fichier zip.

Examinons à nouveau le tableau de données.

Inputs.table(data)

Le tableau contient des informations à plusieurs dates. Il y a donc plusieurs fois le même identifiant (id) dans le tableau de données. La première étape consiste donc à sélectionner une année.

En JavaScript, on utilise l’instruction filter.

data2020 = data.filter(d => d.year == 2020)
Inputs.table(data2020)
Astuce

Manipuler un tableau de données en JavaScript quand on l’habitude de le faire en R peut être déroutant. Mais vous avez la possibilité d’utiliser la bibliothèque arquero qui ressemble beaucoup à dplyr.

Plus d’informations ici : observablehq.com/@neocartocnrs/les-tableaux-de-donnees

Pour réaliser la jointure, on utilise l’instruction viz.tool.merge() .

jointure = viz.tool.merge({geom: world, geom_id: "ISO3", data: data2020, data_id:"id"})

La fonction renvoie le résultat de la jointure mais également un diagnostic pour évaluer la qualité de cette jointure.

Le nouveau fond de carte est donc :

world2020 = jointure.featureCollection

5.2. Symboles proportionnels

Pour représenter des données quantitatives absolues, on utilise en cartographie des symboles qu’on fait varier de façon proportionnelle. Pour cela, on utilisera la fonction viz.plot() avec type: "prop" . C’est un peu la même logique qu’avec r::mapsf.

La carte peut se dessiner comme ceci :

{
let svg = viz.create({width: 790, domain: world2020})
svg.plot({type: "base", data: world2020, fill: "#CCC"})
svg.plot({type: "prop", data: world2020, var: "pop", fill:"#d47988", leg_pos:[10, 200]})
return svg.render()
}

5.3. Typologies

Pour réaliser des typologies, on utilise également la fonction plot() avec type: "typo"

{
let svg = viz.create({width: 790, domain: world2020})
svg.plot({type: "typo", data: world2020, var: "region", leg_pos:[10, 100]})
return svg.render()
}

De nombreuses palettes de couleurs sont disponibles dans dicopal

5.4 Carte choroplèthe

Pour réaliser une carte choroplèthe, on utilisera le type: "choro"

{
let svg = viz.create({width: 790, domain: world2020})
svg.plot({type: "choro", data: world2020, var: "gdppc", leg_pos:[10, 100]})
return svg.render()
}

Avec method, vous pouvez changer la méthode de discrétisation : ‘quantile’, ‘q6’, ‘equal’, ‘jenks’, ‘msd’, ‘geometric’, ‘headtail’, ‘pretty’, ‘arithmetic’ ou ‘nestedmeans’.

Avec nb, vous pouvez changer le nombre de classes.

Avec colors, vous pouvez changer la palette.

5.5 Combinaisons

Avec les types “propchoro” et “proptypo” vous pouvez faire des combinaisons graphiques.

Par exemple

{
let svg = viz.create({width: 790, domain: world2020})
svg.plot({type: "propchoro", data: world2020, var1: "pop", var2: "gdppc"})
return svg.render()
}

5.6 Tout est paramétrable/configurable

Rappelez-vous également que nous sommes dans un environnement réactif et que vous pouvez proposer des interactions pour modifier la carte.

viewof title = Inputs.textarea({label: "Titre de la carte", placeholder: "Titre..."})
viewof k = Inputs.range([10, 70], {step: 1, label: "Rayon du plus grand cercle"})
viewof toggle = Inputs.toggle({label: "Écarter les cercles ?", value: false})
{
let svg = viz.create({width:790, domain: world2020})
svg.path({datum: world2020, fill:"#CCC"})
svg.plot({type:"prop", data: world2020, var: "pop", k:k, fill:"#e02d51", dodge: toggle, leg_pos:[10, 200] })
svg.header({text: title })
return svg.render()
}

Tout est complètement paramétrable.

{
  let svg = viz.create({ projection: d3.geoOrthographic().rotate([-50,-50]), zoomable: "versor", width:790 });
  svg.plot({ type: "outline", fill: svg.effect.radialGradient() });
  svg.plot({
    type: "graticule",
    stroke: "white",
    step: 40,
    strokeWidth: 2,
    strokeOpacity: 0.3
  });
  svg.plot({ type: "typo", data: world2020, var: "region", stroke: "none", legend: false });
  svg.plot({
    type: "prop",
    symbol: "square",
    data: world2020,
    var: "pop",
    fill: "red",
    fillOpacity:0.8,
    leg_type: "nested",
    leg_values_factor: 1 / 1000000,
    leg_pos: [20, 20],
    leg_frame:true,
    leg_title: "Nombre d'habitants",
    leg_subtitle: "(en millions)",
    tip: `$name ($ISO3)`,
    tipstyle: {
    fontSize: 20,
    fill: "white",
    background: "#38896F",
  }
  });
  return svg.render();
}

6. Réalisation d’une carte animée

On importe un widget depuis la plateforme de notebooks Observable.

import {Scrubber} from "@mbostock/scrubber"

Puis on réalise la carte

viewof annees = Scrubber(d3.range(1960,2023), {autoplay: false})
// Tri des données
mydata = data.filter(d => d.year == annees)
mybasemap = viz.tool.merge({geom: world, geom_id: "ISO3", data: mydata, data_id:"id"}).featureCollection
// Carte
{
  let svg = viz.create({width:790, projection: d3.geoBertin1953()})
  svg.outline()
  svg.graticule({stroke: "white", step: 40})
  svg.path({datum: world, fill:"white", fillOpacity:0.3})
  svg.header({text: `Population en ${annees}`})
  svg.plot({type:"prop", data: mybasemap,  var: "pop", fill:"red", fixmax: 1417173173, tip:`$name\n$pop`})
  return svg.render()
}

6 - Aller plus loin

6.1. Les dashboards

Depuis la version 1.4, Quarto propose des mises en page à la façon de dashboards. Tout ce que nous avons vu précédemment est donc facilement mobilisable pour réaliser une application complète. Quelques exemples sont disponibles sur le site de Quarto.

6.2. Tutos et cours

6.3 Exemples de cartes réalisées avec geoviz