Search code examples
javascriptd3.jsstackedbarseries

D3.js animating update of stacked bar graph


I am trying to update a stacked bar chart with transitions as the underlying data is changed. It calls the same "render" function each time and works well when no transitions are involved. However, I would like to animate the changes in values, transitioning from its current state to the next.

I have somewhat solved the problem, but feel like my solution is clunky - hoping there is a better way to do this for stacked bar charts.

My approach has been to do the following:

  1. Load the data
  2. Load the initial conditions (req. for transitions)
  3. Load the final conditions (within a transition)
  4. Copy the current data into another array: prevData
  5. Reload data after interval

Using the above approach, if prevData has values, then use these to set the initial conditions. My problems is that finding and setting the initial conditions feels really clunky:

if (prevData.length > 0) {
                            //get the parent key so we know who's data we are now updating
                            var devKey = d3.select(this.parentNode).datum().key;

                            //find the data associated with its PREVIOUS value
                            var seriesData = seriesPrevData.find(function (s) { return (s.key == devKey); })
                            if (seriesData != null) {
                                //now find the date we are currently looking at
                                var day = seriesData.find(function (element) { return (element.data.Date.getTime() == d.data.Date.getTime()); });

                                if (day != null) {
                                    //now set the value appropriately
                                    //console.debug("prev height:" + devKey + ":" + day[1]);
                                    return (y(day[0]) - y(day[1]));
                                }
                            }

                        }

All I'm doing, is finding the correct key array (created by d3.stack()), then trying to find the appropriate previous data entry (if it exists). However, searching parent nodes, and searching through arrays to find the required key and the appropriate data element feels very long-winded.

So, my question is, is there a better way to do this? or parts of this?

  1. Find the previously bound data values associated with this element or the current values before it is changed within a function.
  2. Better way to find the current key being updated rather than using: d3.select(this.parentNode)... ? I've tried passing key values but don't seem to be getting it right. The best I have achieved, is passing a key function to the parent, and looking for it the way described above.

Sorry for the long post, I just spent a whole day working out my solution, frustrated by the fact that all I really needed, was the previous values of an item. Having to do all these "gymnastics" to get what I needed seems very "un" D3.js like :-)

Thanks


Solution

  • Following is a simple example for an animated bar chart. It'll iterate over two different versions of the dataset to show how one can handle changes in the underlying data very easily with d3. There is no need (in this example) for any manual data preparation for the transition/animation.

    var data = [
      [1, 2, 3, 4, 5],
      [1, 6, 5, 3]
    ];
    
    var c = d3.select('#canvas');
    
    var currentDataIndex = -1;
    function updateData() {
    
        // change the current data
        currentDataIndex = ++currentDataIndex % data.length;
        console.info('updating data, index:', currentDataIndex);
        var currentData = data[currentDataIndex];
      
        // get our elements and bind the current data to it
        var rects = c.selectAll('div.rect').data(currentData);
    
        // remove old items
        rects.exit()
            .transition()
            .style('opacity', 0)
            .remove(); 
    
        // add new items and define their appearance
        rects.enter()
            .append('div')
            .attr('class', 'rect')
            .style('width', '0px');
    
        // change new and existing items
        rects
            // will transition from the previous width to the current one
            // for new items, they will transition from 0px to the current value
            .transition()
            .duration('1000')
            .ease('circle')
            .style('width', function (d) { return d * 50 + 'px'; });
        
    }
    
    // initially set the data
    updateData();
    
    // keep changing the data every 2 seconds
    window.setInterval(updateData, 2000);
    div.rect {
      height: 40px;
      background-color: red;
    }
    
    div#canvas {
      padding: 20px;
      border: 1px solid #ccc;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
    <div id="canvas">  
    </div>