chart = {
const k = width / 120;
const r = d3.randomUniform(k, k * 4);
const n = 4;
const data = Array.from({length: 100}, (_, i) => ({r: r(), group: i && (i % n + 1)}));
const height = width / 3;
const color = d3.scaleOrdinal(d3.schemeTableau10);
const context = DOM.context2d(width, height);
const nodes = data.map(Object.create);
const simulation = d3.forceSimulation(nodes)
.alphaTarget(0.3)
.velocityDecay(0.1)
.force("x", d3.forceX().strength(0.01))
.force("y", d3.forceY().strength(0.01))
.force("collide", d3.forceCollide().radius(d => d.r + 1).iterations(3))
.force("charge", d3.forceManyBody().strength((d, i) => i ? 0 : -width * 2 / 3))
.on("tick", ticked);
d3.select(context.canvas)
.on("touchmove", event => event.preventDefault())
.on("pointermove", pointermoved);
invalidation.then(() => simulation.stop());
function pointermoved(event) {
const [x, y] = d3.pointer(event);
nodes[0].fx = x - width / 2;
nodes[0].fy = y - height / 2;
}
function ticked() {
context.clearRect(0, 0, width, height);
context.save();
context.translate(width / 2, height / 2);
for (let i = 1; i < nodes.length; ++i) {
const d = nodes[i];
context.beginPath();
context.moveTo(d.x + d.r, d.y);
context.arc(d.x, d.y, d.r, 0, 2 * Math.PI);
context.fillStyle = color(d.group);
context.fill();
}
context.restore();
}
return context.canvas;
}Cartographier pour le Web avec quarto {ojs} et geoviz
GEO UNIV’R Tunisie 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 = 5Le 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.filenamesIl 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)
Astuce
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 fondstroke: couleur de contourstrokeWidth: épaisseur des lignesfillOpacity: opacité du fondstrokeOpacity: opacité du contourstrokeDashArray: 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 cerclessquare: des carréshalfcircle: des demis cerclesspike: des pointesgraticule: lignes de latitude et longitudeoutline: espace terrestre dans une projection donnéetile: tuiles rasterheader: titre de la cartefooter: pied de page (sources)north: fleche nordscalebar: barre d’échelletext: 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.featureCollection5.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.
- Voir un exemple ici : neocarto.github.io/geounivr2024/VEN1_geoviz_dashboard
- Code source : github.com/neocarto/geounivr2024/blob/main/VEN1_geoviz_dashboard
- Documentaation Quarto : quarto.org/docs/dashboards