= require("bertin@1.7.3") bertin
bertin
SAGEO, 2023 - Québec, Canada
07 Jun 2023
bertin est une bibliothèque écrite en JavaScript qui permet de réaliser des cartes thématiques pour le web.
Sa conception vise à permettre aux utilisateurs de créer rapidement des cartes thématiques interactives sans forcement connaître le langage JavaScript
Le développemement de bertin
est intimement lié au développement de la plateforme de notebooks Observable.
Observable est aussi une startup fondée par Mike Bostock et Melody Meckfessel, qui propose une plateforme 100% en ligne pour concevoir, partager et diffuser des visualisations de données.
L’Observable javascript (ojs) est un ensemble d’améliorations apportées à vanilla JavaScript créé par Mike Bostock (également auteur de D3). Observable JS se distingue par son exécution réactive, qui convient particulièrement bien à l’exploration et à l’analyse interactives des données. Objectif : faire collaborer une communauté autour de la visualisation de données.
Observable, c’est aussi une plateforme web qui héberge des notebooks computationnels sur la visualisation de données.
NB : Cette présentation est réalisée avec Quarto (qui implémente l’ojs)
Une bibliothèque JavaScript pour la cartographie thématique
Le développement de la bibliothèque bertin
s’appuie en grande partie sur le librairie javascript d3.js développée par Mike Bostock depuis 10 ans. Le développement a débuté en novembre 2021. Il y a 8 contributeurs. 256 ⭐ sur Github.
Pour charger la bibliothèque :
Dans Observable, on utilise require
Elle permet de réaliser
de nombreux types de cartes thématiques.
Parti pris :
👉 cartographie vectorielle
👉 cartographie interactive pour le web
👉 une recherche esthétique
👉 plutôt à partir de maillages administratifs
👉 plutôt avec des données pas trop volumineuses
👉 forte intégration avec l’écosystème d’Observable
Le principe de la bibliothèque bertin est de proposer un outil permettant de réaliser rapidement des cartes thématiques variées sans faire appel à la programmation en JavaScript ni directement à la bibliothèque D3.js.
La bibliothèque bertin
prend en entrée des données JSON, GeoJSON ou topoJSON.
cities = FileAttachment("data/cities.json").json()
cables = FileAttachment("data/cables.json").json()
world = FileAttachment("data/world.json").json()
land = FileAttachment("data/land.json").json()
regions = FileAttachment("data/regions.json").json()
statsall = FileAttachment("data/worldbank_data.csv").csv({typed: true})
stats = statsall.filter(d => d.date == 2019)
Le fond de carte
La fonction match()
permet de tester la compatibilité entre les données et le fonde de carte.
La fonction merge()
permet d’effectuer la jointure
La fonction quickdraw()
permet de visualiser la carte dans la projection d3.geoEquirectangular().
On utilise la fonction draw()
pour dessiner tous les types de cartes. La fonction prend en entrée un objet JavaScript contenant toutes les informations nécéssaires.
L’ordre des couches définit l’ordre d’affichage. Ce qui est écrit au dessus s’affiche au dessus.
Polygones (syntaxe minimale)
Points (syntaxe minimale)
Lignes (syntaxe minimale)
Le rendu peut être très largement paramétré.
viewof simple_strokewidth = Inputs.range([0.1, 5], {label: "strokeWidth", step: 0.1, value: 1})
viewof simple_symbol = Inputs.select(["circle", "cross", "diamond", "square", "star", "triangle", "wye"], { label: "Symbol"})
viewof simple_symbol_size = Inputs.range([50, 300], {label: "symboll_size", step: 1, value: 100})
Les styles (couleurs, transparence, épaisseur, etc) reprennent les noms des attributs SVG.
En plus des couches, la fonction draw prend en entrée des paramètres généraux.
viewof width = Inputs.range([200, 600], { label: "width", value: 500, step: 1 })
viewof top = Inputs.range([0, 100], { label: "top", value: 80, step: 1 })
viewof right = Inputs.range([0, 100], { label: "right", value: 10, step: 1 })
viewof bottom = Inputs.range([0, 100], { label: "bottom", value: 80, step: 1 })
viewof left = Inputs.range([0, 100], { label: "left", value: 10, step: 1 })
viewof background = Inputs.color({label: "background", value: "#e1f5fe"})
La bibliothèque bertin
s’appuie sur l’écosystème de d3. Elle bénificie donc des projections disponible dans d3.geo
et d3.geoprojection
.
prj = ["Polar","Spilhaus","InterruptedSinusoidal", "Armadillo", "Baker", "Gingery", "Berghaus","Loximuthal", "Healpix", "InterruptedMollweideHemispheres", "Miller", "Aitoff", "Globe"]
viewof projection = Inputs.select(prj)
Si on choisit la projection “globe”, on obtient un globe interactif.
La librairie bertin
permet également d’utiliser des projections au format proj4 ou epsg.
Le type outline
permet d’afficher l’espace terrestre.
Le type geolines
permet d’afficher l’équateur, les tropiques et les cercles polaires
Le type graticule
permet d’afficher les lignes de latitude et de longitude.
Le type waterlines
permet d’afficher des lignes autour des terres, comme sur les cartes anciennes
Le type rhumbs
crée des pseudo lignes de rhumbs pour reproduire des styles de cartes anciennes (portulans)
Le type hatch
permet d’ajouter des hachures et une texture à la carte. C’est uniquement esthétique.
bertin.draw({
params: {width: 600, projection: "Bertin1953"},
layers: [
{type: "hatch", angle: hatchangle, spacing: hatchspacing, stroke: "#E63F33", strokeOpacity: 0.4, strokeWidth:hatchstroke},
{type : "outline", stroke: "none", fill:"none"},
{geojson:world, stroke:"none", fill:"#ccc", fillOpacity:0.5}
]
})
Le type shadow
permet d’ajouter une ombre sous une couche
Le type inner
permet d’ajouter un effet de dégradé sur les contours. C’est très utilisé en cartographie d’édition.
Le type tissot
permet d’afficher l’indicatrice de Tissot et visualiser les déformations induites par l’usage d’une projection cartographique.
Le type scalebar
permet d’afficher l’échelle.
Le type minimap
permet d’afficher une carte de localisation
Le type tiles
permet d’afficher des tuiles raster, mais uniquement dans la projection Web Mercator.
viewof tilesstyle = Inputs.select(
[
"openstreetmap",
"opentopomap",
"worldterrain",
"worldimagery",
"worldStreet",
"worldphysical",
"shadedrelief",
],
{ label: "style", value: "worldimagery" }
)
viewof zoomDelta = Inputs.range([0, 4], {
label: "zoomdelta",
step: 1,
value: 0
})
viewof tileclip = Inputs.toggle({label: "clip", value: false})
chn = bertin.properties.subset({
geojson: world,
field: "id",
selection: ["CHN"],
})
Le type logo
vous permet d’afficher une image sur la carte.
Le type header
permet d’ajouter un titre
Le type footer
permet d’ajouter une note de bas de page (source, auteur…)
le type text
parmet d’ajouter du texte n’importe où sur la carte
Le type label
permet d’afficher du texte lié au fond de carte.
Chaque couche basée sur des données parmet l’affichage d’infobulles
Pour réprésenter des données quantitatives absolues, on utilise la variable visuelle TAILLE. En pratique, cela revient la plupart du temps à utiliser des cercles proportionnels. Dans bertin
, on utilisera alors le type bubble
.
bertin.draw({
params: {width: 600, projection: "Bertin1953", margin:[0,0,30,0]},
layers: [
{type: "bubble", geojson: world2019, values: "POP", fill: "#E63F33", k: k1, dorling: bubbledorling, leg_x: 400, leg_y: 250, leg_title: `Nombre
d'habitants`, leg_round:0, tooltip:["$country", d => Math.round(d.properties.POP/1000000) + " millions de personnes"]},
{geojson: land, fill: "white", fillOpacity: 0.5, stroke: "none"},
{ type: "graticule" },
{ type: "outline" }
]
})
Pour réprésenter des données quantitatives absolues, on utilise la variable visuelle TAILLE. En pratique, On peut aussi utiliser des carrés proportionnels. Dans bertin
, on utilisera alors le type square
.
bertin.draw({
params: {width: 600, projection: "Bertin1953", margin:[0,0,30,0]},
layers: [
{type: "square", geojson: world2019, values: "POP", fill: "#E63F33", k: k2, demers, leg_x: 400, leg_y: 250, leg_title: `Nombre
d'habitants`, leg_round:0, tooltip:["$country", d => Math.round(d.properties.POP/1000000) + " millions de personnes"]},
{geojson: land, fill: "white", fillOpacity: 0.5, stroke: "none"},
{ type: "graticule" },
{ type: "outline" }
]
})
Pour réprésenter des données quantitatives absolues, on utilise la variable visuelle TAILLE. En pratique, On peut aussi utiliser la hauteur. Dans bertin
, on utilisera alors le type spikes
.
bertin.draw({
params: {width: 600, projection: "Bertin1953", margin:[0,0,30,0]},
layers: [
{type: "spikes", geojson: world2019, values: "POP", fill: "#E63F33", k: spikek, demers, leg_x: 20, leg_y: 20, leg_title: `Nombre
d'habitants`, leg_round:0, tooltip:["$country", d => Math.round(d.properties.POP/1000000) + " millions de personnes"]},
{geojson: land, fill: "white", fillOpacity: 0.5, stroke: "none"},
{ type: "graticule" },
{ type: "outline" }
]
})
Pour réprésenter des données quanlitatvives absolues, on utilise la variable visuelle COULEUR. Dans bertin
, on utilisera alors le type typo
pour faire varier dynamiquement les couleurs.
Pour réprésenter des données quanlitatvives relatives, on utilise la variable visuelle VALEUR. Dans bertin
, on utilisera alors le type choro
pour faire varier dynamiquement les couleurs.
bertin.draw({ params: {width: 600, projection: "Bertin1953"},
layers: [
{geojson: world2, fill: {type: "choro", values: "gdppc", method, nbreaks, colors: pal2, leg_x: 1, leg_y: 1, leg_title: "PIB par habitant", leg_round: 0}, tooltip:["$country", d => Math.round(d.properties.gdppc) +" $/hab"]},
{ type: "graticule" },
{ type: "outline" }
] })
viewof pal2 = Inputs.select(["BuGn", "BuPu", "GnBu", "OrRd", "PuBuGn", "PuBu", "PuRd", "RdPu", "YlGnBu", "YlGn", "YlOrBr", "YlOrRd"], {value: "BuPu", label: "Palette"})
viewof method = Inputs.select(
["jenks", "q6", "quantile", "equal", "msd"],
{
label: "method",
value: "quantile"
}
)
viewof nbreaks = Inputs.range([3, 9], { label: "nbreaks", step: 1, value: 7, disabled: method == "msd" || method == "q6" ? true : false})
world2 = bertin.properties.add({
geojson: world2019,
field: "gdppc",
expression: "GDP/POP"
})
Avec le système de couches, il est facile de combiner des variables dont la figuration est différente.
On peut aussi choisir de colorier des symboles en modifiant l’attribut fill.
bertin.draw({params: {width: 600, projection: "Bertin1953"} ,layers: [
{type: type, geojson: world2, values: "POP", k: 40, fill: {type: "typo", values: "continent"}, stroke: type == "spikes" ? {type: "typo", values: "continent"} :"white", strokeWidth: 1},
{ geojson: world, fill: "white", fillOpacity:0.3, stroke: "none"},
{ type: "graticule" },
{ type: "outline" }
] })
De la même façon, on peut colorier les contours.
bertin.draw({params: {width: 600, projection: "Bertin1953"} ,layers: [
{type: "bubble", geojson: world2, values: "POP", k: 40, stroke: mylayer == "Cercles" ? {type: "typo", values: "continent"} : "none", fill: "none", strokeWidth, dorling: dor},
{ geojson: world, fill: "white", fillOpacity:0.3, stroke: mylayer == "Pays" ? {type: "typo", values: "continent"} : "none", strokeWidth},
{ type: "graticule" },
{ type: "outline" }
] })
La méthode la plus simple est de réaliser 2 cartes en vis à vis
bertin.draw({
params: {width: 600, projection: "Bertin1953"},
layers: [
{type:"header", text: "POPULATION"},
{type: "bubble", geojson: world2019, values: "POP", fill: "#E63F33", k: kdouble},
{geojson: land, fill: "white", fillOpacity: 0.5, stroke: "none"},
{ type: "graticule" },
{ type: "outline" }
]
})
bertin.draw({
params: {width: 600, projection: "Bertin1953"},
layers: [
{type:"header", text: "RICHESSE"},
{type: "bubble", geojson: world2019, values: "GDP", fill: "#4fabd6", k:kdouble},
{geojson: land, fill: "white", fillOpacity: 0.5, stroke: "none"},
{ type: "graticule" },
{ type: "outline" }
]
})
world02bis = {
let data = bertin.properties.table(world2)
let poptot = d3.sum(data.map((d) => d.POP))
let gdptot = d3.sum(data.map((d) => d.GDP))
let data2 = data.map((d) => ({
id: d.id,
pop_pct: +((+d.POP / poptot) * 100).toFixed(2),
gdp_pct: +((+d.GDP / gdptot) * 100).toFixed(2)
}))
return bertin.merge(world2, "id", data2, "id")
}
Le type mushroom
permet de représenter 2 variables quantitatives
bertin.draw({ params: {width: 600, projection: "Bertin1953", margin:[0,0, 200, 0]}, layers: [
{type: "mushroom", k: mushroomk, geojson: world02bis,top_values: "gdp_pct", bottom_values: "pop_pct", leg_x: 50,
leg_y: 360, leg_title: "Population\net richesse\ndans le Monde", leg_top_fill: "none",
leg_bottom_fill: "none",
leg_top_txt: "PIB (%)",
leg_bottom_txt: "POP (%)",
leg_fontSize: 17, top_tooltip: { fields: ["$name", d => Math.round(d.properties.GDP/1000000000) + " milliards de $"], col: "#d64f4f" }, bottom_tooltip: {
fields: ["$name", d => Math.round(d.properties.POP/1000000) + " millions d'habitants"],
col: "#4fabd6"
}},
{geojson: land, fill: "white", fillOpacity: 0.5, stroke: "none"},
{ type: "graticule" },
{ type: "outline" }
] })
La première carte de densité de points a été conçue par Armand Joseph Frère de Montizon en 1830. Elle consiste à déterminer la valeur d’un point et d’en placer autant que le nombre nécéssaire pour atteindre une quantité.
Avec le type dotdensity
, on positionne les points de façon aléatoire dans les mailles adminstratives.
La première carte de densité de points a été conçue par Armand Joseph Frère de Montizon en 1830. Elle consiste à déterminer la valeur d’un point et d’en placer autant que le nombre nécéssaire pour atteindre une quantité.
Avec le type dotcartogram
, on positionne les points au niveau des centroides
bertin.draw({ params: {width: 600, projection: "Bertin1953"}, layers: [
{type: "dotcartogram", geojson: world2, values: "POP", onedot, radius:dotcartogramradius, fill: "#E63F33", stroke: "none"},
{geojson: land, fill: "white", fillOpacity: 0.5, stroke: "none"},
{ type: "graticule" },
{ type: "outline" }
] })
La bibliothèque bertin
permet de passer de mailles irrégulières (maillage administratif) à des mailles régulières (carrés).
Cela permet de réaliser de nouveaux types de représentation.
Le type regulargrid
permet de passer d’un maillage administratif à une grille régulière.
Le type regularbubble
de répartir des cercles proportionnels régulièrement sur la carte
bertin.draw({
params: {width: 600, projection: "Bertin1953"},
layers: [
{type: "regularbubble", geojson: world2, values: "POP", step : steppb, k: kpb, fill:"#E63F33", dorling: dorlingpb},
{geojson: land, fill: "white", fillOpacity: 0.5, stroke: "none"},
{ type: "graticule" },
{ type: "outline" }
] })
Sur le même principe, on peut utiliser le type regularsquare
bertin.draw({
params: {width: 600, projection: "Bertin1953"},
layers: [
{type: "regularsquare", geojson: world2, values: "POP", step : steprr, k: krr, fill:"#E63F33", dorling: dorlingrr},
{geojson: land, fill: "white", fillOpacity: 0.5, stroke: "none"},
{ type: "graticule" },
{ type: "outline" }
] })
Avec le type Ridgelines
, on peut représenter ces données sur grilles sous la forme d’une fausse 3D.
Avec le type smooth
, on peut représenter ces données de façon continue dans l’espace.
bertin.draw({
params: {width: 600, projection: "Bertin1953"},
layers: [
{type: "bubble", geojson: world2, values: "POP", fill:"black", stroke: "none", k:15},
{
type: "smooth",
geojson: world2,
values: "POP",
bandwidth: bandwidth,
thresholds: thresholds,
clip:clipsmooth1
},
{geojson: land, fill: "white", fillOpacity: 0.5, stroke: "none"},
{ type: "graticule" },
{ type: "outline" }
] })
Même chose en répartissant les masses sur une grille régulière
bertin.draw({
params: {width: 600, projection: "Bertin1953"},
layers: [
{type: "regularbubble", geojson: world2, values: "POP", fill:"black", stroke: "none", k:5, step: smoothstep},
{
type: "smooth",
geojson: world2,
values: "POP",
grid_step : smoothstep,
bandwidth: bandwidth2,
thresholds: thresholds2,
clip: clipsmooth2
},
{geojson: land, fill: "white", fillOpacity: 0.5, stroke: "none"},
{ type: "graticule" },
{ type: "outline" }
] })
viewof bandwidth2 = Inputs.range([2, 60], {
step: 1,
value: 20,
label: "Bandwith"
})
viewof thresholds2 = Inputs.range([5, 150], {
step: 1,
value: 50,
label: "Thresholds"
})
viewof smoothstep = Inputs.range([5, 50], {
step: 1,
value: 20,
label: "Grille (step)"
})
viewof clipsmooth2 = Inputs.toggle({ label: "Clip", value: false })
la bibliothèque bertin
a été pensée pour être utilisée dans l’écosystème Observable. Ainsi, dans cet environnement, on peut utiliser l’instruction viewof
pour transformer n’importe quelle carte en Input
.
Par défaut, ce sont les latitudes et les longitudes qui sont renvoyées.
Mais on peut aussi utiliser l’instruction viewof
sur n’importe quelle couche.
Ainsi, les données liées à cette couche sont renvoyées
Cela permet de faire dialoguer la carte avec d’autres éléments graphiques
Sur chaque type de couche, une fonction update()
permet de mettre à jour certains éléments de la carte sans avoir besoin de tout redessiner. Pour cela, il est nécessaire d’affecter un identifiant à chaque couche qu’on souhaite modifier.
On peut utiliser la fonction update()
pour masquer/afficher des couches.
map8 = bertin.draw({
params: {width: 600, projection: "Bertin1953"},
layers: [
{ id: "title", type: "header", text: "Population dans le Monde" },
{
id: "pop",
type: "square",
geojson: world2,
k:40,
values: "POP",
fill: "#E63F33",
},
{
id: "basemap",
geojson: land,
fill: "#ebc994"
},
{ id: "shadow", type: "shadow", geojson: land },
{ id: "latlong", type: "graticule", stroke: "#375557", strokeOpacity: 0.2 }
]
})
viewof updatek = Inputs.range([10, 60], { label: "Rayon max", step: 1, value: 40 })
viewof updateval = Inputs.radio(["POP", "GDP"], { label: "Variable", value: "POP" })
viewof updatetoggle = Inputs.toggle({ label: "Dorling", value: false })
map5 = bertin.draw({
params: {width: 600, projection: "Bertin1953", margin: [0,0, 200, 0]},
layers: [
{
id: "bub",
geojson: world2,
type: "bubble",
values: "POP",
dorling: false,
k: 40,
leg_x: 430,
leg_y: 270,
leg_round: 0,
leg_title: "Population",
tooltip: [
"$name",
(d) =>
`population: ${Math.round(d.properties.POP / 1000000)} million inh.`,
(d) =>
`gdp: ${Math.round(d.properties.GDP / 10000000000) / 100} billion $.`
],
fill: "#E63F33"
},
{
geojson: land,
fill: "white",
fillOpacity: 0.5,
stroke: "none"
},
{ type: "graticule" },
{ type: "outline" }
]
})
map10 = bertin.draw({
params: { margin: [0, 0, 0, 90], projection: "Globe(-50,-30,0)", width: 700 },
layers: [
{
id: "sp",
type: "spikes",
geojson: world2,
values: "POP",
fill: "#e0483d",
k: 150,
leg_x: 10,
leg_y: 10,
leg_round: 0,
leg_title: "Population",
tooltip: [
"$name",
(d) =>
`population: ${Math.round(d.properties.POP / 1000000)} million inh.`,
(d) =>
`gdp: ${Math.round(d.properties.GDP / 10000000000) / 100} billion $.`
],
},
{
geojson: land,
fill: "white",
fillOpacity: 0.5,
stroke: "none"
},
{ type: "graticule" },
{ type: "outline" }
]
})
Nicolas Lambert, Timothée Giraud, Matthieu Viry et Ronan Ysebaert
👉 Code et documentation : github.com/neocarto/bertin
👉 Exemples : observablehq.com/collection/@neocartocnrs/bertin
👉 Issues : github.com/neocarto/bertin/issues
Cette présentation est disponible ici : neocarto.github.io/bertin-sageo2023