-
Notifications
You must be signed in to change notification settings - Fork 0
Product Biografie | Diego Ramon
Elk voorjaar zwemmen er duizenden vissen door de Utrechtse grachten naar de Kromme Rijn om te paren. Via Visdeurbel.nl kan iedereen live meekijken bij de sluis en voor de vissen aanbellen. Dit concept is inmiddels een megasucces met miljoenen fans wereldwijd. Tijdens het afgelopen seizoen (van maart tot 1 juli) is er bij elke druk op de knop data verzameld.
De opdrachtgevers (de gemeente Utrecht, HDSR en Dutch Wall Fish) willen nu iets cools doen met al die info. De opdracht voor studio Moan is kort maar krachtig: ontwerp en bouw een vette, responsive data-visualisatie (het liefst een one-pager) die binnen de bestaande website past. Omdat de visdeurbel een wereldwijd fenomeen is, moet de pagina supergoed performen (snel laden) en voor iedereen volledig toegankelijk zijn volgens de WCAG A-richtlijnen.
Ik zal mij dit project op de komende onderwerpen focussen binnen het ontwerpen en maken van de website:
- Styling
- Front-end development
- Back-end development
- Planning
- Documentatie
- Product testen met Leonie (Volgensmij)
- Github branchen met Victor
- Code reviewen met Victor
- WCAG & Wetgeving met Leonie
- Javascript voor gevorderde met Jad
- Design review met Vasilis
- Javascript voor gevorderde met Jad
- Geen tweede workshop ivm CSS day
- Geen workshops i.v.m. afronding meesterproef
- Ik wil meer kunnen focussen op één opdracht tegelijkertijd, op dit moment ben ik snel afgeleid en ga ik naar een andere opdracht/functie omdat die bijvoorbeeld in mijn hoofd zit, maar ik moet leren focussen op één tegelijkertijd. Dit is dan bij te houden in Github branches (Bijvoorbeeld een branch maken voor specifiek 1 functie, en deze branch dat ook ALLEEN gebruiken voor die functie)
- Ik wil beter leren begrijpen wat de klant verwacht in het product. Op dit moment heb ik voornamelijk ervaring met voor één persoon, maar ik wil het voor meerdere klanten/opdrachtgevers leren. Nu weet ik heel goed hoe ik één persoon blij kan maken, maar meerdere personen hebben meerdere meningen en dan kan conflicten met elkaar.
- Meer leren met AI om te gaan, op dit moment gebruik ik AI alleen af en toe om code te reviewen of problemen op te lossen, maar de toekomst maakt steeds meer gebruik van AI, dus ik wil ook echt leren coderen met AI erbij (Dus bijvoorbeeld Co-Pilot). Hiermee wil ik mijn snelheid van functies maken versnellen en daardoor sneller mooie producten maken.
Maandag 18 mei zijn wij van start gegaan met het meesterproef project, ik ben samen met mijn team ingedeeld om het project VisDeurbel te doen. Voordat wij begonnen hebben wij meerdere onderzoeken uitgevoerd:
- Wij hebben een onderzoek uitgevoerd naar de huidige visdeurbel website (Visdeurbel.nl) om erachter te komen hoe de website er op dit moment uitzag en welke elementen belangrijk waren
- Wij hebben een debriefing voorbereid zodat wij duidelijk op een rij hadden welke vragen wij hebben, of de opdracht duidelijk is en of wij onderdelen hadden gemist.
- Wij hebben een gesprek gehad met onze coach, Victor. Tijdens dit gesprek heeft Victor ons kort gesproken over onze leerdoelen, wat wij van de opdracht vinden en of wij alles begrepen
- En als laatste hebben wij een debriefing gehad met Cyd. Onze opdrachtgever was op 18 mei afwezig (en voor de rest van de week ook) dus heeft Cyd ons uitgelegd hoe de opdracht precies in elkaar zit
Om te beginnen heb ik een data-fetch gebouwd die alle data uit de Svc file omzet naar bruikbare JSON data. Hoewel de data uiteindelijk naar engels is vertaalt door Mats, heb ik de originele code uitgeschreven het dynamisch gemaakt voor bepaalde onderwepren. Omdat het zo veel Svc data is (ongeveer 40.000 regels aan data), moest ik filteren zodat de HTML pagina het bij kon houden. Hiervoor heb ik origineel de volgende code gebruikt:
const toegestaneKolommen = [
"website_id", "session_id", "visit_id", "event_id",
"browser", "os", "device", "language", "country",
"region", "city", "url_path", "url_query", "referrer_query", "event_name", "created_at"
];
Met deze code kregen wij de volgende resultaten in een console.log op de website zelf:
Op deze foto zijn meerdere values aanwezig omdat deze destijds alweer waren aangepast door een ander teamgenoot, maar de originele file had veel meer gelimiteerde data.
Ik heb gewerkt aan het uitwerken van een tijdstip chart. In deze chart kan je zijn op welk punt van de dag de meeste bellers zij en wanneer er de meeste vissen zijn gespot. Om dit voor elkaar te krijgen heb ik gebruik gemaakt van D3.js, dit visualiseert de charts in de vorm van <svg>'s.
Om de <svg> de creeëren heb ik code gebruikt die ik op de website https://d3js.org/getting-started heb gevonden, een voorbeeld hiervan is:
const svg = d3.select("#chart")
.append("svg")
.attr("viewBox", `0 0 ${baseWidth} ${baseHeight}`)
.attr("width", "100%")
// inplaats van .attr heb ik .style gebruikt omdat je met .attr geen auto mag gebruiken als value
.style("height", "auto")
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
svg.append("g")
.selectAll(".bg-bar")
.data(visibleHours)
.join("rect")
.attr("class", (d, i) => `bg-bar bg-bar-${i}`)
.attr("x", d => x(d.hour))
.attr("y", 0)
.attr("width", x.bandwidth())
.attr("height", height)
.attr("fill", "var(--color-white)")
.attr("rx", x.bandwidth() / 2)
.attr("ry", x.bandwidth() / 2);
Door genoeg van deze svg's te combineren (van lijnen, opvulling en capsules naar kleuren en animaties) heb ik een heel dynamisch en geanimeerde chart kunnen bouwen die past binnen de huisstijl van de pagina en correct de Svc data gebruikt:
// Base chart

// Chart + Tooltip

Tijdens het gesprek met Cyd hebben wij ons werk getoont en onderbouwt. Hoewel dit gesprek soepel liep hebben wij vrijwel meteen gehoord dat we het niet goed genoeg hadden voorbereid. Wij gingen eigenlijk het gesprek in zonder enige presentatie of voorbereiden met de gedachte dat het een normaal gesprek was. Daarnaast hebben wij individueel ons werk laten zien en kregen wij hier feedback op:
- Mats, header emoji werkt niet op windows
- Rafi, vis van seizoen Komt direct uit design, andere manier veel interessanter Leuke scroll animation! text alignment klopt niet
- Diego, beste tijdstip Het lijkt nu op allebei de charts gebruik anchor positioning voor de popup van elke bar chart een button maken of iets accassibility proof gebruiker wat betekent de Y as
- Vis foto van de maan, Jacco niet helemaal de opdracht, foto's zijn hadngekozen let bij de label op toegankelijkheid, gebruik ook een vervangende naam voor elke vis is een spotify wrapped wel leuk!
- Header gewoon weg laten en logo laten staan header past op dit moment ook niet
- Als je een framework gebruikt kan je simpel een component maken en die hergebruiken Wijk vooral af van het design
- code aanleveren op github - met uitleg van elke sectie, of toegankelijkheid hebt getest
- blauwe kleur mag gebruikt worden, maar hou het subtiel
Ik moest meer focussen op het re-designen van het grafiek. Op dit moment heb ik heel erg de designs gebruikt van het originele figma besand, maar Cyd verwacht iets creatievers en nieuws. Zelf ben ik verder gaan brainstormen en ben ik op het volgende gekomen:
- Het grafiek heeft een onder water thema, hier zie je de bodem van een gracht/oceaan
- Inplaats van een simpele kleur om de values te tonen wil ik zeewier of koraal gebruiken om de hoogte aan te tonen.
- Ik wil zowel vissen als bubbles rond laten zweven/zwemmen om wat meer detail te tonen
- Ik wil meerdere values tonen (bijvoorbeeld uploadedFisg en dismissedUpload, nog niet final), dit wil ik dat doen in combinatie met de bodem en het zeewier/koraal.
Ik ben week 2 begonnen het met opnieuw bedenken en redesignen van het grafiek. Vorige week heb ik al gebrainstormed en deze week wil ik ddit uitwerken. Voordat ik het ging uitwerken in HTML ben ik eerst gaan designen in Figma. Dit heb ik gedaan omdat het uitwerken van een design in d3.js best ingewikkeld is en veel verschillenden attributes gebruiken waardoor ik uren kan verliezen. Ik heb het oude design gebruikt in Figma ne deze om getoverd tot een gloed nieuw design die nog wel gebruik maakt van de huisstijl kleuren. Daarnaast heb ik aan Cyd gevraagd of ik de blauwe kleur in de huisstijl mocht gebruiken, haar antwoord hierop was:
"Het mag, maar probeer geen grote onderdelen te vullen met blauw. Het laden tussen de pagina's hebben een water vul animatie en deze valt weg als er veel blauw op de pagina aanwezig is."
Om dit te voorkomen heb ik gebruikt gemaakt van een gradient tussen de blauw kleur en de donker groene kleur, zo heb ik het effect van licht van het oppervlakte van het water, maar ook het donkere van de bodem van de gracht:
// Figma uitwerking:
Nadat ik deze uitwerking met mijn team heb besproken ben ik gelijk gaan coderen in HTML, CSS en Javascript. Hoewel ik het vorige grafiek best goed heb uitgewerkt met d3.js was dit een nieuwe soort uitdaging. Ik moest niet aleen de colommen weer aan elkaar vast maken, maar ook werken met animaties. Ik ben begonnen met de colommen aan elkaar vast connecten en deze een gradient achtergrond te geven, maar een gradient achtergrond kan niet zoals met de normale linear-background in CSS, dit moet handmatig worden gedaan met d3.js sinds het een svg is:
const svg = d3.select("#chart")
.append("svg")
.attr("viewBox", `0 0 ${baseWidth} ${baseHeight}`)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const defs = svg.append("defs");
defs.append("clipPath")
.attr("id", "chart-clip")
.append("rect")
.attr("width", width)
.attr("height", height);
const bgGradient = defs.append("linearGradient")
.attr("id", "bg-gradient")
.attr("x1", "0%").attr("y1", "0%")
.attr("x2", "0%").attr("y2", "100%");
bgGradient.append("stop").attr("offset", "0%").attr("stop-color", "var(--color-blue-green)");
bgGradient.append("stop").attr("offset", "100%").attr("stop-color", "var(--color-dark-green)");
Deze code was ebst complex om te vinden en ik heb veel moeten zoeken en proberen totdat ik de juiste attributes had gevonden, maar uiteindelijk was het resultaat wel een mooie gradient. De CSS optie werkt niet omdat het (technisch gezien) een svg is, deze kan je niet aanpassen met een linear-background maar alleen met een fill en die kun je alleen een gradient geven met javascript (met hoe ik het gebruik).
Daarna ben ik verder gegaan met het uitwerken van de data doormiddel van zeewier en de bodem, dit was een flinke uitdaging en ik heb hier ook gebruik gemaakt van Gemini omdat ik dit echt niet alleen kon (leerdoel). Hoewel het in het begin niet helemaal voelde als mijn eigen code heb ik het wel eigen gemaakt door het flink aan te passen naar mijn eigen wensen. Om het zeewier voor elkaar te krijgen moest ik de eerder gefetchte data omzetten naar zeewier dat dynamisch is in lengte, hiervoor heb ik de volgende code gebruikt:
const seaweedClip = defs.append("clipPath").attr("id", "seaweed-clip");
const clipPoints = [
{ hour: visibleHours[0].hour, uploadedFish: visibleHours[0].uploadedFish, total: visibleHours[0].total, edgeX: 0 },
...visibleHours.map(data => ({ ...data, edgeX: x(data.hour) + x.bandwidth() / 2 })),
{ hour: visibleHours[visibleHours.length - 1].hour, uploadedFish: visibleHours[visibleHours.length - 1].uploadedFish, total: visibleHours[visibleHours.length - 1].total, edgeX: width }
];
seaweedClip.append("path")
.datum(clipPoints)
.attr("d", d3.area()
.x(data => data.edgeX)
.y0(0)
.y1(data => y(data.uploadedFish || 0))
.curve(d3.curveBasis)
);
const seaweedContainer = svg.append("g").attr("clip-path", "url(#seaweed-clip)");
// visibleHours.forEach(createColumn)
visibleHours.forEach((data, hourIdx) => {
const colX = x(data.hour) + x.bandwidth() / 2;
const fishValue = data.uploadedFish || 0;
const groundY = y(fishValue);
const topY = y(data.total);
const seaweedHeight = Math.max(groundY - topY, 0);
const dismissedValue = data.dismissedUploading || 0;
const seaweedCount = dismissedValue > 0 ? Math.min(Math.max(Math.floor(dismissedValue / 500), 2), 6) : 0;
for (let i = 0; i < seaweedCount; i++) {
const offsetWidth = (i - (seaweedCount / 2)) * (x.bandwidth() / (seaweedCount + 1));
const bladeX = colX + offsetWidth;
const dynamicTopY = groundY - (seaweedHeight * (0.8 + (i % 3) * 0.1));
const seaweedPoints = [
[bladeX, groundY + 100],
[bladeX - (10 - i * 2), groundY - (seaweedHeight * 0.35)],
[bladeX + (10 - i * 2), groundY - (seaweedHeight * 0.65)],
[bladeX + (Math.sin(hourIdx + i) * 4), dynamicTopY]
];
const lineGenerator = d3.line().curve(d3.curveBasis);
const colorVariants = [
"var(--color-purple)",
"var(--color-pink)"
];
const bladeColor = colorVariants[i % colorVariants.length];
seaweedContainer.append("path")
.attr("d", lineGenerator(seaweedPoints))
.attr("fill", "none")
.attr("stroke", bladeColor)
.attr("opacity", 0.6)
.attr("stroke-width", isMobile ? 4 : 8)
.attr("stroke-linecap", "round")
.attr("z-index", 0);
}
});
Deze code zorgt ervoor dat de lengte van het zeewier dynamisch wordt aangemaakt, maar zorgt er ook voor dat er een kleine data-lijn wordt aangemaakt zodat je sneller en gemakkelijker kan zien dat het hier om een grafiek gaat. De kleuren heb ik aangegeven met variabele uit de CSS sinds deze de huisstijl beter volgen, maar om het wat beter op het oog te laten vallen heb ik een kleine opacity verandering toegevoegd (Nog niet eind beslissing). De hoogte van het zeewier bepaald ik door de hoogte te berekenen vanaf te top van het grafiek. Ik berekent met const dynamicTopY = groundY - (seaweedHeight * (0.8 + (i % 3) * 0.1)); het zeewier apart van de value van de bodem (sinds deze zin eigen value van uploadedFish heeft). Dat in combinatie met de seaweedHeight die ik eerder bereken met const seaweedHeight = Math.max(groundY - topY, 0); en wat andere values (die voornamelijk zijn ingevult door steeds een andere value in de voeren en weer te testen) is het zeewier zo accuraat als ik mogelijk kon maken.
Om het grafiek wat meer tot leven te brengen heb ik ook animaties toegevoegd doormiddel van bubbels en zwemmen vissen. Omdat ik dit met d3.js moet doen heb ik mijzelf gelimiteerd om het eerst te leren en daarna pas complexere animaties te maken (week 3). Hoewel ik al bekent ben met animaties en objecten toevoegen via javscript, was het niet met svg's en d3.js. Om dit te doen heb ik ook hulp gevraagt aan Gemini en heb ik mijn code eigen gemaakt. Ik heb twee functies gemaakt die allebei vrijwel dezelfde werking hebben maar dan met andere eind punten: function initFishAnimation() en function initBubbleAnimation(). Beide deze functies maken op dezelfde manier objecten aan, maar de uitwerking is anders sinds zij andere start punten hebben (horizontale en verticale as) en andere svg uitwerkingen hebben.
Voordat ik de initFishAnimation kon maken moest ik eerst de vis uitwerken in svg, ik heb dit eerst geprobeert met svg tools online, maar elke keer kwam er een vorm uit die helemaal niet leek op een vis, dus uiteindelijk heb ik aan geminin gevraagt om mijn bestaande svg code aan te passen naar dit:
function createFishPath() {
return "M0,6 C5,3 13,0 25,0 C32,0 40,5 45,8 L55,2 L52,10 L55,18 L45,12 C40,15 32,20 25,20 C13,20 5,17 0,14 C-3,12 -3,8 0,6 Z";
}
Daarna kon ik verder werken aan het uitwerken van beide animaties, hoewel deze ook met Gemini (gedeeltelijk) zijn gemaakt, begrijp ik elk stukje code en weer ik precies hoe het werkt, dit is mijn uitwerking:
function initBubbleAnimation(svg, width, height, isMobile) {
const floatingGroup = svg.append("g")
.attr("class", "bubble-layer")
.attr("clip-path", "url(#chart-clip)")
const numberOfBubbles = isMobile ? 15 : 20;
for(let i = 0; i < numberOfBubbles; i++) {
const bubble = floatingGroup.append("circle")
.attr("class", "individual-bubble")
.attr("fill", "rgba(255, 255, 255, 0.5)")
.attr("stroke", "rgba(255, 255, 255, 0.3)")
.attr("stroke-width", 1)
animateSingleBubble(bubble, width, height);
}
function animateSingleBubble(bubbleInstance, width, height) {
const radius= 2 + Math.random() * 6;
const startX = Math.random() * width;
const startY = height + 20;
const endY = -20;
const duration = 4000 + Math.random() * 5000;
const drift = (Math.random() * 40 -20);
const scale = 0.5 + Math.random() * 0.5;
bubbleInstance
.attr("cx", startX)
.attr("cy", startY)
.attr("r", radius)
.attr("opacity", 0.6)
.transition()
.duration(duration)
.ease(d3.easeLinear)
.attr("cx", startX + drift)
.attr("cy", endY)
.attr("r", radius * scale)
.attr("opacity", 0)
.on("end", () => {
animateSingleBubble(bubbleInstance, width, height);
});
}
}
function initFishAnimation(svg, width, height, isMobile) {
const fishGroup = svg.append("g")
.attr("class", "fish-layer")
.attr("clip-path", "url(#chart-clip)");
const numberOfFish = isMobile ? 3 : 5;
for (let i = 0; i < numberOfFish; i++) {
const fish = fishGroup.append("path")
.attr("d", createFishPath())
.attr("fill", "var(--color-dark-green)")
.attr("stroke-width", 1.5)
.attr("class", "individual-fish")
.attr("opacity", 0.75);
animateSingleFish(fish);
}
function animateSingleFish(fishInstance) {
const direction = Math.random() > 0.5 ? 1 : -1;
const startX = direction === 1 ? -45 : width + 5;
const endX = direction === 1 ? width + 5 : -45;
const randomY = 50 + Math.random() * (height * 0.7);
const duration = 9000 + Math.random() * 3000;
const scale = 0.4 + Math.random() * 0.5;
const flipTransform = direction === -1 ? `scale(1, 1)` : `scale(-1, 1)`;
fishInstance
.attr("transform", `translate(${startX}, ${randomY}) ${flipTransform} scale(${scale})`)
.transition()
.duration(duration)
.ease(d3.easeLinear)
.attr("transform", `translate(${endX}, ${randomY + (Math.random() * 60 - 30)}) ${flipTransform} scale(${scale})`)
.attr("z-index", direction === 1 ? 1 : -1)
.on("end", () => {
animateSingleFish(fishInstance);
});
}
}
// Grafiek + nieuw design
// Grafiek + nieuw design + hover

- Geweigerde uploads niet perse nodig om te zien, is niet nuttig voor de bezoeker.
- Moet net wat meer in de huisstijl, is net iets te 'spongebob'.
- Idee van de grafiek is leuk
- Dag en nacht dingen erbij, van licht naar donker in kleur
- Tekst pijltje moet een gewoom pijltje zijn
Week 3 begon zoals week 2, maar deze keer moesten wij weer naar het kantoor van onze coach. Bij zijn kantoor hebben wij ons product nog een keer gepresenteert. Hier kregen wij nog wat feedback houe wij ons volgende gesprek met de opdrachtgever aan kunnen pakken, waar wij op moetten letten en eventuele andere tips.
Na dit gesprek zijn wij gelijk doro naar school gegaan en hebben wij de feedback nogmaals bekeken. Wij hebben de feedback daarna per onderdeel verdeelt en zijn gelijk verder gegaan met de takenverdeling updaten, een Github release maken en nieuwe branches maken zodat wij gelijk verder konden.
Ik ben gelijk begonnen met het verwerken van mijn feedback voor de section "Het beste tijdstip van het seizoen". Er was veel te doen voor deze section, even een recap:
- Geweigerde uploads niet perse nodig om te zien, is niet nuttig voor de bezoeker.
- Moet net wat meer in de huisstijl, is net iets te 'spongebob'.
- Idee van de grafiek is leuk
- Dag en nacht dingen erbij, van licht naar donker in kleur
- Tekst pijltje moet een gewoom pijltje zijn
Ik moest doen mijn hele grafiek re-designen, verbeteren en een andere manier vinden om de data te tonen. Ik ben gelijk gaan brainstormen en Figma ingedoken om dit te verbeterem. Hoewel dit flink wat tijd heeft gekost heb ik uiteindelijk wel een beter idee verzonnen:
Een grafiek (huidig) maar zonder het spongebob thema. Het maakt gebruik van dezelfde huisstijl als de Visdeurbel nu doet, je hebt nog wel vissen en bubbels op de achtergrond, maar je kan deze keer per vis kijken wat het beste tijdstip is.
Ik ben gelijk begonnen met coderen en heb veel veranderingen gemaakt:
- Ik heb de kleuren van het grafiek zelf veranderd. De water achtergrond is vervavngen voor de licht goude achtergrond van de originele website in combinatie met de paarse kleur als data visualisatie.
- Ik heb een navigatie balk gemaakt aan de linker kant van het grafiek. Hier kan je kiezen uit "Totale activiteit", "Uploadedfish" en de andere vissen. De totale activiteit spreek voor zichzelf, die laat alle interacties met de deurbel zien. De UploadedFish laat zien wanneer iemand daadwerkelijk een foto heeft geüpload, dit betekent dat het alle vissen samen zijn opgeteld. En de vissen individueel laten zien op welk tijdstep over een hele periode het beste moment is om een bepaalde vis te spotten.
- Daarnaast heb ik onder en naast de grafiek door middel van tekst en pijlen aangegeven om welke waardes het gaat
Nadat ik het grafiek af had ben ik een andere section gaan maken zodat wij nóg een visualisatie konden aantonen bij de opdrachtgever. Dit is de al bestaande sectie die aangeeft hoeveel foto's er zijn ingestuurd en hoelang het duurt voordat je een vis zien. Voor ons is het de opdracht om deze om te zetten naar een visualisatie voor wanneer de visdeurbel offline staat. (Ik heb deze section in een vrij korte tijd gemaakt en ben helemaal vergeten dat de visdeurbel offline gaat)
Voor het eerste gedeelte heb ik een soort visuele timer gemaakt zodat je kan zien hoelang het duurt voordat je gemiddeld een vis zien in combinatie met een echt werkende klok, en toon ik aan welke vis als laatste is gespot. Zo krijgen bezoeker een beetje een idee welke vis zij kunnen verwachten en hoelang dat ongeveer kan duren. Dit heb ik gedaan emt de volgende code (niet alles, want het past anders niet :P)
clockGroup.append("circle").attr("r", radius - 6).attr("fill", "none").attr("stroke", "var(--color-dark-green)").attr("stroke-width", 4);
clockGroup.append("rect").attr("x", -6).attr("y", -radius).attr("width", 12).attr("height", 6).attr("fill", "var(--color-dark-green)").attr("rx", 2);
const gradenMinuten = (gemiddeldeMinuten / 60) * 360;
const wijzer = clockGroup.append("line")
.attr("x1", 0).attr("y1", 0).attr("x2", 0).attr("y2", -(radius - 16))
.attr("stroke", "var(--color-purple)")
.attr("stroke-width", 4)
.attr("stroke-linecap", "round");
const secondenWijzer = clockGroup.append("line")
.attr("x1", 0).attr("y1", 0).attr("x2", 0).attr("y2", -(radius - 12))
.attr("stroke", "var(--color-purple)").attr("opacity", 0.6).attr("stroke-width", 2).attr("stroke-linecap", "round")
.attr("class", "stopwatch-second-hand");
wijzer.transition().duration(1200).ease(d3.easeElasticOut.amplitude(1).period(0.4)).attr("transform", `rotate(${gradenMinuten})`);
let huidigeSeconde = new Date().getSeconds();
secondenWijzer.transition().duration(1200).ease(d3.easeElasticOut.amplitude(1).period(0.4)).attr("transform", `rotate(${huidigeSeconde * 6})`);
window.stopwatchInterval = d3.interval(() => {
huidigeSeconde++;
secondenWijzer.transition().duration(100).ease(d3.easeLinear).attr("transform", `rotate(${huidigeSeconde * 6})`);
}, 1000);
clockGroup.append("circle").attr("r", 5).attr("fill", "var(--color-dark-green)");
const textDiv = wrapper.append("div").attr("id", "stopwatch-text");
const minutenTekst = gemiddeldeMinuten === 1 ? "minuut" : "minuten";
textDiv.html(`
Gemiddeld duurt het <span class="stopwatch-text-highlight">${gemiddeldeMinuten} ${minutenTekst}</span> voordat je
een 🐟 vis ziet. De <span class="stopwatch-text-highlight">${latestFishName}</span> was als laatst gespot.
`);
Door weer met d3.js te werken heb ik een klok kunnen tekenen die daarna in combinatie met const startWindow = new Date(endWindow.getTime() - (60 * 60 * 1000)); zo accuraat mogelijk de echte seconde wijzer volgen. Hiermee heb ik uiteindelijk als resultaat een simpel maar werkende section kunnen maken:
Ik wilde het niet laten op één gedeelte dus heb ik, via de pijl knopjes eronder, nog een gedeelte gebouwd die te maken heeft emt deze tijd. Ik heb een tijdlijn gemaakt van het laatste opgenomen uur zodat bezoeker niet alleen de laatst gespotte vis kunnen zien, maar ook alle vissen van het afgelopen uur. Dit heb ik gedaan met de volgende code:
g.append("g")
.attr("transform", `translate(0, ${tlHeight / 2})`)
.attr("class", "timeline-axis")
.call(xAxis);
if (groupedFish.length === 0) {
g.append("text")
.attr("x", tlWidth / 2).attr("y", tlHeight / 2 - 5).attr("text-anchor", "middle")
.text("Geen vissen gezien in dit uur.");
} else {
const dotsGroup = g.append("g").attr("class", "dots-layer");
const interactionGroup = g.append("g").attr("class", "interaction-layer");
dotsGroup.selectAll(".fish-dot")
.data(groupedFish)
.enter()
.append("circle")
.attr("class", (d, i) => `fish-dot dot-index-${i}`)
.attr("cx", d => xScale(d.date))
.attr("cy", tlHeight / 2)
.attr("r", 6);
dotsGroup.selectAll(".fish-label")
.data(groupedFish)
.enter()
.append("text")
.attr("class", "fish-timeline-label")
.attr("x", d => xScale(d.date))
.attr("y", (tlHeight / 2) - 12)
.attr("text-anchor", "start")
.attr("transform", d => `rotate(-45, ${xScale(d.date)}, ${(tlHeight / 2) - 12})`)
.text(d => d.names.length === 1 ? "1 vis" : `${d.names.length} vissen`);
interactionGroup.selectAll(".interaction-rect")
.data(groupedFish)
.join("circle")
.attr("cx", d => xScale(d.date))
.attr("cy", tlHeight / 2)
.attr("r", 6)
.attr("fill", "rgba(255,255,255,0)")
.style("cursor", "pointer")
.on("mouseenter", function (event, d) {
const i = groupedFish.indexOf(d);
d3.select(`.dot-index-${i}`)
.attr("r", 8)
.style("fill", "var(--color-dark-green)");
let tooltipContent = `<h3>${tijdFormaat(d.date)}</h3><div id='line'></div>`;
d.names.forEach(name => {
tooltipContent += `<p>🐟 <strong>${name}</strong><br></p>`;
});
d3.select("#tooltip")
.style("display", "block")
.html(tooltipContent);
})
.on("mousemove", function (event) {
const tooltip = d3.select("#tooltip");
const tooltipNode = tooltip.node();
const tooltipWidth = tooltipNode ? tooltipNode.getBoundingClientRect().width : 180;
let leftPosition = event.pageX + 15;
if (event.clientX + tooltipWidth + 20 > window.innerWidth) {
leftPosition = event.pageX - tooltipWidth - 15;
}
tooltip
.style("left", leftPosition + "px")
.style("top", (event.pageY - 40) + "px");
})
.on("mouseleave", function (event, d) {
const i = groupedFish.indexOf(d);
d3.select(`.dot-index-${i}`)
.attr("r", 6)
.style("fill", "");
d3.select("#tooltip").style("display", "none");
});
}
Deze tijdlijn is in de span van een paar uur gemaakt dus werkt niet helemaal perfect zoals ik het zou willen, maar het laat de data zien op een dynamische manier en op een duidelijke manier. Volgende week (als deze tijdlijn wordt goedgekeurd) wil ik graag verder werken aan het verbeteren van de tijdlijn en meer toepassen van de huisstijl:

Hoelang voordat je de volgende vis ziet / visactiviteit afgelopen uur
- De visdeurbel is uit, dus het heeft geen nut om vissen in het laatste uur te tonen.
- Denk na over hoe je de tijdlijn kan aanpassen naar iets wat ‘niet actueel’ is.
Beste tijdstip van het seizoen
- Gebruik de pijlen die ook op de site staan / of iets wat hierop lijkt.
- Filter is klein. Neemt veel ruimte in en geeft een krap gevoel, geef alles wat meer ruimte.
- Alle tekst in filter is nu uppercase, misschien lowercase?
- Borderradius onderin wordt afgesneden.
- Tijdslijnen zijn moeilijk te zien.
- Doe misschien iets met dag en nacht.
- Geef context om te zeggen wat je laat zien.
- Probeer het dus een beetje aan te sluiten bij de visualisatie met het aquarium.
- Gebruik niet te nieuwe elementen (hergebruik buttons ect)
Week 4 begon vrij normaal, maar ook met een bepaalde druk, het einde kwam meer in zicht en de zenuwen begonnen wel op te bouwen. Wij begonnen weer bij onze coach, zoals vorige week hadden wij ons product nogmaals gepresenteert. Wij kregen nog wat nuttige feedback op zowel ons product als de product biografie en de design rationale! Wij kregen ook nog wat korte tips voor het volgende gesprek, maar op basis van onze info ging dit gesprek een stuk beter.
Na dit gesprek zijn wij gelijk weer door naar school gegaan. Eenmaal op school gingen wij eerst de feedback terug kijken zodat wij een soort mini planning konden maken wat wij nog konden doen, welke onderdelen vervanging nodig hadden en wie wat doet.
Ik ben gelijk weer begonnen met het verwerken van mijn feedback voor zowel de sections "Het beste tijdstip van het seizoen" als "Vis activiteit". Ik zal een korte recap geven vopor beide sections
- Knoppen zijn paars en het is niet duidelijk dat het knoppen zijn
- Uploadedfish is niet duidelijk voor de bezoeken
- Geen duidelijk verschil tussen dag en nacht
- De vissen die op de achtergrond zwemmen hebben maar 1 vorm, voeg net zoals "De vis an het seizoen" verschillende vissen toe
- Nog steeds te cartoon achtig
- Ruimte tussen de tekst links (Deurbellers ->) en de navigatie knoppen is te klein
- Gebruik de pijlen van de website bij de teksten
- Data is niet relevant, het laat de gemiddelde tijd van 'nu' zien terwijl de vis deurbel offline gaat, dus er zal geen 'nu' tijd meer zijn.
- Tijdlijn is ook niet relevant. Tijdlijn laat het afgelopen uur zien, maar de visdeurbel gaat offline.
- Hele idee van de section goed, maar uitvoering moet voor het seizoen worden
Ik ben gelijk verder gaan werken en brainstormen hoe ik de sections nu wil uitwerken, ik zal weer zoals gewoonlijk elke section uitschrijven hieronder:
Hoewel ik flink wat aanpassingen moest maken, hoefde ik niet het hele grafiek opnieuw te maken, dit was een gigantische opluchtig. Ik ben gelijk gaan brainstormen hoe ik de feedback kon vertalen naar oplossingen. Ten eertste moest ik de styling voor de buttons aanpassing zodat het meer past bij de huisstijl, dit houdt in:
- Een witte achtergrond
- Donker groene border
- Donker groene tekst
- Paarse active state
- Beige container achtergrond kleur.
Daarnaast heb ik kleuren aan de grafiek toegevoegd om een duidelijke indicatie te maken voor de dag- en nachturen. De statische SVG-vissen op de achtergrond zijn vervangen door echte foto's van de vissen die ook op de website zelf worden gebruikt, waardoor de grafiek veel meer als een geheel aanvoelt met de website. Als laatste heb ik op basis van feedback van de opdrachtgever, klasgenoten en zelfs mijn eigen teamgenoten een korte uitleg toegevoegd zodat gebruikers direct snappen wat ze hier kunnen doen.
Deze iteratie heeft al flink vernieuwing. Op basis van de feedback van de het vorige gesprek met stdio Moan heb ik ervoor gekozen dat je nu zelf kunt kiezen welke data je ziet. Omdat de visdeurbel offline gaat zie je geen data van 'het afgelopen uur'. Daarom heb ik ervoor gekozen dat je zelf een week kunt kiezen, zodat je kunt vergelijken hoe die periode afliep. De klok en de eerste tekst zijn voor de rest hetzelfde gebleven, maar het tweede gedeelte heeft een compleet nieuw design gekregen.
Er zit nu een kalender in die per dag in deze periode aangeeft hoe druk het was in elk dagdeel. Je hebt hierbij ochtend, middag, avond en nacht. Hier heb ik voor gekozen omdat het per land heel erg kan verschillen hoe druk het was. In Amerika kan het, bij wijze van spreken, heel druk zijn in de ochtend terwijl het hier dan juist heel rustig is. Ook kun je per dagdeel zien hoeveel vissen er in die tijd zijn gespot. Voor de kalender heb ik twee complexe functies gebruikt function setupKalenderBase() en function updateVisKalender(). De eerste functie zal ik in code hieron plaatsen, maar de updateVisKalender functie is de groot voor de productbiografie. Met deze functie creeër ik niet alleen de kalender cellen, maar zorg ik er ook voor dat als de data veranderd dat hij ook wordt geüpdate.
function setupKalenderBase() {
const container = d3.select("#stopwatch-container");
if (container.empty()) return;
container.selectAll(".calendar-section").remove();
const wrapper = container.append("div").attr("class", "calendar-section");
wrapper.append("div").attr("class", "calendar-title").text("Visactiviteit deze week");
wrapper.append("p").attr("class", "calendar-explanation").text("Bekijk op welke dagen en dagdelen de vissen het meest actief waren.");
const layoutContainer = wrapper.append("div").attr("class", "calendar-layout-container");
// Verhoogd om de bredere linkermarge op te vangen zonder de cellen in elkaar te drukken
const viewWidth = 600;
const viewHeight = 250;
// De linkermarge is aanzienlijk vergroot van 35 naar 95 om de langere tekstlabels volledig te tonen
const margin = { top: 15, right: 15, bottom: 0, left: 95 };
const chartWidth = viewWidth - margin.left - margin.right;
const chartHeight = viewHeight - margin.top - margin.bottom;
// Met behulp van Gemini: xMidYMid zorgt ervoor dat jouw visualisatie altijd netjes in het exacte middelpunt van de container wordt geplaatst, zowel horizontaal als verticaal.
const svg = layoutContainer.append("svg")
.attr("viewBox", `0 0 ${viewWidth} ${viewHeight}`)
.attr("preserveAspectRatio", "xMidYMid meet")
.attr("class", "calendar-svg")
.attr("role", "grid")
.attr("aria-label", "Visactiviteit kalender per dag en dagdeel");
svg.datum({ chartWidth, chartHeight, margin });
const chartGroup = svg.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`)
.attr("id", "calendar-chart-group");
chartGroup.append("g").attr("class", "calendar-axis x-axis");
chartGroup.append("g").attr("class", "calendar-axis y-axis");
chartGroup.append("g").attr("id", "calendar-grid-cells");
layoutContainer.append("div")
.attr("id", "side-stats-card")
.attr("class", "side-stats-card");
}
- Vis activiteit: Filter van periode wat lekkerder maken, meer een geheel. De periode knop staat nog raar in het midden, misschien kan je daar nog wat leukst mee. Kijk naar de hiërarchie, wat wil je dat mensen eerst zien? Maak het iets aantrekkelijker om te klikken. Week omzetten naar datum, gewoon een datum zoals Juni. “Week 18” is onduidelijk, mensen weten niet wat elke week is. Een kalender is een leuk idee, maar ziet er nog saai uit. Kalender misschien in paars? Groen ziet er snel te degelijk uit. Visactiviteit is niet heel breed, dus past niet bij de rest van de styles. Kijk naar de algehele stijl.
- Het beste tijdstip van het seizoen: Welke kleuren voor dag of nacht? Er is nu te veel tekst om te lezen, het wordt te druk. Snapt de uitleg wel, maar verstop dit achter een i’tje. Maak het wat figuratiever, laat het bijvoorbeeld meer ogen als water. Pas een soort curve boog toe met de zon (soort zonsopgang en zonsondergang) in de grafiek, zodat mensen begrijpen dat het gaat over de uren en de tijd. Het vismenuutje is allemaal in het midden gezet. Lijn het naar links uit bijvoorbeeld. Visjes naar voren boven de grafiek plaatsen.
De laatste week was voornamelijk voor de allerlaatste puntjes op de i zetten en de laatste feedback verwerken. Alles moest nu samen komen voor het eindproduct. Ik ben gelijk aan de slag gegaan om de feedback van vorige week te verwerken in de laatste iteraties. Grotendeels van de veranderingen deze week zijn design keuzes sinds wij maar een paar dagen hebben voor de eind expo.
Dit is de laatste iteratie. Bij deze versie heb ik alle feedback in één keer meegenomen om de mooiste en definitieve versie te bouwen. Ik heb op basis van de feedback van de opdrachtgever het aquarium-idee weer teruggebracht, maar deze keer op een subtielere manier. Om de website consistent te houden heb ik dezelfde stenen achtergrond gebruikt als bij "Vis van het seizoen". Daarnaast heb ik de knoppen omgezet naar knoppen met sliders erin. Dit volgt voor 100% de huisstijl van de originele visdeurbel-website.
Om het dag- en nachtconcept wat subtieler te maken heb ik de achtergrond dynamisch gemaakt. Tijdens het gesprek met de opdrachtgever gaf hij aan dat hij de achtergrondkleuren te hard vond, dus nu passen de kleuren zich aan in de vorm van een gradient tijdens een hover over een kolom heen. Om nog meer toe te voegen aan het dag/nacht-gedeelte heb ik een zonnelijn gemaakt waar je de stand van de zon kunt zien in de grafiek. Hoe hoger de zon staat, hoe meer licht er is. Als laatste heb ik de tekst verplaatst naar een aparte popover die je kunt vinden bij de ? knop aan de rechterkant van de titel.

De laatste iteratie heeft alle feedback samengevat en verbeterd. Op basis van de feedback van de opdrachtgever heb ik de border om de weekselectie weg gelaten en ook de dagen in de tekst gezet in plaats van alleen 'Week 18'. Alleen het weeknummer tonen was niet duidelijk genoeg voor gebruikers.
In het tweede gedeelte heb ik ook wat veranderingen gemaakt. Ten eerste heb ik de kleuren van de kalender-cellen aangepast naar de paarse kleur van de visdeurbel omdat de opdrachtgever vond dat er te veel groen aanwezig was. Om de cellen zelf op te vullen laat ik nu in elke cel de meest gespotte vis zien van dat dagdeel. On hover heb ik de lijst opnieuw georderend zodat de meest gespotte vis boven aan staat.
Om het tweede gedeelte op te vullen en minder leeg te laten lijken heb ik ervoor gekozen om nog een totale statistiekenlijst te bouwen. Zo zie je in één oogopslag hoeveel vissen er in totaal in deze periode zijn gespot. Ook deze data past zich dynamisch aan en heeft op aanvraag van de opdrachtgever veel kleurvarianten gekregen.

- Vis activiteit: Dit was de laatste iteratie, dus om de puntjes op de i te zetten heeft de opdrachtgever minimale feedback gegeven (onderandere tekst kleuren, tekst groottes en border width).
- Het beste tijdstip van het seizoen: Dit was de laatste bespreking met de opdrachtgever, ik heb hier weinig feedback gehad. Er waren een paar kleine elementen die ik moest aanpassen (Voornamelijk kleuren, margins of andere kleinigheidjes)
Nu ik terug kijk op dit project ben ik zeer tevreden met wat ik heb bereikt, geleerd en gemaakt. In het begin van het project had ik persoonlijk nog best wat moeite met de opdracht, het was niet allemaal even duidelijk en het was veel nieuwe informatie en code die ik in een korte tijd moest leren. Gelukkig heb ik over de tijd van het project heel veel kunnen leren over het verwerken van csv data, astro componenten en alle data omzetten met Javascript. Ik heb ook meer geleerd met AI te werken en niet zo lang koppig te zijn met mijn functies, ik bleef voor dit project vaak hangen aan één functie; maar als ik aan AI had gevraagd hoe ik het kon oplossen dan zou ik er veel sneller door heen zijn geweest. Vooral bij het uitwerken van d3.js componenten heb ik super veel geleerd, hoe ik met .attr kan werken, en bepaalde elementen kan mappen.
Mijn belangrijkste leerdoel, voor mij, was: Ik wil beter leren begrijpen wat de klant verwacht in het product. Op dit moment heb ik voornamelijk ervaring met voor één persoon, maar ik wil het voor meerdere klanten/opdrachtgevers leren. Nu weet ik heel goed hoe ik één persoon blij kan maken, maar meerdere personen hebben meerdere meningen en dan kan conflicten met elkaar. Ik had voor dit project heel veel moeite met het luisteren naar de opdrachtgever, hoe raar het ook klinkt. Ik wilde altijd mijn eigen verbeteringen toepassen en tonen bij de opdrachtgever, maar dit project heeft mij juist geleerd dat jij als ontwerper niet altijd gelijk hebt. Je kan hele mooie projecten maken, maar uiteindelijk bepaald de opdrachtgever of hij jouw product wilt gebruiken of niet.
Ik vond de samenwerking met mijn team ook heel fijn. Tijdens dit project heb ik met iedereen veel samen gewerkt. Ik heb voornamelijk samengewerkt met Jacco, sinds hij naast mij zat. Samen met hem heb ik veel verschillende functies kunnen maken en zelfs kunnen verbeteren met zijn feedback. Jacco heeft een beter oog voor detail dan ik, dus het was fijn dat iemand mee keek die goed op designs let. De planning binnen het tema verliep ook heel soepel; in het begin moesten wij al ons werk bijhouden via trello, maar op een gegeven moment verliep alles zo soepel dat wij geen trelo meer nodig ahdden, wij hadden dagelijks heel veel contact en zijn ook buiten school uren om in teams meetings geweest.
Voor mijn volgende projecten zou ik nog graag meer willen groeien in hulp vragen en in presenteren. Hoewel in tijdens dit project veel sneller om hulp heb gevraagd dan bij elk ander project ooit, heb ik voor mijn gevoel nog niet genoeg om hulp gevraagd. Ik wachtte vaak tot het laatste moment met hulp vragen terwijl ik veel verder kon zijn in mijn progressie als ik dat vanaf het begin had gedaan. Ook wil ik meer presenteren. Dit project liet ik Mats voornamelijk presenteren sinds hij daar beter in is, maar volgende projecten wil ik ook beter worden in presenteren.
- https://www.w3schools.com/js/js_dates.asp
- https://www.w3schools.com/js/js_const.asp
- https://www.w3schools.com/jsref/jsref_filter.asp
- https://d3js.org/d3-selection/selecting
- https://d3js.org/d3-selection/joining-data
- https://d3js.org/d3-ease
- https://d3js.org/d3-scale/linear
- https://d3js.org/d3-timer/interval
- https://d3js.org/d3-axis
- https://d3js.org/d3-selection/events
- https://d3js.org/d3-shape/line
- https://d3js.org/d3-shape/area
- https://d3js.org/d3-transition