Symboolkaart
Geografie
Grafiek #20: een bellenkaart waarvan overlappingen worden opgelost door een krachtsimulatie. Migratiedoel voor de legacy sdg-trade MapSymbolForce - een dot-only bellenkaart waarbij de coördinaten van elk item op het vlak worden geprojecteerd, waarna een eenmalige krachtsimulatie overlappende cirkels net genoeg uit elkaar duwt om leesbaar te blijven, zonder ze ver van hun werkelijke positie te verplaatsen. In tegenstelling tot Choroplethenkaart is geografie hier optioneel: laat het weg voor de dot-only look van de legacy-grafiek (de standaard), of geef het op om een gedempte achtergrondlandmassa achter de symbolen te tekenen.
Een concentrische tweede ring (valueSecond) leest als een submetriek van de buitenste cirkel - bijvoorbeeld "waarvan" een specifieke partner of kanaal:
Exacte posities of force-ontklontering
positionMode bepaalt wat de positie van een symbool betekent:
"force"(standaard, legacy-pariteit): de eenmalige simulatie duwt overlappende cirkels uit elkaar. Leesbaar wanneer veel symbolen op één plek samenklonteren, maar elk symbool kan van zijn echte coördinaten afdrijven, en op een klein plot kan die afwijking groot zijn."precise": elk symbool blijft exact op zijn geprojecteerdelng/lat; overlappende cirkels zijn toegestaan.
Een symbool boven het verkeerde land is een cartografisch nauwkeurigheidsprobleem en voor sommige doelgroepen politiek gevoelig. Kies "precise" zodra er een geography-achtergrond zichtbaar is: een landmassa nodigt uit om posities letterlijk te lezen.
Schakel naar force en zie de bellen van hun echte coördinaten wegdrijven om botsingen op te lossen. Met een zichtbare landmassa is precise meestal de eerlijke keuze.
Alleen demogeografie - en grenzen zijn politiek
De wereldatlas op deze pagina is een vereenvoudigd GeoJSON-bestand uit het publieke domein, meegeleverd met de documentatievoorbeelden ter illustratie. Hij kan fout zijn: grenzen, namen en vormen zijn NIET gezaghebbend, en sommige zijn omstreden. Welke grenzen en namen "juist" zijn verschilt per publiek, rechtsgebied en organisatie, en een kaart die het mis heeft is een politiek probleem, geen cosmetisch. Niets wat hier wordt getekend is een erkenning van een grens. Controleer vóór productiegebruik de grenzen en naamgeving van het geography-bestand dat je doorgeeft aan het cartografische beleid van je organisatie: de bibliotheek tekent het bestand zoals het is en corrigeert niets.
De grafiek hierboven is dezelfde engine in elk framework - alleen de integratiecode hieronder verschilt.
Zeshoekige tegels in een honingraat
Soms moet elke plek even zwaar wegen en hoort de waarde in de kleur: een tegelkaart. Met shape: "hexagon" en positionMode: "honeycomb" wordt elk symbool een even grote zeshoek die op een zeshoekig rooster klikt: tegels betegelen het vlak, overlappen nooit, en de tegel van een land blijft zo dicht bij zijn zwaartepunt als het rooster toelaat. colorScale (een scaleThreshold-domein/bereik, hetzelfde contract als de Choropletenkaart) draagt de waarde; markeringen worden op volle dekking geschilderd omdat de kleur de codering is.
De waarschuwing over demogeografie hierboven geldt hier ook: de tegels liggen op een illustratieve atlas, en de positie van elke tegel, en welk land hij voorstelt, is alleen zo juist als de
geographyen coördinaten die je aanlevert.
honeycomb-{ radius, gap, orientation, placement, reserve }: omgeschreven straal van de tegel in px (standaard 11), ruimte tussen tegelranden (standaard 2), en"flat"(standaard, platte kant boven) of"pointy". De oriëntatie stuurt zowel de zeshoekcontour als het rooster, zodat de tegels passen.placementenreservestaan hieronder beschreven.- Botsingen - standaard (
honeycomb.placement: "first") - worden indataSet-volgorde opgelost: een latere tegel waarvan de cel bezet is, loopt ring voor ring naar buiten tot de eerste vrije cel, deterministisch. Is er binnen zes ringen geen cel vrij, dan overlapt de tegel en noemt eenonDataWarningvan het typelayout-overflowhem. honeycomb.placement: "geographic"houdt buren in hun echte volgorde. De tegel die het dichtst bij het midden van een cel ligt, houdt die cel; een verplaatste tegel krijgt de vrije cel die het dichtst bij zijn echte positie ligt, en een herstelronde verwisselt of verschuift buurtegels die oost voor west of noord voor zuid zouden lezen. Het resultaat hangt niet af van de volgorde indataSet. Het doet meer werk dan"first"(een herstelronde nadat de cellen zijn toegewezen), ongeveer 25 ms voor 200 tegels. Buurparen die nog in de verkeerde volgorde staan, worden geteld in de context (stats.outOfOrderCount, gemeten voor elkeplacement, zodat je"first"kunt vergelijken; bij"geographic"is het wat na het herstel overblijft) en alleen bij"geographic"genoemd in eenonDataWarningvan het typelayout-out-of-order;stats.displacedCounttelt tegels die niet in hun dichtstbijzijnde cel staan.honeycomb.reserve: "all"laat ook tegels zonder waarde een cel claimen, zodat de indeling gelijk blijft als de waarden veranderen (een kaart die wisselt tussen producten of jaren). Tegels diedisabledItemsofradiusVisibleMinweghalen, doen nog steeds niet mee. De standaard"valued"houdt tegels zonder gegevens uit de weg.fit: "width"berekent de indeling één keer opwidthxheighten schaalt de tekening naar de container (nooit groter danwidth), zodat tegels blijven staan als het venster van grootte verandert. De standaard"none"tekent op exacte pixelgrootte.- Geen waarde? Laat
valueweg. Het item krijgtnoDataColor(standaard#d2d7dd, geef"transparent"om het te verbergen), valt buiten de kleurschaal en claimt, met de standaardhoneycomb.reserve: "valued", nooit een cel: een "geen data"-tegel kan echte data niet wegduwen (honeycomb.reserve: "all"laat ze ook een cel houden, zie hierboven). offsetop een item verschuift het in px na de projectie en vóór het klikken: de uitweg voor een klein land dat onder een grote buur schuilgaat.markerstekent decoratieve spelden boven de tegels op een lng/lat (bijvoorbeeld een hoofdkantoor): de ingebouwde kaartspeld, of je eigenpathin zijnpathSize-vak, geschaald naarsizepx en met de punt verankerd. Geef een markertile(deidoflabelvan een tegel) om de speld op die tegel te zetten, waar de indeling hem ook plaatste;lng/latblijven de terugval. Demarkersin de context houden de opgegevenlng/lat; lees voor een speld op een tegel dex/yvan die tegel uitsymbols. Spelden worden niet gepositioneerd, niet aangeklikt en hebben geen tooltip; stijl ze via.symbol-map-marker. De context vermeldt dex/yvan elk geplaatst symbool voor je eigen overlays en legenda.
shape: "hexagon" werkt ook alleen met "force"- of "precise"-plaatsing, en colorScale werkt met cirkels: de bouwstenen combineren.
Wat van jou blijft
De bibliotheek tekent tegels, spelden en kleuren. Legendatekst, tooltip-tekst, getalnotatie, welk land "thuis" is en welk palet het krijgt zijn van de consument: geef ze door via colorScale, markers en tooltipFormatter.
Breng je eigen coördinaten mee
In tegenstelling tot de legacy-grafiek (die een statische CSV van ongeveer 200 landcoördinaten bundelde), bundelt @michi-vz/core geen coördinatentabel. Elk dataSet-item levert zijn eigen 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 },
// ...
]}
/>Omdat de extent die wordt gebruikt om de dot-only lay-out op het plotgebied te passen, wordt afgeleid van de coördinaten van je eigen dataset (niet van een gebundelde, altijd-volledige wereldtabel), strekt de "wereld" die de grafiek tekent zich alleen zo ver uit als de punten die je aanlevert - zie Gedragsnotities hieronder.
Symbolen plaatsen vanuit de geografie
Met een geography-achtergrond kun je lng/lat weglaten: een item komt dan op het zwaartepunt van de feature waarvan de id gelijk is aan zijn geoId (standaard: de eigen id). Rijen met een ISO-code sluiten zo zonder enige coördinatentabel aan op een wereldbestand met dezelfde sleutels; een item dat geen feature vindt wordt weggelaten met een invalid-geometry-waarschuwing die de gezochte id noemt.
<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 bepaalt de tekst in een symbool dat groot genoeg is (standaard: het label), zodat een aandeel of een waarde naast de naam kan staan; de tekst wordt nog steeds in de cirkel gepast. onSymbolClick(item, event) vuurt bij een klik op een symbool, in svg en via de canvas-hittest; de klik pint de tooltip nog steeds.
Speel door de jaren heen
Geef elk symbool een date en zet timeline aan: de kaart wordt een verhaal per jaar, met een eigen afspeelknop en scrubber die telkens de symbolen van één periode plaatst. Standaard uit - zonder opt-in verandert er niets.
Druk op de afspeelknop onder de grafiek: de data stapt door de jaren, één momentopname per keer. Sleep de scrubber om naar een jaar te springen.
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>speedMsbepaalt het tempo,loopbegint opnieuw,autoplay: truestart bij mounten,showControl: falseverbergt de ingebouwde balk.- Waarden glijden standaard tussen periodes (
interpolate); stem de beweging af mettweenMseneasing, of zetinterpolate: falsevoor harde overgangen. Met reduced motion is de overgang altijd hard. - De headless controller is altijd beschikbaar:
chart.timeline()biedtplay() / pause() / toggle() / seek(period) / seekIndex(i) / stepForward() / stepBack(), plusonStepenformatPeriodin de config voor eigen UI. seek(period)zoekt eerst de periode en vergelijkt als tekst, dusseek(2021)enseek("2021")komen allebei op 2021 uit, of je datums nu getallen of strings zijn. Een getal telt alleen als positie (0 = de eerste) als geen periode past, en een string die nergens bij past doet niets.seekIndex(i)gaat altijd op positie, net als de ingebouwde scrubber.- Symbolen zonder
dateblijven in elke periode zichtbaar.
Gebruik
import { SymbolMapChart } from "@michi-vz/react";
export default () => <SymbolMapChart {...props} />; // props = de grafiekopties<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 - registreer de elementen eenmalig
import "@michi-vz/angular";
import { applySymbolMapChartProps } from "@michi-vz/angular";
// component (gebruikt 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(); // renderer-onafhankelijk, LLM-klaar
chart.destroy();API
Props zijn getypeerd als SymbolMapChartProps in @michi-vz/core. Gedeeld door alle grafieken: width, height, margin, colors / colorsMapping, renderer ("svg", "canvas", of experimenteel "webgpu"), highlightItems, disabledItems, en de on*-callbacks. onChartDataProcessed / getContext() retourneren de renderer-onafhankelijke ChartContext.
Gedragsnotities
De eenmalige krachtgebaseerde overlap-oplossing
De lng/lat van elk item projecteert naar een "echte" pixelpositie; forceX/forceY zetten de simulatie vast op precies die positie als doel, forceManyBody() voegt lichte scheiding toe, en forceCollide (straal + 2px opvulling, 3 iteraties) is wat overlappende cirkels daadwerkelijk uit elkaar duwt. De simulatie stabiliseert synchroon tot DEZELFDE alfa-drempel als de legacy-grafiek (0.0011) - deterministisch: identieke invoer stabiliseert altijd tot dezelfde lay-out, dus twee keer dezelfde data monteren levert pixel-identieke posities op. Twee items met exact dezelfde coördinaten beginnen gestapeld en scheiden netjes zodra de simulatie stabiliseert. positionMode: "precise" slaat de simulatie volledig over: elk symbool blijft dan op zijn exacte geprojecteerde positie en overlappingen zijn toegestaan (zie de schakel-demo hierboven).
Dot-only vs. achtergrondprojectie
Zonder geography wordt de gekozen projection (standaard "geoMercator", zoals de legacy-grafiek) ongetuned gebruikt - een kale projectiefabriek zonder de translate/scale/rotate/center-afstemming van Choroplethenkaart - en de extent van de geprojecteerde punten wordt vervolgens herschaald om het plotgebied te vullen, wat de eigen berekening van de legacy-grafiek weerspiegelt. Met geography opgegeven neemt in plaats daarvan DEZELFDE afgestemde dispatch die Choroplethenkaart gebruikt het over, zodat de achtergrondlandmassa en de symboolcoördinaten één consistente geografische framing delen (geen extent-herschaling).
radiusVisibleMin - gefilterd op de ruwe waarde
radiusVisibleMin verbergt items waarvan de ruwe value op of onder de drempel ligt (en, wanneer valueSecond is ingesteld, waarvan valueSecond ook eronder ligt) voordat de krachtlay-out wordt uitgevoerd - overgenomen van het eigen filter van de legacy-grafiek, dat de ruwe waarde vergeleek, nooit de geschaalde straal. Het domein van de straal-/opaciteitsschaal wordt nog steeds opgebouwd uit elk item met geldige coördinaten, ongeacht dit filter, zodat de straal van een symbool vergelijkbaar blijft tussen renders, zelfs als de drempel verandert.
De concentrische tweede ring
valueSecond tekent een tweede cirkel, dezelfde kleur als de buitenste, op opacity - 0.3 (begrensd op niet-negatief), BOVENOP de primaire cirkel getekend - exact overgenomen van de eigen gelaagdheid van de legacy ForceNode. Wanneer de tweede cirkel kleiner is dan de buitenste, leest deze als een dovere kern binnen een lichtere ring (bijv. "waarvan"); wanneer groter, bedekt hij de buitenste cirkel volledig.
Optionele achtergrondgeografie
geography (een GeoJSON FeatureCollection of een vooraf genormaliseerde GeoFeatureItem[], zelfde contract als de geography van Choroplethenkaart) is een nieuwe mogelijkheid - de legacy-grafiek tekende nooit landmassa. Laat het weg voor de dot-only legacy-look; geef het op voor een gedempte, niet-interactieve achtergrond (geographyColor, standaard #eef1f5) achter de symbolen.
Rendering: SVG, canvas en een gedelegeerde WebGPU
renderer="svg" (standaard) tekent één <circle class="symbol"> per zichtbaar item (plus een optionele <circle class="symbol-second">). renderer="canvas" tekent dezelfde markeringen op een 2D-canvas, waarbij de vulkleur wordt opgelost via dezelfde consumenten-CSS-sonde die elke enkelvoudige-markeringsgrafiek gebruikt. renderer="webgpu" delegeert naar de canvas-2D-renderer, dezelfde reden als Choroplethenkaart: de optionele achtergrond is willekeurige (mogelijk concave, meerringige) GeoJSON, dus correcte GPU-tessellatie is hier onevenredig veel werk.
Laden / geen gegevens
isLoading en isNodata sturen de overlay aan (React: isLoadingComponent / isNodataComponent), identiek aan elke andere grafiek in het huis.
Kleurautoriteiten aan consumentzijde: de context bevat
legendData({ label, color, dataLabelSafe }) zodat een CSS-injectie-kleursysteem regels per label kan koppelen;onChartDataProcessedwordt alleen uitgezonden wanneer de context verandert.
