Dan Oswalt
Dan Oswalt

Reputation: 2189

Dynamically updating d3 bar chart

This function runs on page load, and also when data is entered in a form. It draws the graph fine on page load, but it doesn't redraw when new info is submitted.

When I log the element after the data join, it shows the updated length of the data array, and the updated number of elements, but they don't redraw. Any ideas, just scanning this code? I feel like it's close, but I'm not understanding the steps of the data updating process.

This is based on the basic bar graph tutorial let's make a bar chart, which uses divs instead of svg elements, but I don't think that should matter.

this.render = function(self){
  if(self.data){
    self.x = d3.scaleLinear()
               .domain([0, d3.max(self.data.map(function(d) {
                   return parseInt(d.daily_count);
                 }))
               ])
               .range([0, 500]);

    var bars = d3.select("#chart")
                 .selectAll("div")
                 .data(self.data)
                 .enter().append('div')
                   .attr('class', 'bar-container row');

    console.log('chart', d3.select("#chart").data(self.data));

    bars.append('div')
          .attr('class', 'label four columns')
          .text(function(d) {return d.date_string; });

    bars.append("div")
          .attr('class', 'bar eight columns')
          .style('background-color', function(d){
            return (d.daily_count == 0 ? 'white' : '#1EAEDB')
          })
          .style('color', function(d){
            return (d.daily_count == 0 ? 'red' : 'white')
          })
          .style("width", function(d) {
            return self.x(d.daily_count) + "px";
          })
          .text(function(d) {
            return (d.daily_count == 0 ? 'closed' : d.daily_count)
          });
  }

EDIT: Attempt to use .merge() and .each() to create and update chart. I can't figure out how to update dynamically where each bar is a div that contains two child divs which actually display the data. This still doesn't work dynamically, although it renders like the above:

this.render = function(self){
  if(self.data){
    self.x = d3.scaleLinear()
               .domain([0, d3.max(self.data.map(function(d) {
                   return parseInt(d.daily_count);
                 }))
               ])
               .range([0, 500]);

    var chart = d3.select("#chart");

    var bars = chart.selectAll("d")
                 .data(self.data)

    bars.enter().append('div')
        .attr('class', 'bar-container row')
        .each(function(d) {
          d3.select(this).append('div')
            .attr('class', 'label four columns');
          d3.select(this).append('div')
            .attr('class', 'bar eight columns');
        })
        .merge(bars)
        .each(function(d){
          d3.select(this).selectAll('div.label')
            .text(function(d) {return d.date_string; });
          d3.select(this).selectAll('div.bar')
            .style('background-color', function(d){
              return (d.daily_count == 0 ? 'white' : '#1EAEDB')
            })
            .style('color', function(d){
              return (d.daily_count == 0 ? 'red' : 'white')
            })
            .style("width", function(d) {
              return self.x(d.daily_count) + "px";
            })
            .text(function(d) {
              return (d.daily_count == 0 ? 'closed' : d.daily_count)
            });
        })

    bars.exit().remove();

Upvotes: 0

Views: 979

Answers (1)

Gerardo Furtado
Gerardo Furtado

Reputation: 102218

Your render function needs a proper "enter", "update" and "exit" selections. Besides that, have in mind that Bostock's example you linked uses d3 version 3.x, while you're using d3 version 4.x.

In the following demo, this binds the data:

var bars = body.selectAll("div")
    .data(data);

After that, the "enter" + "update" selections, with the merge:

bars.enter().append("div")
    .attr("class", "row")
    .merge(bars)
    .style('background-color', function(d){
        return (d.daily_count == 0 ? 'white' : '#1EAEDB')
    })
    .style('color', function(d){
        return (d.daily_count == 0 ? 'red' : 'white')
    })
    .style("width", function(d) {
        return x(d.daily_count) + "px";
    })
    .text(function(d) {
        return (d.daily_count == 0 ? 'closed' : d.daily_count)
    });

And the exit selection:

bars.exit().remove();

This is a demo based on your code (using only daily_count, because I don't know how you are positioning the other divs):

var dataset = [{daily_count: 19},
{daily_count: 29},
{daily_count: 13},
{daily_count: 45},
{daily_count: 22},
{daily_count: 42},
{daily_count: 9}];

render(dataset)

d3.select("#btn").on("click", function(){
	var newDataset = [];
	var length = Math.ceil(Math.random()*20);
	for (var i = 0; i < length; i++){
		newDataset.push({daily_count: Math.floor(Math.random()*50)})
	}
	render(newDataset);
})

function render(data){

    x = d3.scaleLinear()
               .domain([0, d3.max(data.map(function(d) {
                   return parseInt(d.daily_count);
                 }))
               ])
               .range([0, 300]);
							 
		var body = d3.select("body");

    var bars = body.selectAll("div")
                 .data(data);

    bars.enter().append("div")
		 .attr("class", "row")
          .merge(bars)
          .style('background-color', function(d){
            return (d.daily_count == 0 ? 'white' : '#1EAEDB')
          })
          .style('color', function(d){
            return (d.daily_count == 0 ? 'red' : 'white')
          })
          .style("width", function(d) {
            return x(d.daily_count) + "px";
          })
          .text(function(d) {
            return (d.daily_count == 0 ? 'closed' : d.daily_count)
          });
					
		bars.exit().remove();

}
.row {
  font: 10px sans-serif;
  text-align: right;
  padding: 3px;
  margin: 2px;
  color: white;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<button id="btn">Click</button>

Upvotes: 1

Related Questions