Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

d3 monthly data set - update data- add new group

I'm working on a visualisation of personal finances for learning d3 with something that feels like a useful project. I've managed to make the chart as I want it (daily + or minus) for each month. Now I want to be able to change from one month to the next. This works if the old month (before update) has more days (aka data points) than the new month (after updating). If the old has less data points than the new one the additional data points are added on top of the chart. I'm adding each data point in my bar chart as a group (bar itself, data label + date label). I'm translating the entire group downwards for each new day. What I need to figure out is if after the update I have more or less data points and if I have less I need to translate the new ones downwards. You know what I mean? Here is the code of how I'm adding the bars originally:

bar = chart.selectAll("g")
      .data(data)
      .enter().append("g")
      .attr("transform", function(d, i) { return "translate(0," + i * barHeight + ")"; });
    //bar


//grey background bars 
  bar.append("rect")
          .attr("class", "backgroundBar")
          .attr("x", 10)
          .attr("width", (width-30))
          .attr("height", barHeight-1)
          .attr("fill", "#dddddd")
          .attr("fill-opacity", "0.3");

  //dateLabel
  bar.append("text")
        .attr("class", "dateLabel")
        .attr("x", width/2-20)
        .attr("y", barHeight-5)
       .attr("fill", "black")
        .text(function(d){ return d.key})
  bar.append("rect")
          .attr("class", "bar")
          .attr("x", function(d) { if(scale(d.values.total)<0){return width/2+widthDateLabel;}else{return width/2-scale(d.values.total)-widthDateLabel;}})
          .attr("width", function(d) { return Math.abs(scale(d.values.total)); })
          .attr("height", barHeight - 1)
          .attr("fill", function(d) { if(scale(d.values.total)<0){ return "DeepPink"}else{return "MediumSeaGreen"}});
  //BarLabel
  bar.append("text")
        .attr("class", "barLabel")
        .attr("x",function(d) { if(scale(d.values.total)<0){return window.width/2-scale(d.values.total)+5+widthDateLabel;}else{return window.width/2-scale(d.values.total)-5-widthDateLabel;}})
        .attr("y", barHeight/2)
        .attr("dy", ".35em")
        .attr("text-anchor", function(d) { if(scale(d.values.total)<0){ return "start"}else{return "end"}})
        .attr("fill", function(d) { if(scale(d.values.total)<0){ return "DeepPink"}else{return "MediumSeaGreen"}})
        .text(function(d) { return Math.round(d.values.total*100)/100; });

I could obviously not work with groups but translate each y coordinate but that feels like a dirty solution, no? Here is a screenshot of the problem:enter image description here

EDIT: And here is my current update function. that works sort of but produces the result in the screenshot

//update the bar itself


var bar=chartgroups.selectAll(".bar")
          .data(data);
  bar.enter().append("rect")
          .attr("class", "bar")
          .attr("x", function(d) { if(scale(d.values.total)<0){return width/2+widthDateLabel;}else{return width/2-scale(d.values.total)-widthDateLabel;}})
          .attr("width", function(d) { return Math.abs(scale(d.values.total)); })
          .attr("height", barHeight - 1)
          .attr("fill", function(d) { if(scale(d.values.total)<0){ return "DeepPink"}else{return "MediumSeaGreen"}});
  bar.exit().remove();
  bar
    .transition().duration(750)
      .attr("height", barHeight - 1)
      .attr("x", function(d) { if(scale(d.values.total)<0){return width/2+widthDateLabel;}else{return width/2-scale(d.values.total)-widthDateLabel;}})
      .attr("width", function(d) { return Math.abs(scale(d.values.total)); })
      .attr("fill", function(d) { if(scale(d.values.total)<0){ return "DeepPink"}else{return "MediumSeaGreen"}});

  //update the barLabel
  var barLabel=chart.selectAll(".barLabel").data(data);
  barLabel.enter().append("text")
        .attr("x",function(d) { if(scale(d.values.total)<0){return window.width/2-scale(d.values.total)+5+widthDateLabel;}else{return window.width/2-scale(d.values.total)-5-widthDateLabel;}})
        .attr("y", barHeight/2)
        .attr("dy", ".35em")
        .attr("text-anchor", function(d) { if(scale(d.values.total)<0){ return "start"}else{return "end"}})
        .attr("fill", function(d) { if(scale(d.values.total)<0){ return "DeepPink"}else{return "MediumSeaGreen"}})
        .text(function(d) { return Math.round(d.values.total*100)/100; });
  barLabel.exit().remove();
  barLabel
    .transition().duration(750)
      .attr("x",function(d) { if(scale(d.values.total)<0){return window.width/2-scale(d.values.total)+5+widthDateLabel;}else{return window.width/2-scale(d.values.total)-5-widthDateLabel;}})
      .attr("y", barHeight/2)
      .attr("text-anchor", function(d) { if(scale(d.values.total)<0){ return "start"}else{return "end"}})
      .attr("fill", function(d) { if(scale(d.values.total)<0){ return "DeepPink"}else{return "MediumSeaGreen"}})
      .text(function(d) { return Math.round(d.values.total*100)/100; });

  //update dates
  var dateLabel=chart.selectAll(".dateLabel").data(data);
  dateLabel.enter().append("text")
        .attr("class", "dateLabel")
        .attr("x", width/2-20)
        .attr("y", barHeight-5)
        .attr("fill", "black")
        .text(function(d){ return d.key})
  dateLabel.exit().remove();
  dateLabel
    .transition().duration(750)
        .text(function(d){ return d.key})
        .attr("y", barHeight-5)

  //update background bars
  var backgroundBar=chart.selectAll(".backgroundBar").data(data);
  backgroundBar.enter().append("rect")
          .attr("class", "backgroundBar")
          .attr("x", 10)
          .attr("width", (width-30))
          .attr("height", barHeight-1)
          .attr("fill", "#dddddd")
          .attr("fill-opacity", "0.3");
  backgroundBar.exit().remove();
  backgroundBar
    .transition().duration(750)
      .attr("height", barHeight-1)
like image 541
suMi Avatar asked Apr 09 '16 14:04

suMi


2 Answers

Here is a working code snippet that transitions between two months when you push the button. It is very close to your code. The are only subtle differences in how the update is occurring.

var january = [
    { "day": "1/1/2015", "value": 105},
    { "day": "1/2/2015", "value": -119},
    { "day": "1/3/2015", "value": 148},
    { "day": "1/4/2015", "value": -161},
    { "day": "1/5/2015", "value": 142},
    { "day": "1/6/2015", "value": -105},
    { "day": "1/7/2015", "value": 131},
    { "day": "1/8/2015", "value": 42},
    { "day": "1/9/2015", "value": -74},
    { "day": "1/10/2015", "value": 175},
    { "day": "1/11/2015", "value": 154},
    { "day": "1/12/2015", "value": 164},
    { "day": "1/13/2015", "value": 31},
    { "day": "1/14/2015", "value": 81},
    { "day": "1/15/2015", "value": 5},
    { "day": "1/16/2015", "value": -194},
    { "day": "1/17/2015", "value": -90},
    { "day": "1/18/2015", "value": 8},
    { "day": "1/19/2015", "value": 161},
    { "day": "1/20/2015", "value": -99},
    { "day": "1/21/2015", "value": -42},
    { "day": "1/22/2015", "value": -145},
    { "day": "1/23/2015", "value": 168},
    { "day": "1/24/2015", "value": -44},
    { "day": "1/25/2015", "value": -2},
    { "day": "1/26/2015", "value": 177},
    { "day": "1/27/2015", "value": -21},
    { "day": "1/28/2015", "value": -29},
    { "day": "1/29/2015", "value": 192},
    { "day": "1/30/2015", "value": 199},
    { "day": "1/31/2015", "value": 79}
  ];

  var february = [
    {    "day": "2/1/2015", "value": "36"},
    {    "day": "2/2/2015", "value": "151"},
    {    "day": "2/3/2015", "value": "-157"},
    {    "day": "2/4/2015", "value": "39"},
    {    "day": "2/5/2015", "value": "-69"},
    {    "day": "2/6/2015", "value": "97"},
    {    "day": "2/7/2015", "value": "-55"},
    {    "day": "2/8/2015", "value": "156"},
    {    "day": "2/9/2015", "value": "151"},
    {    "day": "2/10/2015", "value": "-72"},
    {    "day": "2/11/2015", "value": "-17"},
    {    "day": "2/12/2015", "value": "154"},
    {    "day": "2/13/2015", "value": "77"},
    {    "day": "2/14/2015", "value": "80"},
    {    "day": "2/15/2015", "value": "-112"},
    {    "day": "2/16/2015", "value": "-155"},
    {    "day": "2/17/2015", "value": "21"},
    {    "day": "2/18/2015", "value": "-63"},
    {    "day": "2/19/2015", "value": "-136"},
    {    "day": "2/20/2015", "value": "127"},
    {    "day": "2/21/2015", "value": "-43"},
    {    "day": "2/22/2015", "value": "-66"},
    {    "day": "2/23/2015", "value": "105"},
    {    "day": "2/24/2015", "value": "2"},
    {    "day": "2/25/2015", "value": "-92"},
    {    "day": "2/26/2015", "value": "-160"},
    {    "day": "2/27/2015", "value": "13"},
    {    "day": "2/28/2015", "value": "163"}
  ];

  function updateData(data) {

    var maxValue = d3.max(data, function(d) { return Math.abs(d.value); });
    scaleX.domain([0, maxValue]);

    //update background bars
    var backgroundBar = chartgroups.selectAll(".backgroundBar").data(data);
    backgroundBar.enter().append("rect");
    backgroundBar.attr("class", "backgroundBar")
      .attr("x", 0 - margin)
      .attr("y", function (d, i) {
        return (i * barHeight);
      })
      .attr("width", chartWidth*2 + margin)
      .attr("height", barHeight - 1)
      .attr("fill", "#dddddd")
      .attr("fill-opacity", "0.3");
    backgroundBar.exit().remove();

    var bars = chartgroups.selectAll(".bar")
      .data(data);
    bars.enter().append("rect")
      .attr("fill", function (d) {
        if (d.value < 0) {
          return "DeepPink"
        } else {
          return "MediumSeaGreen"
        }
      });
    bars.attr("class", "bar")
      .transition()
      .duration(1000)
      .attr("x", function (d) {
        if (d.value < 0) {
          return negativeStart;
        } else {
          return scaleWidth - scaleX(d.value);
        }
      }).attr("y", function (d, i) {
        return i * barHeight;
      })
      .attr("width", function (d) {
        return scaleX(Math.abs(d.value));
      })
      .attr("height", barHeight - 1)
      .attr("fill", function (d) {
        if (d.value < 0) {
          return "DeepPink"
        } else {
          return "MediumSeaGreen"
        }
      });
    bars.exit().remove();

    //update the barLabel
    var barLabel = chartgroups.selectAll(".barLabel").data(data);
    barLabel.enter().append("text");
    barLabel.attr("class", "barLabel")
      .transition()
      .duration(1000)
      .attr("x", function (d) {
        if (d.value < 0) {
          return negativeStart + scaleX(Math.abs(d.value));
        } else {
          return scaleWidth - scaleX(d.value);
        }
      })
      .attr("y", function (d, i) {
        return (i * barHeight) + (barHeight/2);
      })
      .attr("dy", ".35em")
      .attr("text-anchor", function (d) {
        if (d.value < 0) {
          return "start"
        } else {
          return "end"
        }
      })
      .attr("fill", function (d) {
        if (d.value < 0) {
          return "DeepPink"
        } else {
          return "MediumSeaGreen"
        }
      })
      .text(function (d) {
        return Math.round(d.value * 100) / 100;
      });
    barLabel.exit().remove();

    // //update dates
    var dateLabel = chartgroups.selectAll(".dateLabel").data(data);
    dateLabel.enter().append("text");
    dateLabel.attr("class", "dateLabel")
      .attr("fill", "black")
      .transition()
      .duration(1000)
      .attr("x", scaleWidth)
      .attr("y", function (d, i) {
        return (i * barHeight) + (barHeight/2) + 1;
      })
      .text(function (d) {
        return d.day;
      });
    dateLabel.exit().remove();
  }

  var container = d3.select(".chart");
  var margin = 60;
  var containerWidth = container.node().getBoundingClientRect().width;
  var chartWidth = containerWidth - (2*margin);
  var barHeight = 20;
  var dateLabelWidth = 80;
  var chartHeight = 31 * barHeight;
  var scaleWidth = (chartWidth - dateLabelWidth) / 2;
  var negativeStart = chartWidth - scaleWidth;
  var scaleX = d3.scale.linear()
    .range([0, scaleWidth]);
  var chartgroups = container.append("svg")
    .attr("width", containerWidth)
    .attr("height", chartHeight)
    .append("g")
    .attr("transform", "translate(" + margin + "," + 0 + ")");

  updateData(january);

  d3.select(".january").on("click", function() {
    updateData(january);
  });
  d3.select(".february").on("click", function() {
    updateData(february);
  });
.chart {
      width: 100%;
    }
<!DOCTYPE html>
<html>
<meta charset="utf-8">
<head>
</head>
<body>
<button type="button" class="january">January</button>
<button type="button" class="february">February</button>
<div class="chart">
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
</body>
</html>

Typically negative numbers would be on the left. You could also probably use a y scale and there may be an cleaner way to create the x scale.

like image 114
brenzy Avatar answered Oct 17 '22 05:10

brenzy


You will need to do a few things:

  1. Update your dataset.
  2. Update the D3 selection with new things
  3. Remove any deleted things from the D3 selection
  4. Animate any updated things using a transition

A quick example:

data.unshift(newData); // #1; add new data at the beginning
// (You could also remove stuff; update data in the middle; do whatever you need.)

var selection = chart.selectAll("g")
    .data(data, function(d) { return d.id; }));

selection.enter() // #2; Add the new stuff just like you did before
    .append("g")
    .attr("transform", function(d, i) { return "translate(0," + i * barHeight + ")"; });

// . . . all the other setup goes here, too

selection.exit() // #3; Hide any removed elements (should slide them down and turn them invisible)
    .transition().duration(400)
    .attr("transform", function(d, i) { return "translate(0," + (i + 1) * barHeight + ")"; });
    .style("opacity", 0)
    .remove();

selection // #4; Move everything to the right location
    .transition().duration(400)
    .attr("transform", function(d, i) { return "translate(0," + i * barHeight + ")"; });

There are some really good examples of how to do these kinds of updates in this series: General Update Pattern.

One issue to explicitly note involves key functions. You will have to include a key function similar to one I added above — read through the General Update Pattern articles and it should explain what you need to know.

like image 31
MrHen Avatar answered Oct 17 '22 05:10

MrHen