Skip to content

Documentatie Joost

Joost Krebbers edited this page Jun 9, 2026 · 10 revisions

Project Documentatie

Joost · Visdeurbel · 2026


week 1/2 — Visdeurbel Wereldkaart

Wat heb ik gedaan

HTML-structuur & layout

  • Navbar gebouwd met branding en navigatielinks
  • Pagina-header met eyebrow-tekst, grote titel en decoratieve emoji's
  • Periodeknoppenbalk (week / maand) met laad-indicator
  • Stats-balk met vier kaartjes: totaal events, landen, vis gespot, gesloten
  • Verticale paginalayout met secties onder elkaar
  • Per-land lijst als losse sectie naast de kaart

SVG-wereldbol (D3 + TopoJSON)

  • Orthografische projectie via d3.geoOrthographic() voor het echte boleffect
  • Radiale gradiënten voor oceaan (lichtblauw glanseffect) en een shine-overlay bovenop de bol
  • Dropshadow-filter op de oceaanbol voor dieptegevoel
  • Graticule (breedte- en lengtegraadlijnen) als dunne achtergrondlijnen
  • TopoJSON-landkaart geladen van CDN en omgezet naar GeoJSON-features
  • Landvulling, hover-darkening en outline-stroke geïmplementeerd

Interactie

  • Muisdrag-rotatie: mousedown slaat beginhoek op, mousemove berekent delta × 0.3 gevoeligheid
  • Touchscreen-ondersteuning via touchstart en touchmove
  • Scroll-to-zoom via wheel-event, schaal begrensd tussen 150 en 800
  • Auto-rotatie via requestAnimationFrame-lus (0.012 graden per milliseconde)
  • Auto-rotatie pauzeert bij interactie, hervat na 4 seconden inactiviteit
  • Reset-knop herstelt schaal en rotatie naar beginpositie
  • Klikken op een land in de lijst laat de bol animeren naar dat land

Data laden & verwerken

  • loadData() haalt NDJSON op en parseert elke regel afzonderlijk
  • aggregate() groepeert events per land via ISO alpha-2 → numerieke TopoJSON-ID mapping
  • Per land bijgehouden: events, uploads, dismissals, steden, vissoorten, uren, apparaattype, OS, browser
  • normalizeOS() en normalizeBrowser() mappen ruwe user-agent strings naar schone categorieën
  • firstKnown() helper slaat "unknown"/"Overig"/"onbekend" over bij topFish, topOS, topBrowser
  • Periodewissel laadt nieuwe data, dimpt de bol tijdens laden, en werkt alle views atomisch bij

Kaartmodi in de eerste versie

Modus Wat het toont
Bellen Proportionele cirkels (sqrt-schaal) op landcentroïden
Upload % Gradiënt voor verhouding uploadedFish / totaal
Taarten Mini-taartdiagrammen upload vs. dismiss per land
Lijnen Great-circle lijnen van elk land naar Utrecht, dikte proportioneel aan bezoeken
Apparaat Taartjes desktop vs. mobiel vs. overig
Vis soort Landkleur op meest geziene vissoort
Tijdstip Gemiddeld actief uur: nacht / ochtend / middag / avond
OS Landkleur op meest gebruikte besturingssysteem
Browser Landkleur op meest gebruikte browser

Tooltip & visueel ontwerp

  • Tooltip toont contextgevoelige rijen afhankelijk van de actieve modus
  • isVisible() via dot-product om overlays aan de achterkant te verbergen
  • Volledig CSS custom-properties systeem gebaseerd op het officiële Visdeurbel stijlgids
  • Typografie: Bricolage Grotesque (800, koppen/knoppen) + PT Sans (body)
  • Alle kleuren, spaties en radii via design tokens — geen hardgecodeerde waarden
  • De losse legenda onder de kaart en de recente-events feed zijn later verwijderd om de pagina rustiger te maken

Tijdsindeling

| Onderdeel | Geschatte tijd | | SVG-bol & D3 setup | ±2–3 uur | | Drag / zoom / touch interactie | ±1–2 uur | | Data pipeline & aggregatie | ±2–3 uur | | Tien kaartmodi | ±3–4 uur | | Tooltip & per-land lijst | ±1–2 uur | | CSS & visuele afwerking (stijlgids) | ±2–3 uur | | Periodewissel & bugfixes | ±1 uur | | Totaal geschat | ±12–18 uur |

Wat heb ik geleerd

D3.js & geografische visualisatie

  • d3.geoOrthographic() vs. andere projecties — clipAngle: 90 snijdt de achterkant van de bol weg
  • projection.rotate([lambda, phi, gamma]) — lambda is oost-west, phi is noord-zuid kantel
  • pathGen({ type: 'Sphere' }) als oceaan-achtergrond; geoGraticule() voor het coördinatenraster
  • SVG <defs> met radialGradient en feDropShadow voor diepte en glans
  • d3.scaleSqrt() voor bubbles (perceptueel eerlijker dan lineair), d3.scaleSequentialLog() voor choropleth
  • d3.arc() voor taartdiagrammen direct in SVG

Globe-interactie & animatie

  • requestAnimationFrame-loop met delta-time voor vloeiende, frame-rate-onafhankelijke rotatie
  • Dot-product om te bepalen of een punt aan de voorzijde van de bol zit (isVisible)
  • Drag-sensitivity van 0.3 graden per pixel — hoger voelt chaotisch
  • event.preventDefault() nodig op wheel om pagina-scroll te blokkeren
  • Touch-events met { passive: true } voor betere scroll-performance op mobiel

week 3 — React-migratie & uitbreidingen

Wat heb ik gedaan

Migratie van vanilla HTML/JS naar React

  • Volledige herschrijving van de pagina van losse HTML/CSS/JS-bestanden naar een React-component boom
  • initJoost() imperatieve aanroep vervangen door React useEffect hooks
  • Alle D3-logica behoudt directe DOM-mutaties via refs — D3 en React's virtual DOM mengen via useRef zodat D3 de SVG-elementen beheert en React de omliggende UI
  • initialized ref voorkomt dubbele initialisatie in React StrictMode
  • useCallback gebruikt voor stopAutoRotate en runMode om onnodige re-renders te voorkomen
  • Stale closure-probleem opgelost: alle D3-event handlers lezen uit projRef.current in plaats van de gesloten proj variabele uit de init-closure

Projectsplitsing in componenten

  • Monolithisch Joost.jsx opgesplitst in losse bestanden onder src/components/world-map/: | Bestand | Verantwoordelijkheid | | index.jsxJoost.jsx (pages) | Datalaag: laden, periodewissel, state doorgeven | | GlobeMap.jsx | SVG-canvas, D3-initialisatie, drag/zoom/rotatie, modus- en projectietabs | | mapModes.js | Eén exportfunctie per kaartmodus + MODE_RENDERERS dispatcher | | CountryList.jsx | Gesorteerde landenlijst met geanimeerde balken | | StatsBar.jsx | Vier stat-kaartjes met inloop-animatie | | MapTooltip.jsx | Hover-tooltip als pure React component | | Nav.jsx | Breadcrumb-navigatie | | constants.js | Alle kleuren, centroïdes, ALPHA2-mapping, moduslijst | | utils.js | aggregate(), loadData(), flag(), buildTooltipRows(), normalizeOS(), normalizeBrowser() |

Platte kaart (Kaart-modus)

  • Projectiewissel toegevoegd: 🌍 Bol (d3.geoOrthographic) ↔ 🗺️ Kaart (d3.geoNaturalEarth1)
  • Op de platte kaart: slepen = verschuiven (pan), scrollen = inzoomen naar cursorpositie
  • Auto-rotatie stopt bij overschakelen naar platte kaart en hervat bij terugkeer naar bol

Lay-out & CSS

  • CountryList verplaatst naar rechterkolom binnen het kaartpaneel
  • Globale reset in joost.css gescoord naar .joost-page zodat gedeelde nav niet wordt overschreven
  • Alle hardgecodeerde hex-waarden vervangen door officiële Visdeurbel design tokens

Tijdsindeling

| React-migratie & useEffect/useRef structuur | ±2–3 uur | | Componenten opsplitsen & imports herschrijven | ±2 uur | | Platte kaart projectie & interactie | ±1–2 uur | | Oneindige tiling & achtergrond fix | ±2–3 uur | | Lay-out twee-kolommen & per-land in paneel | ±1–2 uur | | CSS herschrijven & design token cleanup | ±1 uur | | Stale closure bugfix (projRef) | ±1 uur | | Totaal geschat | ±10–14 uur |

Wat heb ik geleerd

React + D3 integratie

  • D3 en React kunnen naast elkaar bestaan als D3 de SVG-inhoud volledig beheert via useRef en React de omliggende UI — nooit beide tegelijk de DOM laten muteren
  • Stale closures zijn de meest voorkomende bug bij D3-in-React: altijd ref.current lezen in de handler zelf
  • useCallback met lege dependency array werkt niet als de callback interne state leest — gebruik een ref als spiegel van state

Geografische projecties & tiling

  • d3.geoNaturalEarth1 heeft geen clipAngle en projecteert de hele wereld — ideaal voor platte weergave
  • Zoom naar cursorpunt: nieuwe translate = cursorPx + (oldTranslate - cursorPx) * (newScale / oldScale)

week 4 — Toegankelijkheid, UX, visuele verfijning & opschoning

Wat heb ik gedaan

Commentaren toegevoegd aan alle bestanden

  • Alle functies, hooks, effecten en JSX-blokken voorzien van single-line // commentaren
  • JSDoc-stijl vervangen door compacte inline opmerkingen zodat de code leesbaar blijft zonder te overladen
  • Commentaren beschrijven het waarom, niet alleen het wat — bijv. waarom Edge vóór Chrome gecontroleerd wordt in normalizeBrowser()

Toegankelijkheid (a11y) — CountryList

  • Alle landkaartjes voorzien van tabIndex={0} en role="button" zodat ze bereikbaar zijn via Tab
  • aria-label per kaartje met gesproken tekst: "Nummer 4. Land: Nederland. Aantal vissen gespot: 1.234."
  • Decoratieve inhoud (vlag, balk, rangnummer) gemarkeerd met aria-hidden="true" om dubbel voorlezen te voorkomen
  • "Bekijk meer"-knop uitgerust met aria-expanded en aria-controls voor schermlezers
  • Escape-toets sluit de uitgevouwen lijst en stuurt focus terug naar de knop via toggleBtnRef
  • aria-live="polite" op het uitvouwbare gebied zodat de Escape-hint automatisch wordt aangekondigd
  • Escape-hint visueel verborgen via .sr-only klasse, maar leesbaar voor schermlezers
  • Tabvolgorde gecorrigeerd: knop vóór de rest-kaartjes in de DOM via flex order-eigenschap — Tab gaat top-3 → knop → rest
  • Rest-kaartjes krijgen tabIndex={-1} zolang de lijst gesloten is zodat Tab er niet doorheen loopt

Interactie: alleen op de bol

  • isOnGlobe(px, py) helper toegevoegd: controleert via Math.hypot(px - cx, py - cy) <= r of het punt binnen de bolcirkel valt
  • clientToSVG(clientX, clientY) helper converteert muiscoördinaten naar SVG-ruimte rekening houdend met schaling
  • mousedown, touchstart en wheel events bailten vroeg uit als de cursor buiten de bol is — pagina scrollt normaal over de achtergrond van het kaartpaneel

Globe-grootte & positie

  • Beginschaal verdubbeld: INIT_R = R * 2 (290 px in plaats van 145 px) zodat de bol het paneel beter vult bij opstarten
  • Zoom-begrenzing aangepast van max 800 naar max 1600 zodat verder inzoomen nog mogelijk is
  • Globe verschoven naar rechts via negatieve viewBox x-offset: viewBox="${-W * 0.10} 0 ${W} ${H}"
  • D3's .attr('viewBox', ...) verwijderd uit de init-code — JSX is nu de enige eigenaar van de viewBox zodat de bol niet meer terugspringt naar het midden bij elke mount
  • Reset-knop en projectiewissel gebruiken ook INIT_R als beginschaal

Visuele verbeteringen kaart

  • Modusknoppen verplaatst van rechtsonder naar gecentreerd onderaan het kaartpaneel
  • Knoppen vergroot: hoogte 52 px, lettergrootte var(--text-button), padding space-4 space-7
  • Knoppen 80 px naar rechts verschoven met transform: translateX(calc(-50% + 80px)) zodat ze visueel boven het midden van de bol staan
  • Kaartpaneel verplaatst buiten .page-content in Joost.jsx zodat het van nature de volledige breedte vult — geen negatieve marges of 100vw hacks meer nodig
  • Top-3 landkaartjes gewrapped in <div className="country-list-top3"> met gap: var(--space-4) voor meer witruimte tussen de kaartjes
  • Scrollbalk in de uitgevouwen landenlijst zichtbaarder gemaakt: 6 px breed, donkergroen met lichtgroene track via scrollbar-color (Firefox) en ::-webkit-scrollbar (Chromium)
  • Harde afkapping van de landenlijst vervangen door een CSS mask-image gradient: boven 20 px fade-in, onder 40 px fade-out

Opschoning, bugfixes & code kwaliteit

Platte kaart volledig verwijderd
  • De tweede GlobeMap-instantie met defaultProjection="map" verwijderd uit Joost.jsx
  • De bijbehorende "Kaart"-sectie met heading en subtitel verwijderd
  • flatMapRef uit Joost.jsx verwijderd
  • switchProjType() functie volledig verwijderd uit GlobeMap.jsx
  • defaultProjection en containerClass props verwijderd — altijd globe, altijd map-panel
  • panStartRef, projType state en projTypeRef verwijderd
  • getFlatMinScale() en clampFlatProjection() functies verwijderd
  • Flat-map init branch (if defaultProjection === 'map') verwijderd uit de init useEffect
  • Pan-branches uit drag, touch en wheel handlers verwijderd
  • #map-bg water-achtergrond rect verwijderd uit JSX
  • Alle .map-area2, .section--map, .proj-btn, .proj-toggle CSS-regels verwijderd
Dead code opgespoord en verwijderd
  • handleReset() verwijderd uit GlobeMap.jsx — functie was gedefinieerd maar nooit aangeroepen, er bestaat geen reset-knop in de UI
  • loading-overlay div verwijderd uit GlobeMap.jsx — had de klasse hidden hardgecodeerd en werd nooit getoggled; loading state wordt afgehandeld in Joost.jsx
  • containerClass variabele verwijderd en direct inlined als 'map-panel' — was een prop die na de flat-map-verwijdering nooit meer gevarieerd werd
  • getCountryFill() verwijderd uit utils.js — geëxporteerde functie die nergens geïmporteerd of aangeroepen werd na het verwijderen van de oude kaartmodi
  • export keyword verwijderd van normalizeOS() en normalizeBrowser() in utils.js — alleen intern gebruikt door aggregate(), nergens anders geïmporteerd
  • cy0 in de arcData-builder behouden — bleek toch gebruikt in de lift-berekening van de flow arcs
Ongebruikte CSS-klassen verwijderd uit joost.css
  • .event-feed, .event-item, .event-icon, .event-type, .event-meta, .event-detail — van de verwijderde EventFeed.jsx component
  • .legend-item, .legend-list, .legend-pill en varianten — van de verwijderde externe MapLegendSection.jsx
  • .map-tab--reset, .map-tabs-divider — van de reset-knop en tabbenscheider die niet meer in de UI bestaan
  • .proj-btn, .proj-toggle — van de verwijderde projectiewissel
  • .section-heading, .section-sub — gebruikt in de verwijderde "Kaart"-sectiekoppen
  • .loading-overlay, .loading-overlay.hidden — van de nooit-getoggled overlay in GlobeMap
Volledige breedte kaart hersteld
  • GlobeMap en zijn laadplaatshouder verplaatst buiten .page-content in Joost.jsx — zelfde structurele fix als eerder, maar was per ongeluk teruggedraaid bij het kopiëren van de verkeerde bestandsversie
  • Periodeknoppenbalk en stats-balk blijven in .page-content zodat ze de normale maximale breedte respecteren
  • .map-panel--loading klasse opnieuw toegevoegd aan joost.css voor de laadtoestand buiten de container
Brace-mismatch bugfix in GlobeMap.jsx
  • Na het verwijderen van de if (defaultProjection === 'map') { ... } else { ... } wrapper bleef er een los sluitend } achter dat de componentfunctie te vroeg afsloot
  • Hierdoor stond de return buiten de functie — Vite gaf de fout 'return' outside of function
  • Opgelost door de losliggende } te verwijderen en de inspringing van de globe-init code te normaliseren

Performance-optimalisaties flow-animatie

  • SVG feGaussianBlur filter verwijderd van de trail-paden — filter alleen behouden op het kleine particle-bolletje, wat de renderkosten per frame drastisch verlaagt
  • Arcpunten teruggebracht van 80 → 40 → 20 stappen — bij 20 stappen is de curve visueel identiek maar een kwart van het oorspronkelijke werk per frame
  • Per-frame D3-selector overhead geëlimineerd door DOM-node refs (arcEls[]) bij setup te cachen — geen querySelectorAll meer in de hot loop
  • Directe setAttribute calls vervangen D3 wrapper methodes in de rAF-loop
  • Path-string opbouw herschreven van .filter().map() (twee array-allocaties) naar een gewone for-lus
  • Utrecht-beacon translate nu elke frame bijgewerkt (was elke 3e frame) — hierdoor loopt de beacon synchroon mee met de globerotatie; alleen de puls-berekening wordt nog elke 3e frame gedaan

Utrecht-beacon: Visdeurbel-logo

  • Roze stip en solid circle vervangen door de Visdeurbel SVG-logo (/images/visdeurbel-logo.svg) via een SVG <image> element
  • Logo 16×16 px, gecentreerd op het beacon-punt via x=-8, y=-8 in de group-coördinaten
  • Logo 90° tegen de klok in gedraaid via transform="rotate(-90, 0, 0)"
  • Pulsring aangepast naar r=10 om de kleinere logo-afmetingen te omkaderen
  • Utrecht-tekstlabel hersteld boven het beacon-punt

Tooltip uitbreidingen

  • withCities() helper toegevoegd in utils.js die een 📍 Steden-rij toevoegt aan elke tooltip-modus wanneer staddata beschikbaar is
  • Top-3 steden worden getoond in alle drie modi: Bezoeken+Lijnen, Vis soort en Tijdstip

Visuele verfijning tabs & focus

  • Inactieve modusknoppen zichtbaarder gemaakt: volledige witte tekst, opacity: 0.7, duidelijkere border
  • Actieve tab gebruikt --color-purple-bell als achtergrond met font-weight: 800 en een glowing box-shadow ring — meteen herkenbaar als geselecteerd
  • :focus-visible stijl toegevoegd aan landkaartjes in de CountryList — keyboard-focusring in paars-bell kleur met translateY(-2px) lift, zichtbaar bij Tab maar onzichtbaar bij muisklik

Vis soort opgeschoond

  • unknown verwijderd uit FISH_COLORS in constants.js
  • Legenda en tooltip filteren nu via UNKNOWN_VALS zodat "unknown", "onbekend" en "Overig" nooit als vissoort verschijnen

Tijdsindeling

| Toegankelijkheid (a11y) | ±2–3 uur | | Globe-grootte, viewBox, visuele verfijning | ±1–2 uur | | Performance flow-animatie | ±2 uur | | Utrecht-beacon vervangen door logo | ±1 uur | | Tooltip steden + vis opschoning | ±30 min | | Tab/focus zichtbaarheid | ±30 min | | Dead code & CSS opschoning | ±1–2 uur | | Bugfixes (brace, witruimte, snap) | ±2 uur | | Totaal geschat | ±10–13 uur |

Wat heb ik geleerd

Toegankelijkheid & toetsenbordnavigatie

  • Tab-volgorde volgt DOM-volgorde, niet visuele volgorde — order in flexbox verandert de visuele positie maar niet wanneer de browser een element tegenkomt bij Tab
  • tabIndex={-1} is de juiste manier om een element uit de tabvolgorde te halen zonder het visueel te verbergen
  • :focus-visible toont de focusring alleen bij toetsenbordnavigatie, niet bij muisklik — betere UX zonder de muisgebruiker te storen
  • .sr-only (positie absoluut, 1×1 px, overflow hidden, clip) is de standaard voor visueel verborgen maar toegankelijke tekst

SVG-performance

  • SVG feGaussianBlur filters op grote paden zijn de duurste SVG-operatie — één filter op een klein punt kost een fractie van een filter op een lang pad
  • DOM-node refs cachen bij setup (el.node()) en setAttribute direct aanroepen is significant sneller dan D3 selectors in een 60fps loop
  • Minder arcpunten (20 i.p.v. 80) is visueel niet merkbaar op bolschaal maar spaart aanzienlijk CPU per frame

SVG viewBox & D3

  • De SVG viewBox bepaalt welk deel van het canvas zichtbaar is — negatieve x-waarde verschuift het venster waardoor de inhoud naar rechts lijkt te schuiven
  • D3 en React mogen niet allebei hetzelfde attribuut beheren — als D3 viewBox overschreef bij init sprong de bol terug naar het midden bij elke mount

CSS masking & scrollbars

  • mask-image: linear-gradient(...) is een non-destructieve manier om inhoud te laten vervagen zonder layout of scrollbaarheid te beïnvloeden
  • scrollbar-width en scrollbar-color zijn de Firefox-standaard; ::-webkit-scrollbar* voor Chromium — beide nodig voor brede browserondersteuning

Problemen & Oplossingen

Probleem Oorzaak Oplossing
Zoomen/slepen kapot na projectiewissel Event handlers sloten over initiële proj variabele Alle handlers herschreven om projRef.current te lezen
ALPHA2_TO_NUMERIC niet gevonden Oude constants.js zonder exports gekopieerd Vervangen door versie met export const op elke declaratie
Witte ruimte naast platte kaart #map-bg rect alleen zo breed als de viewBox Rect uitgebreid naar x=-5000, width=12000
Per-land lijst strekt het paneel uit Geen hoogte-beperking op .map-body height: 520px op .map-body, overflow: hidden op kolom
Import fout in joost.jsx entry Pad ./pages/world-map niet ../pages/Joost Relatief pad gecorrigeerd naar ../pages/Joost.jsx
Gedeelde nav krijgt verkeerde achtergrondkleur joost.css definieerde .vdb-nav opnieuw .vdb-nav blok volledig verwijderd uit joost.css
Globale reset brak stijl van gedeelde nav *, body {} in joost.css waren niet gescoord Reset omgezet naar .joost-page *, .joost-page {}
Joost-tab in nav nooit actief Route in Nav.jsx stond op /world-map Route gecorrigeerd naar /joost in Nav.jsx
nav-shared.css kon niet via JS geïmporteerd worden Bestand staat in public/ — geen ESM-imports mogelijk <link rel="stylesheet"> toegevoegd in index.html
Globe springt terug naar midden bij elke mount D3 overschreef viewBox attribuut in init D3's .attr('viewBox', ...) verwijderd — JSX beheert viewBox
Knop was 128e in tabvolgorde Rest-kaartjes hadden tabIndex={0} ook als gesloten tabIndex={-1} op rest-kaartjes als expanded === false
Top-3 gap CSS-selector miste Kaartjes waren directe fragment-kinderen, geen wrapper Top-3 gewrapped in <div className="country-list-top3">
Platte kaart verwijderd, brace bleef achter else { verwijderd maar } niet Diepte-teller script geschreven om de exacte regellocatie te vinden
Witruimte links/rechts van kaart teruggekeerd Verkeerde versie van Joost.jsx ingeplakt met map in .page-content GlobeMap structureel buiten .page-content geplaatst
getCountryFill nooit aangeroepen Overgebleven na verwijderen oude kaartmodi Functie volledig verwijderd uit utils.js
loading-overlay nooit zichtbaar Klasse hidden hardgecodeerd, niets togglede het Div verwijderd; loading afgehandeld in Joost.jsx
SVG-filter lag op flow-arcs feGaussianBlur op elke trail-path elke frame Filter verwijderd van paden, alleen behouden op particle-bolletje
Utrecht-beacon loopt achter bij roteren Beacon-positie alleen elke 3e frame bijgewerkt translate elke frame bijwerken, alleen puls op elke 3e frame
"unknown" verscheen in vis-legenda unknown stond in FISH_COLORS en werd niet gefilterd Verwijderd uit FISH_COLORS, gefilterd via UNKNOWN_VALS in legenda en tooltip
Utrecht-beacon loopt achter bij roteren Beacon-positie alleen elke 3e frame bijgewerkt translate elke frame bijwerken, puls op elke 3e frame
"unknown" verscheen in vis-legenda unknown stond in FISH_COLORS, niet gefilterd Verwijderd uit FISH_COLORS, gefilterd via UNKNOWN_VALS

Clone this wiki locally