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:
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)
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.
You will need to do a few things:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With