Kijk in je grafieken
Een grafiek tekent pixels, maar de bugs zitten in de status erachter: de data die daadwerkelijk de engine bereikte, de asdomeinen, de host-box waartegen de grafiek gemeten werd, en welke punten waargenomen zijn versus voorspeld. @michi-vz/devtools is een opt-in, in-page paneel dat dit allemaal blootlegt voor elke michi-vz-grafiek op de pagina. Geen browserextensie nodig: het is één import, geversioneerd met je app.
What the panel sees: actual 0predicted 0
Klik op Mount devtools: het zwevende Michi-schild verschijnt rechtsonder - het ingeklapte gezicht van de devtools. Klik erop (of druk op
Ctrl/Cmd+Shift+M) om het paneel te openen, kies de grafiek in de lijst, en loop de tabbladen af: Overview (context, itemtabel, live bewerking), Props, Sizing, Scales, Diff, Hit-test, Profiler, A11y, en Insights - waar ✦ Narrate en ✦ Detect anomalies echte@michi-vz/insights-plugins tegen de live grafiek uitvoeren (de piek in de 2022 Cost wordt gemarkeerd; highlight hem vanuit het resultaat). Dit is het echte pakket, dat in je browser draait.
Snelstart
npm i -D @michi-vz/devtoolsimport { mountDevtools } from "@michi-vz/devtools";
// Call this BEFORE mounting charts so they register themselves.
// The floating Michi shield button appears; click it (or Ctrl/Cmd+Shift+M) to open the panel.
const devtools = mountDevtools();
import { mountLineChart } from "@michi-vz/core";
mountLineChart(host, { dataSet, xAxisDataType: "number" });
// later
devtools.destroy();Gebruik je React? Er is een oneliner die de devtools mount terwijl die in de boom staat en niks rendert (standaard alleen-dev - productiebuilds laten de devtools-chunk volledig vallen):
import { MichiVzDevtools } from "@michi-vz/react";
<MichiVzDevtools />Grafieken in dezelfde boom registreren zich bij het paneel, ook als ze in dezelfde render als <MichiVzDevtools /> mounten: de component zet de hook aan in een layout-effect, voordat de mount-effects van de grafieken draaien.
Voor Vue, Svelte, Angular, of plain web components is het recept dezelfde drie regels: roep mountDevtools() aan in de mount-hook van je rootcomponent, destroy() bij unmount. En voor builds waar devtools inert moet blijven zonder de importlocatie te veranderen, exporteert de @michi-vz/devtools/production-ingang een no-op mountDevtools.
De zwevende knop
De devtools mounten dekt je app nooit af: het start als het kleine Michi-schild (het wapenschild van de bibliotheek) in een hoek. Klik erop om het paneel te openen, sluit het paneel om de knop terug te krijgen, en de open/gesloten status wordt per browser onthouden - herlaad de pagina en de devtools komt precies terug zoals je het achterliet. Deel je een hoek met een chatwidget of een andere devtools-knop? Sleep het schild overal naartoe - die plek wordt ook onthouden. buttonPosition kiest de starthoek.
mountDevtools(options?) geeft een handle terug: { open, close, toggle, isOpen, refresh, getRoot, destroy }.
| Optie | Standaard | Opmerkingen |
|---|---|---|
container | document.body | Waar de shadow-host van het paneel aan gekoppeld wordt. |
open | onthouden | Forceer true/false; standaard herstelt de laatste status, gesloten bij de eerste run. |
hotkey | Ctrl/Cmd+Shift+M | Zet op null om de toetsenbord-toggle uit te schakelen. |
theme | "auto" | "auto" volgt prefers-color-scheme; of forceer "dark" / "light". |
buttonPosition | "bottom-right" | Starthoek voor de knop; een versleepte plek wint bij latere mounts. |
Het paneel rendert in zijn eigen Shadow DOM, zodat zijn stijlen niet kunnen lekken in je app (en de CSS van je app het paneel niet kan breken). De grafieken zelf blijven light DOM - het paneel raakt het kleurcontract nooit aan.
Werk je met een grote context of een lange serietabel? Sleep de linkerbovenhoek van het paneel om het te herschalen (de grootte wordt per browser onthouden), of druk op de ⛶-knop in de header om het te maximaliseren tot de volledige viewport en terug.
Dashboards met veel grafieken blijven overzichtelijk: de chartlijst heeft een filtervak, elk lijstitem heeft een ◎ locate-knop die de grafiek in beeld scrolt en er een omlijning omheen laat flitsen, en na 8 grafieken voegt het paneel updatebursts samen tot één re-render, zodat een drukke pagina nooit hapert omdat devtools open staat. Het paneel doet totaal geen polling - het reageert alleen op de events van de hook, en historie-snapshots slaan grafieken over waarvan de context en de props niet veranderd zijn.
De tabbladen
Sizing - "waarom is mijn grafiek onzichtbaar / overloopt hij?"
De meest voorkomende chartbug in elke bibliotheek is een sizing-bug: een host gemeten op 0×0 binnen een verborgen tab, of een grafiek geschaald vanuit clientWidth zonder padding af te trekken (ja, clientWidth bevat padding), zodat hij over zijn card heen loopt. Het Sizing-tabblad toont de gerenderde rect van de host, de client-box, en de padding naast de breedte/hoogte waar de grafiek om vroeg, markeert de mismatch in gewone taal, en bevat een kant-en-klaar ResizeObserver-recept - omdat michi-vz-grafieken by design vaste grootte hebben en responsiviteit bij de host hoort.
Scales - "waarom kloppen mijn asWaarden niet?"
Rendert de live xAxis / yAxis-domeinen rechtstreeks vanuit de ChartContext, met gezondheidscontroles voor de drie klassieke faalmodi: een NaN-domein (een datum of waarde kon niet geparst worden), een nul-breed domein (elke waarde identiek, marks vallen samen), en een omgekeerd domein (een handmatige domain-prop achterstevoren doorgegeven). Grafieken zonder x/y-assen die hun waarden toch via een schaal tekenen, tonen die schaal: het radiale domein [0, maxValue] van de radar, de boog [min, max] van de gauge, de laagste en hoogste gekoppelde waarde van de choropleth, en de colorScale-drempels van de symboolkaart (of het waardebereik als die geen colorScale heeft). Pie, sankey en treemap plaatsen marks zonder waardeschaal, en zeggen dat.
Diff - "wat is er veranderd tussen deze twee renders?"
Het paneel maakt een snapshot van de ChartContext van elke grafiek bij elke update en houdt een korte historie bij. Het Diff-tabblad vergelijkt de laatste twee snapshots diepgaand tot een toegevoegd/verwijderd/veranderd-lijst met exacte paden, zodat "mijn grafiek ziet er anders uit en ik weet niet waarom" een antwoord van twee regels wordt. Items in arrays worden gekoppeld op hun label, key, id of code: een pad noemt het item (series["Revenue"].max: 140 → 555) en een nieuwe Top-N-volgorde verschijnt als één reordered-regel in plaats van een muur aan indexwijzigingen (hetzelfde geldt voor een lijst als renderedRankedIds). Elke snapshot bewaart ook een kopie van de props zonder data, dus een update die alleen props wijzigt (een highlight, een aangepaste barRadius) is ook een snapshot en verschijnt onder props. (props.highlightItems). Stap terug door de History-balk en de diff volgt de snapshot die je bekijkt.
Insights - de grafiek legt zichzelf uit
Elke michi-vz-grafiek draagt al een summary in gewone taal in zijn context - dezelfde tekst die een AI-agent of schermlezerpipeline consumeert. Het Insights-tabblad toont het in een AI-gestileerde bubbel, en wanneer @michi-vz/insights aan de grafiek gekoppeld is, licht het one-click acties op die via getTools() ontdekt worden:
- ✦ Narrate -
chart.use(narrate())- deterministische prozanarratie van de huidige status. - ✦ Detect anomalies -
chart.use(anomaly())- markeert uitschieters per serie; het resultaat biedt een one-click highlight van de gemarkeerde serie op de live grafiek. - ✦ Forecast -
chart.use(forecast())- de geprojecteerde punten, nauwkeurigheid, en elke drempeloverschrijding.
Al het andere dat een plugin blootstelt, verschijnt onder Advanced als een rauwe tool-runner (JSON-argumenten erin, JSON-resultaat eruit).
Hit-test - "waarom vuurt mijn tooltip niet?"
Canvas-marks hebben geen DOM, dus wanneer een hover stopt met werken, is er niets te inspecteren in het Elements-paneel - je kunt een hit-test-bug niet onderscheiden van een dode listener of een CSS-pointer-events-probleem. Voor de grafieken die hun canvas-hit-tests melden (bubble, fountain, line, radial tree, scatter, symbol map en treemap) streamt het Hit-test-tabblad ze live: elke pointer-beweging logt zijn coördinaten en de mark die eruit kwam (of een misser), en een groene/rode marker volgt het laatste event op de grafiek zelf. De doorslaggevende diagnose is stilte: als je aan het hoveren bent en de log beweegt niet, is de canvas-listener van de grafiek dood. De andere grafieken melden nog geen canvas-hits, en het tabblad zegt precies dat in plaats van de listener de schuld te geven.
Voor een grafiek die als SVG getekend is, wordt het tabblad een SVG-inspector: beweeg de pointer over de grafiek en hij logt het bovenste element onder de pointer (tag, class, positie tussen zijn broers en zussen) en een kleursleutel, gelezen van de dichtstbijzijnde voorouder met data-label-safe / data-label van het eerste element onder de pointer dat er een heeft. Een transparant hitdoel draagt geen sleutel, dus boven een mark van een symboolkaart toont de rij circle.symbol-hit over circle.symbol en de sleutel van de mark eronder. De kleursleutel is het haakje voor het kleurcontract, niet de identiteit van de mark: een sankey-link draagt bijvoorbeeld de sleutel van zijn bron- of doelknoop. Lagen met pointer-events: none, zoals de annotaties van de gauge, zijn er onzichtbaar voor.
Profiler - "waarom werd dit traag?"
Elke update() wordt getimed aan de engine-grens. Het Profiler-tabblad toont de laatste/gemiddelde/maximale render-duren met een per-update balkenstrip, en waarschuwt wanneer de rendertijd oploopt - de gebruikelijke verdachten zijn groeiende data, niet-gememoïzeerde props die volledige re-renders forceren, of gelekte listeners.
A11y - de audit die geen enkele chart-devtool doet
Op Chartability geïnspireerde heuristieken lopen tegen de live context: een ontbrekende summary in gewone taal (schermlezers en AI-agents krijgen niets), een a11y-tabel met minder rijen dan series, twee series die één kleur delen (niet te onderscheiden zonder zicht), en kleuren onder de 3:1-graphics-contrastratio op een lichte of donkere achtergrond, één keer per kleur gecontroleerd, met elke serie die die kleur gebruikt erbij genoemd. Choropleth- en symboolkaarten, en elke grafiek met een colorScale, kleuren per klasse, dus plekken in dezelfde klasse delen met opzet een kleur. Daar controleert het tabblad in plaats daarvan de kleurschaal: naburige stappen die moeilijk uit elkaar te houden zijn, en een noDataColor die (bijna) gelijk is aan een stap. Onder de audit rendert het tabblad de daadwerkelijke a11y-datatabel - precies wat een schermlezer krijgt.
Props - "met welke instellingen draait deze grafiek?"
Veel opties komen nooit in de ChartContext terecht: barRadius, stacked / stackOffset van een vlakdiagram, hoverHighlight van de sankey, de boog van een gauge, zoom, en de timeline- en reveal-configuraties. Het Props-tabblad toont wat getProps() teruggeeft, als boom: de dataprops (dataSet, series, nodes, links, ...) klappen in tot Array(n) en functies verschijnen als ƒ met hun naam.
Props horen ook bij de historie: een update die alleen props wijzigt (een highlight, een stijlschakelaar) is een snapshot die het Diff-tabblad kan tonen. Als highlightItems of disabledItems meer dan 10 keer per seconde verandert, telkens in een update die geen andere prop wijzigt, waarschuwt het tabblad en noemt het de prop: meestal stuurt een app dan onHighlightItem bij elke hover terug naar highlightItems. Laat de grafiek de hover zelf afhandelen (bijvoorbeeld met hoverHighlight van de sankey) en houd highlightItems voor selecties. De andere props worden volledig vergeleken, data inbegrepen: een responsieve grafiek schalen, een slider op barRadius verslepen of een tijdlijn afspelen die bij elk frame een nieuwe dataSet doorgeeft en intussen de huidige koploper uitlicht, geeft nooit een waarschuwing. Data die ter plekke is aangepast en opnieuw als dezelfde array wordt doorgegeven, telt alleen als wijziging wanneer de lengte verandert. Functies worden op naam vergeleken, dus de verse callbacks die een React-render doorgeeft verbergen geen echo. De waarschuwing verdwijnt na 30 seconden zonder zulke wijzigingen.
Bewerkingen vanuit het paneel gaan rechtstreeks naar de grafiekinstantie. Bij een grafiek die door React of een web component beheerd wordt, geeft de volgende render van de wrapper zijn eigen props opnieuw door en overschrijft die bewerkingen.
Overview - inspecteren, besturen, bewerken
De klassieke inspector: de samenvatting, de statistieken, en een Items-tabel opgebouwd uit de array per item van de context - series, de jets van een fontein, de rings van een gauge, de nodes van een sankey, de leaves van een treemap, de regions of symbols van een kaart, enzovoort, met een keuzelijst als er meer dan één is. Klik op een kolom om te sorteren (de jets van een fontein op bereik, bijvoorbeeld) en hover over een rij om dat item op de grafiek te highlighten. Die hover komt niet in de historie, en zodra de pointer de tabel verlaat, komt de highlight terug die de app had ingesteld. Series met een voorspelling tonen de daadwerkelijk-versus-voorspeld-splitsing hieronder. De highlight/disable-toggles tonen elk label dat de grafiek kent (legenda, kleuren, de huidige highlight- en disable-props), zodat een serie die je uitschakelt in de lijst blijft. De JSON-editor bewerkt de dataprop die de grafiek heeft (dataSet, series, data, of nodes en links samen bij een sankey) - bewerk, druk op Apply, en kijk hoe de grafiek opnieuw rendert. Te veel mee gerommeld? Reset chart herstelt de data, highlight, en disable-status naar precies wat ze waren toen devtools de grafiek voor het eerst zag - elke door het paneel gedreven bewerking in één klik teruggedraaid.
Trouwens: de ✦-acties op het Insights-tabblad zijn standaard geen taalmodel - ze voeren de insights-plugins van de grafiek lokaal uit (deterministische regels en statistiek; de tooltip van elke actie zegt precies wat hij berekent). Er wordt niets gedownload, niets verlaat de pagina.
Tijdreizen door status
Wanneer een grafiek meer dan één keer veranderd is, verschijnt een History-balk: stap ◀ / ▶ door vorige snapshots om precies te zien hoe de status evolueerde, of klik op ● live om terug te keren naar de laatste. Snapshots vangen ook de renders die een grafiek zelf start: een timeline-stap, het einde van een timeline-overgang, een zoom op een lijngrafiek, de asynchrone layout van een bubbelgrafiek, de overstap naar WebGPU, en een plugin die met use() is toegevoegd. Terwijl je een eerdere snapshot bekijkt, zijn de besturingselementen alleen-lezen (je inspecteert historie, je bestuurt de grafiek niet). In combinatie met het Diff-tabblad beantwoordt dit binnen enkele seconden "hoe zag deze grafiek eruit één update geleden, en wat is er veranderd?". Wanneer een grafiek vernietigd wordt of van de pagina verdwijnt, gooit het paneel de historie en de rest van de status die het voor die grafiek bijhield weg.
Daadwerkelijk versus voorspeld
Het paneel maakt de herkomst van een grafiek expliciet. Markeer voorspelde punten met predicted: true op het datapunt (het is achterwaarts compatibel: wanneer weggelaten, wordt het afgeleid van certainty === false, dezelfde vlag die een segment gestreept tekent):
const dataSet = [{
label: "Revenue",
series: [
{ date: 2022, value: 104, certainty: true }, // observed
{ date: 2023, value: 121, certainty: false, predicted: true }, // forecast
],
}];Dat stroomt door in de ChartContext van elke grafiek als actualCount, predictedCount, en forecastStart (per serie op Line, Fan, en Range), zodat het paneel - en elke AI-agent die de context leest - het verleden van de projectie kan onderscheiden zonder te gokken op streepjes.
De gestapelde Area-grafiek deelt één x per datum, dus daar wordt predicted op de hele rij gezet en verschijnt het op chartniveau als stats.actualRows, stats.predictedRows, en stats.forecastStart:
const series = [
{ date: 2022, cloud: 60, onprem: 44 }, // observed
{ date: 2023, cloud: 78, onprem: 40, predicted: true }, // forecast row
];Geef de voorkeur aan `predicted` boven `certainty` om een forecast te markeren.
certainty wordt ook false voor automatisch gedetecteerde gaten in de data (detectGaps), dus het kan een forecast niet onderscheiden van een gat in de data. predicted is ondubbelzinnig.
Dekking. Herkomst is een tijdreeksidee, dus het wordt gedragen door de grafieken waar een forecast natuurlijk is: Line, Fan, en Range (per serie), en Area (per rij). De categorische, part-to-whole, en relationele grafieken (stacked bar, bar-bell, comparable, dual, gap, pie/donut, bubble, sankey, treemap, radar, scatter) hebben geen forecast-as, dus dragen ze geen predicted-vlag.
Waarom geen browserextensie?
Je hebt er geen nodig. Elke michi-vz-grafiek is Light DOM en stelt zijn status al bloot (getContext(), getTools()), zodat een in-page paneel alles rechtstreeks kan lezen. Dat maakt de devtools:
- Zero-install - het is gewoon een
import, geversioneerd met je app. - Testbaar voordat je shipt - het draait in jsdom/Playwright als elke andere module.
- Framework-onafhankelijk - het ontdekt zowel imperatieve
mountXChart()-instanties als<michi-vz-*>-webcomponents. - Prod-veilig - gate het achter
process.env.NODE_ENV !== "production"(de React-component doet dit voor je) of importeer@michi-vz/devtools/production; hoe dan ook downloaden je gebruikers het nooit.
Een echte browserextensie is pas later de moeite waard, om michi-vz te inspecteren op pagina's die de devtools-module niet bundelen. Het zou dezelfde hook hergebruiken, dus niets hier is weggegooid werk.
Hoe het werkt
@michi-vz/core levert een kleine opt-in hook. mountDevtools() roept enableDevtools() aan, die globalThis.__MICHI_VZ_DEVTOOLS_HOOK__ installeert - een registry waar elke mountXChart() naar schrijft bij het mounten en wist bij destroy(). Het paneel abonneert zich erop voor updates. Om web components te vinden die vóór de devtools gemount zijn, zoekt het ook in de DOM naar grafiekhosts (de class .michi-vz) en klimt het naar het omringende <michi-vz-*>-element, waarbij het overslaat wat de hook al kent. Elk element houdt het id dat het de eerste keer kreeg: zijn eigen id-attribuut de eerste keer dat het paneel dat id tegenkomt, anders een genummerd id dat nog geen andere grafiek had. Twee grafieken delen nooit een id, en het verwijderen van één grafiek geeft nooit zijn historie of besturingselementen door aan een andere. Een hook-listener die een fout gooit, wordt gemeld met console.error en breekt nooit je chart.update(). Wanneer devtools nooit ingeschakeld wordt, wordt de hook nooit aangemaakt en betalen grafieken slechts één vlagcontrole per mount.
Je kunt je eigen UI (of een toekomstige extensie) bouwen tegen hetzelfde oppervlak:
import { getDevtoolsHook, enableDevtools } from "@michi-vz/core";
enableDevtools();
const hook = getDevtoolsHook(); // { charts: Map, subscribe, ... }
hook?.subscribe((charts) => {
for (const c of charts) console.log(c.chartType, c.getContext());
});Elke grafiekentry zegt ook of zijn engine canvas-hits meldt (hitReporting: "canvas" of "none"); zo weet het Hit-test-tabblad wanneer stilte een dode listener betekent.
