html`<div class="explorer">
<div class="explorer-controls">
${viewof affichage_france}
${viewof niveau_france}
${viewof annee_france}
${viewof drom_rapproches}
${viewof simplification_france}
${telechargements(requete_france)}
</div>
<div class="explorer-viz">${visualisation(donnees_france, affichage_france)}</div>
<div class="explorer-code">${panneauCode(programmes(requete_france, niveau_france.variable), notes)}</div>
</div>`API = "https://cartiflette-api.lab.sspcloud.fr"
// Premier millésime publié en GeoParquet
PARQUET_FIRST_YEAR = 2023
annees = ["2026", "2025", "2024", "2023", "2022"]
// 2022 n'est publié qu'en GeoJSON, sans version à 80 % : on y prend 50 %
function simplificationDisponible(annee, simplification) {
return Number(annee) < PARQUET_FIRST_YEAR && simplification == 80 ? 50 : simplification
}// Niveaux proposés pour la France entière : libellé, nom de variable dans les
// extraits de code
niveaux = new Map([
["Départements", {code: "DEPARTEMENT", variable: "departements"}],
["Régions", {code: "REGION", variable: "regions"}],
["Bassins de vie", {code: "BASSIN_VIE", variable: "bassins_vie"}],
["Zones d'emploi", {code: "ZONE_EMPLOI", variable: "zones_emploi"}],
["Unités urbaines", {code: "UNITE_URBAINE", variable: "unites_urbaines"}],
["Aires d'attraction des villes", {code: "AIRE_ATTRACTION_VILLES", variable: "aires_attraction"}]
])// Requête cartiflette : { borders, filter_by, values, year, simplification,
// where (facultatif) }. route : "geojson" ou "geoparquet" ; extra : paramètres
// en plus (download)
function apiUrl(q, route = "geojson", extra = {}) {
const params = new URLSearchParams({
year: q.year,
borders: q.borders,
filter_by: q.filter_by,
simplification: q.simplification,
...extra
})
for (const v of q.values) params.append("values", v)
if (q.where) params.append("where", q.where)
return `${API}/v1/${route}?${params}`
}SOURCES = ({
ign: {court: "IGN", label: "IGN, ADMIN EXPRESS COG CARTO", url: "https://geoservices.ign.fr/adminexpress"},
tagc: {court: "Insee", label: "Insee, appartenance géographique des communes", url: "https://www.insee.fr/fr/information/7671844"},
cartiflette: {court: "cartiflette", label: "construite par cartiflette", url: "https://github.com/InseeFrLab/cartiflette"},
filosofi: {court: "Insee", label: "Insee, Filosofi 2021, données carroyées", url: "https://www.insee.fr/fr/statistiques/8735243"}
})
COLONNES = ({
ID: ["ign", "Identifiant IGN"],
NOM: ["ign", "Nom officiel"],
STATUT: ["ign", "Statut de la commune"],
POPULATION: ["ign", "Population municipale (somme pour les niveaux agrégés)"],
INSEE_COM: ["ign", "Code commune"],
CODE_IRIS: ["ign", "Code IRIS"],
NOM_IRIS: ["ign", "Nom de l'IRIS"],
TYP_IRIS: ["ign", "Type d'IRIS : habitat (H), activité (A), divers (D), commune non découpée (Z)"],
NOM_COM: ["ign", "Nom de la commune"],
idcar_200m: ["filosofi", "Identifiant du carreau de 200 m (projection, taille, coin bas-gauche)"],
idcar_1km: ["filosofi", "Identifiant du carreau de 1 km"],
idcar_nat: ["filosofi", "Identifiant du carreau de niveau naturel"],
lcog_geo: ["filosofi", "Communes que recouvre le carreau (géographie des données)"],
i_est_200: ["filosofi", "1 si les valeurs du carreau de 200 m sont imputées (moins de 11 ménages)"],
i_est_1km: ["filosofi", "1 si les valeurs du carreau de 1 km sont imputées"],
ind: ["filosofi", "Nombre d'individus"],
men: ["filosofi", "Nombre de ménages"],
men_pauv: ["filosofi", "Nombre de ménages pauvres"],
ind_snv: ["filosofi", "Somme des niveaux de vie winsorisés des individus"],
log_soc: ["filosofi", "Nombre de logements sociaux"],
INSEE_COG: ["cartiflette", "Code commune, ou d'arrondissement municipal à Paris, Lyon et Marseille"],
AREA: ["cartiflette", "Territoire : métropole ou DROM"],
PAYS: ["cartiflette", "Pays"],
SOURCE: ["cartiflette", "Source des contours"],
CTCD: ["tagc", "Collectivité territoriale ayant les compétences départementales"],
EPCI: ["tagc", "Intercommunalité (EPCI)"],
EPT: ["tagc", "Établissement public territorial (Grand Paris)"],
ARR: ["tagc", "Arrondissement départemental"],
CANOV: ["tagc", "Canton-ville"],
ZE2020: ["tagc", "Zone d'emploi 2020"],
UU2020: ["tagc", "Unité urbaine 2020"],
AAV2020: ["tagc", "Aire d'attraction des villes 2020"],
BV2022: ["tagc", "Bassin de vie 2022"],
INSEE_DEP: ["tagc", "Code département"],
INSEE_REG: ["tagc", "Code région"],
LIBELLE_DEPARTEMENT: ["tagc", "Nom du département"],
LIBELLE_REGION: ["tagc", "Nom de la région"]
})
// Tableau des données attributaires : chaque en-tête porte le nom de la
// colonne et une étiquette de sa source (lien, description au survol)
function tableAttributs(rows) {
const columns = Object.keys(rows[0])
const entete = columns.map(c => {
const [source, description] = COLONNES[c] ?? []
const etiquette = source
? html`<a class="source source-${source}" href=${SOURCES[source].url} target="_blank"
title=${`${description} (${SOURCES[source].label})`}>${SOURCES[source].court}</a>`
: ""
return html`<th><span class="nom-colonne">${c}</span>${etiquette}</th>`
})
const cellule = v => typeof v == "number"
? html`<td class="nombre">${v.toLocaleString("fr-FR")}</td>`
: html`<td>${v ?? ""}</td>`
const lignes = rows.map(r => html`<tr>${columns.map(c => cellule(r[c]))}</tr>`)
return html`<div class="attributes-table"><table>
<thead><tr>${entete}</tr></thead><tbody>${lignes}</tbody>
</table></div>`
}
// Carte ou tableau des données attributaires
function visualisation(geojson, mode) {
if (geojson.features.length == 0) {
return html`<p class="viz-meta">Aucun polygone pour cette sélection.</p>`
}
const rows = geojson.features.map(f => f.properties)
const columns = Object.keys(rows[0])
const resume = html`<p class="viz-meta">${rows.length} polygones, ${columns.length} colonnes</p>`
if (mode == "Carte") {
return html`<div>${carte(geojson)}${resume}</div>`
}
return html`<div>${tableAttributs(rows)}${resume}</div>`
}indicateursCarreaux = new Map([
["Niveau de vie moyen (€ par an)", (p) => p.ind > 0 ? p.ind_snv / p.ind : null],
["Habitants", (p) => p.ind],
["Ménages pauvres (%)", (p) => p.men > 0 ? 100 * p.men_pauv / p.men : null]
])
// Contours extérieurs dans le sens attendu par d3 (sinon, « tout le globe
// sauf le polygone »)
function redresser(geojson) {
const anneaux = (rings) => d3.geoArea({type: "Polygon", coordinates: rings}) > 2 * Math.PI
? rings.map((r) => r.slice().reverse()) : rings
return geojson.features.map((f) => ({
...f,
geometry: {
...f.geometry,
coordinates: f.geometry.type == "MultiPolygon"
? f.geometry.coordinates.map(anneaux)
: anneaux(f.geometry.coordinates)
}
}))
}
// contours : communes ou arrondissements tracés par-dessus, avec leur numéro
function carteCarreaux(geojson, valeur, contours) {
const carres = redresser(geojson)
const limites = redresser(contours)
const libelle = [...indicateursCarreaux].find(([, v]) => v === valeur)[0]
const texte = (d) => {
const v = valeur(d.properties)
return `${d.properties.NOM_COM}
${libelle} : ${v === null ? "—" : Math.round(v).toLocaleString("fr-FR")}
${Math.round(d.properties.ind).toLocaleString("fr-FR")} habitants${(d.properties.i_est_200 ?? d.properties.i_est_1km) == 1 ? " (valeurs imputées)" : ""}`
}
return Plot.plot({
projection: {type: "conic-conformal", parallels: [44, 49], rotate: [-3, 0], domain: {type: "FeatureCollection", features: carres}},
width: largeurCarte,
color: {type: "quantile", n: 6, scheme: "ylorbr", legend: true, label: libelle, tickFormat: (d) => Math.round(d).toLocaleString("fr-FR")},
marks: [
Plot.geo(carres, Plot.centroid({
fill: (d) => valeur(d.properties),
stroke: "var(--bs-body-bg)",
strokeWidth: 0.5,
title: texte,
tip: true
})),
Plot.geo(limites, {stroke: "var(--bs-body-color)", strokeWidth: 1.2}),
Plot.text(limites, Plot.centroid({
text: (d) => `${Number(d.properties.INSEE_COG.slice(3))}e`,
fontSize: 14,
fontWeight: "bold",
fill: "var(--bs-body-color)",
stroke: "var(--bs-body-bg)",
strokeWidth: 4
}))
]
})
}l93 = "+proj=lcc +lat_0=46.5 +lon_0=3 +lat_1=49 +lat_2=44 +x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs"
topohelper = import("https://cdn.jsdelivr.net/npm/topohelper@0.4.3/+esm")
// Largeur de la colonne de la carte, d'après la fenêtre (mêmes seuils que
// .explorer dans styles.scss) : la carte est un canvas de taille fixe
largeurCarte = Generators.observe(notify => {
const compute = () => {
const explorer = window.innerWidth - 120
notify(Math.floor(window.innerWidth > 991 ? (explorer - 40) * 2.2 / 4.8 : explorer))
}
compute()
window.addEventListener("resize", compute)
return () => window.removeEventListener("resize", compute)
})
function carte(geojson) {
const w = largeurCarte
return topohelper
.fromGeojson(geojson, {deep: true})
.project({proj: l93})
.view({tooltip: true, zoom: true, size: [w, Math.round(w * 0.95)]})
}// Boutons de téléchargement, par l'API : les polygones affichés, dans un
// fichier nommé d'après le niveau et le millésime (DEP2026.geojson…)
function telechargements(q) {
const geojson = html`<a class="primary" href=${apiUrl(q, "geojson", {download: 1})} rel="noopener">
<i class="fa-solid fa-download"></i> GeoJSON</a>`
const parquet = Number(q.year) >= PARQUET_FIRST_YEAR
? html`<a class="secondary" href=${apiUrl(q, "geoparquet")} rel="noopener">
<i class="fa-solid fa-download"></i> GeoParquet</a>`
: html`<a class="disabled">GeoParquet</a>
<small>Pas de GeoParquet publié pour ${q.year}</small>`
return html`<div class="downloads">${geojson}${parquet}</div>`
}// Sélection multiple de départements, sans dépendance externe : un champ de
// recherche (code ou nom, sans tenir compte des accents) ouvre une liste de
// suggestions, cliquables ou navigables au clavier ; les départements choisis
// s'affichent en étiquettes, retirées avec ×. Suit le protocole des entrées
// Observable : propriété value (liste de codes) et événement input.
function selecteurDepartements(departements, {value = [], placeholder = ""} = {}) {
const sansAccent = t => t.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase()
const nom = new Map(departements.map(d => [d.code, d.nom]))
const input = html`<input type="text" placeholder=${placeholder} autocomplete="off"
class="form-control form-control-sm" role="combobox" aria-expanded="false">`
const liste = html`<ul class="suggestions" role="listbox" hidden></ul>`
const chips = html`<div class="chips"></div>`
const form = html`<form class="multi-select"><div class="champ">${input}${liste}</div>${chips}</form>`
let selection = [...value]
let suggestions = []
let active = 0
// Suggestions : code commençant par la saisie, puis nom commençant par
// la saisie, puis nom la contenant
function filtrer() {
const t = sansAccent(input.value.trim())
const rang = d => !t ? 0
: d.code.toLowerCase().startsWith(t) ? 0
: sansAccent(d.nom).startsWith(t) ? 1
: sansAccent(d.nom).includes(t) ? 2
: -1
return departements
.filter(d => !selection.includes(d.code) && rang(d) >= 0)
.sort((a, b) => rang(a) - rang(b))
}
function ouvrir() {
suggestions = filtrer()
active = 0
liste.replaceChildren(...suggestions.map((d, i) => {
const li = html`<li role="option" class=${i == active ? "active" : ""}>
<span class="code">${d.code}</span><span class="nom">${d.nom}</span></li>`
// mousedown (avant la perte du focus du champ) plutôt que click
li.addEventListener("mousedown", e => { e.preventDefault(); ajouter(d.code) })
li.addEventListener("mousemove", () => surligner(i))
return li
}))
liste.hidden = suggestions.length == 0
input.setAttribute("aria-expanded", String(!liste.hidden))
}
function fermer() {
liste.hidden = true
input.setAttribute("aria-expanded", "false")
}
function surligner(i) {
active = i
Array.from(liste.children).forEach((li, j) => li.classList.toggle("active", j == i))
liste.children[i]?.scrollIntoView({block: "nearest"})
}
function ajouter(code) {
if (code && !selection.includes(code)) {
selection.push(code)
update()
}
input.value = ""
fermer()
}
function renderChips() {
chips.replaceChildren(...selection.map(code => {
const remove = html`<button type="button" aria-label="Retirer ${nom.get(code)}">×</button>`
remove.onclick = () => { selection = selection.filter(s => s != code); update() }
return html`<span class="chip"><b>${code}</b> ${nom.get(code)}${remove}</span>`
}))
}
function update() {
form.value = [...selection]
renderChips()
form.dispatchEvent(new Event("input", {bubbles: true}))
}
// Les frappes ne remontent pas jusqu'à Observable (sinon l'explorateur est
// redessiné et le champ perd le focus) : seule la sélection met à jour
input.addEventListener("input", e => { e.stopPropagation(); ouvrir() })
input.addEventListener("change", e => e.stopPropagation())
input.addEventListener("focus", ouvrir)
input.addEventListener("blur", fermer)
input.addEventListener("keydown", e => {
if (e.key == "ArrowDown" || e.key == "ArrowUp") {
e.preventDefault()
if (liste.hidden) ouvrir()
const n = suggestions.length
if (n) surligner((active + (e.key == "ArrowDown" ? 1 : n - 1)) % n)
} else if (e.key == "Enter") {
e.preventDefault()
ajouter(suggestions[active]?.code)
} else if (e.key == "Escape") {
fermer()
}
})
form.addEventListener("submit", e => e.preventDefault())
form.value = [...selection]
renderChips()
return form
}function pyValues(values) {
return values.length == 1
? `"${values[0]}"`
: `[${values.map(v => `"${v}"`).join(", ")}]`
}
function rValues(values) {
return values.length == 1
? `"${values[0]}"`
: `c(${values.map(v => `"${v}"`).join(", ")})`
}
// Découpe une condition where en morceaux, avant les fonctions ST_, pour tenir
// dans le panneau
function whereParts(where, largeur) {
const morceaux = [""]
for (const mot of where.split(/(?<=, )(?=ST_)/)) {
const i = morceaux.length - 1
if (morceaux[i] && (morceaux[i] + mot).length > largeur) morceaux.push(mot)
else morceaux[i] += mot
}
return morceaux
}
// Condition where en Python : sur une ligne si elle est courte, sinon en
// chaînes juxtaposées
function pyWhere(where, largeur = 45) {
if (where.length <= largeur) return `"${where}"`
return `(\n${whereParts(where, largeur).map(m => ` "${m}"`).join("\n")}\n )`
}
// Condition where en R : sur une ligne si elle est courte, sinon avec paste0
function rWhere(where, largeur = 45) {
if (where.length <= largeur) return `"${where}"`
return `paste0(\n${whereParts(where, largeur).map(m => ` "${m}"`).join(",\n")}\n )`
}
// Découpe une URL d'API en morceaux lisibles (un paramètre par ligne)
function urlParts(url) {
const [base, query] = url.split("?")
const parts = query.split("&")
return [`${base}?${parts[0]}`, ...parts.slice(1).map(p => `&${p}`)]
}
function programmes(q, variable) {
const url = apiUrl(q)
const parts = urlParts(url)
return {
Python: `from cartiflette import carti_download
${variable} = carti_download(
values=${pyValues(q.values)},
borders="${q.borders}",
filter_by="${q.filter_by}",
year=${q.year},
simplification=${q.simplification},${q.where ? `\n where=${pyWhere(q.where)},` : ""}
)`,
R: `library(cartiflette)
${variable} <- carti_download(
values = ${rValues(q.values)},
borders = "${q.borders}",
filter_by = "${q.filter_by}",
year = ${q.year},
simplification = ${q.simplification}${q.where ? `,\n where = ${rWhere(q.where)}` : ""}
)`,
JavaScript: `const url = [
${parts.map(p => ` "${p}"`).join(",\n")}
].join("")
const ${variable} = await fetch(url).then((r) => r.json())`
}
}function panneauCode(codes, note) {
const code = codes[langage]
const tabs = langages.map(l => {
const b = html`<button type="button" class=${l == langage ? "active" : ""}>${l}</button>`
b.onclick = () => { mutable langage = l }
return b
})
const copy = html`<button type="button" class="copy"><i class="fa-regular fa-copy"></i> Copier</button>`
copy.onclick = async () => {
await navigator.clipboard.writeText(code)
copy.textContent = "Copié"
}
return html`<div class="snippet">
<div class="snippet-tabs">${tabs}${copy}</div>
<pre><code>${code}</code></pre>
</div>
<p class="snippet-note">${note[langage]}</p>`
}
notes = ({
Python: html`<code>pip install cartiflette</code> (version 0.2.1 ou plus). Le client lit le GeoParquet et ne télécharge que les polygones demandés.`,
R: html`<code>remotes::install_github("inseefrlab/cartiflette-r")</code> (version 0.2.0 ou plus). Le package lit le GeoParquet avec DuckDB et renvoie un objet <code>sf</code>.`,
JavaScript: html`${avertissement("JavaScript")}L'API renvoie du GeoJSON (EPSG:4326), utilisable avec d3, Observable Plot, Leaflet ou MapLibre.`
})
// Note Python des explorateurs qui utilisent where, les IRIS ou les carreaux :
// client 0.4.0
notesV4 = ({
...notes,
Python: html`L'argument <code>where</code> et le niveau IRIS demandent la
version 0.4.0 du client : <code>pip install -U cartiflette</code>.`
})
notesCarreaux = ({
...notes,
Python: html`Les carreaux demandent la version 0.4.0 du client
(<code>pip install -U cartiflette</code>). Ce sont des carrés : ils ne sont
publiés que sans simplification.`
})
// Le package JavaScript ne suit pas encore les évolutions des clients Python
// et R (GeoParquet, millésimes depuis 2023) : on passe par l'API en attendant
function avertissement(langage) {
return html`<span class="snippet-warning"><i class="fa-solid fa-person-digging"></i>
Le package <code>cartiflette</code> pour ${langage} n'est pas encore à jour des
dernières évolutions ; il va évoluer. En attendant, ce code interroge
directement l'API.</span>`
}departements = [
{code: "01", nom: "Ain"},
{code: "02", nom: "Aisne"},
{code: "03", nom: "Allier"},
{code: "04", nom: "Alpes-de-Haute-Provence"},
{code: "05", nom: "Hautes-Alpes"},
{code: "06", nom: "Alpes-Maritimes"},
{code: "07", nom: "Ardèche"},
{code: "08", nom: "Ardennes"},
{code: "09", nom: "Ariège"},
{code: "10", nom: "Aube"},
{code: "11", nom: "Aude"},
{code: "12", nom: "Aveyron"},
{code: "13", nom: "Bouches-du-Rhône"},
{code: "14", nom: "Calvados"},
{code: "15", nom: "Cantal"},
{code: "16", nom: "Charente"},
{code: "17", nom: "Charente-Maritime"},
{code: "18", nom: "Cher"},
{code: "19", nom: "Corrèze"},
{code: "21", nom: "Côte-d'Or"},
{code: "22", nom: "Côtes-d'Armor"},
{code: "23", nom: "Creuse"},
{code: "24", nom: "Dordogne"},
{code: "25", nom: "Doubs"},
{code: "26", nom: "Drôme"},
{code: "27", nom: "Eure"},
{code: "28", nom: "Eure-et-Loir"},
{code: "29", nom: "Finistère"},
{code: "2A", nom: "Corse-du-Sud"},
{code: "2B", nom: "Haute-Corse"},
{code: "30", nom: "Gard"},
{code: "31", nom: "Haute-Garonne"},
{code: "32", nom: "Gers"},
{code: "33", nom: "Gironde"},
{code: "34", nom: "Hérault"},
{code: "35", nom: "Ille-et-Vilaine"},
{code: "36", nom: "Indre"},
{code: "37", nom: "Indre-et-Loire"},
{code: "38", nom: "Isère"},
{code: "39", nom: "Jura"},
{code: "40", nom: "Landes"},
{code: "41", nom: "Loir-et-Cher"},
{code: "42", nom: "Loire"},
{code: "43", nom: "Haute-Loire"},
{code: "44", nom: "Loire-Atlantique"},
{code: "45", nom: "Loiret"},
{code: "46", nom: "Lot"},
{code: "47", nom: "Lot-et-Garonne"},
{code: "48", nom: "Lozère"},
{code: "49", nom: "Maine-et-Loire"},
{code: "50", nom: "Manche"},
{code: "51", nom: "Marne"},
{code: "52", nom: "Haute-Marne"},
{code: "53", nom: "Mayenne"},
{code: "54", nom: "Meurthe-et-Moselle"},
{code: "55", nom: "Meuse"},
{code: "56", nom: "Morbihan"},
{code: "57", nom: "Moselle"},
{code: "58", nom: "Nièvre"},
{code: "59", nom: "Nord"},
{code: "60", nom: "Oise"},
{code: "61", nom: "Orne"},
{code: "62", nom: "Pas-de-Calais"},
{code: "63", nom: "Puy-de-Dôme"},
{code: "64", nom: "Pyrénées-Atlantiques"},
{code: "65", nom: "Hautes-Pyrénées"},
{code: "66", nom: "Pyrénées-Orientales"},
{code: "67", nom: "Bas-Rhin"},
{code: "68", nom: "Haut-Rhin"},
{code: "69", nom: "Rhône"},
{code: "70", nom: "Haute-Saône"},
{code: "71", nom: "Saône-et-Loire"},
{code: "72", nom: "Sarthe"},
{code: "73", nom: "Savoie"},
{code: "74", nom: "Haute-Savoie"},
{code: "75", nom: "Paris"},
{code: "76", nom: "Seine-Maritime"},
{code: "77", nom: "Seine-et-Marne"},
{code: "78", nom: "Yvelines"},
{code: "79", nom: "Deux-Sèvres"},
{code: "80", nom: "Somme"},
{code: "81", nom: "Tarn"},
{code: "82", nom: "Tarn-et-Garonne"},
{code: "83", nom: "Var"},
{code: "84", nom: "Vaucluse"},
{code: "85", nom: "Vendée"},
{code: "86", nom: "Vienne"},
{code: "87", nom: "Haute-Vienne"},
{code: "88", nom: "Vosges"},
{code: "89", nom: "Yonne"},
{code: "90", nom: "Territoire de Belfort"},
{code: "91", nom: "Essonne"},
{code: "92", nom: "Hauts-de-Seine"},
{code: "93", nom: "Seine-Saint-Denis"},
{code: "94", nom: "Val-de-Marne"},
{code: "95", nom: "Val-d'Oise"},
{code: "971", nom: "Guadeloupe"},
{code: "972", nom: "Martinique"},
{code: "973", nom: "Guyane"},
{code: "974", nom: "La Réunion"},
{code: "976", nom: "Mayotte"}
]
cartiflette
Les fonds de carte officiels français, prêts à joindre à vos données.
Vous voulez faire une carte mais n’avez que des codes Insee ? cartiflette fournit les contours qui vont avec : les fonds de carte de l’IGN enrichis des métadonnées de l’Insee, prêts à être associés à vos données, en moins de 10 lignes de code.
$pip install cartiflette
Un fond de carte France entière
Découpages administratifs ou zonages d’étude de l’Insee: les fonds de carte sont prêts à l’emploi pour répondre à de nombreux besoins cartographiques.
Voir un exemple de la vraie vie
Les communes d’une sélection de départements
Si on n’a besoin que de quelques départements ou régions, ou si on désire représenter des données mélangeant arrondissements et communes, on va devoir faire des opérations compliquées. Avec cartiflette, ce type de fond est prêt à l’emploi !
Voir un exemple de la vraie vie
html`<div class="explorer">
<div class="explorer-controls">
${viewof affichage_departements}
${viewof choix_departements}
${viewof annee_departements}
${viewof arrondissements}
${viewof simplification_departements}
${telechargements(requete_departements)}
</div>
<div class="explorer-viz">${
requete_departements.values.length == 0
? html`<p class="viz-meta">Choisissez au moins un département.</p>`
: visualisation(donnees_departements, affichage_departements)
}</div>
<div class="explorer-code">${panneauCode(programmes(requete_departements, "communes"), notes)}</div>
</div>`Ne garder que les polygones utiles
Pas besoin de télécharger toute une région pour n’en garder que quelques communes : l’argument where, une condition SQL, filtre les polygones selon leurs attributs (population, intercommunalité…) ou selon leur géométrie (emprise, distance à un point), dès le téléchargement.
Voir un exemple de la vraie vie
html`<div class="explorer">
<div class="explorer-controls">
${viewof affichage_where}
${viewof filtre_where}
${filtre_where == "attribut" ? viewof seuil_where : ""}
${viewof annee_where}
<p class="viz-meta">${description_where}</p>
${telechargements(requete_where)}
</div>
<div class="explorer-viz">${visualisation(donnees_where, affichage_where)}</div>
<div class="explorer-code">${panneauCode(programmes(requete_where, filtre_where == "attribut" ? "grandes_communes" : "camargue"), notesV4)}</div>
</div>`Les IRIS, à l’intérieur des communes
Pour descendre sous la commune, cartiflette fournit les contours IRIS de l’IGN, à partir de 2025 : des quartiers d’environ 2 000 habitants, le niveau auquel l’Insee diffuse revenus, logement ou emploi. Avec where, on ne garde que ceux d’une intercommunalité ou d’un rayon autour d’un point.
Voir un exemple de la vraie vie
html`<div class="explorer">
<div class="explorer-controls">
${viewof affichage_iris}
${viewof espace_iris}
${viewof annee_iris}
<p class="viz-meta">${espace_iris.description}</p>
${telechargements(requete_iris)}
</div>
<div class="explorer-viz">${visualisation(donnees_iris, affichage_iris)}</div>
<div class="explorer-code">${panneauCode(programmes(requete_iris, "iris"), notesV4)}</div>
</div>`Des carreaux de 200 mètres, plus fins que les quartiers
Les données carroyées de l’Insee découpent le territoire en carreaux de 200 mètres ou d’1 kilomètre, avec la population, les ménages, les logements et les revenus de leurs habitants (Filosofi). cartiflette les fournit avec leur géométrie, rattachés chacun à une commune : on les demande pour quelques arrondissements comme pour un département, sans télécharger tout le fichier national.
Voir un exemple de la vraie vie
html`<div class="explorer">
<div class="explorer-controls">
${viewof affichage_carreaux}
${viewof grille_carreaux}
${viewof indicateur_carreaux}
${viewof annee_carreaux}
<p class="viz-meta">Les carreaux des 10e, 11e, 18e, 19e et 20e arrondissements de Paris, rattachés à l'arrondissement qui contient leur centre.</p>
${telechargements(requete_carreaux)}
</div>
<div class="explorer-viz">${
affichage_carreaux == "Carte"
? html`<div>${carteCarreaux(donnees_carreaux, indicateur_carreaux, arrondissements_carreaux)}<p class="viz-meta">${donnees_carreaux.features.length} carreaux</p></div>`
: visualisation(donnees_carreaux, affichage_carreaux)
}</div>
<div class="explorer-code">${panneauCode(programmes(requete_carreaux, "carreaux"), notesCarreaux)}</div>
</div>`viewof affichage_france = Inputs.radio(["Carte", "Métadonnées"], {value: "Carte"})
viewof niveau_france = Inputs.select(niveaux, {label: "Niveau", key: "Départements"})
viewof annee_france = Inputs.select(annees, {label: "Millésime"})
viewof drom_rapproches = Inputs.toggle({label: "DROM rapprochés et zoom sur l'Île-de-France", value: true})
viewof simplification_france = Inputs.radio(
new Map([["80 % des points retirés", 80], ["50 % des points retirés", 50], ["Contours complets", 0]]),
{label: "Simplification", value: 80}
)requete_france = ({
borders: niveau_france.code,
filter_by: drom_rapproches ? "FRANCE_ENTIERE_DROM_RAPPROCHES" : "FRANCE_ENTIERE",
values: ["France"],
year: annee_france,
simplification: simplificationDisponible(annee_france, simplification_france)
})
donnees_france = d3.json(apiUrl(requete_france))viewof affichage_departements = Inputs.radio(["Carte", "Métadonnées"], {value: "Carte"})
viewof choix_departements = selecteurDepartements(departements, {
placeholder: "Ajouter un département : code ou nom",
value: ["75", "92", "93", "94"]
})
viewof annee_departements = Inputs.select(annees, {label: "Millésime"})
viewof arrondissements = Inputs.toggle({label: "Arrondissements municipaux (Paris, Lyon, Marseille)", value: true})
viewof simplification_departements = Inputs.radio(
new Map([["80 % des points retirés", 80], ["50 % des points retirés", 50], ["Contours complets", 0]]),
{label: "Simplification", value: 80}
)requete_departements = ({
borders: arrondissements ? "COMMUNE_ARRONDISSEMENT" : "COMMUNE",
filter_by: "DEPARTEMENT",
values: choix_departements,
year: annee_departements,
simplification: simplificationDisponible(annee_departements, simplification_departements)
})
donnees_departements = requete_departements.values.length == 0
? {type: "FeatureCollection", features: []}
: d3.json(apiUrl(requete_departements))viewof affichage_where = Inputs.radio(["Carte", "Métadonnées"], {value: "Carte"})
viewof filtre_where = Inputs.radio(
new Map([["Selon un attribut", "attribut"], ["Selon la géométrie", "geometrie"]]),
{label: "Filtre", value: "attribut"}
)
viewof seuil_where = Inputs.select(
new Map([["2 000 habitants", 2000], ["10 000 habitants", 10000], ["50 000 habitants", 50000]]),
{label: "Population minimale"}
)
viewof annee_where = Inputs.select(annees.filter(a => Number(a) >= PARQUET_FIRST_YEAR), {label: "Millésime"})requete_where = filtre_where == "attribut"
? ({
borders: "COMMUNE",
filter_by: "REGION",
values: ["76"],
year: annee_where,
simplification: 80,
where: `POPULATION > ${seuil_where}`
})
: ({
borders: "COMMUNE",
filter_by: "REGION",
values: ["76", "93"],
year: annee_where,
simplification: 80,
where: "ST_Intersects(geometry, ST_MakeEnvelope(4.1, 43.3, 4.9, 43.75))"
})
description_where = filtre_where == "attribut"
? `Les communes d'Occitanie de plus de ${seuil_where.toLocaleString("fr-FR")} habitants.`
: "Les communes d'Occitanie et de Provence-Alpes-Côte d'Azur qui touchent un rectangle autour de la Camargue : longitude de 4,1 à 4,9, latitude de 43,3 à 43,75."
donnees_where = d3.json(apiUrl(requete_where))viewof affichage_iris = Inputs.radio(["Carte", "Métadonnées"], {value: "Carte"})
espaces_iris = [
{
label: "Toute la Seine-Saint-Denis",
description: "Tous les IRIS de la Seine-Saint-Denis."
},
{
label: "Selon un attribut",
where: "EPT = '200057867'",
description: "Les IRIS de Plaine Commune (Saint-Denis, Aubervilliers, La Courneuve…), d'après le code de leur établissement public territorial."
},
{
label: "Selon la géométrie",
where: "ST_Distance_Sphere(ST_Centroid(geometry), ST_Point(2.3599, 48.9356)) < 3000",
description: "Les IRIS dont le centre est à moins de 3 km de la basilique de Saint-Denis."
}
]
viewof espace_iris = Inputs.radio(espaces_iris, {label: "Espace", format: d => d.label, value: espaces_iris[1]})
viewof annee_iris = Inputs.select(["2026", "2025"], {label: "Millésime"})requete_iris = ({
borders: "IRIS",
filter_by: "DEPARTEMENT",
values: ["93"],
year: annee_iris,
simplification: 80,
where: espace_iris.where
})
donnees_iris = d3.json(apiUrl(requete_iris))viewof affichage_carreaux = Inputs.radio(["Carte", "Métadonnées"], {value: "Carte"})
viewof grille_carreaux = Inputs.radio(
new Map([["200 mètres", "CARREAU_200M"], ["1 kilomètre", "CARREAU_1KM"]]),
{label: "Carreaux", value: "CARREAU_200M"}
)
viewof indicateur_carreaux = Inputs.select(indicateursCarreaux, {label: "Couleur"})
// Carreaux Filosofi rattachés aux communes de 2023 à 2026
viewof annee_carreaux = Inputs.select(["2026", "2025", "2024", "2023"], {label: "Millésime des communes"})requete_carreaux = ({
borders: grille_carreaux,
filter_by: "COMMUNE_ARRONDISSEMENT",
values: ["75110", "75111", "75118", "75119", "75120"],
year: annee_carreaux,
simplification: 0
})
donnees_carreaux = d3.json(apiUrl(requete_carreaux))
// Contours des cinq arrondissements, pour la carte
arrondissements_carreaux = d3.json(apiUrl({
borders: "COMMUNE_ARRONDISSEMENT",
filter_by: "DEPARTEMENT",
values: ["75"],
year: annee_carreaux,
simplification: 50,
where: `INSEE_COG IN (${requete_carreaux.values.map((v) => `'${v}'`).join(", ")})`
}))