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 (```)
= 5 radius
**``{ojs} radius``** Le rayon du cercle est égal à
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 :
= FileAttachment("data/worldbank.zip").zip() worldbank
Ce fichier zip contient 3 fichiers.
.filenames worldbank
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
= worldbank.file("data.csv").csv() data
- Les métadonnées
= worldbank.file("metadata.csv").csv() metadata
- Le fond de carte (pays du monde)
= worldbank.file("world.json").json() world
3.2 Visualiser les données attributaires
Pour visualiser un tableau de données, on peut utiliser l’instruction Inputs.table()
.
.table(data) Inputs
.table(metadata) Inputs
On peut également combiner cet affichage par table avec la fonction Inputs.search()
.
= Inputs.search(data, { query: "Tunisia" })
viewof search .table(search) Inputs
Astuce
Il est possible de combiner des chunks R et des chunks ojs grace à l’instruction ojs_define()
Par exemple :
# chunk r
<- read.csv("data/data.csv")
datafromr ojs_define(ojsdata = datafromr)
// chunk ojs
.table(transpose(ojsdata)) Inputs
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")
<- st_read("data/world.gpkg")
geomfromr 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()
.
= require("geoviz@0.6.1") viz
Pour visualiser simplement les géométries avec une couleur aléatoire, on tape :
.path({data: world}) viz
La carte est un peu grande. Nous pouvons la redimensionner en utilisant le paramètre svg_width
.
.path({data: world, svg_width: 790}) viz
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.
.path({data: world, svg_width: 790, fill: "#d66bb3", strokeWidth:0.5}) viz
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.
= Inputs.color({label: "Fond", value: "#4682b4"})
viewof colorfill = Inputs.select(["red", "green", "blue"], {label: "Contour"})
viewof colorstroke = Inputs.range([0, 10], {step: 0.1, label: "Epaisseur", value:1}) viewof thickness
.path({data: world, svg_width:790, fill: colorfill, stroke: colorstroke, strokeWidth: thickness}) viz
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.
.circle({ r: 40, fill: "#38896F" }) viz
.square({ side: 60, fill: "#38896F", angle: 45 }) viz
Si on utiliser l’attribut data
, alors, les marques sont placées au centre des unités géographiques. Par exemple
.square({ data: world, svg_width: 790, side: 6, fill: "#38896F", angle: 45 }) viz
.text({ data: world, svg_width: 785, text: "ISO3", fill: "#38896F" }) viz
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()
.path({data:world})
svgreturn 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})
.path({data:world, fill :"#38896F"})
svgreturn 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 :
= require("d3", "d3-geo", "d3-geo-projection", "d3-geo-polygon") d3
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()})
.path({data:world, fill :"#38896F"})
svgreturn svg.render()
}
Note
Notez que vous auriez aussi simplement pu écrire :
.path({data:world, fill :"#38896F", svg_width: 790, svg_projection: d3.geoNaturalEarth1()}) viz
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() })
.outline()
svg.graticule({stroke: "white", step: 40})
svg.path({datum:world, fill :"white", fillOpacity:0.3})
svg.header({text: "Hello World"})
svgreturn 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() },
{ ]
= Inputs.select(projections, {
viewof projection label: "Projection",
format: (x) => x.name
})
{let svg = viz.create({width: 790, projection: projection.proj})
.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})
svgreturn 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 })
.outline()
svg.graticule({stroke: "white"})
svg.path({datum:world, fill :"white", fillOpacity:0.3})
svgreturn 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" })
.outline()
svg.graticule({stroke: "white"})
svg.path({datum:world, fill :"white", fillOpacity:0.3})
svgreturn 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" })
.outline()
svg.graticule({stroke: "white"})
svg.path({datum:world, fill :"white", fillOpacity:0.3})
svgreturn 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 })
.tile({url:"natgeo"})
svg.path({datum:world, fill :"none", stroke:"white"})
svgreturn 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()})
.path({data:world, fill :"#38896F", stroke:"white", strokeWidth:0.3, tip:true})
svgreturn svg.render()
}
Mais tout est personnalisable
{let svg = viz.create({width: 790, projection: d3.geoNaturalEarth1()})
.path({data:world, fill :"#38896F", stroke:"white", strokeWidth:0.3, tip: `Ce pays est $NAMEfr et son code est : $ISO3`})
svgreturn 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.
.table(data) Inputs
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
.
= data.filter(d => d.year == 2020)
data2020 .table(data2020) Inputs
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()
.
= viz.tool.merge({geom: world, geom_id: "ISO3", data: data2020, data_id:"id"}) jointure
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 :
= jointure.featureCollection world2020
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})
.plot({type: "base", data: world2020, fill: "#CCC"})
svg.plot({type: "prop", data: world2020, var: "pop", fill:"#d47988", leg_pos:[10, 200]})
svgreturn 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})
.plot({type: "typo", data: world2020, var: "region", leg_pos:[10, 100]})
svgreturn 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})
.plot({type: "choro", data: world2020, var: "gdppc", leg_pos:[10, 100]})
svgreturn 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})
.plot({type: "propchoro", data: world2020, var1: "pop", var2: "gdppc"})
svgreturn 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.
= Inputs.textarea({label: "Titre de la carte", placeholder: "Titre..."})
viewof title = Inputs.range([10, 70], {step: 1, label: "Rayon du plus grand cercle"})
viewof k = Inputs.toggle({label: "Écarter les cercles ?", value: false}) viewof toggle
{let svg = viz.create({width:790, domain: world2020})
.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 })
svgreturn svg.render()
}
Tout est complètement paramétrable.
{let svg = viz.create({ projection: d3.geoOrthographic().rotate([-50,-50]), zoomable: "versor", width:790 });
.plot({ type: "outline", fill: svg.effect.radialGradient() });
svg.plot({
svgtype: "graticule",
stroke: "white",
step: 40,
strokeWidth: 2,
strokeOpacity: 0.3
;
}).plot({ type: "typo", data: world2020, var: "region", stroke: "none", legend: false });
svg.plot({
svgtype: "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
= Scrubber(d3.range(1960,2023), {autoplay: false})
viewof annees // Tri des données
= data.filter(d => d.year == annees)
mydata = viz.tool.merge({geom: world, geom_id: "ISO3", data: mydata, data_id:"id"}).featureCollection
mybasemap // Carte
{let svg = viz.create({width:790, projection: d3.geoBertin1953()})
.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`})
svgreturn 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