-
Notifications
You must be signed in to change notification settings - Fork 0
D3 Update Pattern
Om je data dynamish te laten updaten, gebruik je de update pattern.
Enter gebruik je altijd wanneer je bars wilt gaan visualiseren. Enter gebruik je om nieuwe elementen weer te geven. Enter wordt op de volgende manier gebruikt:
.selectAll('rect').data(d => d)
.enter().append('rect')
.attr('x', d => scaleX(d.data.province))
.attr('y', d => scaleY(d[1]))
.attr('width', scaleX.bandwidth())
.attr('height', d => scaleY(d[0]) - scaleY(d[1]))
Simpelweg gezegd zorgt enter() ervoor dat er 'genoeg' elementen gemaakt worden voor de data. Bij .selectAll('rect') selecteer je alle 'rectangles', ofwel vierkanten, vervolgens maak je een data join om de data in te laden. Enter gaat dan kijken hoeveel data-elementen er zijn en maakt dan voor al die data-elementen automatisch een DOM-element. In dit geval, een vierkant.
Met exit kun je al wat meer dynamische data weergeven. Exit gebruik je zo:
svg.selectAll('.layer').data(data)
.exit()
.attr('x', '50') // Aanpassing voor DOM-elementen die niet meer bestaan in de data
Exit doet eigenlijk precies het omgekeerde van enter. Wanneer je bijvoorbeeld een filter toevoegt die data-elementen verwijderd, moeten de DOM-elementen die deze data laat zien ook verdwijnen. Dat is mogelijk met exit(). Exit kijkt naar je DOM-elementen eerst en vergelijkt dat met de data. Als er te veel DOM-elementen zijn kunnen deze aangepast worden. Door exit().remove() te gebruiken kun je de DOM-elementen verwijderen. Dit ziet er dan zo uit:
svg.selectAll('.layer').data(data)
.exit()
.remove() // Verwijder DOM-elementen die niet meer bestaan in de data
In tegenstelling tot enter() en exit() is bij update het element zelf de update functie. Wanneer je dus selection uitvoert en vervolgens geen enter() of exit() gebruikt, ben je bezig met een update.
svg.selectAll('.layer').data(data)
.attr('x', d => scaleX(d.data.province))
.attr('y', d => scaleY(d[1]))
.attr('width', scaleX.bandwidth())
.attr('height', d => scaleY(d[0]) - scaleY(d[1]))
Je overschrijft dus eigenlijk de data die je eerder bij een enter() hebt aangegeven. Op die manier 'update' je de DOM-elementen.
Ik begon eerst met het creëren van de rectangles
NOTE: Ik maak gebruik van een stacked bar chart. De uitleg over hoe ik dit werkend heb gekregen kunt u hier vinden
Voor het gemak laat ik voor nu alleen het appenden van de rectangles zien
// CREATING FUNCTION
// Define d3 variables
stackedBars = stackGenerator(data)
g = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
g.selectAll('.layer').data(stackedBars)
.enter().append('g')
.attr('class', 'layer')
.attr("fill", d => colorScale(d.key))
.selectAll('rect').data(d => d)
.enter().append('rect')
.attr('x', d => scaleX(d.data.province))
.attr('y', d => scaleY(d[1]))
.attr('width', scaleX.bandwidth())
.attr('height', d => scaleY(d[0]) - scaleY(d[1]))
Wanneer er op een filter wordt geklikt, moet de data getransformeerd worden en de bars opnieuw getekend. In mijn functie heb ik 2 filters. Daarom heb ik kleine functies voor het definiëren van specifieke variablen. En 1 grote algemene functie die de aangepastte variable toepast op de DOM-elementen.
In dit voorbeeld gebruik ik de filter filterBigBar die de grootste bar van de provincies eruit haalt voor een leesbaarder visualisatie.
De filter begint met een checkInput functie
const checkInput = function(){
const bigBarFilter = d3.select('#filterBigBar')
.on("click", filterBigBar)
const unknownFilter = d3.select('#filterUnknown')
.on('click', filterUnknownProvince)
}
Deze functie controleert of de checkbox wordt aangeklikt. Als de checkbox wordt aangeklikt, wordt de betreffende functie uitgevoerd. Voor in dit voorbeeld wordt dit filterBigBar()
const filterBigBar = function(){
let filterOn = d3.select('#filterBigBar')._groups[0][0].checked // Must be accessed this way due to calling of this function in the else statement of the other filter
if (filterOn){
const noUnknownProvinces = data.filter(garage => garage.province !== 'onbekend')
const highestCapacity = d3.max(noUnknownProvinces.map(province => province.totalCapacity)) // calculate highestCapacity
filteredData = filteredData.filter(province => province.totalCapacity !== highestCapacity) // return array without that highestCapacity
}
else {
filteredData = data
if(d3.select('#filterUnknown')._groups[0][0].checked){
filterUnknownProvince()
}
console.log('filterBigBar', d3.select('#filterBigBar')._groups[0][0].checked)
console.log('filterunknown', d3.select('#filterUnknown')._groups[0][0].checked)
checkInput()
}
updateBars()
}
Hier wordt gecontroleerd of de checkbox is geactiveerd. Als deze wordt geactiveerd wordt eerst de array opgehaald zonder de province 'onbekend'. Onbekend is namelijk geen provincie. Vervolgens wordt de capaciteit van elke provincie opgevraagd en hier wordt de hoogste van bewaard. Deze 'hoogste waarde' wordt dan vervolgens gefilterd uit de array. Als laatste wordt deze data opgeslagen op de variable filteredData. Deze variable wordt dan vanaf nu gebruikt om de visualisatie te updaten.
Na het vastzetten van de variable wordt de hoofdfunctie uitgevoerd: updateBars(). In deze functie worden eerst de layers en bars gegevens geüpdate zodat deze gebruikt kunnen worden.
stackedBars = stackGenerator(filteredData)
setScales(filteredData)
// Save the layers and collection of bars into variables
const layers = svg.selectAll('.layer').data(stackedBars)
const bars = layers.selectAll('rect').data(d => d)
Na het definiëren van de variablen worden eerst de bestaande bars geüpdate
bars
.attr('x', d => scaleX(d.data.province))
.attr('y', d => scaleY(d[1]))
.attr('width', scaleX.bandwidth())
.attr('height', d => scaleY(d[0]) - scaleY(d[1]))
Hier worden dus opnieuw alle DOM-elementen geselecteerd en opgeslagen in variablen. Vervolgens worden deze overschreven door de attributen opnieuw mee te geven aan die bestaande rectangles. Er wordt dus geen enter().append() gebruikt.
Maar het kan zijn dat er ook nieuwe data is bijgekomen. Bijvoorbeeld wanneer de filter weer wordt uitgezet. Deze DOM-elementen bestaan nog niet. Daarom gebuik ik naast het updaten ook enter().append() om eventuele nieuwe layers en bars te creëren.
// Create new rectangles inside the layers
bars.enter()
.append('rect')
.attr('x', d => scaleX(d.data.province))
.attr('y', d => scaleY(d[1]))
.attr('width', scaleX.bandwidth())
.attr("height", 0) // set height 0 for the transition
.transition().duration(800)
.attr('height', d => scaleY(d[0]) - scaleY(d[1]))
Daarnaast is het ook mogelijk dat er data is verdwenen. In dat geval zijn er te veel DOM-elementen. Om dat op te lossen gebruik ik nog een exit().remove() om DOM-elementen waarvan er te veel zijn te verwijderen.
// UPDATE FUNCTION
// Remove rectangles from deleted data
bars.exit().remove()
Nu zijn de rectangles volledig geupdate.
Alleen worden nu de assen nog niet geüpdate. Als data veranderd is er een grote kans dat de assen ook niet helemaal meer kloppen. Ik gaf daarom bij de eerste keer aanmaken van de assen classes mee aan die assen zodat ik die assen hier weer kan opvragen met de selectAll generator van d3. Op die manier kon ik de assen opnieuw callen, zonder deze dubbel aan te maken
// Call the x-axis
const callXAxis = svg.select('.xAxis')
.call(d3.axisBottom(scaleX))
callXAxis.selectAll('.tick>text')
.attr('transform', 'rotate(45)')
callXAxis.selectAll('.domain, .tick line').remove()
// Call the y-axis
svg.select('.yAxis')
.call(d3.axisLeft(scaleY).tickSize(-innerWidth))