d3 transition of areas for shrinking datasets - d3.js

I'm wondering why the following transition does not work as expected:
in a line chart, users can select different timespans they want the chart to display. If the timespan increases, then the animation is fine: the line moves a bit to the right and the area below that line moves 100% perfect (no gaps or such between line and area).
If on the other hand the timespan decreases then the area transition displays an ugly behavior:
1) initial state
2) change to one week, transition starts
3) transition has a duration of 1 sec
4) end state - no problem here.
I'm using basically the same transition functions for the line:
var valueline = d3.svg.line()
.x(function (d) { return x(d.date); })
.y(function (d) { return y(d.result2); });
and for the area:
var area1 = d3.svg.area()
.x(function (d) { return x(d.date); })
.y0(height)
.y1(function (d) { return y(d.result2); });
I'm not using any explicit enter() or exit() in the area update:
// update area1
svg.selectAll(".area1")
.transition()
.duration(1000)
.attr("d", area1(data));
I could not figure out how to use enter() or exit() in the case of areas, maybe an explicit exit().remove() that would instantly remove unneeded datapoints would solve my problem? But .. how to do that for a line / area??
Thanks for any help!
EDIT
Alright, it was no problem of exit().remove() because for a single line, that doesn't make much sense as I see now, too.
What fixed the transition from large to small dataset was cloning the array of data for the full 4 weeks max-timespan, then giving all datapoints that are outside of the currently visible timespan a y-value of 0 and using this array to feed the areas:
svg.selectAll(".area1")
.data([data2]) // contains data for 4 weeks, with 0's on time outside of current span
.transition()
.duration(1000)
.attr("d", area1);
The x-axis is still feed the array with data of only the currently visible timespan, so it adjusts:
// array "data" contains only data for the desired time horizon, eg 1 week
x.domain(d3.extent(data, function (d) { return d.date; }));

Related

d3 Exit / Update function in animation

I am trying to do an animation that shows data from ten different points. The graphic is coded so that the sensors (displayed through circles) change their color and size depending on the overall data obtained over one hour (total number of entries and average of speed).
Through this entry (Transition not working d3) and the code from this simulation of Gapminder (https://bost.ocks.org/mike/nations/), I have been able to animate the chart. However, because of the structure of the code, the exit and update function do not work. The first entry in the data at Hour 1 only has one object and therefore only one circle is drawn. This circle gets updated through time, but the other sensors are not drawn (and therefore not updated).
I am considering recreating a first empty object for each sensor to draw them at the beginning of the animation. However, I would like to avoid that.
The code is this:
//FUNCTION TO GET THE DATA BY HOUR
function getDataByHour (hour) {
var allBridges = new Array();
var found;
for (b = 0; b < boatsByHour.length; b++){
bridges.forEach(function(br,i){
if (boatsByHour[b].sensorID== br.id){
xy = projection([br.longitude, br.latitude])}
});
if (boatsByHour[b].hour == hour){
found = true;
bridgeNumber = boatsByHour[b].sensorID;
allBridges.push({
"numberBoats": (boatsByHour[b].numberBoats),
"speed": (boatsByHour[b].speedAvg),
"bridge": boatsByHour[b].sensorID,
"longitude": xy[0],
"latitude": xy[1],
"hour": boatsByHour[b].hour
})
}
}
return allBridges;
}
//SENSORS
var sensor = plot.append("g")
.attr("class","bridges")
.selectAll(".sensors")
.data(getDataByHour(timeRange[0]))
.call(animateSensors)
.enter()
.append("circle")
.attr("class","sensors")
.attr("cx", function(d,i){return d.longitude})
.attr("cy", function(d,i){return d.latitude})
.on("mouseover",function(d){console.log(d)});
sensor.exit().remove();
plot
.transition()
.duration(300000)
.ease(d3.easeLinear)
.tween("hour", tweenHour)
//FUNCTION THAT UPDATES THE ANIMATION
function animateSensors (sensor){
sensor
.attr("r",function(d){return radiusScale(d.numberBoats)})
.style("fill",function(d){return colorScale(d.speed)});
}
function tweenHour(){
var hour = d3.interpolateNumber (timeRange[0],timeRange[1]);
return function(h){
displayHour(hour(h))}
}
function displayHour(hour) {
sensor.data(getDataByHour(Math.floor(hour))).call(animateSensors);
}
I have tried different ways of including the enter() and exit (). If I add the enter() and append the circles inside the 'animateSensors' function, all the circles (sensors) are drawn. However they are not being updated, so at the end, I get thousands of circles drawn in the SVG even if the exit().remove() update is in it.
Thanks
Ok, now it works. Instead of calling the first part of the data (where there is only 1 circle is drawn instead of the total 10), I had to call data that had data for all the circles (like the last point):
var sensor = plot.append("g")
.attr("class","bridges")
.selectAll(".bridge")
.data(getDataByHour(timeRange[1])) // instead of getDataByHour(timeRange[0])
.enter()
.append("circle")
.attr("class",function(d){return "bridge " + d.bridge})
.attr("cx", function(d,i){return d.longitude})
.attr("cy", function(d,i){return d.latitude})
.on("mouseover",function(d){console.log(d)});
The animation starts correctly thanks to the function tweenHour
function tweenHour(){
var hour = d3.interpolateNumber (timeRange[0],timeRange[1]);
return function(h){
displayHour(hour(h))}
}

D3.js binding nested data

I'm really new to coding, and also to asking questions about coding. So let me know if my explanation is overly complex, or if you need more context on anything, etc.
I am creating an interactive map of migration flows on the Mediterranean Sea. The flows show origin and destination regions of the migrant flows, as well as the total number of migrants, for Italy and Greece. Flows should be displayed in a Sankey diagram like manner. Because I am displaying the flows on a map and not in a diagram fashion, I am not using D3’s Sankey plugin, but creating my own paths.
My flow map, as of now (curved flows are on top of each other, should line up next to each other)
For generating my flows I have four points:
2 points for the straight middle part of the flow (country total)
1 point each for the curved outer parts (origin and destination region), using the two points of the straight middle part as starting points
The straight middle and both curved outer parts are each generated independently from their own data source. Flow lines are updated by changing the data source and calling the function again. The flow lines are generated using the SVG path mini-language. In order for the curved outer parts of the flows to show correctly, I need them to be lined up next to each other. To line them up correctly, I need to shift their starting points. The distance of the shift for each path element is determined by the width of the path elements before it. So, grouping by country, each path element i needs to know the sum of the width of the elements 0-i in the same group.
After grouping my data with d3.nest(), which would allow me to iterate over each group, I am not able to bind the data correctly to the path elements
I also can't figure out a loop function that adds up values for all elements 0-i. Any help here? (Sorry if this is kind of unrelated to the issue of binding nested data)
Here is a working function for the curved paths, working for unnested data:
function lineFlow(data, flowSubGroup, flowDir) {
var flowSelect = svg.select(".flowGroup").select(flowSubGroup).selectAll("path");
var flow = flowSelect.data(data);
var flowDirection = flowDir;
flow.enter()
.append("path").append("title");
flow
.attr("stroke", "purple")
.attr("stroke-linecap", "butt")
.attr("fill", "none")
.attr("opacity", 0.75)
.transition()
.duration(transitionDur)
.ease(d3.easeCubic)
.attr("d", function(d) {
var
slope = (d.cy2-d.cy1)/(d.cx2-d.cx1),
dist = (Math.sqrt(Math.pow((d.rx2-d.rx1),2)+Math.pow((d.ry2-d.ry1),2)))*0.5,
ctrlx = d.rx1 + Math.sqrt((Math.pow(dist,2))/(1+Math.pow(slope,2)))*flowDirection,
ctrly = slope*(ctrlx-d.rx1)+d.ry1;
return "M"+d.rx1+","+d.ry1+"Q"+ctrlx+","+ctrly+","+d.rx2+","+d.ry2})
.attr("stroke-width", function(d) {return (d.totalmig)/flowScale});
flowSelect
.select("title")
.text(function(d) {
return d.region + "\n"
+ "Number of migrants: " + addSpaces(d.totalmig)});
};
I tried adapting the code to work with data grouped by country:
function lineFlowNested(data, flowSubGroup, flowDir) {
var g=svg.select(".flowGroup").select(flowSubGroup).append("g").data(data).enter();
var gflowSelect=g.selectAll("path");
var gflow=gflowSelect.data (function(d) {return d.values});
gflow.enter()
.append("path");
gflow.attr("stroke", "purple")
.attr("stroke-linecap", "butt")
.attr("fill", "none")
.attr("opacity", 0.75)
// .transition()
// .duration(transitionDur)
// .ease(d3.easeCubic)
.attr("d", function(d) {
var
slope = (d.cy2-d.cy1)/(d.cx2-d.cx1),
dist = (Math.sqrt(Math.pow((d.rx2-d.rx1),2)+Math.pow((d.ry2-d.ry1),2)))*0.5,
ctrlx = d.rx1 - Math.sqrt((Math.pow(dist,2))/(1+Math.pow(slope,2)))*flowDirection,
ctrly = slope*(ctrlx-d.rx1)+d.ry1;
return "M"+d.rx1+","+d.ry1+"Q"+ctrlx+","+ctrly+","+d.rx2+","+d.ry2})
.attr("stroke-width", function(d) {return (d.totalmig)/flowScale});
};
which isn't working. What am I doing wrong? Thanks for any hints!

D3 data binding [D3js in Action]

I'm new to d3.js, and am working my way through the book "D3.js in action". So far I have been able to figure out all the questions I had, but this one I can't completely answer on my own, it seems.
I post the source code from the book here, since it is available on the books website and the authors homepage. This is the bl.ocks: http://bl.ocks.org/emeeks/raw/186d62271bb3069446b5/
The basis idea of the code is to create a spreadsheet-like layout out of div elements filled with fictious twitter data. Also implemented is a sort function to sort the data by timestamp and reorder the sheet. As well as a function to reestablish the original order.
Here is the code (I left out the part where the table structure is created, except the part where the data is bound):
<html>
<...>
<body>
<div id="traditional">
</div>
</body>
<footer>
<script>
d3.json("tweets.json",function(error,data) { createSpreadsheet(data.tweets)});
function createSpreadsheet(incData) {
var keyValues = d3.keys(incData[0])
d3.select("div.table")
.selectAll("div.datarow")
.data(incData, function(d) {return d.content})
.enter()
.append("div")
.attr("class", "datarow")
.style("top", function(d,i) {return (40 + (i * 40)) + "px"});
d3.selectAll("div.datarow")
.selectAll("div.data")
.data(function(d) {return d3.entries(d)})
.enter()
.append("div")
.attr("class", "data")
.html(function (d) {return d.value})
.style("left", function(d,i,j) {return (i * 100) + "px"});
d3.select("#traditional").insert("button", ".table")
.on("click", sortSheet).html("sort")
d3.select("#traditional").insert("button", ".table")
.on("click", restoreSheet).html("restore")
function sortSheet() {
var dataset = d3.selectAll("div.datarow").data();
dataset.sort(function(a,b) {
var a = new Date(a.timestamp);
var b = new Date(b.timestamp);
return a>=b ? 1 : (a<b ? -1 : 0);
})
d3.selectAll("div.datarow")
.data(dataset, function(d) {return d.content})
.transition()
.duration(2000)
.style("top", function(d,i) {return (40 + (i * 40)) + "px"});
}
function restoreSheet() {
d3.selectAll("div.datarow")
.transition()
.duration(2000)
.style("top", function(d,i) {return (40 + (i * 40)) + "px"});
}
}
</script>
</footer>
</html>
What I don't fully understand is how sortSheet and restoreSheet work.
This part of sortSheet looks like it rebinds data, but after console logging I think it doesn't actually rebind data to the DOM. Instead it just seems to redraw the div.tablerow elements based on the array index of the sorted array.
But then what purpose does the key-function have?
And why is the transition working? How does it know which old element to put in which new position?
EDIT:
---After some more reading I now know that selectAll().data() does indeed return the update selection. Apparenty the already bound data identified by the key function is re-sorted to match the order of the keys in the new dataset? Is that correct?
So the update selection contains the existing div.datarow s, but in a new ordering. The transition() function works on the new order, drawing the newly ordered div.datarow s beginning with index 0 for the first element to determine its position on the page, to index n for the last element. The graphical transition then somehow (how? by way of the update selection?) knows where the redrawn div.datarow was before and creates the transition-effect.
Is that correct so far?---
d3.selectAll("div.datarow")
.data(dataset, function(d) {return d.content}) //why the key function?
.transition()
.duration(2000)
.style("top", function(d,i) {return (40 + (i * 40)) + "px"});
And what happens when the original order is restored? Apparently during both operations there is no actual rebinding of data, and the order of the div.datarows in the DOM does not change. So the restore function also redraws the layout based on the array index.
But what kind of selection does the .transition() work on? Is it an update? It is an update.
And why does the drawing using the index result in the old layout? Shouldn't the index of the DOM elements always be 0,1,...,n? I think it is. Apparently the old page layout is redrawn, with the DOM never having changed. But how can the transition() function create the appropriate graphical effect?
function restoreSheet() {
d3.selectAll("div.datarow")
.transition()
.duration(2000)
.style("top", function(d,i) {return (40 + (i * 40)) + "px"});
}
I have been thinking for hours about this, but I can't find the correct answer I think.
Thanks for your help!
It all becomes clear when you understand where all these functions were called: inside the json function, where the data was originally bound. When a button calls the sortSheet function, a new array of objects is made and bound to the rows. The transition simply starts with the original order and move the rows according to the new order of the objects inside the array.
And what happens when the original order is restored?
Now comes the interesting part: restoreSheet is called inside the json function and has no access to the dataset variable. So, the data restoreSheet uses is the original data. Then, a transition simply moves the rows according to the order of the objects inside the original array.
I just made a fiddle replicating this: https://jsfiddle.net/k9012vro/2/
Check the code: I have an array with the original data. Then, a button called "sort" creates a new array.
When I click "original" the rectangles move back to the original position. But there is nothing special in that function, no new data being bound:
d3.select("#button1").on("click", function(){
rects.transition()
.duration(500).attr("x", function(d, i){ return i * 30})
});
It moves all the rectangles to the original positions because this function uses the same original data.

Multiple kernel density estimations in one d3.js chart

I'm trying to make a plot that shows density estimations for two different distributions simultaneously, like this:
The data is in two columns of a CSV file. I've modified code from Mike Bostock's block on kernel density estimation, and have managed to make a plot that does what I want, but only if I manually specify the two separate density plots -- see this JSFiddle, particularly beginning at line 66:
svg.append("path")
.datum(kde(cola))
.attr("class", "area")
.attr("d", area)
.style("fill", "#a6cee3");
svg.append("path")
.datum(kde(colb))
.attr("class", "area")
.attr("d", area)
.style("fill", "#b2df8a");
I've tried various incantations with map() to try to get the data into a single object that can be used to set the color of each density area according to the color domain, e.g.:
var columns = color.domain().map(function(column) {
return {
column: column,
values: data.map(function(d) {
return {kde: kde(d[column])};
})
};
});
I don't have a great grasp of what map() does, but this definitely does not work.
How can I structure my data to make this plot in a less brittle way?
To make this generic and remove column dependency first prepare your data:
var newData = {};
// Columns should be numeric
data.forEach(function(d) {
//iterate over all the keys
d3.keys(d).forEach(function(col){
if (!newData[col])
newData[col] = [];//create an array if not present.
newData[col].push(+d[col])
});
});
Now newData will hold the data like this
{
a:[123, 23, 45 ...],
b: [34,567, 45, ...]
}
Next make the color domain like this:
var color = d3.scale.category10()
.domain(d3.keys(newData))//this will return the columns
.range(["#a6cee3", "#b2df8a"]);
Finally make your area chart like this:
d3.keys(newData).forEach(function(d){
svg.append("path")
.datum(kde(newData[d]))
.attr("class", "area")
.attr("d", area)
.style("fill", color(d));
})
So now the code will have no dependency over the column names and its generic.
working code here

D3 Trend Line Domain

I draw a line chart using D3 with some data from our database, I got some data for the entire year to calculate what would be our trendline (taking some values for 8am, 12m, 4pm and 9pm), I'm drawing this in the chart with path and values for each X (time).
Now my problem is the domain of the trendline is of course bigger than our current values (lets say its 2 pm and my trendline will always go to 9 pm). The closes I got was setting the trendline's domain to my current data domain, which returns this:
Test1
xTrend.domain(d3.extent(trendData, function (d) { return d.date; }));
How can I cut it so it doesn't go beyond the SVGs width? I tried setting the width attribute and it doesn't work, so my guess is it has something to do with the domain.
If I set the trendline's domain to its data, I get this:
Test2
xTrend.domain(d3.extent(data, function (d) { return d.date; }));
PS: While we are on this, if anyone can point me on how I could see if my line is above-below my trendline it would be great ;)
Update:
Thanks to #lhoworko
I added this
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
And this to my line path
.attr("clip-path", "url(#clip)")
Take a look at d3 clip paths. It will make sure the line beyond the end of the chart isn't displayed.
Take a look here.
https://developer.mozilla.org/en-US/docs/Web/SVG/Element/clipPath

Resources