D3 interactie - RoyCsuka/frontend-data GitHub Wiki
Zonder enige oefening of tutorial te volgen ben ik gewoon meteen in D3 gedoken met het voorbeeld van Laurens. In de tweede week ben ik begonnen aan D3 en heb ik er als eerst voor gezorgd dat de map werkte.
Hieronder leg ik stap voor stap uit hoe mijn code in elkaar steekt.
Maak algemene waardes aan en importeer mijn modules die de data schoonmaken.
import { select , geoNaturalEarth1} from 'd3'
import { feature } from 'topojson'
import { cleanedArr } from './cleanData.js';
import { drawMap } from './drawMap.js';
// Eigen query
const query = `mijn query`
// Mijn end-point
const endpoint = "https://api.data.netwerkdigitaalerfgoed.nl/datasets/ivo/NMVW/services/NMVW-14/sparql"
// Algemene svg variabele om mijn HTML svg element te kunnen selecteren
const svg = select('svg')
Map settings die ik mee geef in een parameter aan het einde van mijn code
const mapSettings = {
projection: geoNaturalEarth1().rotate([-11,0]),
circleDelay: 11
}
// Global data variable
let data
// De standaard waarde
let centuryVar = 2000;
Nadat alle algemene variable aan zijn gemaakt en alle modules + data is ingeladen is het tijd om mijn hoofdfunctie uit te voeren.
// Voer mijn hoofdfunctie uit
makeVisualization()
// Our main function which runs other function to make a visualization
async function makeVisualization(){
//Draw the map using a module
drawMap(svg, mapSettings.projection)
//Use the cleanedArr module to get and process our data
data = await cleanedArr(endpoint, query)
setUpCenturys(data)
// klik op het eerste element en zorg ervoor dat de onchange wordt getriggerd
clickFirstItem()
}
Hierboven worden verschillende functies stap voor stap uitgevoerd.
Teken eerst de map d.m.v. een module die Laurens geschreven heeft. De enige code die ik hieraan veranderd heb is hieronder te vergelijken. Als externe bron heb ik deze code ook gebruikt: http://bl.ocks.org/piwodlaiwo/c0e10c375a7704f18b1bc813bc5eeddb
Wat ik van Laurens zijn code heb gebruikt
function drawCountries(container, pathGenerator) {
d3.json('DIT HEB IK VERANDER').then(data => {
const countries = feature(data, data.objects.DIT HEB IK VERANDER);
console.log(countries.features)
container
.selectAll('path')
.data(countries.features)
.enter()
.append('path')
.attr('class', 'country')
.attr('d', pathGenerator)
})
}
Wat ik van de externe code van het internet heb gebruikt
Een andere JSON file ingeladen (https://piwodlaiwo.github.io/topojson//world-continents.json) en in Laurens zijn code vervangen.d3.json('https://piwodlaiwo.github.io/topojson//world-continents.json')
Mijn uiteindelijke code
import { geoPath } from 'd3'
import { feature } from 'topojson'
export function drawMap(container, projection){
const pathGenerator = geoPath().projection(projection)
setupMap(container, pathGenerator)
drawCountries(container, pathGenerator)
}
function setupMap(container, pathGenerator){
container
.append('path')
.attr('class', 'sphere')
.attr('d', pathGenerator({ type: 'Sphere' }))
}
function drawCountries(container, pathGenerator) {
d3.json('https://unpkg.com/[email protected]/world/110m.json').then(data => {
const countries = feature(data, data.objects.countries);
console.log(countries.features)
container
.selectAll('path')
.data(countries.features)
.enter()
.append('path')
.attr('class', 'country')
.attr('d', pathGenerator)
})
}
Clean mijn data in Javascript zie mijn readme
Met de setUpCenturys(data) functie geef ik de data mee die schoongemaakt & getransfomeerd is en maak ik een input veld aan met D3 met daarin radio buttons. Ik heb naar Laurens zijn voorbeeld gekeken en heb er de volgende stukjes uitgehaald:
Laurens zijn code
//This awesome function makes dynamic input options based on our data!
//You can also create the options by hand if you can't follow what happens here
function setupInput(fields){
const form = d3.select('form')
.style('left', '16px')
.style('top', '16px')
.append('select')
.on('change', selectionChanged)
.selectAll('option')
.data(fields)
.enter()
.append('option')
.attr('value', d => d)
.text(d => d)
console.log("form",form)
}
Van Laurens zijn code heb ik de .on('change' selectionChanged) gebruikt die een functie triggerd als er iets anders wordt geselecteerd van de input fields.
function setUpCenturys(data) {
const form = d3.select('form')
.selectAll('input')
.data(data)
.enter()
.append('label')
.append('span')
.text(d => d.key)
.append('input')
.attr('type', 'radio')
.attr('name', 'century')
.attr('value', d => d.key)
.on('change', selectionChanged)
}
- De styling is weg (dit doe ik met css)
- De .append('select') want ik gebruikt radio buttons dus dat ziet er anders uit
- De selectAll('option') heb ik aangepast naar: selectAll('input')
- Ik geef attribute waardes mee aan mijn input fields en ik heb deze genest in een span
- Er wordt een span element aangemaakt die de eeuw waarde toont.
- on change wordt als laatst aangeroepen
clickFirstItem() is een functie die ervoor zorgt dat het eerste element van de radion button wordt aangeklikt zodra die is aangemaakt met de code hieronder die ik zelf geschreven heb:
function clickFirstItem(){
document.querySelector("#form label:first-of-type span input").click();
}
Nu .on('change', selectionChanged) is getriggerd als het element is aangemaakt wordt de selectionChanged() functie uitgevoerd die de volgende dingen doet:
"this" verwijst naar het geklikte input field (wat in dit geval 2000 is).
// Laurens zijn code
centuryVar = this ? parseInt(this.value) : centuryVar
Als de class .active bestaat remove .active en voeg .active weer toe aan het huidige geklikte element.
if (document.querySelector('.active')) {
document.querySelector("form .active").classList.remove('active')
this.closest('span').closest('label').classList.add('active')
} else {
this.closest('span').closest('label').classList.add('active')
}
De code hieronder gaat opzoek naar de key die overeen komt met "this.value" in de data.
// Laurens heeft mij hiermee geholpen
let arrOfSelectedData = data.find(element => element.key == this.value);
In mijn titel staat namelijk het volgende:
Mijn HTML is hieronder te zien. In de titel heb ik twee elementen aangemaakt die ik kan selecteren met JavaScript.
<h2>Totaal <b></b> objecten tussen het jaar <b></b></h2>
De code hieronder zorgt ervoor dat het laatste "b" element in de html wordt vervangen door de geselecteerde eeuw en de geselecteerde eeuw + 100 (wat dus als resultaat in deze situatie 2000 & 2100 geeft)
document.querySelector("p b:last-of-type").innerHTML = centuryVar + " & " + (centuryVar + 100);
Bij de eerste regel zorg ik ervoor dat er een array terug komt van alle items en in een variabele gestopt wordt. Bij de tweede regel zorg ik ervoor dat alle waardes uit de eerste regel bij elkaar opgeteld worden en in een variabele gestopt wordt. Bij de derde regel wordt de eerste "b" geselecteerd in de HTML en vervangen door het resultaat uit de tweede regel
let amountOfCountryValues = arrOfSelectedData.values.map(e => e.value).map(v => v.amountOfCountryItems);
let amountOfAllItems = d3.sum(amountOfCountryValues)
document.querySelector('p b:first-of-type').innerHTML = amountOfAllItems;
Hieronder bereken ik de minimale en maximale waarde door de waardes op te roepen die ik in een variabele heb gezet (let arrOfSelectedData = data.find(element => element.key == this.value);
.
// Credits to: https://stackoverflow.com/questions/11488194/how-to-use-d3-min-and-d3-max-within-a-d3-json-command/24744689
let max = d3.entries(amountOfCountryValues)
.sort(function(a, b) {
return d3.descending(a.value, b.value);
})[0].value;
let min = d3.entries(amountOfCountryValues)
.sort(function(a, b) {
return d3.ascending(a.value, b.value);
})[0].value;
Wat de D3 functie hierboven doet is het volgende:
- Geef aan wat de enteries zijn en zet de functie in een variabele
let max = d3.entries(amountOfCountryValues)
- Sorteer de waardes met:
.sort(function(a, b) {
- Geef de gesorteerde waarde terug in een bepaalde volgorde
return d3.descending(a.value, b.value);
- Sluit functie en selecteer de eerste waarde uit deze nieuwe volgorde
})[0].value;
Het zelfde doe ik voor de minimale waarde maar dan i.p.v. "d3.descending" uit stap 3 gebruik ik "d3.ascending"
Dit stukje hieronder heeft kris geschreven ik snap niet helemaal wat het doet maar ik begrijp het resultaat.
// props van Kris
const flattened = arrOfSelectedData.values.reduce((newArray, countries) => {
newArray.push(countries.value)
return newArray.flat()
}, [])
Dit witte blok verplaats ik door een functie aan te roepen die "moveWhiteBlok()" heet
Hoe deze functie eruit ziet:
// https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
function moveWhiteBlok(){
let getTop = document.querySelector('.active').getBoundingClientRect().top;
document.querySelector('.active-bk').style.top = getTop - 63.40625;
}
Deze functie krijgt het aantal pixels van het label terug die .active heeft. De .active is een class die zich steeds verplaatst (zoals in het gifje te zien is .active de dik gedrukte tekst). En deze pixels worden als inline styling gezet op het witte blok als top: (pixel waarde komt hier);
. Van de pixel waarde wordt de hoogte van het witte blok er nog afgetrokken zodat het blok op de juiste positie komt.
Aan het einde van de functie setUpCenturys(data) run ik de functie plotLocations(svg, flattened, mapSettings.projection, min, max) met een aantal parameters die terug komen in mijn D3 plotLocations functie.
De parameters van mijn functie:
- min, max Deze twee parameters geven de minimale en maximale waardes mee van hoe groot de cirkels mag zijn op basis van een scale die ik aanmaak binnen de functie.
- svg, mapSettings.projection Zijn algemene variabele die ik mee geef
- flattend Is de array van de geselecteerde data die geen geneste array meer is.
Hoe mijn D3 plotLocations functie werkt kun je kijken op mijn andere pagina van mijn wiki waar ik uitleg wat enter update en exit inhoud.