11 · Het web als graaf: opslag, analyse en weergave
Onderzoeksagent R11 · 24 september 2026 · regie: agent B · alleen onderzoek, niets in de app gewijzigd
Hoe er gemeten is. Op deze Mac (16 kernen, Node 26.9; de server met Node 24 is niet gemeten). De testgraaf: de 21.476 echte BAG-adressen uit server/data/wijk/ met straat, wijk en bouwdecennium, plus synthetisch 40 knopen en 60 kanten per huis (Sanne heeft er 46 en 60). Samen 0,88 miljoen knopen en 1,33 miljoen kanten. Bundels: esbuild 0.28.2 geminificeerd, daarna gzip -9. De scripts staan in de scratchpad. Wat niet gemeten is, heet schatting.
1 · Samenvatting in tien regels
- Blijf bij node:sqlite. Op stadsschaal antwoordt het in 0,1 tot 3 ms op wat een bezoeker vraagt.
- De graaf is een afgeleide van relaties, tafel en registers, in twee lagen. De stenen (huis, straat, wijk, bron, jaar, naamgever) mogen vrij geanalyseerd worden; de mensen (jij, zinnen, fans) alleen opgeteld en met k ≥ 5.
- LadybugDB, de opvolger van het in oktober 2025 gearchiveerde Kuzu, is jong en zwaarder (740 tegen 130 MB) en heeft geen musl-bouw voor Alpine in de lijst. Dat is iets voor later.
- Neo4j en Memgraph geven Cypher en algoritmen, maar ook een tweede plek om persoonsgegevens te wissen. Neo4j's weergave (NVL) mag niet met Community.
- RDF (Oxigraph) hoort aan de rand: voor import en voor een latere Linked Data-export. Het gebruikte 1,9 GB voor 1,33 miljoen triples.
- Weergave: het huis-web blijft d3-force op eigen canvas, de stad krijgt Sigma.js (37 kB, MIT), later komt @cosmos.gl/graph (MIT). Cosmograph zelf is CC-BY-NC.
- Supersexy én begrijpelijk: klein beginnen, plaats die iets betekent, beweging alleen voor wat verandert, en elke lijn leesbaar als zin.
- Drie beelden: de stad als sterrenhemel, vier zoomniveaus, en een verhaalpad dat Carmen voorleest.
- Elke nuttige analyse lukt in SQL en JavaScript. "Lijkt op" kost 0,7 ms per huis, Louvain op 343 straten 2 ms.
- Zaterdag alleen uitbreidingen van het bestaande scherm, de stadsgraaf over twee weken, en een graafdatabase pas bij een meetbare aanleiding.
2 · Opslag en analyse
| Optie | Kan | Gemeten of gedocumenteerd | Last, licentie | Migratie | Zaterdag |
|---|---|---|---|---|---|
| (a) node:sqlite + eigen module | paden via recursieve CTE of BFS in JS; clusters, centraliteit en gelijkenis in JS (graphology); tijd via sinds/tot |
laden 3,4 s; 256 MB; ego 1/2/3: 0,1/0,2/2,6 ms; "lijkt op" 0,3 ms; straat × decennium 72 ms; kortste pad 14 ms; 130 MB geheugen | geen extra: zelfde bestand en back-up; in Node 24 een release candidate | web-graaf.mjs levert al knopen en kanten |
de hele stad live in milliseconden |
| (b) LadybugDB (ex-Kuzu) | Cypher, kolomopslag, vector- en tekstindex, algoritmen | COPY 2,9 s; 112 MB; ego 1/2: 2,4/11 ms; "lijkt op" 217 ms; kortste pad 645 ms; 740 MB geheugen | MIT; fork sinds november 2025; binair bestand van 30 MB; geen linux-musl | CSV uit SQLite, dan COPY | hetzelfde, met Cypher |
| (c) Neo4j Community | Cypher; GDS met alle algoritmen, maar hoogstens 4 threads en 3 modellen | niet gemeten; standaard 512 MB heap + 512 MB cache, realistisch 2–4 GB (schatting) | GPLv3; tweede container, JVM, synchronisatie | neo4j-driver, synchronisatie per nacht |
Browser achter de schermen; NVL alleen met Aura of commercieel |
| (d) Memgraph | Cypher, 50+ algoritmen (MAGE) | volgens hun formule ≈ 0,46 GB zonder eigenschappen (berekend) | BSL 1.1 (geen OSI-licentie); alles in geheugen | via Bolt | Lab achter de schermen |
| (e) Oxigraph (RDF) | SPARQL 1.1, bron per uitspraak, federatie met Wikidata en RCE | JS/WASM: laden 3,5 s; ego 1: 12 ms; "lijkt op" 2,5 ms; 1,9 GB geheugen; in JS alleen in geheugen | MIT/Apache; de server bewaart wel op schijf | alles omzetten naar triples | weinig nieuws |
Tussen (a) en (b) verschillen de ego-tellingen iets, doordat drukke knopen anders worden overgeslagen; het gaat om de orde van grootte. DuckDB met DuckPGQ is een tussenvorm, maar het is een community-extensie in ontwikkeling en we hebben hem niet nodig.
Advies met fasering
- Nu, tot en met 26 september. Niets nieuws opslaan: de graaf blijft een projectie die bij elk verzoek wordt opgebouwd. Hoogstens een route
GET /api/stad/webdie alleen leest, bij het opstarten opgebouwd uit de wijkbestanden en straat-maten (zoalslib/wijk.mjs). - Over twee weken. Migratie
0003-web.sqlmet de tabellenknoopenkant. Elke rij krijgtlaag(steen of mens),huis_id(om te kunnen wissen),bron,sindsentot. De tabellen zijnWITHOUT ROWID, met indexen op(van, soort)en(naar, soort), zoals in de meting.- Gedeelde knopen krijgen vaste id's:
straat:<slak>|<plaats>,wijk:<buurtcode>,bron:bag,jaar:1910,naamgever:<Wikidata-QID>. Knopen van één huis komen onderhuis:<id>/….jij,fansencarmenworden nooit gedeeld. - Schrijven gebeurt in
meldWebVeranderd: per huis alles wissen en opnieuw schrijven, in één transactie. De bron van waarheid blijft waar hij is, dusmagKlinken, verbergen en "wis alles" blijven op één plek. - Drukke knopen worden nooit doorlopen (
bron:BAGalleen al raakt 64.428 kanten). - Analyses draaien als nachtjob in een worker en eindigen als tabellen, zodat een route alleen hoeft te lezen.
- Gedeelde knopen krijgen vaste id's:
- Later, alleen met een meetbare aanleiding. Denk aan een p95 boven 100 ms, ad-hoc Cypher voor een analist, embeddings op structuur, of meer dan ~30 miljoen kanten (schatting: het stenen skelet van heel Nederland). Dan eerst LadybugDB als zijspoor dat alleen leest en alleen de stenenlaag bevat (op een glibc-basis). De mensenlaag gaat er nooit in.
3 · Weergave
| Bibliotheek | Renderer | Soepel tot | gzip (gemeten) | Licentie | Svelte, tokens | Toegankelijk, mobiel |
|---|---|---|---|---|---|---|
| d3-force + eigen canvas (nu) | Canvas 2D | 370 knopen op 60 Hz (gemeten 23-09) | 5,7 kB | ISC | past; leest tokens | knoppenlijst, reduced motion, gebaren |
| Sigma.js 3 + Graphology | WebGL | ~100k kanten (volgens Linkurious, een concurrent) | 37 kB; 45 kB met Louvain en ForceAtlas2 | MIT | in onMount; tokens via getComputedStyle |
niets ingebouwd; aanraken werkt |
| @cosmos.gl/graph | WebGL2, lay-out op de GPU | "honderdduizenden" | 188 kB | MIT (Cosmograph: CC-BY-NC-4.0, 578 kB) | geen labels, zelf erboven | aanraken sinds v3 |
| force-graph / 3d-force-graph | Canvas / three.js | voorbeeld met ~4k | 60 / 375 kB | MIT | stijl via accessors | 3D zwaar op een telefoon; bestaat ook voor VR en AR |
| AntV G6 5 | Canvas, SVG, WebGL, 3D | groot (WASM/WebGPU) | 430 kB | MIT | eigen themasysteem | niet vastgesteld |
| Cytoscape.js | Canvas; WebGL als preview | 1.200 knopen / 16k kanten: 20 → 100+ fps (blog) | 139 kB | MIT | CSS-achtig; algoritmen ingebouwd | aanraken werkt |
| vis-network | Canvas | enkele duizenden (schatting) | 155 kB | Apache-2.0/MIT | eigen code | gedateerd |
| deck.gl + graph-layers | WebGL2/WebGPU | experimenteel | 530 kB | MIT | sterk op een kaart | experimenteel |
| Gephi Lite | app op Sigma | — | — | GPL-3 | niet inbouwen; wel als teamgereedschap | — |
Om hun licentie vallen ook af: Neo4j NVL (volgens LICENSE.txt in het npm-pakket alleen voor Aura of commercieel Neo4j) en deepscatter, de motor van Nomic Atlas (CC BY-NC-SA).
Advies
- Deze week: het eigen canvas houden, met twee toevoegingen zonder nieuwe afhankelijkheid. Een afspeelknop laat het web groeien in de volgorde van
sindsenwanneer(zoals de time-lapse in Obsidian). Een verhaalpad-stand dimt de rest met--web-dim. - Over twee weken: Sigma.js voor de stad, met de lay-out vooraf berekend op de server. Wikidata Graph Builder draait op onze stack (Svelte, Vite, Canvas met workers) met duizenden knopen.
- Later: @cosmos.gl/graph voor alle adressen. 3D hoogstens één keer, als podiummoment.
4 · Drie beeldideeën
Wat werkt: klein beginnen en uitvouwen (Kumu: de hele kaart in één keer overweldigt), dimmen in plaats van verbergen (showcase), een tijdschuif als verhaalstap (Flourish) en één boodschap per beeld (The Pudding). Daarbij onze eigen regels: goud alleen als haarlijn voor een band met jou (MASTER §10), papier voor wat je leest (§9), nachtblauw voor de kamer (§11).
A · De stad als sterrenhemel
- Wat je ziet: een nachtblauw doek met 21.476 adressen als lichtjes op hun echte plek; straten als dunne sterrenbeelden; tien wijknamen in kapitalen. Een tijdschuif van 1869 tot nu laat de stad decennium voor decennium oplichten. Jouw huis en je droomhuizen krijgen een gouden ring, rijmende huizen inkt.
- Wat je doet: door de tijd schuiven. Een straat aantikken opent een passe-partout met haar maten en haar straatfeit, met de bron in de onderrand.
- Wat het bewijst: de hele stad uit open bronnen, niet 21 demohuizen.
- Wat het kost: Canvas 2D voor stilstand, Sigma of cosmos.gl voor animatie. Opus, 1–2 dagen.
- Grens: nooit laten zien welke adressen een gebruiker hebben.
B · Vier zoomniveaus
- Wat je ziet: stad (tien wijken), wijk (straten, met rijmgroepen als zachte vlakken), straat (huizen per bouwjaar) en huis (het bestaande web). Elk niveau heeft hoogstens ~50 knopen, in een passe-partout met onderschrift ("Wittevrouwen · 2.977 adressen · BAG").
- Wat je doet: inzoomen brengt je een niveau dieper, niet alleen dichterbij.
- Wat het bewijst: één model op vier schalen; het weerbericht is dezelfde graaf, opgeteld.
- Wat het kost: Opus, 2–3 dagen, na de opslagstap.
C · Verhaalpad
- Wat je ziet: een route als huis → 1910 → jaarweetje → straat → naamgever → wijk. Alleen het pad staat in volle inkt. Per kant zegt het huis of Carmen één zin, en die zin staat gedrukt in de onderrand.
- Wat je doet: een van drie paden kiezen, of zelf twee knopen aanwijzen.
- Wat het bewijst: "Anderen kennen de dingen. Wij kennen wat ertussen zit."
- Wat het kost:
kies(), de camerarit,HuisStemen de werkwoorden bestaan al. Opus, een halve tot één dag. Alleen watmagKlinkentoestaat, klinkt.
5 · Analyses die iets opleveren
| Analyse | Voor wie | Hoe (gemeten waar het kon) | Graafdatabase? | Grens |
|---|---|---|---|---|
| Lijkt op jouw huis | bezoeker: "Acht van ons…" | vector (bouwjaar, oppervlak, water, park, bomen, rust, ochtendzon) plus gedeelde buren; top-10 in 0,7 ms, alle huizen in 3,5 s | nee | een ander huis heet alleen naar zijn straat |
| Straten die rijmen | Carmen: "Ik heb drie straten" | k-NN (k=5) en dan Louvain: 343 straten, 13 groepen, 2 ms | nee | vast zaadje; een mens geeft de namen |
| Dromen per thema per straat of wijk | eigenaar, wijk | kanten van thema naar straat, HAVING count(DISTINCT persoon) >= 5 |
nee | k ≥ 5, nooit een lijst |
| Ontbrekende kennis | wij (oogst) | anti-join: straat zonder naamverklaring, feit zonder bron | nee | — |
| Kern van een web | redactie | de drie tellingen uit v3 §7 | nee | wordt nooit benoemd |
| Centraliteit van bronnen | wij | graphology-metrics |
nee | huizen nooit rangschikken (D6) |
| Wat mensen leuk vinden | wij | per soort relatie en verhaalpad tellen hoe vaak "klopt", ♥ of bewaard volgt | nee | opgeteld, k ≥ 5, geen persoons-id |
| Web door de tijd | zaal | sinds/tot |
nee | niet faken (D51) |
Aanbod en vraag. Aan de aanbodkant is genoeg: van de 343 straten hebben er 133 ochtendzon, 245 water binnen 100 m, 147 zijn rustig en 146 hebben een park binnen 150 m. De vraagkant is in de demo te dun voor k ≥ 5, dus we beginnen per wijk (v3 §13). Een graafdatabase helpt pas bij patronen van wisselende lengte, embeddings op structuur, of data groter dan het geheugen.
6 · Podium
- Live groeien bestaat al (SSE-event
web). De risico's zijn stilte, te veel tegelijk en te kleine letters. Daartegen: een wachtrij met één knoop per--web-vlucht, de afspeelknop als terugval, en labels van minstens 28 px met hoogstens 12 tegelijk (vuistregel, niet gemeten op een projector). - De stadsgraaf van de demohuizen is dun: de seed heeft 21 huizen. Gebruik de stenen-stad als achtergrond, met de demowebben als enige verlichting, op straatniveau. Wittevrouwensingel 84 bestaat echt in de BAG.
- Het verhaalpad loopt volgens een script, met een vaste camera en een video als reserve.
Hoe je een hairball voorkomt (zie Grooming the hairball):
- Teken opgetelde kanten, per knoop hoogstens de 5 sterkste.
- Maak een drukke knoop een plek in de kantlijn in plaats van een knoop.
- Geef vaste posities of een vooraf berekende lay-out.
- Gebruik semantische zoom.
- Dim wat buiten de keuze valt in plaats van het te verbergen.
- Toon hoogstens ~150 knopen per beeld (vuistregel).
7 · Plan op agent-niveau
Prijzen per miljoen tokens (Anthropic, juni 2026): Opus 5 $5 in / $25 uit (Opus 5.5 $4/$20), Sonnet 5 $2/$10, Haiku 4.5 $1/$5; cache lezen ≈ een tiende daarvan. Met een aangenomen koers van $1 ≈ €0,90 kost een agentuur naar schatting €8–15 met Opus, €3–6 met Sonnet en €1–3 met Haiku.
| # | Stap | Wanneer | Model | Uren | € (schatting) | Risico |
|---|---|---|---|---|---|---|
| 1 | Afspeelknop en verhaalpad in Web.svelte, achter een parameter |
24–25 sep | Opus | 2–3 | 20–45 | regressie vóór de oefening |
| 2 | GET /api/stad/web (alleen lezen, zonder personen) plus een eenvoudige sterrenhemel |
alleen als stap 1 donderdag af is | Opus | 3–4 | 30–60 | tijd |
| 3 | QA en lekcontrole | na 1 en 2 | agent B | 1 | — | — |
| 4 | 0003-web.sql, projectie, "wis alles" en reset, lektests |
week 40 | Opus | 6–8 | 50–120 | tweede kopie van persoonsdata |
| 5 | lib/graaf en een nachtjob met graphology |
week 40 | Sonnet | 4–6 | 15–35 | goed af te bakenen na stap 4 |
| 6 | Stadsweergave met Sigma: zoomniveaus, tokens, toegankelijkheid, telefoon | week 40–41 | Opus | 8–10 | 70–150 | labels, prestaties op de telefoon |
| 7 | Rijmgroepen een naam geven, straatnamen aanvullen met bron | week 41 | Haiku, Opus keurt | 2–4 | 5–20 | verzinsels; alleen met een bron-url die 200 gaf |
| 8 | Drie verhaalpaden en een reservevideo | week 41 | Sonnet | 2 | 8–12 | — |
| 9 | Sterrenhemel op cosmos.gl | later | Opus | 12–16 | 120–240 | WebGL op de zaalapparatuur |
| 10 | Proef met LadybugDB als zijspoor | alleen met een aanleiding | Opus | 4 | 35–60 | jonge fork |
Opgeteld: nu €50–105, over twee weken €150–340, later €155–300. E wil sinds 24 september doe-werk door Opus; voor stap 5, 7 en 8 is dat ongeveer 2,5 keer zo duur.
8 · Wat we niet moeten doen
- Geen Neo4j- of Memgraph-container naast de app. Dat is een tweede bron van waarheid en een tweede plek om te wissen, zonder zichtbaar verschil.
- Niet gebruiken: Kuzu (gearchiveerd),
@cosmograph/*(CC-BY-NC), deepscatter en NVL (licentie). LadybugDB nog niet in productie. Gephi Lite (GPL-3) niet inbouwen. - Geen mensen in de gedeelde graaf: geen
jijover huizen heen, geen tellingen onder k = 5, geen fanlijst, geen huisnummers van anderen, geen ranglijst van huizen (D6). - De graaf wordt geen hoofdscherm voor bewoners. Brainstorm 09 wil geen netwerkplaatje in de app. Voor de bewoner is er het sterrenbeeld: hoogstens negen sterren en in rust geen lijnen (12 §1.6).
- Geen gefaakte groei in de demo en geen 3D als hoofdvorm.
- Geen hex-kleuren in componenten. Lees de tokens bij het opstarten, zoals
webgraaf.jsal doet.
Bronnen. Kuzu naar Ladybug · Neo4j GDS, grenzen · Neo4j in Docker · NVL · Memgraph, geheugen · Memgraph, licentie · Oxigraph JS · node:sqlite · cosmos.gl v3 · Sigma · G6 · 3d-force-graph · deck.gl-community · Gephi Lite · deepscatter