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.
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 :
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 à sonlng/latprojeté ; 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.
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
geographyet 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.placementetreservesont décrits plus bas.- Les collisions - par défaut (
honeycomb.placement: "first") - se résolvent dans l'ordre dedataSet: 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 unonDataWarningde typelayout-overflowla 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 dudataSet. 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 chaqueplacement, 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 unonDataWarningde typelayout-out-of-order;stats.displacedCountcompte 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 pardisabledItemsouradiusVisibleMinquittent 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 àwidthxheightpuis met le dessin à l'échelle du conteneur (jamais au-delà dewidth) : 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 ennoDataColor(#d2d7ddpar défaut, passez"transparent"pour le masquer), exclu de l'échelle de couleurs, et, avec la valeur par défauthoneycomb.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). offsetsur 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.markersdessine des épingles décoratives au-dessus des tuiles à un lng/lat (un siège, par exemple) : l'épingle intégrée, ou votre proprepathdans sa boîtepathSize, mis à l'échellesizepx et ancré par la pointe. Donnez à un marqueurtile(l'idou lelabeld'une tuile) pour poser l'épingle sur cette tuile, où que la disposition l'ait placée ;lng/latrestent la solution de repli. Lesmarkersdu contexte gardent leslng/latdéclarés ; pour une épingle posée sur une tuile, lisez lex/yde la tuile danssymbols. 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 lex/yde 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 :
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é.
<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.
const ref = useRef<SymbolMapChartHandle>(null);
<SymbolMapChart ref={ref} {...props} timeline={{ speedMs: 1000, loop: true }} />;
// ref.current?.timeline() -> play() / pause() / seek(year) / seekIndex(i) / stepForward()<SymbolMapChart :options="{ ...props, timeline: { speedMs: 1000, loop: true } }" /><div use:symbolMapChart={{ ...props, timeline: { speedMs: 1000, loop: true } }}></div>applySymbolMapChartProps(this.c.nativeElement, { ...props, timeline: { speedMs: 1000, loop: true } });<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>speedMsrègle le rythme,loopreboucle,autoplay: truedémarre au montage,showControl: falsemasque la barre intégrée.- Les valeurs glissent d'une période à l'autre par défaut (
interpolate) ; ajustez le mouvement avectweenMseteasing, ou passezinterpolate: falsepour des coupes nettes. Avec reduced motion, la coupe est toujours nette. - Le contrôleur headless reste disponible :
chart.timeline()exposeplay() / pause() / toggle() / seek(period) / seekIndex(i) / stepForward() / stepBack(), plusonStepetformatPerioddans la config pour une UI maison. seek(period)cherche d'abord la période, en comparant le texte :seek(2021)etseek("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
daterestent visibles à chaque période.
Usage
import { SymbolMapChart } from "@michi-vz/react";
export default () => <SymbolMapChart {...props} />; // props = les options du graphique<script setup>
import { SymbolMapChart } from "@michi-vz/vue";
</script>
<template>
<SymbolMapChart :options="props" />
</template><script>
import { symbolMapChart } from "@michi-vz/svelte";
</script>
<div use:symbolMapChart={props}></div>// 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);<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>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 ;onChartDataProcessedn'est émis que lorsque le contexte change.
