Skip to content

D3 Update Pattern

marcoFijan edited this page Nov 12, 2020 · 4 revisions

Om je data dynamish te laten updaten, gebruik je de update pattern.

Hoe werkt de update pattern?

Enter

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.

Exit

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

Update

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.

Visualisatie updaten voorbeeld uit eigen code

Creëren van de rectangles

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]))

Updaten van de variablen

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.

Updaten van de DOM-elementen

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))