In deze tweedelige serie combineren we het veelzijdige canvaselement met de robuuste jQuery-bibliotheek om een staafgrafiekplug-in te maken. In dit tweede deel gaan we het omzetten naar een jQuery-plug-in, en dan wat eye candy en extra functies toevoegen.
Het afsluiten van de Plezier met canvas tweedelige reeks, vandaag gaan we een staafgrafiekplugin maken; geen gewone plug-in, let wel. We gaan jQuery love to the canvas-element laten zien om een zeer robuuste plug-in te maken.
In deel één hebben we alleen gekeken naar het implementeren van de logica van de plug-in als een zelfstandig script. Aan het einde van deel één zag onze staafgrafiek er zo uit.
In dit laatste deel zullen we werken aan het converteren van onze code en het een goede jQuery-plug-in maken, wat wat visuele aardigheden toevoegt en uiteindelijk een aantal extra functies bevat. Uiteindelijk ziet onze uitvoer er zo uit:
Alle opgewarmd? Laten we erin duiken!
Voordat we beginnen met het converteren van onze code naar een plug-in, moeten we eerst een paar formaliteiten bekijken als het gaat om het invoegen van plug-ins.
We beginnen met het kiezen van een naam voor de plug-in. ik heb gekozen staafdiagram en hernoemde het JavaScript-bestand naar jquery.barGraph.js. We plaatsen nu alle code uit het vorige artikel in het volgende fragment.
$ .fn.barGraph = functie (instellingen) // code hier
instellingen bevat alle optionele parameters doorgegeven aan de plug-in.
Bij het maken van jQuery-plug-ins wordt over het algemeen een best practice overwogen om te gebruiken jQuery in plaats van de $ alias in uw code, om conflicten met andere Javascript-bibliotheken tot een minimum te beperken. In plaats van al die problemen door te nemen, kunnen we alleen aangepaste aliassen gebruiken zoals vermeld in de jQuery-documenten. We voegen al onze plugin-code toe aan deze zelfuitvoering met anonieme functie, zoals hieronder weergegeven:
(functie ($) $ .fn.barGraph = functie (instellingen) // plug-in implementatiecode hier) (jQuery);
In wezen kapselen we al onze code binnen een functie in en geven we jQuery hieraan door. We zijn vrij om de $ alias nu zo vaak in onze code te gebruiken als we willen, zonder ons zorgen te hoeven maken dat deze mogelijk in strijd is met andere JavaScript-bibliotheken.
Bij het ontwerpen van een plug-in is het verstandig om een redelijk aantal instellingen aan de gebruiker bloot te stellen, terwijl gebruik wordt gemaakt van verstandige standaardopties als de gebruikers de plug-in gebruiken zonder er opties aan te geven. Met dat in het achterhoofd, zullen we de gebruiker in staat stellen om elk van de variabelen voor de grafiekopties die ik in dit vorige artikel in deze reeks noemde, te veranderen. Dat is eenvoudig; we definiëren elk van deze variabelen als eigenschappen van een object en benaderen deze vervolgens.
var standaard = barSpacing = 20, barWidth = 20, cvHeight = 220, numYlabels = 8, xOffset = 20, maxVal, gWidth = 550, gHeight = 200; ;
We moeten uiteindelijk de standaardopties samenvoegen met de doorgegeven opties, waarbij de voorkeur wordt gegeven aan de doorgegeven opties. Deze lijn zorgt ervoor.
var option = $ .extend (standaardinstellingen, instellingen);
Vergeet niet om de variabelennamen waar nodig te wijzigen. Als in -
return (param * barWidth) + ((param + 1) * barSpacing) + xOffset;
… veranderd naar:
return (param * option.barWidth) + ((param + 1) * option.barSpacing) + option.xOffset;
Hier wordt de plug-in gehamerd. Onze oude implementatie kon maar één enkele grafiek in een pagina produceren en de mogelijkheid om meerdere grafieken op een pagina te maken is de belangrijkste reden dat we een plug-in voor deze functionaliteit maken. Bovendien moeten we ervoor zorgen dat de gebruiker geen canvaselement hoeft te maken voor elke grafiek die moet worden gemaakt. Met dat in het achterhoofd, gaan we de canvas-elementen dynamisch maken als dat nodig is. Laten we doorgaan. We zullen kijken naar de eerdere en bijgewerkte versies van de relevante delen van de code.
Voordat we beginnen, wil ik erop wijzen hoe onze plug-in wordt opgeroepen.
$ ("# years"). barGraph (barSpacing = 30, barWidth = 25, numYlabels = 12,);
Simpel als dat. jaar is de ID van de tabel die al onze waarden bevat. We geven de opties door als dat nodig is.
Om dingen uit te schakelen, hebben we eerst een referentie nodig naar de gegevensbron voor de grafieken. We hebben nu toegang tot het bronelement en verkrijgen de ID. Voeg de volgende regel toe aan de reeks grafiekvariabelen die we eerder hebben verklaard.
var dataSource = $ (this) .attr ("id");
We definiëren een nieuwe variabele en wijzen deze toe aan de waarde van het ID-kenmerk van het doorgegeven element. Binnen onze code, deze verwijst naar het momenteel geselecteerde DOM-element. In ons voorbeeld verwijst het naar de tabel met een ID van jaar.
In de vorige implementatie is de ID voor de gegevensbron hard gecodeerd. Nu vervangen we die door het ID-kenmerk dat we eerder hebben geëxtraheerd. De eerdere versie van de grabValues functie is hieronder:
function grabValues () // Toegang tot de vereiste tabelcel, uitpakken en de waarde ervan toevoegen aan de waardenreeks. $ ("# data tr td: nth-child (2)"). each (function () gValues.push ($ (this) .text ());); // Open de vereiste tabelcel, extraheer en voeg de waarde toe aan de array xLabels. $ ("# data tr td: nth-child (1)"). each (function () xLabels.push ($ (this) .text ()););
Het is hiertoe bijgewerkt:
function grabValues () // Toegang tot de vereiste tabelcel, uitpakken en de waarde ervan toevoegen aan de waardenreeks. $ ("#" + dataSource + "tr td: nth-child (2)"). each (function () gValues.push ($ (this) .text ());); // Open de vereiste tabelcel, extraheer en voeg de waarde toe aan de array xLabels. $ ("#" + dataSource + "tr td: nth-child (1)"). each (function () xLabels.push ($ (this) .text ()););
function initCanvas () $ ("#" + dataSource) .after (""); // Probeer toegang te krijgen tot het canvas element cv = $ (" # bargraph - "+ dataSource) .get (0); if (! Cv.getContext) return; // Probeer een 2D-context te krijgen voor de canvas en gooi een fout als het niet lukt om ctx = cv.getContext ('2d'); if (! ctx) return;
We maken een canvaselement en injecteren dit in de DOM na de tabel, die fungeert als de gegevensbron. jQuery na functie komt hier echt van pas. Een klassenattribuut van staafdiagram en een ID-kenmerk in het formaat bargraph-dataSourceID wordt ook toegepast om de gebruiker in staat te stellen om ze allemaal als een groep of individueel naar eigen inzicht in te stellen.
Er zijn twee manieren om deze plug-in daadwerkelijk aan te roepen. U kunt elke grafiek afzonderlijk doorgeven aan slechts één gegevensbron of u kunt een aantal bronnen doorgeven. In het laatste geval zal ons huidige construct een fout tegenkomen en stoppen. Om dit te verhelpen, gebruiken we de elk construct om de verzameling doorgegeven elementen te herhalen.
(functie ($) $ .fn.barGraph = functie (instellingen) // Optievariabelen var-standaard = // opties hier; // Voeg de doorgegeven parameters samen met de standaardwaarden var option = $ .extend (standaardinstellingen, instellingen ); // Blader door elk gepasseerd object this.each (function () // Implementatiecode hier); // Retourneert het jQuery-object om de mogelijkheid te koppelen, retourneer dit;) (jQuery);
We encapsuleren alle code na het verkrijgen en samenvoegen van de instellingen in de this.each construeren. We zorgen er ook voor dat het jQuery-object aan het einde wordt geretourneerd om de ketenbaarheid mogelijk te maken.
Hiermee is onze refactoring voltooid. We zouden onze plugin moeten kunnen aanroepen en zoveel mogelijk grafieken kunnen maken als dat nodig is.
Nu onze conversie is voltooid, kunnen we eraan werken om het visueel beter te maken. We gaan hier een aantal dingen doen. We zullen ze elk afzonderlijk bekijken.
De oudere versie gebruikte een neutraal grijs om de grafieken te tekenen. We gaan nu een thematiemechanisme voor de bars implementeren. Dit bestaat op zichzelf uit een reeks stappen.
var defaults = // Andere standaardinstellingen hier thema: "Ocean",;
We voegen een toe thema optie op de standaardinstellingen, waardoor de gebruiker het thema kan wijzigen in een van de vier beschikbare presets.
function grabValues () // Vorige codeschakelaar (option.theme) case 'Ocean': gTheme = thBlue; breken; case 'Foliage': gTheme = thGreen; breken; case 'Cherry Blossom': gTheme = thPink; breken; case 'Spectrum': gTheme = thAssorted; breken;
Een eenvoudige schakelaar construct bekijkt de option.theme instellen en wijst op de gTheme variabele naar de benodigde kleurenarray. We gebruiken beschrijvende namen voor de thema's in plaats van generieke.
// Thema's var thPink = ['#FFCCCC', '# FFCCCC', '# FFC0C0', '# FFB5B5', '# FFADAD', '# FFA4A4', '# FF9A9A', '# FF8989', '# FF6D6D ']; var thBlue = ['# ACE0FF', '# 9CDAFF', '# 90D6FF', '# 86D2FF', '# 7FCFFF', '# 79CDFF', '# 72CAFF', '# 6CC8FF', '# 57C0FF']; var thGreen = ['# D1FFA6', '# C6FF91', '# C0FF86', '# BCFF7D', '# B6FF72', '# B2FF6B', '# AAFE5D', '# A5FF51', '# 9FFF46']; var thAssorted = ['# FF93C2', '# FF93F6', '# E193FF', '# B893FF', '# 93A0FF', '# 93D7FF', '# 93F6FF', '# ABFF93', '# FF9B93'];
Vervolgens definiëren we een aantal matrices, elk met een reeks tinten van specifieke kleuren. Ze beginnen met de lichtere tint en blijven stijgen. We zullen later door deze arrays lopen. Het toevoegen van thema's is net zo eenvoudig als het toevoegen van een array voor de specifieke kleur die u nodig hebt, en dan het modificeren van het vorige schakelaar om de veranderingen te weerspiegelen.
functie getColour (param) return Math.ceil (Math.abs (((gValues.length / 2) -param)));
Dit is een kleine functie waarmee we een gradiëntachtig effect op de grafieken kunnen bereiken en toepassen. In wezen berekenen we het absolute verschil tussen de helft van het aantal waarden dat moet worden weergegeven en de doorgegeven parameter, die de index is van het momenteel geselecteerde item in de array. Op deze manier kunnen we een vloeiend verloop maken. Omdat we slechts negen kleuren in elk van de kleurenarrays hebben gedefinieerd, zijn we beperkt tot achttien waarden per grafiek. Uitbreiding van dit aantal moet vrij triviaal zijn.
functie drawGraph () for (index = 0; indexDit is waar we de grafieken feitelijk thematiseren. In plaats van het instellen van een statische waarde op de fillStyle eigendom, gebruiken we de getColour functie om de benodigde index van het element in de matrix van het momenteel geselecteerde thema op te halen.
ondoorzichtigheid
Vervolgens gaan we de gebruiker de mogelijkheid geven om de dekking van de getrokken staven te controleren. Instellingen dit is een proces in twee stappen.
Zonder transparantie
Met een waarde van 0,8Toevoegen aan de opties
var defaults = // Overige standaardwaarden hier barOpacity: 0.8,;We voegen een toe barOpacity optie naar de standaardwaarden, waardoor de gebruiker de dekking van de grafieken kan veranderen in een waarde van 0 tot 1, waarbij 0 volledig transparant is en 1 volledig dekkend is.
Het instellen van globalAlpha
functie drawGraph () for (index = 0; indexDe globalAlpha eigenschap bepaalt de dekking of transparantie van het gerenderde element. We stellen de waarde van deze eigenschap in op de gepasseerde waarde of de standaardwaarde om een beetje transparantie toe te voegen. Als een verstandige standaard gebruiken we een waarde van 0,8 om het een klein beetje transparant te maken.
rooster
Een raster kan uitermate nuttig zijn bij het verwerken van de gegevens die in een grafiek worden gepresenteerd. Hoewel ik aanvankelijk een goed raster wilde, heb ik later genoegen genomen met een reeks horizontale lijnen in de rij met de labels van de Y-as en de verticale lijnen volledig weggegooid, omdat ze de gegevens gewoon in de weg stonden. Met dat uit de weg, laten we een manier implementeren om het weer te geven.
Met raster uitgeschakeld
Met raster ingeschakeldDe lijnen maken met behulp van paden en de lineTo methode leek de meest voor de hand liggende oplossing voor het tekenen van de grafieken, maar ik kwam toevallig een rendering-bug tegen die deze benadering ongeschikt maakte. Daarom blijf ik bij de fillRect methode om ook deze regels te maken. Hier is de functie in zijn geheel.
functie drawGrid () for (index = 0; indexDit lijkt sterk op het tekenen van de Y-aslabels, behalve dat in plaats van het renderen van een label, we een horizontale lijn tekenen die de breedte van de grafiek beslaat met een breedte van 1 px. De Y functie helpt ons bij de positionering.
Toevoegen aan de opties
var defaults = // Andere standaardwaarden hier disableGrid: false,;We voegen een toe disableGrid optie naar de standaardinstellingen, waardoor de gebruiker kan bepalen of een raster wordt weergegeven of niet. Standaard wordt het weergegeven.
// Functie roept als (! Option.disableGrid) drawGrid ();We controleren alleen of de gebruiker wil dat het raster wordt weergegeven en dienovereenkomstig doorgaan.
contouren
Nu de balken allemaal gekleurd zijn, ontbreekt het accent tegen een lichtere achtergrond. Om dit recht te zetten, hebben we een slag van 1 px nodig. Er zijn twee manieren om dit te doen. De eerste en eenvoudigste manier zou zijn om gewoon een toe te voegen strokeRect methode om de DrawGraph methode; of, we kunnen de lineTo methode om snel de rechthoeken te aaien. Ik koos de vroegere route sindsdien net als voor de lineTo methode gooide wat vreemde rendering bug naar mij.
Zonder strelen
Met aaienToevoegen aan opties
Eerst voegen we het toe aan de defaults object om de gebruiker controle te geven over de vraag of dit wordt toegepast of niet.
var defaults = // Andere standaardwaarden hier showOutline: true,;functie drawGraph () // Previous code if (option.showOutline) ctx.fillStyle = "# 000"; ctx.strokeRect (x (index), y (gValues [index]), width (), height (gValues [index])); // Rest van de codeWe controleren of de gebruiker de contouren wil weergeven en zo ja, we gaan door. Dit is bijna hetzelfde als het weergeven van de daadwerkelijke balken, behalve dat in plaats van het gebruik van de fillRect methode gebruiken we de strokeRect methode.
shading
In de oorspronkelijke implementatie is er geen onderscheid tussen het canvas-element zelf en de daadwerkelijke rendering-ruimte van de balken. We zullen dit nu corrigeren.
Zonder schaduw
Met schaduwfunction shadeGraphArea () ctx.fillStyle = "# F2F2F2"; ctx.fillRect (option.xOffset, 0, gWidth-option.xOffset, gHeight);Dit is een kleine functie die het gewenste gebied in de schaduw stelt. We bedekken het canvaselement minus het gebied dat wordt bedekt door de labels van beide assen. De eerste twee parameters wijzen naar de x- en y-coördinaten van het startpunt en de laatste twee wijzen naar de vereiste breedte en hoogte. Door te beginnen bij option.offset, we elimineren het gebied bedekt door de Y-aslabels en door de hoogte te beperken tot gHeight, we elimineren de X-as labels.
Functies toevoegen
Nu onze grafiek er aantrekkelijk genoeg uitziet, kunnen we ons concentreren op het toevoegen van een aantal nieuwe functies aan onze plug-in. We zullen ze elk afzonderlijk bekijken.
Beschouw deze grafiek van de beroemde 8K-pieken.
Wanneer de hoogste waarde voldoende hoog genoeg is en de meeste waarden binnen 10% van de maximale waarde vallen, is de grafiek niet meer bruikbaar. We hebben twee manieren om dit recht te zetten.
ShowValue
We beginnen eerst met de eenvoudigere oplossing. Door de waarde van de respectieve grafieken bovenaan weer te geven, is het probleem vrijwel opgelost, omdat de afzonderlijke waarden eenvoudig kunnen worden onderscheiden. Hier is hoe het geïmplementeerd is.
var defaults = // Overige standaardwaarden hier showValue: true,;Eerst voegen we een item toe aan de defaults object om de gebruiker in staat te stellen het naar believen in en uit te schakelen.
// Functieaanroepen if (option.showValue) drawValue ();We controleren of de gebruiker wil dat de waarde wordt getoond en gaat vervolgens door.
functie drawValue () for (index = 0; indexWe itereren door de gValues array en render elke waarde afzonderlijk. De berekeningen met betrekking tot valAsString en VALX zijn niets dan kleine berekeningen om ons te helpen bij de juiste inkepingen, dus het ziet er niet misplaatst uit.
Schaal
Dit is de moeilijker van de twee oplossingen. In deze methode beginnen we, in plaats van de Y-aslabels op 0 te starten, veel dichter bij de minimumwaarde. Ik zal het uitleggen als we gaan. Houd er rekening mee dat in het bovenstaande voorbeeld het verschil tussen opeenvolgende waarden met betrekking tot de maximale waarde tamelijk onbeduidend is en niet zozeer de effectiviteit ervan aangeeft. Andere gegevenssets zouden het gemakkelijker moeten maken om resultaten te analyseren.
Toevoegen aan opties
var defaults = // Overige standaardwaarden hier schaal: false;De schaalfunctie bijwerken
Sinds de schaal functie is een integraal onderdeel van het weergaveproces, we moeten het bijwerken om de schaalfunctie toe te staan. We werken het als volgt bij:
functie schaal (param) return ((option.scale)? Math.round (((param-minVal) / (maxVal-minVal)) * gHeight): Math.round ((param / maxVal) * gHeight));Ik weet dat dit er een beetje gecompliceerd uitziet, maar het lijkt er alleen op als gevolg van het gebruik van de ternaire conditionele operator. In wezen controleren we de waarde van option.scale en als het false zegt, wordt de oudere code uitgevoerd. Als het waar is, normaliseren we in plaats van de waarde te normaliseren als een functie van de maximumwaarde in de array, dat dit een functie is van het verschil tussen de maximum- en minimumwaarden. Wat ons brengt om:
Updaten van de maxValues Functie
We moeten nu zowel de maximale als de minimale waarde achterhalen, in tegenstelling tot alleen het maximum dat we daarvoor hadden. De functie is hiertoe bijgewerkt:
functie minmaxValues (arr) maxVal = 0; voor (i = 0; iparseInt (arr [i])) minVal = parseInt (arr [i]); maxVal * = 1.1; minVal = minVal - Math.round ((maxVal / 10)); Ik weet zeker dat je hetzelfde zou kunnen bereiken in een enkele lus zonder zoveel regels code te gebruiken als ik, maar ik voelde me in die tijd bijzonder oncreatief dus wees geduldig. Met de berekeningsformaliteiten uit de weg, geven we een verhoging van 5% uit aan de maxval variabele en naar de minVal variabele, we trekken een waarde af gelijk aan 5% van maxval's waarde. Dit is om ervoor te zorgen dat de balken de bovenkant niet keer op keer raken en dat de verschillen tussen elke Y-as labels uniform zijn.
Updaten van de drawYlabels Functie
Nu al het voorbereidende werk is gedaan, gaan we nu verder met het renderen van de etiketteerroutine van de Y-as om de schaal aan te geven.
functie drawYlabels () ctx.save (); voor (index = 0; indexMooie vlezige update als je het mij vraagt! De kern van de functie blijft hetzelfde. We controleren alleen of de gebruiker het aanpassen van de code heeft ingeschakeld en de code zo nodig vertakt. Indien ingeschakeld, veranderen we de manier waarop de Y-labels worden toegewezen om ervoor te zorgen dat ze voldoen aan het nieuwe algoritme. In plaats van de maximumwaarde verdeeld in n aantal gelijk gespreide getallen, berekenen we nu het verschil tussen de maximum- en minimumwaarde, verdelen deze in uniform gespreide getallen en voegen deze toe aan de minimumwaarde om onze array Y-aslabels te bouwen. Hierna gaan we verder zoals normaal, waarbij elk label afzonderlijk wordt weergegeven. Omdat we de onderste 0 handmatig hebben gerenderd, moeten we controleren of schalen is ingeschakeld en vervolgens de minimumwaarde op zijn plaats weergeven. Let niet op de kleine numerieke toevoegingen aan elke gepasseerde parameter; het is alleen maar om ervoor te zorgen dat elk element van de grafiek naar verwachting op een lijn ligt.
Dynamic Resizing
In onze eerdere implementatie hebben we de afmetingen van de grafiek hard gecodeerd, wat grote problemen oplevert wanneer het aantal waarden verandert. We gaan dit nu corrigeren.
Toevoegen aan de opties
var defaults = // Overige standaardwaarden hier cvHeight: 250, // In px;We laten de gebruiker de hoogte van het canvaselement alleen instellen. Alle andere waarden worden dynamisch berekend en indien nodig toegepast.
Updaten van de initCanvas Functie
De initCanvas functie verwerkt alle canvas-initialisatie en moet daarom worden bijgewerkt om de nieuwe functionaliteit te implementeren.
function initCanvas () $ ("#" + dataSource) .after (""); // Probeer toegang te krijgen tot het canvas-element cv = $ (" # bargraph - "+ dataSource) .get (0); cv.width = gValues.length * (option.barSpacing + option.barWidth) + option.xOffset + option.barSpacing; cv.height = option.cvHeight; gWidth = cv.width; gHeight = option.cvHeight-20; if (! cv.getContext) return; // Probeer een 2D-context te krijgen voor het canvas en een fout genereren als niet mogelijk is voor ctx = cv.getContext ('2d'); if (! ctx) return;Na het injecteren van het canvaselement verkrijgen we een verwijzing naar het gemaakte element. De breedte van het canvaselement wordt berekend als een functie van het aantal elementen in de array - gValues , de ruimte tussen elke balk - option.barSpacing, de breedte van elke staaf zelf - option.barWidth en tenslotte option.xOffset. De breedte van de grafiek verandert dynamisch op basis van elk van deze parameters. De hoogte is door de gebruiker aanpasbaar en standaard ingesteld op 220px, waarbij het weergavegebied voor de balk zelf 220px is. De 20px wordt toegewezen aan de X-aslabels.
De bron verbergen
Het is logisch dat de gebruiker de brontabel mogelijk wil verbergen als de grafiek eenmaal is gemaakt. Met dit in gedachten, laten we de gebruiker beslissen om de tafel te verwijderen of niet.
var defaults = // Andere standaardinstellingen hier hideDataSource: true,;if (option.hideDataSource) $ ("#" + dataSource) .remove ();We controleren of de gebruiker de tabel wil verbergen en zo ja, we verwijderen deze volledig uit de DOM met behulp van jQuery's verwijderen methode.
Onze code optimaliseren
Nu al het harde werk is gedaan, kunnen we bekijken hoe onze code kan worden geoptimaliseerd. Aangezien deze code volledig is geschreven voor onderwijsdoeleinden, is het meeste werk ingekapseld als afzonderlijke functies en bovendien zijn ze veel gedetailleerder dan ze nodig zijn.
Als u echt de meest leanest code mogelijk wilt, kan onze gehele plug-in, met uitzondering van de initialisatie en berekening, binnen twee loops herschreven worden. Een lus door de gValues array om de balken zelf en de X-aslabels te tekenen; en de tweede lus die itereert van 0 tot numYlabels om de labels van het raster en de Y-as weer te geven. De code ziet er veel rommeliger uit, maar deze moet leiden tot een aanzienlijk kleinere codebasis.
Samenvatting
Dat zijn de mensen! We hebben een plug-in op hoog niveau helemaal opnieuw gemaakt. We hebben gekeken naar een aantal onderwerpen in deze serie, waaronder:
- Kijkend naar het weergaveschema van het canvaselement.
- Enkele renderingmethoden van het canvaselement.
- Waarden normaliseren waardoor we het kunnen uitdrukken als een functie van een andere waarde.
- Enkele bruikbare technieken voor gegevensextractie met behulp van jQuery.
- De kernlogica van het renderen van de grafiek.
- Ons script converteren naar een volwaardige jQuery-plug-in.
- Hoe het visueel te verbeteren en het nog meer kenmerkend uit te breiden.
Ik hoop dat je net zoveel plezier hebt gehad dit te lezen als ik het had geschreven. Dit is een 270-tal lijn werk, ik weet zeker dat ik iets weggelaten heb. Voel je vrij om de reacties te raken en het mij te vragen. Of kritiek op mij. Of prijs me. Weet je, het is jouw oproep! Happy codering!