Cartographie thématique avec {ojs}

École d’été internationale, Ouidah, 2023

Nicolas Lambert, Manhamady Ouedraogo

08 Mar 2023

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

Petite histoire de la cartographie

Les premières cartes


“Cadastre” de Bedolina (découverte en en Italie du nord) 2000 ans av.n.è. J.-C. Quatre périodes au moins de gravure se superposent, et cette mystérieuse topographie apparaît sur la deuxième couche, voisinant avec des représentations de cervidés. S’il s’agit bien d’un premier vocabulaire topographique, nous ne savons pas le lire.


Ces cartes polynésiennes appelées Rebbelibs, Medosou ou Mattangs représentants les courants marins et la direction de la houle qui est perturbée par la présence des îles avec des batons de bois courbés, les coquillages représentants la position des îles. Elle datent de 1000 av.n.è.

Des représentations du Monde qui s’affinent

Lambert, N., Zanin C. (2019). Mad Maps - L’Atlas qui va changer votre vision du monde (p. 144p). Armand Colin.

Les débuts de la dataviz

Nicole Oresme (1370) est un des premiers à concevoir le principe et l’utilité des coordonnées cartésiennes pour la représentation graphique de phénomènes quantitatifs

William Playfair (1786)
Commercial and Political Atlas.

Florence Nightingale (1857)
Notes on Matters Affecting the Health, Efficiency and Hospital Administration of the British Army.

La rencontre de 2 mondes

Quand la cartographie rencontre la visualisation de données statistiques

Carte figurative de l’instruction populaire en France (Charles Dupin, 1826)

Frère de Montizon (1830)
Emile Cheysson (1886)

Le GOLD STANDARD de la dataviz

Carte Figurative des pertes successives en hommes de l’armée française dans la campagne de Russie 1812–1813.
Charles Joseph Minard (1869). Minard représente en 1869 les pertes colossales de l’armée française dans la campagne de Russie au début du XIXe siècle. Cette fameuse « carte figurative » raconte l’histoire de cette armée, qui arrive à Moscou avec moins d’un quart de son effectif de départ, avant de se faire à nouveau décimer sur le voyage du retour.

Bertin, 1967 (et 1973)

  • Enorme travail de synthèse.
  • Approche plutôt pragmatique.
  • Pour le papier.
  • En noir et blanc.
  • Mais un travail qui reste central pour toutes celles et ceux qui travaillent sur la visualisation de données aujourd’hui.

Révolutions technologiques

Les ordinateurs

Internet

JavaScript 🖥️

Historique

Le JavaScript est un langage de programmation qui a presque 30 ans.

1995

Le langage Javascript a été créé en dix jours en mai 1995 pour la Netscape Communications Corporation par Brendan Eich. Au départ, l’idée était de construire un petit langage pour faire des interactions sur les pages web. Attention, Javascript n’est pas JAVA !

1997

Le langage Javascript est normalisé depuis 1997 par la commission TC39 de l’organisation ECMA International.

2008

Les navigateurs web ont travaillé à de nouveaux moteurs pour améliorer les performances. V8 est un moteur JavaScript open-source développé par le projet Chromium pour les navigateurs Web Google Chrome et Chromium (dernière version 31 janvier 2022). Il y a aussi SpiderMonkey pour Firefox, Chakra pour Microsoft Edge et JavaScriptCore pour Safari.

2009

Création de Node.js par Ryan Dahl, qui permet d’utiliser le JavaScript comme langage de programmation côté serveur (back-End).

2015

Depuis 2015 (ES6 ou ES2015), le langage JavaScript est mature. Performant. Et est implémenté de manière harmonisée dans tous les navigateurs. On parle de modern JavaScript

demain ?

De nouvelles fonctionnalitées sont ajoutées au langage chaque année.

Voir : observablehq.com/@robertbatty/ecmascript-2015-2022

Une grande communauté

C’est un langage ancien qui dispose d’une très grande communauté.

JavaScript est à ce jour un des langages les plus utilisé par les developpeurs informatiques.

Il y a à ce jour plus de 2 millions de packages disponibles sur npm

Un langage souvent décrié 👎

JavaScript a souvent mauvaise réputation chez les développeurs. Et pour cause :

Des absurdités logiques

Null est un objet 🤔

typeof null

Not a Number est un nombre 🙃

typeof NaN

Cf smashingmagazine.com/2011/05/10-oddities-and-secrets-about-javascript

Il est faiblement typé

On peut donc définir une variable et changer son type. Attention danger.

{
let a = "bonjour"
a = 10
return a
}

2 variables de 2 types différents peuvent être considérés comme égaux.

{
let a = "40"
let b = 40
return a == b
}

Et ça peut être source de confusion et de bugs

{
let someVar = 0
return someVar == false
}

Heureusement, il y a le triple égal

{
let someVar = 0
return someVar === false
}

Il n’a pas été conçu pour l’analyse de données

Par exemple, l’opération de tri par défaut trie les valeurs par ordre alphabétique.

{
let a = [7,1,9,3,10,4,5,6,2,8]
return a.sort()
}

…mais incontournable et prometeur 👍

C’est le langage du web :

  • Il n’y a rien à installer “en dur”* sur les ordinateurs. Il faut juste un navigateur web.
  • Il tourne partout et s’execute dans votre navigateur web (compilation just in time)
  • L’implémentation de Javascript dans les navigateurs est de plus en plus rapide 🚀
  • Le langage est aujourd’hui standardisé (ECMA)
  • Grande communauté. Beaucoup de ressources et tutos, conférences sur youtube.
  • Beaucoup de choses déjà développées : Plus de 2 millions de packages sur npm

Certes, JavaScript n’a pas été conçu pour faire de l’analyse de données. Mais :

  • Le langage peut être étendu pour mieux le prendre en charge.
  • De nombreuses librairies permettent déjà de manipuler et représenter les données (tidy.js, Arquero…).
  • Les performances s’améliorent encore avec WebGPU, qui permet de faire des calculs particulièrement efficaces pour l’execution d’algorithmes tres paralleles comme le traitement d’image, le machine learning et le rendu 3D.

  • Avec WebAssembly, il est désormais possible d’incorporer des briques développées dans d’autres langages (C, rust, go…) à l’interieur des bibliothèques JavaScript.

Du coup, certains pensent que le JavaScript est le langage de demain pour traiter et analyser des données.

https://towardsdatascience.com/javascript-for-data-analysis-2e8e7dbf63a7

Observable Javascript 📊

Un langage dédié à la visualisation de données pour le web

Observable, kezako ?

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 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.

C’est aussi une plateforme web disponilble à l’adresse https://observablehq.com/ qui héberge des notebooks computationnels sur la visualisation de données. Les notebooks sont comme des billets de blog. Ils contiennent du texte, des images et du contenu multimédia. Ils peuvent être rangés dans des collections. Ils sont disponibles en ligne via une url. Comme on peut s’y attendre, ils contiennent aussi des lignes de code. L’objectif : faire de la Programmation lettrée (literate programming).

Références

Reactive, reproducible, collaborative: computational notebooks evolve, par Jeffrey M. Perkel. https://www.nature.com/articles/d41586-021-01174-w

Javascript for data Analysis, par Mike Bostock https://towardsdatascience.com/javascript-for-data-analysis-2e8e7dbf63a7

JavaScript ≠ ojs

OJS c’est du JavaScript + plein de bibliothèques pré chargées :

Symbol Name Version
_ Lodash 4.17.21
aq Arquero
Arrow Apache Arrow 4.0.1
d3 D3.js
dot Graphviz 0.2.1
htl Hypertext Literal
Inputs Observable Inputs
L Leaflet
mermaid Mermaid 9.1.6
Plot Observable Plot
SQLite SQL.js 1.7.0
topojson TopoJSON Client 3.1.0
vl Vega, Vega-Lite 5.22.1, 5.2.0

OJS c’est aussi une évolution du langage javascript pour en faire un langage adapté à l’analyse et la visualisation de données sur le web.

Observable dans Observable

Pour utiliser Observable, on peut donc créer un compte avec une adresse email. Ca permet :

  • coder dans son navigateur web
  • d’héberger son code et ses textes
  • partager son travail
  • sauvegarder, versionner, archiver
  • voir ce que font les autres, faire de la veille
  • suivre les compte de son choix (comme sur une réseau social)
  • échanger avec la communauté (commentaires, forum, …)
  • travailler à plusieurs sur un même document
  • se connecter à des bases de données ou des fichiers dans le cloud (google sheet)
  • travailler sans rien avoir à installer sur son ordinateur
  • concevoir des visualisations et les embarquer facilement dans des sites web.

Il faut bien sur avoir une bonne connexion internet

Mais, Observable n’est pas completement gratuit.

Les comptes gratuits permettent de tout faire, mais obligent à travailler de façon publique. Tout ce qu’on écrit est directement visible en ligne. Pour travailler de façon privée, il faut souscrire à un compte pro 😟

Heureusement, les comptes pro sont gratuits pour l’enseignement supérieur et la recherche. Tout universitaire peut donc créer un compte et l’utiliser pour sa recherche ou l’enseignement 🔬

⚠️ Attention, Observablehq est une société privée. Ils peuvent donc changer ces conditions ⚠️

Observable dans Quarto

Si on ne souhaite pas créer de compte sur observablehq, il est possible de faire du Observable sans Observable, en travaillant avec le logiciel Quarto. En effet, le runtime d’Observable est Open Source et peut donc être utilisé dans d’autres contextes et dans d’autres logiciels. Cela permet aussi de travailler en local sur son ordinateur sans connexion internet.

Qu’est ce que Quarto® ?

Quarto® est un système de publication scientifique et technique à code source ouvert basé sur Pandoc.

Quarto permet de créer des documents markdown, des articles, des rapports, des présentations, des sites web, des blogs et des livres, aux formats HTML, PDF, Word, ePub, etc.

Il permet de créer un contenu dynamique dans différents langages : Python, R, Julia et Observable JavaScript.

Cela permet de créer des documents, des rapports et des analyses entièrement reproductibles

Les cellules / chunk {ojs}

Dans Quarto, on peut écrire/executer du code Observable en utilisant des chuncks {ojs}.

Chaque ligne définit une variable et une cellule qui doit être unique.

cellule 1 :

a = 5

cellule 2 :

b = 7

cellule 3 :

a + b

Avec ojs, l’ordre d’écriture n’a pas d’importance 🤔 On peut donc écrire :

Ceci :

c * d

Avant ça

c = 3

et ça

d = 8

La raison est que la relation entgre les cellules s’effectue de manière topologique.

Chaque cellule doit impérativement être unique. En conséquance, je n’ai pas le droit de redéfinir une de ces variables.

e = 10
e = 5

Il est donc souvent utile de créer des blocs de code avec des {…} quand le traitement devient plus complexe.

{
  let val1 = 8;
  let val2 = 7;
  val1 = 5;
  return val1 * val2;
}

Mais la plupart du temps, pour faire ce genre de calcul, on écrira plutôt des fonctions. Comme ceci :

function sum(a, b) {
  return a + b;
}
// Appel de la fonction
sum(10, 30)

Ou comme cela :

multi = (a, b) => a * b
// Appel de la fonction
multi(3, 8)

Ce parti pris fort, peut être déroutant. Mais il a un gros avantage. Il permet d’organiser un document indépendemment de la façon dont on code. Cela permet par exemple de mettre une carte en haut de la page et en annexe technique tout en bas le code qui la génère.

D3.js

L’OJS a été créé par Mike Bostock, qui est aussi le developpeur de la bibliothèque D3.js (ou D3). D3 a été dircetement intégrée à l’Observable JavaScript.

D3.js est une bibliothèque graphique JavaScript développée par Mike Bostock qui permet l’affichage de données numériques sous une forme graphique et dynamique.

  • D3 est populaire (200 millions de téléchargements et 100 000 ⭐ sur Github)
  • D3 est flexible (approche bas niveau qui permet de tout faire)
  • D3 est réputé pour ses animations et ses interactions.

Cette puissance a bien sûr un coût. Il y a beaucoup à apprendre : D3 compte plus de trente modules et un millier de méthodes.

Mais c’est le langage de base pour faire de la visualisation de données pour le web.

myarray = [7,1,9,3,10,4,5,6,2,8]

d3.array permet de manipuler les données. On peut par exemple remplacer notre exemple de tout à l’heure

myarray.sort()

par :

d3.sort(myarray)

Bien d’autres opérations sont possibles

d3.min(myarray)
d3.max(myarray)
d3.mean(myarray)
d3.median(myarray)
d3.variance(myarray)
d3.deviation(myarray)
d3.cumsum(myarray)

Mais d3, c’est surtout un framework de développement pour dessiner des visualisations à partir de données. C’est aujourd’hui un standard dans le domaine que tout le monde utilise.

D3 est aussi à la base de nombreuses bibliothèques JavaScript, comme les librairie Plot et bertin que nous allons voir.

Dessiner pour le web

La structure d’une page html

Une page html s’organise sous la forme de contenus placés à l’intérieur de balises.

<html>
  <head>
    <!-- En tête de la page -->
    <title>Titre de la carte</title>
  </head>
  <body>
    <!-- Contenu de la page -->
    <h1>Hello World!</h1>
  </body>
</html>

A l’intérieur de la balise body, on peut dessiner directement des images raster et vecteur.

Raster vs Vecteur

Il existe deux principaux moyens pour dessiner des éléments dans une page web : SVG & CANVAS.

Un raster, c’est une image composée de pixels. Un grille organisée en lignes et colonnes. Chaque cellule est un pixel unique. Une image raster a une résolution.

Une image vecteur n’a pas de résolution. Elle est composée de composée de nœuds (des points dans l’espace) et des formules mathématiques pour calculer les arcs (des lignes) qui relient ces nœuds entre eux et qui forment ainsi une géométrie. On distingue trois grands types de géométries : point, ligne et polygone.

{_}

Canvas

L’élément canvas est un composant du langage Web HTML qui permet d’effectuer des rendus dynamiques d’images bitmap (raster).

Ce code

<canvas id="myCanvas2" width="100" height ="100" style="border:1px
solid #000000; background-color: steelblue;"></canvas>

donne ceci :

SVG

Le format SVG (Scalable Vector Graphics) est un format de données conçu pour décrire des ensembles de graphiques vectoriels. C’est le format qu’on utilise dans le logiciel Inkscape.

Ce code

<svg viewBox="0 0 1000 100" xmlns="http://www.w3.org/2000/svg">
  <rect x="0" y="0" width="100px" height="100px" fill="#F2CD3B" stroke="#06000C" />
</svg>

donne ceci :

Dessiner avec D3.js (le DOM)

Avec D3, il est possible de dessiner en JavaScript des images vectorielles au format CANVAS et SVG en manipulant le DOM.

Le Document Object Model (DOM) est une interface de programmation normalisée par le W3C, qui permet à des scripts d’examiner et de modifier le contenu du navigateur web. Par le DOM, la composition d’une page web est représentée sous forme d’un arbre avec des objets dedans.

Le but c’est d’écrire par la programmation des instructions qui vont produire le dessin plutôt que d’écrire directement en dur le dessin.

Dessinons du SVG avec D3.js

Créons un document svg, de 500 pixels de large, 60 pixels de haut et avec du gris en couleur de fond.

{
  const svg = d3
    .create("svg")
    .attr("viewBox", [0, 0, 500, 60])
    .style("background-color", "#CCC");
    
  return svg.node();
}

On ajoute un cercle rouge avec svg.append("circle")

{
  const svg = d3
    .create("svg")
    .attr("viewBox", [0, 0, 500, 60])
    .style("background-color", "#CCC");
    
  svg
    .append("circle")
    .attr("cx", 50)
    .attr("cy", 30)
    .attr("r", 25)
    .style("fill", "#e04a28");
    
  return svg.node();
}

Attention, les coordonnées [0,0] sont en haut à gauche.


On dessine un carré rouge avec svg.append("rect")

{
  const svg = d3
    .create("svg")
    .attr("viewBox", [0, 0, 500, 60])
    .style("background-color", "#CCC");
    
  svg
    .append("rect")
    .attr("x", 100)
    .attr("y", 5)
    .attr("height", 50)
    .attr("width", 50)
    .style("fill", "#5277bf");
    
  return svg.node();
}

On dessine une ligne avec svg.append("line")

{
  const svg = d3
    .create("svg")
    .attr("viewBox", [0, 0, 500, 60])
    .style("background-color", "#CCC");
    
  svg
    .append("line")
    .attr("x1", 10)
    .attr("y1", 50)
    .attr("x2", 490)
    .attr("y2", 10)
    .style("stroke", "#5277bf");
    
  return svg.node();
}

On écrit du texte avec svg.append("text")

{
  const svg = d3
    .create("svg")
    .attr("viewBox", [0, 0, 500, 60])
    .style("background-color", "#CCC");
    
    svg
    .append("text")
    .attr("x", 150)
    .attr("y", 30)
    .attr("text-anchor", "middle")
    .attr("alignment-baseline", "middle")
    .attr("font-size", 20)
    .text("This is a text")
    

  return svg.node();
}

On peut même faire des choses plus compliquées

{
  const svg = d3
    .create("svg")
    .attr("viewBox", [0, 0, 900, 300])
    .style("width", 900)
    .style("height", 300)
    .style("background-color", "white");

  svg
    .append("path")
    .attr("id", "MyPath")
    .attr(
      "d",
      "M 63.076304,216.52334 C 106.76999,160.0853 184.40984,142.61427 252.12247,155.01632 c 99.64554,14.70621 189.6073,65.26698 288.91023,81.72698 85.56711,17.5306 180.8469,21.86234 258.93554,-23.57809 C 832.7795,182.25763 827.06329,125.86632 794.25369,97.048457 749.49459,54.888253 680.9657,40.400439 622.07801,56.259559 578.03603,66.8154 539.24683,112.13225 552.52931,158.88503 c 11.25647,40.10204 58.40308,53.33813 95.48846,51.23085 25.59819,-1.89352 59.88192,-16.69692 56.86407,-47.47388 -3.50716,-31.7693 -26.27339,-57.34863 -59.6113,-41.94273"
    )
    .attr("fill", "none")
    .attr("stroke", "#5277bf");

  svg
    .append("text")
    .append("textPath")
    .attr("href", "#MyPath")
    .attr("dominant-baseline", "baseline")
    .attr("font-size", `48px`)
    .text(
      "Et maintenant, voici un texte qui suit une ligne. C'est trop cool, non ?"
    );

  return svg.node();
}

Dessiner des données avec D3.js

On crée des données fictives

myData = [10, 30, 2, 20, 10]

On veut créer des cercles. crée une fonction pour calculer le rayon.

valmax = d3.max(myData)
radius = d3.scaleSqrt([0, valmax], [0, 35]);

Puis, on dessine un SVG.

{
  const width = 500;
  const height = 75;
  
  const svg = d3
    .create("svg")
    .attr("viewBox", [0, 0, width, height])

  svg
    .append("g")
    .attr("fill", "#e04a28")
    .selectAll("circle")
    .data(myData)
    .join("circle")
    .attr("cx", (d, i) => 50 + i * 100)
    .attr("cy", height / 2)
    .attr("r", (d) => radius(d));

  return svg.node();
}

Il est aussi possible de beaucoup d’autres choses graces aux méthodes proposées par d3.

La fonction d3.pie() transforme les données en un objet contenant des informations pour dessiner des quartiers.

pie = d3.pie().padAngle(0.1)
pie(myData)

La fonction d3.arc() permet de dessiner ces quartiers.

arc = d3.arc().innerRadius(45).outerRadius(220)

Ainsi, on a :

{
  const height = 500;
  const svg = d3
    .create("svg")
    .attr("viewBox", [-width / 2, -height / 2, width, height]);

  svg
    .append("g")
    .attr("fill", "#e04a28")
    .attr("stroke", "#000")
    .attr("stroke-width", "1.5px")
    .attr("stroke-linejoin", "round")
    .selectAll("path")
    .data(pie(myData))
    .join("path")
    .attr("d", arc.cornerRadius(3));

  return svg.node();
}

Animer avec D3.js

{
  const svg = d3
    .create("svg")
    .attr("viewBox", [0, 0, 500, 60])
    .style("background-color", "#CCC");

  let moncercle = svg
    .append("circle")
    .attr("cx", 50)
    .attr("cy", 30)
    .attr("r", 25)
    .attr("fill", "#e04a28");

  if (anim) {
    moncercle
      .transition()
      .duration(2000)
      .attr("cx", 450)
      .transition()
      .delay(2000)
      .duration(3000)
      .attr("cx", 50)
      .attr("r", 10)
      .attr("fill", "blue")
      .transition()
      .attr("r", 25)
      .attr("fill", "#e04a28");
  }

  return svg.node();
}

Les Inputs

Dans Observable, on a à disposition des Inputs directement prêts à l’emploi.

👉 button

viewof clicks = Inputs.button("Click")
clicks

👉 toogle

viewof mute = Inputs.toggle({label: "Mute", value: true})
mute

👉 range

viewof gain = Inputs.range([0, 11], {value: 5, step: 0.1, label: "Gain"})
gain

👉 checkbox

viewof colors = Inputs.checkbox(["red", "green", "blue"], {label: "color"})
colors

👉 radio

viewof color = Inputs.radio(["red", "green", "blue"], {label: "color"})
color

👉 select

villes = ["Cotonou", "Porto-Novo", "Ouidah"]
viewof maville = Inputs.select(villes, {value: "steelblue", label: "Favorite color"})
maville

👉 text

viewof text = Inputs.text()
text

👉 textarea

viewof textarea = Inputs.textarea()
textarea

👉 date

viewof date = Inputs.date()
date

👉 color

viewof pickcolor = Inputs.color({label: "couleur préférée", value: "#4682b4"})
pickcolor

👉 file

viewof file = Inputs.file()
file

👉 formulaire

range1
range2
range3

Tout est réactif 🔥

Dans Observable, grace à la relation topologique entre les cellules, tout est réactif. Chaque fois qu’on bouge quelque chose, ce qui en dépend est rééxécuté.

viewof age = Inputs.range([15, 70], {label: "age", value: 30, step: 1,})
viewof nom = Inputs.text({label: "nom", value: "Nicolas"})

Par exemple :

md`Je m'appelle **${nom}** et j'ai **${age}** ans :-)`

Reprenons notre SVG de tout à l’heure

{
  const svg = d3
    .create("svg")
    .attr("viewBox", [0, 0, 500, 60])
    .style("background-color", "#CCC");
    
  svg
    .append("circle")
    .attr("cx", 50)
    .attr("cy", 30)
    .attr("r", 25)
    .style("fill", "#e04a28");
    
  return svg.node();
}

On peut facilement proposer à l’utilisateur de moidifier ce dessin en remplaçant des valeurs par des variables pilotées par des inputs.

{
  const svg = d3
    .create("svg")
    .attr("viewBox", [0, 0, 500, 60])
    .style("background-color", "#CCC");
    
  svg
    .append("circle")
    .attr("cx", cx)
    .attr("cy", 30)
    .attr("r", r)
    .style("fill", col);
    
  return svg.node();
}

Par exemple :

Les données

L’ajout de données s’effectue avec l’instruction FileAttachment()

👉 fichier csv

data1 = FileAttachment("data/afrika_data.csv").csv()

Les données sont importées et converties automatiquement au format json

Pour les visualiser, on utilise Inputs.table()

Inputs.table(data1)

👉 fichier xlsx

On peut également importer des fichiers excel

classeur = FileAttachment("data/afrika.xlsx").xlsx()

On obtient la liste des feuilles comme cela :

classeur.sheetNames

Puis, on peut choisir la feuille à ouvrir

data2 = classeur.sheet("data", {
  headers: true
})

👉 Mise en forme des données

Pour mettre en forme les données, on peut le faire en pure JavaScript.

subdata1 = data1
  .filter((d) => +d.ESPVIE > 65)
  .map((d) => ({ code: d.iso3, nom: d.nom, POP: d.POP, PIB: d.PIB, ESPVIE: d.ESPVIE }))
  .sort((a, b) => d3.descending(b.ESPVIE, a.ESPVIE))

Ca donne ceci :

On peut aussi utiliser arquero. Voir : https://observablehq.com/@observablehq/data-wrangler.

subdata2 = aq
  .from(data1)
  .filter((d) => d["ESPVIE"] > 65)
  .rename({ iso3: "code" })
  .select("code", "nom", "POP", "PIB", "ESPVIE")
  .orderby("ESPVIE")
  .objects()

Le résultat est le même.

👉 fichier geoJSON

Pour utiliser des géométries, on utilisera prioritairement le format geoJSON.

basemap = FileAttachment("data/africa.json").json()

Voilà la structure d’un geoJSON

Et voici à quoi il ressemble si on l’affiche (nous verrons plus tard comment…)

Faire une carte avec D3.js

La fonction d3.geoPath() du module d3.geo permet de convertir un fond de carte en dessin svg. Cette fonction prend comme paramètre des fonctions de projections telles que définies dans les modules d3.geo & d3.geoprojection.

On importe des données

monde = FileAttachment("data/world.json").json()

On définit une fonction de projection

myproj = d3.geoNaturalEarth1()

On crée une fonction path qui permet de convertir des coordonnées lat/lon en un chemin SVG dans une projection donnée.

path = d3.geoPath(myproj)

Puis, on dessine la carte

On crée un document SVG

const svg = d3
  .create("svg")
  .attr("viewBox", [0, 0, 1000, 500])
  .style("width", "100%")
  .style("height", "auto");

On ajoute une première couche (calque g) au SVG : Un polygone qui correspond à l’espace terrestre dans la projection donnée.

svg
  .append("g")
  .append("path")
  .datum({ type: "Sphere" })
  .attr("fill", "#a9daeb")
  .attr("d", path);

Avec la fonction d3.geoGraticule10(), on ajoute des lignes de latitude et loingitude. On les affiche en pointillé.

svg
  .append("g")
  .append("path")
  .datum(d3.geoGraticule10())
  .attr("d", path) 
  .style("fill", "none")
  .style("stroke", "white")
  .style("stroke-width", 0.8)
  .style("stroke-opacity", 0.5)
  .style("stroke-dasharray", 2);

Et enfin, on ajoute les pays de monde.

svg
  .append("g")
  .append("path")
  .datum(monde)
  .attr("fill", "#508bab")
  .attr("fill-opacity", 0.9)
  .attr("stroke", "white")
  .attr("stroke-width", 0.2)
  .attr("d", path); 

Et tout à la fin, on renvoie le noeud SVG.

return svg.node();

Et voilà le résultat :

Interopérabilité R et OJS

Dans Quarto, il est possible de combiner du code en R et du code en OJS.

Pour cela, on utilise la fonction ojs_define()

Une simple variable

  • chunks {r}

On définit la variable myvar

myvar = 12

Puis, avec l’instruction ojs_define, on rend cette variable accessible en javascript.

ojs_define(myvar)
  • chunks {ojs}

Et maintenant, myvar existe dans ojs.

myvar
[1] 12

Un dataframe

  • chunks {r}

Dans R, On charge un dataframe avec `read.csv()``

data <- read.csv("data/afrika_data.csv")

Comme précédemment, nous utilisons ojs_define pour rendre cette variable accessible dans Observable. Nous renommons cette variable en newdata.

ojs_define(newdata = data)
  • chunks {ojs}

Côté ojs, les données sont disponibles. Elles ont été converties au format json. Mais elles ne sont pas tout à fait au format attendu.

newdata

La fonction transpose permet de les convertir au bon format.

Inputs.table(transpose(newdata))

Un Spatial dataframe

  • chunks {r}

Ici, on ouvre un fichier au format gpkg.

library("sf")
countries <- st_read("data/countries.gpkg", quiet = T)

Puis, à l’aide de la librarie geojsonsf, on le convertit au format geojson avant d’utiliser ojs_define

library("geojsonsf")
ojs_define(countries = sf_geojson(countries))
  • chunks {ojs}

Notez qu’avec ojs_define, nous avons passé la variable geo comme une chaîne et non comme un objet.

countries.substr(1, 300)

Nous utilisons l’instruction javascript JSON.parse pour refabriquer un geojson valide.

countries2 = JSON.parse(countries) 
countries2

On a bien un beau geoJSON qu’on peut afficher

Librairies externes

Observable javascript n’est pas un écosystème fermé. Des millions de librairies javascript existent sur NPM (équivalent du CRAN pour R). Il est possible de les utiliser.


Ici, nous pouvons les importer directement avec l’instruction require()

La bibliothèque geocountries

Avec @, on spécifie la version de la bibliothèque.

geocountries = require("geocountries@latest")

Vous pouvez aussi utiliser une bibliothèque localement en spécifiant le chemin sur votre ordinateur.

geocountries = require("./lib/geocountries.js")

On peut maintenant utiliser la fonction getcode du package geocountries

geocountries.getcode("Bénin")

La bibliothèque statsbreaks

Une bibliothèque pour faire des discrétisations

discr = require("statsbreaks@latest")

ou

discr = require("./lib/statsbreaks.js")
gdppc = data1.map(d => d.PIB/d.POP)
discr.breaks({ values: gdppc, method: "quantile", nb: 5, precision: 2 })

Imports

On l’a dit tout à l’heure, Observable c’est aussi une plateforme web hebergeant des notebooks.


Si on a une connexion internet, il ets possible d’importer n’importe quelle cellule de n’importe quel notebook avec la fonction import. Dit autrement, toutes les notebooks hébergés sur observablehq.com fonctionnent comme des api.

import {SankeyChart} from "@d3/sankey"
import {energy} from "@d3/sankey"

Puis, on peut utiliser cette fonction.

chart = SankeyChart({
  links: energy
}, {
  nodeGroup: d => d.id.split(/\W/)[0], // take first word for color
  width,
  height: 600
})

La bibliothèque bertin 🗺️

Une bibliothèque JavaScript pour la cartographie thématique

Principes

bertin est une bibliothèque (un ensemble de fonctionnalités) écrite en JavaScript qui permet de réaliser des cartes thématiques pour le web. Son développement 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. 240 ⭐ sur Github.

On charge la bibliothèque avec l’instruction require en indiquant le numéro de version ou en prenant la dernière version.

bertin = require("bertin@1.6.5")

Si on souhaite travailler sans connexion internet, il est possible de télécharger la bibliotheque dans un dossier, puis d’indiquer le lien vers ce dossier.

Lien de téléchargement : https://cdn.jsdelivr.net/npm/bertin@latest

Puis

bertin = require("./lib/bertin.js")

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.

Elle permet de réaliser de nombreux types de cartes thématiques.

Préparation des données

Voyons ici le début du processus de création cartographique.

Import

Import des données sur les pays du monde

Les géométries

world = FileAttachment("data/world.json").json()

Les données attributaires

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

On regarde les données

Inputs.table(stats)

On ne garde que l’années 2019

stats2019 = stats.filter(d => d.date == 2019)

Et on affiche le résultat

Inputs.table(stats2019)

Jointure

On réalise une jointure grâce aux fonctionnalités match et merge disponibles dans bertin.

bertin.match(world, "id", stats2019, "iso3c")

Le niveau de compatibilité est bon. On réalise la jointure et on crée le nouveau jeu de données world2019

world2019 = bertin.merge(world, "id", stats2019, "iso3c")

quickdraw

La fonction quickdraw permet de visualiser la carte.

bertin.quickdraw(world2019)

properties.table

La fonction properties.table permet de récupérer la table attributaire.

Inputs.table(bertin.properties.table(world2019))

Syntaxe

Avec bertin, 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.

Carte simple

La fonction draw permet de dessiner n’importe quel type de carte.

La syntaxe minimale est la suivante :

NB : Le type “simple” est facultatif car c’est le type par défaut.

bertin.draw({ layers: [ {geojson: world2019} ] })

Définir les styles

Le rendu peut être très largement paramétré. Les styles (couleurs, transparence, épaisseur, etc) reprennent les noms des attributs SVG.

bertin.draw({ 
  layers: [
    { 
      geojson: world2019,
      fill: "#F6836F",
      fillOpacity: 0.8, 
      stroke:"#F25842",
      strokeWidth: 2,
      strokeDasharray: [3,5] 
    }
  ]
})

Généralisation

En cartographie thématique, il est souvent utile de généraliser (simplifier) un fond de carte. Cela permet d’améliorer la lecture et de réduire le poids le fond de carte à afficher, ce qui est très important en cartographie sur le web. Une façon de faire cela est d’utiliser la bibliothèque geotoolbox

On charge la bibliothèque avec l’instruction require

geo = require("geotoolbox@latest")

(Pour travailler sans connexion, on peut aussi la télécharger ici : https://cdn.jsdelivr.net/npm/geotoolbox@latest)

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

Puis, on utilise la fonction simplify()

geo.simplify(world2019, {k: 0.1})

Si on met la valeur de k dans un slider, on a alors une généralisation inteactive.

Notez que la bibliothèque geotoolbox vous permet de faire la plupart des opérations SIG utiles en cartographie thématique (extractions des contours, agrégations, généralisation, centroides, etc.)

{_}

Exemple de traitement avec geotoolbox

On charge le fond de carte de l’Afrique

afr = FileAttachment("data/africa.json").json()

On selectionne le Bénin et on construit un buffer autour du pays. La distance est gérée par un slider. On intersecte les pays avec le buffer. Le tout en temps réel.

On extrait le Bénin

ben = geo.filter(afr, d => d.id == "BEN")

On calcule un buffer autour du Bénin

buff = geo.buffer(ben, { dist: distance })

On intersecte les pays d’Afrique avec ce Buffer

clip = geo.clip(afr, { clip: buff })

Puis on affiche les différentes couches

Ce n’est pas juste de l’affichage. On a effcté une vraie opération SIG. La couche intersectée est bien créée comme nouvel objet geoJSON.

Projection

Faire une carte, c’est aussi choisir comment on passe de la sphère au plan : d’un monde en 3 dimensions à une carte en 2 dimensions.


Dans bertin, les projections se définissent le l’objet params.

bertin.draw({ 
  params: {projection: "nom de la projection"},
  layers: []
})

De nombreuses projection sont disponibles dans l’écosystème de D3.js grace aux modules d3.geo et d3.geoprojection

Mais la librairie bertin permet également d’utiliser des projections au format proj4 ou epsg.

robinson = "+proj=robin +lon_0=0 +x_0 +y_0=0 +ellps=WGS84 +datum=WGS84 +units=m +no_defs"
bertin.draw({ 
  params: {projection: robinson},
  layers: []
})

Marges et couleur de fond

On peut aussi définir les parametres généraux de la carte, comme la taille de la carte, les marges et la couleur de fond.

{params : {width: 500, margin : [20, 0, 0, 0], background: "grey"} }


Couches d’habillage

Il existe de nombreuses couches d’habillage disponibles dans bertin. Elles ont tous des paramètres par défaut.

Outline

Le type outline permet d’afficher l’espace terrestre.

{type: "outline" }


Graticule

Le type graticule permet d’afficher les lignes de latitude et de longitude.

{type: "graticule", step: [10,20]}


Geolines

Le type geolines permet d’afficher l’équateur, les tropiques et les cercles polaires

{type: "geolines"}


Waterlines

Le type waterlines permet d’afficher des lignes autour des terres, comme sur les cartes anciennes

{type: "waterlines", geojson: world, nb: 5}


Rhumbs

le type rhumbs crée des pseudo lignes de rhumbs pour reproduire des styles de cartes anciennes (portulans)

{type: "rhumbs"}


Le type header permet d’ajouter un titre

{type: "header", text : "Titre de la carte" }


Le type footer permet d’ajouter la source

{type: "footer", text : "Banque mondiale, 2019", anchor: "middle" }


Text

le type text parmet d’ajouter du texte n’importe où sur la carte

{type: "text", position : [100,200], text: "Mon texte ici" }


Label

Le type label permet d’afficher du text lié au fond de carte.

{type : "label", geojson : africa, values: "id"}


Shadow

Le type shadow permet d’ajouter une ombre sous une couche

{type: "shadow" }


Inner

Le type inner permet d’ajouter un effet de dégradé sur les contours. C’est très utilisé en cartographie d’édition.

{type: "inner" }


Hatch

Le type hatch permet d’ajouter des hachures sur la carte. Ca rajoute une texture à la carte. C’est uniquement esthétique.

{type: "Hatch", angle: 45, spacing: 4 }


Tissot

Le type tissot permet d’afficher l’indicatrice de Tissot.

{type: "tissot" }


scalebar

Le type scalebar permet d’afficher l’échelle.

{type: "scalebar" }


minimap

Le type minimap permet d’afficher une carte de localisation dans n’importe quelle projection.

{type: "minimap" }


tiles

Le type tiles permet d’afficher des tuiles raster, mais uniquement dans la projection Web Mercator.

{type: "tiles", style: "worldimagery" }


Le type logo vous perlet d’afficher une image sur la carte.

{type : "logo", url: "http://myimage.png", position: "left"}


Symboles proportionnels

Pour réprésenter des données quantgitatives 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.

{type "bubble", geojson : world, values: "pop"}

💡 En paramétrant l’attribut tooltip, on peut survoler les cercle spour avoir une infobulle 💡

tooltip: [
  "$country",
  d => Math.round(d.properties.POP/1000000) + " millions de personnes"
]


On fait varier la taille des cercles avec le paramètre k, qui corrrespond au rayon du plus gros cercle.

{type "bubble", geojson : world, values: "pop", k: 100}


Avec l’argument dorling: true, on obtient un cartogramme de Dorling.

{type "bubble", geojson : world, values: "pop", dorling: true}


Carrés

Des variantes sont possibles. On peut remplacer les cercles par des carrés en utilisant le type squares

{type "square", geojson : world, values: "pop"}

Avec le paramètre demers = true, on obtient un cartogramme de Demers.


Spikes

On peut aussi faire varier la hauteur avec le type spikes

{type "spikes", geojson : world, values: "pop"}


Typologies

Pour cartographier des donnes qualitatives nominales, on peut utiliser la variable visuelle COULEUR (teinte). Dans bertin, cela revient à faire varier l’attribut fill avec le type typo (pour typologie).

{geojson: world, fill: { type: "typo", values: "regions", colors: "Tableau10" } }


Cartes choroplèthes

Pour réaliser une carte choroplèthe, on utilise le type choro sur le paramètre fill.

{geojson: world, fill: { type: "choro", values: "gdppc", colors: "BuPu" } }

On peut choisir une palette de couleurs.

On peut aussi faire varier le nombre de classes (nbreaks) et la méthode de discrétisation (method)


Bien sûr, avant de réaliser une carte choroplèthe, il est toujours nécéssaire de caractériser la distribution statistique.

Poutr cela, on peut utiliser la bibliothèque Plot. Il y a plein de façons de faire.

Les concepts de Plot

Il n’y a pas de type de graphique prédéfini, mais des marques, et des transformations qu’il est possible de combiner pour déssiner n’importe quel type de graphique.

Les marques

Les transformations

Récupérer la table attributaire

mytable = bertin.properties.table(world)

On la visualise

Inputs.table(mytable)

TickX

Plot.plot({
  marginTop: 10,
  marks: [
    Plot.tickX(mytable, { x: "gdppc" }),
  ]
})


Boxplot

Plot.plot({
  marks: [Plot.boxX(mytable, { x: "gdppc", fill: "red" })]
})


rectY

Plot.plot({
  marks: [
    Plot.rectY(
      mytable,
      Plot.binX({ y: "count" }, { x: "gdppc", thresholds: 5 })
    )
  ]
})


Dodge

Plot.plot({
  height: 340,
  marks: [
    Plot.dotX(
      mytable,
      Plot.dodgeY({ x: "gdppc"})
    )
  ]
})


Combinaisons

La bibliothèque bertin est assez flexible. On peut donc faire des combinaisons.

Supperposition de couches

La méthode la plus simple pour combiner des informations sur une carte est de supperposer des couches.

layers : [
  {type "bubble", geojson: world, values: "pop", fill: "none" }, // cercles
  {geojson: world, fill: { type: "typo", values: "regions" } } // typo
]


Symboles colorés (fill)

Mais on peut aussi faire varier la couleur de n’importe quel objet. Ici, sur le type bubble.

layers : [
  {
    type "bubble", 
    geojson: world,
    values: "pop",
    fill: { type: "typo", values: "regions" }
  }
]


Symboles colorés (stroke)

Et ca fonctionne aussi pour les contours.

layers : [
  {
    type "bubble", 
    geojson: world,
    values: "pop",
    stroke: { type: "typo", values: "regions" },
    fill: "none"
  }
]


Combiner 2 données de stock

Pour combiner 2 variables quantitatives absolues, on peut utiliser une représentation par double cercle. Ici, on va comparer population et PIB.

Rendre les données comparables

Pour rendre les données comparables, on les exprime en % du total. Ca reste des données quantitatives absolues, mais exprimées en pourcentages.

On crée donc 2 nouvelles variables : pop_pct et gdp_pct.

world02bis = {
let data = bertin.properties.table(world02)
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(world02, "id", data2, "id")
}

Cartographie

Puis, on réalise la carte avec le type mushroom.

{
  type: "mushroom",
  geojson: world02bis,
  top_values: "gdp_pct",
  bottom_values: "pop_pct"
}


Carte par points

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’e placer autant de d’en placer le npmbre nécéssaire pour atteindre une quantité.

Méthode 1

De façon classique, on positionne les points de façon aléatoire dans les mailles adminstratives.

{type: "dotdensity", geojson: world, values: "pop", dotvalue : 5000000}


Avec ce type de représentation, plus le maillage de départ est fin, plus la carte est efficace.

Méthode 2

Une autre méthode conssite à positionner les points au centre des pays et de les écarter à la façon d’une cartogramme de Dorling.

{type: "dotcartogram", geojson: world, values: "pop", radius: r}


Les grilles

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.

Carroyage

Astuce : On peut prendre 2 variables de stock pour calculer directement le ratio. Ici, PIB par habitant.

{type : "regulargrid", geojson: world, values: ["gdp","pop"] }


Points Bertin

Sur le même principe, on peut répartir des masses régulièrement dans l’espace.

{type : "regularbubble", geojson: world, values: "pop", step: 20 }

Le résultat ressemble à des “points Bertin” qu’on peut paramétrer.

Cela fonctionne égallement avec des carrés

{type : "regularsquare", geojson: world, values: "pop", step: 20 }


Ridgelines

Cette méthode de passage sur grille permet d’autres représentations plus expérimentales.

{type : "ridge", geojson: world, values: "pop", step: 20 }


Cartes de chaleur

La bibliothèque bertin permet également de faire de cartes lissées à partir d’un semis de points ou de masses réaprties dans l’espace.

A partir des centroides

{type: "smooth", values: "pop"}


Via une grille

Récap’

{width:100%}

Documentation

{width:100%}

Bibliographie

  • Allaire J., Teague C., Scheidegger C., Xie Y., Dervieux C., (2022). Quarto (Version 1.2) [Computer software]. https://doi.org/10.5281/zenodo.5960048
  • Bostock M. (2021). Script for Data Analysis, https://towardsdatascience.com/javascript-for-data-analysis-2e8e7dbf63a7
  • Bostock M., Ogievetsky V., Heer J. (2011). D³ Data-Driven Documents, IEEE, Volume: 17, https://ieeexplore.ieee.org/abstract/document/6064996
  • Grandjean M. (2022). La visualisation de données, entre usages démonstratifs et heuristiques.
  • Lambert N. (2022). La bibliothèque bertin. https://observablehq.com/collection/@neocartocnrs/bertin
  • Perkel J.M. (2021). Reactive, reproducible, collaborative: computational notebooks evolve, https://www.nature.com/articles/d41586-021-01174-w
  • Tucker T. (2022). Classic Research in Data Visualization. https://observablehq.com/@tophtucker/classic-research-in-data-visualization

Exercices

Mise en pratique de bertin dans Quarto

TP1


Apprendre à réaliser des cartes interactives
avec Quarto et la bibliothèque JavaScript bertin

TP2

Réaliser une single page application (dashboard) avec bertin et Quarto.







Contacts


Nicolas Lambert
(France)


nicolas.lambert@cnrs.fr
https://twitter.com/neocartocnrs
https://vis.social/@neocarto
https://github.com/neocarto
https://observablehq.com/@neocartocnrs

Manhamady Puedraogo
(Burkina Faso)


nonresse@gmail.com