Hannan
Hannan

Reputation: 1191

D3 How to update the chart after selection from drop down menu with new data

I'm building a waterfall chart in D3. When the page will load, it will render the default page but user will have choice to select different 'Company' and 'Year' from the drop down menu. I have been able to create the chart what I want. But when I select any different Company or Year, D3 adds another chart on top of the existing instead of replacing it and thats because I'm targeting a particular div / svg from the HTML. How can I use D3 to update the chart with new data instead add another one of top? And if I can have that movement of chart bars with transition, that will be awesome.

HTML is a simple svg:

<svg class="chart"></svg>

Here is the function to create the chart which I call when Ajax call is successful:

function waterfallChart (dataset) {

            var data = [];

            for (var key in dataset[0]) {
              data.push({
                name: key,
                value: dataset[0][key]
              })
            }

        var margin = {top: 20, right: 30, bottom: 30, left: 40},        
            width = 960 - margin.left - margin.right,
            height = 500 - margin.top - margin.bottom,
            padding = 0.3;

    var x = d3.scaleBand()
            .domain(data.map(function(d) {
                return d.name
                }))
            .range([0, width])
            .padding(padding);

    var y = d3.scaleLinear()
        .range([height, 0]);

    var xAxis = d3.axisBottom(x)


    var yAxis = d3.axisLeft(y)
        .tickFormat(function(d) {
            return dollarFormatter(d);
        });

    var chart = d3.select(".chart")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
        .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

        var cumulative = 0;
        for (var i = 0; i < data.length; i++) {
            data[i].start = cumulative;
            cumulative += data[i].value;
            data[i].end = cumulative;

            data[i].class = (data[i].value >= 0) ? 'positive' : 'negative'
        }
        data.push({
            name: 'Total',
            end: cumulative,
            start: 0,
            class: 'total'
        });

        x.domain(data.map(function(d) {
            return d.name;
        }));
        y.domain([0, d3.max(data, function(d) {
            return d.end;
        })]);

        chart.append("g")
            .attr("class", "x axis")
            .attr("transform", "translate(0," + height + ")")
            .call(xAxis);

        chart.append("g")
            .attr("class", "y axis")
            .call(yAxis);

        var bar = chart.selectAll(".bar")
            .data(data)
            .enter().append("g")
            .attr("class", function(d) {
                return "bar " + d.class
            })
            .attr("transform", function(d) {
                return "translate(" + x(d.name) + ",0)";
            });

        bar.append("rect")
            .attr("y", function(d) {
                return y(Math.max(d.start, d.end));
            })
            .attr("height", function(d) {
                return Math.abs(y(d.start) - y(d.end));
            })
            .attr("width", x.bandwidth());

        bar.append("text")
            .attr("x", x.bandwidth() / 2)
            .attr("y", function(d) {
                return y(d.end) + 5;
            })
            .attr("dy", function(d) {
                return ((d.class == 'negative') ? '-' : '') + ".75em"
            })
            .text(function(d) {
                return dollarFormatter(d.end - d.start);
            });

        bar.filter(function(d) {
                return d.class != "total"
            }).append("line")
            .attr("class", "connector")
            .attr("x1", x.bandwidth() + 5)
            .attr("y1", function(d) {
                return y(d.end)
            })
            .attr("x2", x.bandwidth() / (1 - padding) - 5)
            .attr("y2", function(d) {
                return y(d.end)
            })

            function dollarFormatter(n) {
                  n = Math.round(n);
                  var result = n;
                  if (Math.abs(n) > 1000) {
                    result = Math.round(n/1000) + 'B';
                  }
                  return '$ ' + result;
                }
        }

Here is code where I have event listener and on selection it will run the above function:

$("#airline-selected, #year-selected").change(function chartsData(event) {
            event.preventDefault();
            var airlineSelected = $('#airline-selected').find(":selected").val();
            var yearSelected = $('#year-selected').find(":selected").val();

            $.ajax({
                url: "{% url 'airline_specific_filtered' %}",
                method: 'GET',
                data : {
                    airline_category: airlineSelected,
                    year_category: yearSelected
                        },
                success: function(dataset){
                        waterfallChart(dataset)
         },
                error: function(error_data){
                console.log("error")
                console.log(error_data)
         }
            })
        });

Upvotes: 2

Views: 2603

Answers (1)

Mark
Mark

Reputation: 92461

You are missing some pretty important things here. If you are going to do updates on your data you need to do a couple things.

  1. Give a key to the data() function. You need to give D3 a way to identify data when you update it so it knows if it should add, remove, or leave existing data. The key does this. For instance you might do something like this:

    .data(data, function(d) { return d.name })
    

    Now d3 will be able to tell you data items apart assuming d.name is a unique identifier.

  2. You need an exit() for data that is removed during update. You need to save the data joined selection so you can call enter and exit on it:

    var bar = chart.selectAll(".bar")
              .data(data, function(d) { return d.name})
    

    now you can call: bar.exit().remove() to get rid of deleted items and bar.enter() to add items.

  3. You need to make a selection that hasn't had enter() called on it to update attributes.

  4. Probably more a matter of style, but you should set up the SVG and margins outside the update function since they state the same. You can still update the axis and scales by calling the appropriate functions in the update.

The code you posted is a little hard for other people to run — you'll always get better faster answers if you post code that has been reduced to the main problem and that others can run without needing access to offsite data or apis.

Here's an example that updates on a setInterval between two data sets based on your code. But you should also look at the General Update Patterns - they are very simple but have almost everything you need to know. (https://bl.ocks.org/mbostock/3808234)

dataset = [
        {name: "Albert", start: 0, end:220},
        {name: "Mark", start: 0, end:200},
        {name: "Søren", start: 0, end:100},
        {name: "Immanuel", start: 0, end:60},
        {name: "Michel", start: 0, end:90},
        {name: "Jean Paul", start: 0, end: 80}
    ]
     dataset2 = [
        {name: "Albert", start: 0, end:20},
         {name: "Immanuel", start:0, end:220},
        {name: "Jaques", start: 0, end:100},
        {name: "Gerhard", start:0 , end:50},
        {name: "Søren", start: 0, end:150},
        {name: "William", start: 0, end: 180}
    ]

var margin = {
    top: 10,
    right: 30,
    bottom: 30,
    left: 40
  },
  width = 400 - margin.left - margin.right,
  height = 200 - margin.top - margin.bottom,
  padding = 0.3;

var chart = d3.select(".chart")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
  .append("g")
  .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

var x = d3.scaleBand()
  .range([0, width])
  .padding(padding);

var y = d3.scaleLinear()
  .range([height, 0])

chart.append("g")
  .attr("class", "x axis")
  .attr("transform", "translate(0," + height + ")")
chart.append("g")
  .attr("class", "y axis")

var currentData = dataset

waterfallChart(currentData)

setInterval(function() {
  currentData = currentData === dataset ? dataset2 : dataset
  waterfallChart(currentData)
}, 3000)

function waterfallChart(data) {
  var t = d3.transition()
    .duration(750)

  x.domain(data.map(function(d) {
    return d.name
  }))
  y.domain([0, d3.max(data, function(d) {
    return d.end
  })])
  var xAxis = d3.axisBottom(x)

  var yAxis = d3.axisLeft(y)

  d3.select('g.x').transition(t).call(xAxis)
  d3.select('g.y').call(yAxis)

  var bar = chart.selectAll(".bar")
    .data(data, function(d) {
      return d.name
    })

  // ENTER -- ADD ITEMS THAT ARE NEW IN DATA
  bar.enter().append("g")
    .attr("transform", function(d) {
      return "translate(" + x(d.name) + ",0)"
    })
    .attr("class", 'bar')
    .append("rect")
    .attr("y", function(d) {
      return y(Math.max(d.start, d.end));
    })
    .attr("height", function(d) {
      return Math.abs(y(d.start) - y(d.end));
    })
    .attr("width", x.bandwidth())

  // UPDATE EXISTING ITEMS
  chart.selectAll(".bar")
    .transition(t)
    .attr("transform", function(d) {
      return "translate(" + x(d.name) + ",0)"
    })
    .select('rect')
    .attr("y", function(d) {
      return y(Math.max(d.start, d.end))
    })
    .attr("height", function(d) {
      return Math.abs(y(d.start) - y(d.end))
    })
    .attr("width", x.bandwidth())

  // REMOVE ITEMS DELETED FROM DATA
  bar.exit().remove()
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg class="chart"></svg>

Upvotes: 2

Related Questions