Skip to content

Carte à symboles ​

Géographie

Graphique n°20 : une carte à bulles dont les chevauchements sont résolus par une simulation de force. Cible de migration pour le legacy sdg-trade MapSymbolForce - une carte à bulles dot-only où les coordonnées de chaque élément se projettent sur le plan, puis une simulation de force en une passe écarte juste assez les cercles qui se chevauchent pour rester lisibles, sans les éloigner de leur position réelle. Contrairement à Carte choroplèthe, la géographie est optionnelle ici : omettez-la pour l'apparence dot-only du graphique legacy (le défaut), ou fournissez-la pour dessiner un fond de carte discret derrière les symboles.

Example
canvas · responsive
Aller plus loin : Guide Insights·Guide DevTools

Un second anneau concentrique (valueSecond) se lit comme une sous-métrique du cercle extérieur - par exemple « dont » un partenaire ou un canal spécifique :

Example
canvas · responsive
Aller plus loin : Guide Insights·Guide DevTools

Positions exactes ou anti-chevauchement ​

positionMode définit ce que signifie la position d'un symbole :

  • "force" (défaut, parité héritée) : la simulation en une passe écarte les cercles qui se chevauchent. Lisible quand beaucoup de symboles se concentrent au même endroit, mais chaque symbole peut dériver de ses coordonnées réelles, et sur un petit graphique la dérive peut être forte.
  • "precise" : chaque symbole reste exactement à son lng/lat projeté ; les chevauchements sont permis.

Un symbole dessiné sur le mauvais pays est un problème d'exactitude cartographique, et pour certains publics un sujet politiquement sensible. Préférez "precise" dès qu'un fond geography est visible : une carte invite à lire les positions au pied de la lettre.

Passez en force : les bulles dérivent de leurs coordonnées réelles pour résoudre les collisions. Avec un fond de carte visible, precise est en général le choix honnête.

Géographie de démonstration uniquement - et les frontières sont politiques

L'atlas mondial de cette page est un fichier GeoJSON simplifié du domaine public, fourni avec les exemples de la documentation à titre d'illustration. Il peut être faux : frontières, noms et tracés ne font PAS autorité, et certains sont contestés. Les frontières et noms « corrects » varient selon le public, la juridiction et l'organisation, et une carte qui se trompe est un problème politique, pas cosmétique. Rien de ce qui est rendu ici ne constitue une reconnaissance d'une frontière. Avant tout usage en production, vérifiez les frontières et la toponymie de votre propre fichier geography selon la politique cartographique de votre organisation : la bibliothèque rend le fichier tel quel, sans correction.

Le graphique ci-dessus est le même moteur dans chaque framework - seul le code d'intégration ci-dessous diffère.

Tuiles hexagonales en nid d'abeille ​

Parfois chaque lieu doit peser autant que les autres et la valeur appartient à la couleur : une carte à tuiles. Avec shape: "hexagon" et positionMode: "honeycomb", chaque symbole devient un hexagone de taille égale accroché à un réseau hexagonal : les tuiles pavent le plan, ne se chevauchent jamais, et la tuile d'un pays reste aussi proche de son centroïde que le réseau le permet. colorScale (un domaine/plage scaleThreshold, le même contrat que la Carte choroplèthe) porte la valeur ; les marques sont peintes à pleine opacité puisque la couleur est l'encodage.

Example
canvas · responsive
Aller plus loin : Guide Insights·Guide DevTools
Example
canvas · responsive
Aller plus loin : Guide Insights·Guide DevTools

L'avertissement « géographie de démonstration » ci-dessus s'applique ici aussi : les tuiles reposent sur un atlas illustratif, et la position de chaque tuile, comme le pays qu'elle représente, n'est correcte que dans la mesure où le sont la geography et les coordonnées que vous fournissez.

  • honeycomb - { radius, gap, orientation, placement, reserve } : rayon circonscrit de la tuile en px (11 par défaut), espace entre les bords (2 par défaut), et sommets "flat" (défaut, côté plat en haut) ou "pointy". L'orientation régit à la fois le contour de l'hexagone et le réseau, pour que les tuiles s'emboîtent. placement et reserve sont décrits plus bas.
  • Les collisions - par défaut (honeycomb.placement: "first") - se résolvent dans l'ordre de dataSet : une tuile dont la cellule est prise avance anneau par anneau jusqu'à la première cellule libre, de façon déterministe. Sans cellule libre dans six anneaux, la tuile chevauche et un onDataWarning de type layout-overflow la nomme.
  • honeycomb.placement: "geographic" garde les voisins dans leur ordre réel. La tuile la plus proche du centre d'une cellule la conserve, une tuile déplacée prend la cellule libre la plus proche de sa vraie position, puis une passe de réparation échange ou décale les tuiles voisines qui se liraient est pour ouest ou nord pour sud. Le résultat ne dépend pas de l'ordre du dataSet. Elle fait plus de travail que "first" (une passe de réparation après l'attribution des cellules), environ 25 ms pour 200 tuiles. Les paires voisines qui restent mal ordonnées sont comptées dans le contexte (stats.outOfOrderCount, mesuré pour chaque placement, ce qui permet de comparer "first" ; avec "geographic", c'est ce qui reste après la réparation) et, avec "geographic" seulement, nommées par un onDataWarning de type layout-out-of-order ; stats.displacedCount compte les tuiles éloignées de leur cellule la plus proche.
  • honeycomb.reserve: "all" fait aussi réserver une cellule aux tuiles sans valeur : la disposition ne change pas quand les valeurs changent (une carte qui passe d'un produit ou d'une année à l'autre). Les tuiles retirées par disabledItems ou radiusVisibleMin quittent tout de même la disposition. La valeur par défaut "valued" tient les tuiles sans donnée à l'écart.
  • fit: "width" calcule la disposition une seule fois à width x height puis met le dessin à l'échelle du conteneur (jamais au-delà de width) : les tuiles ne bougent pas quand la fenêtre change de taille. La valeur par défaut "none" dessine à la taille exacte en pixels.
  • Pas de valeur ? Omettez value. L'élément est peint en noDataColor (#d2d7dd par défaut, passez "transparent" pour le masquer), exclu de l'échelle de couleurs, et, avec la valeur par défaut honeycomb.reserve: "valued", ne réclame jamais de cellule : une tuile « sans donnée » ne peut pas déplacer une vraie donnée (honeycomb.reserve: "all" leur fait aussi garder une cellule, voir plus haut).
  • offset sur un élément le décale en px après la projection et avant l'accrochage : la soupape pour un petit pays caché sous un grand voisin.
  • markers dessine des épingles décoratives au-dessus des tuiles à un lng/lat (un siège, par exemple) : l'épingle intégrée, ou votre propre path dans sa boîte pathSize, mis à l'échelle size px et ancré par la pointe. Donnez à un marqueur tile (l'id ou le label d'une tuile) pour poser l'épingle sur cette tuile, où que la disposition l'ait placée ; lng/lat restent la solution de repli. Les markers du contexte gardent les lng/lat déclarés ; pour une épingle posée sur une tuile, lisez le x/y de la tuile dans symbols. Les épingles ne sont ni disposées, ni testées au survol, et n'ont pas d'infobulle ; stylez-les via .symbol-map-marker. Le contexte liste le x/y de chaque symbole placé pour vos propres superpositions et légende.

shape: "hexagon" fonctionne aussi seul avec un placement "force" ou "precise", et colorScale fonctionne avec des cercles : les briques se composent.

Ce qui reste à vous

La bibliothèque dessine tuiles, épingles et couleurs. Le texte de légende, le contenu de l'infobulle, le format des nombres, quel pays est « chez vous » et quelle palette il reçoit appartiennent au consommateur : passez-les par colorScale, markers et tooltipFormatter.

Apportez vos propres coordonnées ​

Contrairement au graphique legacy (qui embarquait un CSV statique d'environ 200 lignes de coordonnées de pays), @michi-vz/core n'embarque aucune table de coordonnées. Chaque élément de dataSet fournit son propre lng/lat :

ts
import { SymbolMapChart } from "@michi-vz/react";

<SymbolMapChart
  dataSet={[
    { id: "usa", label: "United States", lng: -95.7, lat: 38.9, value: 100 },
    { id: "deu", label: "Germany", lng: 13.4, lat: 52.5, value: 60, valueSecond: 30 },
    // ...
  ]}
/>

Comme l'étendue utilisée pour ajuster la disposition dot-only au tracé est dérivée des coordonnées de votre propre jeu de données (et non d'une table monde toujours complète embarquée), le « monde » que le graphique dessine ne s'étend qu'aussi loin que les points que vous fournissez - voir les notes de comportement ci-dessous.

Placer les symboles depuis la géographie ​

Avec un fond geography, vous pouvez omettre lng/lat : l'élément est alors placé au centroïde de l'entité dont l'id correspond à son geoId (par défaut : son propre id). Des lignes indexées par code ISO rejoignent un fichier monde indexé de la même façon sans aucune table de coordonnées ; un élément qui ne correspond à aucune entité est écarté avec un avertissement invalid-geometry qui nomme l'id cherché.

ts
<SymbolMapChart
  geography={world}
  dataSet={[
    { id: "DEU", label: "Germany", value: 60, valueSecond: 30 },
    { id: "row-9", geoId: "FRA", label: "France", value: 40 },
  ]}
  labelFormatter={(d) => `${d.label} ${Math.round((d.valueSecond! / d.value!) * 100)}%`}
  onSymbolClick={(d) => go(d.id)}
/>

labelFormatter définit le texte dessiné dans un symbole assez grand (par défaut : le libellé), pour placer une part ou une valeur à côté du nom ; le texte reste ajusté au cercle. onSymbolClick(item, event) se déclenche au clic sur un symbole, en svg comme via le test de collision du canvas ; le clic épingle toujours l'infobulle.

Faire defiler les annees ​

Donnez un date à chaque symbole et activez timeline : la carte devient un récit année par année, avec son propre bouton lecture et son curseur, plaçant les symboles d'une période à la fois. Désactivé par défaut - sans opt-in, rien ne change.

Appuyez sur le bouton lecture sous le graphique : les données défilent année après année, un instantané à la fois. Faites glisser le curseur pour sauter à une année.

tsx
const ref = useRef<SymbolMapChartHandle>(null);

<SymbolMapChart ref={ref} {...props} timeline={{ speedMs: 1000, loop: true }} />;
// ref.current?.timeline() -> play() / pause() / seek(year) / seekIndex(i) / stepForward()
vue
<SymbolMapChart :options="{ ...props, timeline: { speedMs: 1000, loop: true } }" />
svelte
<div use:symbolMapChart={{ ...props, timeline: { speedMs: 1000, loop: true } }}></div>
ts
applySymbolMapChartProps(this.c.nativeElement, { ...props, timeline: { speedMs: 1000, loop: true } });
html
<michi-vz-symbol-map-chart id="c"></michi-vz-symbol-map-chart>
<script>
  const el = document.getElementById("c");
  el.timeline = { speedMs: 1000, loop: true };
  // el.getTimeline() -> play() / pause() / seek(year) / seekIndex(i)
</script>
  • speedMs règle le rythme, loop reboucle, autoplay: true démarre au montage, showControl: false masque la barre intégrée.
  • Les valeurs glissent d'une période à l'autre par défaut (interpolate) ; ajustez le mouvement avec tweenMs et easing, ou passez interpolate: false pour des coupes nettes. Avec reduced motion, la coupe est toujours nette.
  • Le contrôleur headless reste disponible : chart.timeline() expose play() / pause() / toggle() / seek(period) / seekIndex(i) / stepForward() / stepBack(), plus onStep et formatPeriod dans la config pour une UI maison.
  • seek(period) cherche d'abord la période, en comparant le texte : seek(2021) et seek("2021") vont tous les deux sur 2021, que vos dates soient des nombres ou des chaînes. Un nombre ne compte comme une position (0 = la première) que si aucune période ne correspond, et une chaîne qui ne correspond à rien ne fait rien. seekIndex(i) va toujours par position, comme le curseur intégré.
  • Les symboles sans date restent visibles à chaque période.

Usage ​

tsx
import { SymbolMapChart } from "@michi-vz/react";

export default () => <SymbolMapChart {...props} />; // props = les options du graphique
vue
<script setup>
import { SymbolMapChart } from "@michi-vz/vue";
</script>

<template>
  <SymbolMapChart :options="props" />
</template>
svelte
<script>
  import { symbolMapChart } from "@michi-vz/svelte";
</script>

<div use:symbolMapChart={props}></div>
ts
// main.ts - enregistrer les éléments une fois
import "@michi-vz/angular";
import { applySymbolMapChartProps } from "@michi-vz/angular";

// composant (utilise CUSTOM_ELEMENTS_SCHEMA)
// template : <michi-vz-symbol-map-chart #c></michi-vz-symbol-map-chart>
applySymbolMapChartProps(this.c.nativeElement, props);
html
<script type="module" src="https://cdn.jsdelivr.net/npm/@michi-vz/wc/dist/michi-vz-wc.bundle.js"></script>

<michi-vz-symbol-map-chart id="c"></michi-vz-symbol-map-chart>
<script>
  Object.assign(document.getElementById("c"), props); // dataSet, title, …
</script>
ts
import { mountSymbolMapChart } from "@michi-vz/core";

const chart = mountSymbolMapChart(el, props);
chart.update(next);
chart.getContext(); // agnostique du moteur de rendu, prêt pour un LLM
chart.destroy();

API ​

Les props sont typées comme SymbolMapChartProps dans @michi-vz/core. Communes à tous les graphiques : width, height, margin, colors / colorsMapping, renderer ("svg", "canvas", ou expérimentalement "webgpu"), highlightItems, disabledItems, et les callbacks on*. onChartDataProcessed / getContext() renvoient le ChartContext agnostique du moteur de rendu.

Notes de comportement ​

La résolution de chevauchement en une passe ​

Le lng/lat de chaque élément se projette vers une position pixel « réelle » ; forceX/forceY épinglent la simulation sur cette position exacte comme cible, forceManyBody() ajoute une légère séparation, et forceCollide (rayon + 2px de marge, 3 itérations) est ce qui écarte réellement les cercles qui se chevauchent. La simulation se stabilise de manière synchrone au MÊME seuil alpha que le graphique legacy (0.0011) - déterministe : des entrées identiques se stabilisent toujours sur la même disposition, donc deux montages des mêmes données produisent des positions identiques au pixel près. Deux éléments avec des coordonnées exactement identiques démarrent empilés et se séparent proprement une fois la simulation stabilisée. positionMode: "precise" saute entièrement la simulation : chaque symbole reste à sa position projetée exacte et les chevauchements sont permis (voir la démo à bascule plus haut).

Projection dot-only vs. fond de carte ​

Sans geography, la projection choisie (par défaut "geoMercator", comme le graphique legacy) est utilisée sans réglage - une simple fabrique de projection sans aucun des réglages translate/scale/rotate/center de Carte choroplèthe - et l'étendue des points projetés est ensuite redimensionnée pour remplir le tracé, reprenant les mêmes calculs que le graphique legacy. Avec geography fourni, c'est la MÊME répartition réglée que Carte choroplèthe qui prend le relais, afin que le fond de carte et les coordonnées des symboles partagent un cadrage géographique cohérent (pas de redimensionnement d'étendue).

radiusVisibleMin - filtré sur la valeur brute ​

radiusVisibleMin masque les éléments dont la valeur brute value est inférieure ou égale au seuil (et, quand valueSecond est défini, dont valueSecond est également en dessous) avant l'exécution de la disposition de force - repris du propre filtre du graphique legacy, qui comparait la valeur brute, jamais le rayon mis à l'échelle. Le domaine de l'échelle rayon/opacité reste construit à partir de tous les éléments aux coordonnées valides, indépendamment de ce filtre, afin que le rayon d'un symbole reste comparable d'un rendu à l'autre même quand le seuil change.

L'anneau concentrique secondaire ​

valueSecond dessine un second cercle, de la même couleur que l'extérieur, à opacity - 0.3 (plafonné à une valeur non négative), dessiné AU-DESSUS du cercle principal - repris exactement de la superposition du ForceNode legacy. Quand le second cercle est plus petit que l'extérieur, il se lit comme un noyau plus estompé à l'intérieur d'un anneau plus clair (par ex. « dont ») ; s'il est plus grand, il recouvre entièrement le cercle extérieur.

Fond de carte optionnel ​

geography (une FeatureCollection GeoJSON ou un GeoFeatureItem[] pré-normalisé, même contrat que geography de Carte choroplèthe) est une nouvelle capacité - le graphique legacy n'a jamais dessiné de fond de carte. Omettez-la pour l'apparence dot-only legacy ; fournissez-la pour un fond discret et non interactif (geographyColor, par défaut #eef1f5) derrière les symboles.

Rendu : SVG, canvas et un WebGPU délégué ​

renderer="svg" (par défaut) dessine un <circle class="symbol"> par élément visible (plus un <circle class="symbol-second"> optionnel). renderer="canvas" peint les mêmes marques sur un canvas 2D, en résolvant la couleur de remplissage via la même sonde CSS consommateur que tout graphique à marque unique. renderer="webgpu" délègue au moteur canvas-2D, même logique que Carte choroplèthe : le fond de carte optionnel est un GeoJSON arbitraire (potentiellement concave, multi-anneaux), donc une tessellation GPU correcte serait disproportionnée ici.

Chargement / absence de données ​

isLoading et isNodata pilotent la superposition (React : isLoadingComponent / isNodataComponent), identique à tout autre graphique de la maison.

Autorités de couleur côté consommateur : le contexte porte legendData ({ label, color, dataLabelSafe }) pour qu'un système de couleur par injection CSS puisse indexer des règles par étiquette ; onChartDataProcessed n'est émis que lorsque le contexte change.