transitioning multiple elements with d3 - d3.js

I'm having a heck of a time with transitions in D3js. I posted a fiddle here: dynamic area graph. The problem that I run into is that when trying to follow this tutorial path transitions, I run into sync problems with the xAxis. Bostock indicates that the domain should be skewed slightly so that the data appears to "shift" in from the side. However, when I do that, the data reflected will be listed under a tick mark that is "2 minutes" behind the actual time it should be listed. If I just update the data as-is, without doing the tricky stuff with the clip-path, it works fine. All of the data is in sync. Just for reference, the xAxis is an integer, linear scale. Dealing with date strings was madding, even though d3 has great time manipulation, I just find dealing with epoch easier. If someone could check out the fiddle and let me know how to transition the entire drawing...I want it to be smooth like in the examples that bostock has.
Since SO requires some code, here's the data structure that I'm generating. The rest is in the fiddle:
setInterval(function(){
lastTime = lastTime.add('m',1);
var data = {"apikey":"FOO",
"description":"a dumb description",
"beg_effective_dt_tm":lastTime,
"data":{
"sum":getRandomInt(385,4000),
}
};
tick(data);
},1000)

I think this is close to what you are after: http://jsfiddle.net/JJ7Rj/1/
It is close because the graph is delayed by one value.
Most of the architecture was already there, and your concern that you might lose the sync between the xAxis and the data was correct. Mike gets around it by changing his range of the scales. The other (and better, IMO) way is to do it by changing the domains. I made the following two primary changes.
The domain of the axis
I have modified the minMax function such that it does not include the latest and the last point in the domain of the xAxis. Note that this means that the most recent value (as well as the oldest value) is actually displayed outside the visible region. This is an unfortunate limitation of using the monotone interpolation, which Mike talks about at the bottom of his post.
function minMax(pd) {
return [
d3.min(pd.slice(1).slice(0, -1),function(d){ return d.beg_effective_dt_tm; }),
d3.max(pd.slice(1).slice(0, -1),function(d){ return d.beg_effective_dt_tm; })
];
}
If you want to have all the values visible, then you'll get the wiggling effect of the discontinuous tangent suddenly forming when the new value comes in. You can obtain that by removing the .slice(0, -1).
The initial transform
For each element, I have initially placed the DOM element one step to the right.
var step = x(newVal.beg_effective_dt_tm) - x(pd[pd.length - 1].beg_effective_dt_tm);
// ...
.attr("transform", 'translate(' + step + ')');
// ...
Then finally, I have transitioned everything back to their rightful place:
clipPath.selectAll("path,circle,.dp").transition()
.ease("linear")
.attr("transform", "translate(" + 0 + ",0)");
Also, I have enabled the transition for the xAxis.

Related

d3.zoom unexpected behavior: what am I missing?

I've got a behavior with d3.zoom whose solution I'm sure is to be found in something I'm obviously missing, but I can't seem to make sense of it. I've reviewed and reviewed examples, and seem to be following them precisely, but something is causing this particular function to not behave.
The following, rather than zoom to focusElement as intended, flips between zooming away from it, and then back to it. The values of -focusBBox['x'], for example, flip between the following two values on subsequent executions. 2500 is svgWidth/2
-208.586669921875
2500
function focusObject(focusElement) {
var focus = document.getElementById(focusElement);
var focusBBox = focus.getBoundingClientRect();
gridGroup.transition().duration(750).call(zoom.transform,d3.zoomIdentity.translate(-svgWidth / 2, -svgHeight / 2).translate(-focusBBox['x'], -focusBBox['y']));
}
Can someone just please take a moment to give a kind virtual slap to point out what it is that I'm missing?
Aha! From a previous incarnation of this particular endeavor, I was using a fixed-position SVG to capture mouse events and applying my transformations to a child SVG. The fact that getBbox() returns local coordinates and getBoundingClientRect() returns coordinates from the outer SVG coord system thus mucked things up.
I've included the revised snippet below. Note that focusBBox2 uses getBBox rather than getBoundingClientRect(), and that grid is the parent SVG as distinct from gridGroup in the original post.
function focusObject(focusElement) {
var focus = document.getElementById(focusElement);
var focusBBox2 = focus.getBBox();
grid.transition()
.duration(750)
.call(zoom.transform,d3.zoomIdentity.translate(focusBBox2.x, focusBBox2.y));
}

DC.JS how to animate the order of the rows in a rowchart?

I have a rowchart ordered by value (from greater to lesser). When filtering data, the row order changes as expected. However, the row's order change instantly.
Is there a way to animate the reordering of the rows when it changes?
I have searched for possible ways, but I couldn't find anything.
I see that each row has a class according to its position. So. the first one has the classes row _0, the next one row _1, etc., but couldn't figure out how to get the _0, _1 ... classes and using a pretransition function, such as
myChart.on('pretransition', function(chart) {...}
to animate the order. But couldn't figure it out.
Doing this animation was pretty simple, but testing it exposed a major bug in the base mixin, which I fixed. So this demo will only work with version 4.0.4.
There's no good way to implement the animation without changing the source.
This fiddle copies dc.RowChart out to create an AnimatedRowChart, makes a few minor changes to locate symbols in dc.js and d3, and here are the important changes:
Using a key function
When adding the data, we'll use a key function,
.data(this._rowData, ({key}) => key);
This will join the data so D3 knows which bar went where.
Animating the rows
Instead of just setting the positions of the rows
rows.attr('transform', (d, i) => `translate(0,${(i + 1) * this._gap + i * height})`)
we'll give them an animated transition
dc.transition(rows, this.transitionDuration(), this.transitionDelay())
.attr('transform', (d, i) => `translate(0,${(i + 1) * this._gap + i * height})`);
I also thought it looked better if new rows start from their final position rather than the top, so I changed that as well. The resulting transitions still have plenty of issues, but they mostly make sense.
I'm not ready to release this, so it's on a branch of dc.js.
Here is the demo fiddle with complete AnimatedRowChart that you can paste into your own app.
SO doesn't seem to let my GIF loop, so reload the page if you missed it!

Strange Axis Behavior

I have a color coded map of China, and it was working fine until I added a color scale and an axis to the right of the map. You may view it here:
https://bl.ocks.org/diggetybo/4c42aafc20c21e416585c9e37079eda2
The problem is, a province went missing after I added the axis. By missing, I mean it is not visible anymore. I'm not sure if it's actually still there but has a fill of none, or for some reason the path was not drawn altogether. It is a very large province too, in the northwest of China. For those hard core in geography, the name of the province is Xinjiang.
At first I was convinced it was user error on my part, but after much toiling I slowly started to suspect it could be a bug of some kind. I did manage to narrow it down to which line: line 69, or the .call(axis); line in the snippet below:
var axisNodes = svg.append('g')
.attr('transform', 'translate(' + (margins.left+865) + ',' + (margins.top) + ')')
.call(axis);
styleAxisNodes(axisNodes);
I literally commented out every other line, and only the .call(axis); line makes the province disappear. You can try for yourself, just comment out line 69 and the province will reappear.
My question: What does an axis have to do with a totally different section of my code, the fill of a json parsed map path? Why did it only affect that province? And what can I do to avoid this?
Thank you
When you do this inside your d3.json function:
svg.selectAll("path")
You are in fact selecting an already existing path, which is the axis' path, and binding data to it.
Because of that, your "enter" selection will have one element less.
Solution: select something that doesn't exist:
svg.selectAll("foo")
Here is your updated bl.ocks: https://bl.ocks.org/anonymous/387781c5bccb0339141b519c098f5605
PS: another solution is calling the axis after painting the map.
PPS: "Why did it only affect that province?" That province is the first one in the data.

Maintaining data positions when zooming in d3

I am using D3.js to plot a lot of little data points (as circles) on a map. No big deal there. I would like to add the ability to zoom and pan, however.
I have been using this version of the zooming function: http://bl.ocks.org/mbostock/2206340
It works great on my map base (e.g. the country forms). But it does not move the data points. (I attempted to simply add the data to the "features" layer but this did not work well — it put them underneath the landforms and made it so that they no longer triggered mouseover events, presumably because they are under the layer that looks for pan/zoom events.)
Here is what my map look like:
If I do nothing when I pan/zoom, it obviously doesn't look very good:
I have tried to use the zoom/translate events to adjust the projection, then re-project the data points. This works great for panning but totally fails for zooming:
You can't really tell from the image above but the relative distances between the images have indeed moved with the zooming. But the pan translation is totally off.
Here is the function I am using to handle the zooming:
function zoomed() {
features.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
features.select(".state-border").style("stroke-width", 1.5 / d3.event.scale + "px");
features.select(".county-border").style("stroke-width", .5 / d3.event.scale + "px");
projection.scale(projection_scale*d3.event.scale);
projection.translate([((width/2)+d3.event.translate[0]),((height/2)+d3.event.translate[1])]);
for(i in cdata) {
var ll = cdata[i].LatLng.split(",");
if(!ll[0]) ll[0]=-500;
if(!ll[1]) ll[1]=-500;
positions[cdata[i].id]=(projection([parseFloat(ll[1]),parseFloat(ll[0])]));
}
svg.selectAll(".data_circle")
.attr("cx", function(d, i) { return positions[d.id][0]; })
.attr("cy", function(d, i) { return positions[d.id][1]; })
;
}
cdata is an object with all of my csv-loaded data in it, including lat/lng data as a comma-separated string (hence the split). The -500 thing is just for bad data; it puts it where you can't see it (a temporary fix). positions is an array of all the projected lat/lng positions.
Obviously I'm thinking about this incorrectly. I've tried scaling the translate function by the zoom amount (e.g. ((width/2)+d3.event.translate[0])*d3.event.scale) ) but this produces really odd results as well (it seems to help a bit but the pan/zoom become a little "unhinged" — it is almost as if the dots are hovering above the map in 3D space, and moving left and right makes them almost seem a little stereoscopic... anyway, not the effect I am trying to produce).
What should I be doing differently? Again, any solution needs to take into account that the dots have mouseover events on them that need to be able to fire.
I've searched the zooming D3 examples for something that does this and not found one, which surprised me, since this seemed like kind of basic functionality (and is something easy to do in Google Maps, for once), but maybe I didn't search deep enough or in the right place.
Ah, I figured it out! It suddenly occurred to me what the problem was. All it required was changing:
projection.translate([((width/2)+d3.event.translate[0]),((height/2)+d3.event.translate[1])]);
to
projection.translate([((width*d3.event.scale/2)+d3.event.translate[0]),((height*d3.event.scale/2)+d3.event.translate[1])]);
Which makes sense — the original width/height were not the same because it was being zoomed, so I needed to apply the translation to the zoomed width/height. Now it works great.

NVD3.js multiChart x-axis labels is aligned to multiple lines, but not multiple bars

This question relates to NVD3.js multiChart x-axis labels is aligned to lines, but not bars
I am using NVD3.js multiChart to show multiple lines and multiple bars in the chart. All is working fine, but the x-axis labels is aligned only to the line points, not bars. I want to correctly align labels directly below the bars as it should. But I get this:
As you can see - x-axis (example, 2014-Feb) is not aligned to Bars.
1) How to align x-axis labels to bars and lines at the same time?
2) I need this solution for NVD3.js or how to properly integrate.
I made jsFiddle: http://jsfiddle.net/n2hfN/28/
Thanks!
The problem here is that nv.models.multiChart uses a linear scale for its x-axis, and then when it draws the bars it calls nv.models.multiBar, which uses an ordinal scale with .rangeBands().
You can follow this mess through the source code:
First lets look at multiChart.js
HERE is where it sets the x-scale to be a linear scale.
HERE it calls the nv.models.multiBar model to create the bars.
If we jump over to have a look at multiBar.js
HERE it creates an ordinal scale, and HERE it sets the range of the scale using .rangeBands()
The result is that the ordinal scale used for placing the bars, and the linear scale used for the chart's axis do not align. Here's what the two scales look like on their own if plotted on an axis:
The solution would be to force the chart to render the line graphs and the x-axis in terms of the ordinal scale used by the bars. This would work in your case because the bars and the lines all use the same data for the x-axis. This is very simple to do if you are making your own chart and not relying on nvd3, as I showed in my answer to your previous question HERE. This is extraordinarily complicated to do if you're trying to work within nvd3, and many others have tried and failed to switch out the default scales used by nvd3 charts. Have a look at this issue on the nvd3 github page that has been open since January, 2013 for example.
I've tried a number of approaches myself to reuse the bars' ordinal scale, but with little success. If you want to poke around and try to brute-force it yourself, I can tell you that from my experiments I came closest when using chart.bars1.xScale().copy() to make a copy of the bars' scale, and set its domain and rangeBands. Unfortunately, since the chart's width is computed at render time, and I can't seem to create a hook into the chart.update function, it is impossible to set the rangeBands' extent to the correct values.
In short, if you can't live with the labels being offset, you're probably going to need to code up your own chart without nvd3, or else find a different type of layout for your visualization.
After playing around with the NVD3 v1.7.1 source code with the immensely helpful guidance offered by jshanley's answer, I think I've managed to come up with an answer (perhaps more of a kludge than a good solution).
What I did was to have the x-axis labels align with the bars, and have the line data points align with the bars.
1.1. To align the x-axis label, I shifted the x-axis to the right so that the first label appears underneath the middle of the first bar. I then shifted the last label to the left, so that it appears underneath the middle of the last bar. See code here. The amount to shift by is computed at drawing time using .rangeBand() and saved in a rbcOffset variable (I had to modify multiBar.js for this to work).
1.2. To align the line data points with the bars, a similar shift is also required. Luckily, this part is easy because scatter.js (which is used by line chart) comes with a padData boolean variable that does what we want already. So basically, I just set padData to true and the lines shift to align with the bars, see here.
In order to properly integrate with NVD3 and make everything look good, some additional changes are required. I've forked NVD3 on GitHub so you can see the complete solution there. Of course, contributions are welcome.
I use last solution and it runs. So, you can specify
lines1.padData(true)
in order to align lines too.
Same here, I used the last solution,it worked for me as well. Find the following line in multiChart.js
if(dataLines1.length){
lines1.scatter.padData(true); // add this code to make the line in sync with the bar
d3.transition(lines1Wrap).call(lines1);
}
I encountered the same problem and fixed it with below code:
at lines 7832 and 7878 replace
.attr('transform', function(d,i) { return 'translate(' + x(getX(d,i)) + ',0)'; })
with :
var w = (x.rangeBand() / (stacked && !data[j].nonStackable ? 1 : data.length));
var sectionWidth = availableWidth/(bars.enter()[0].length - 1);
if(bars.enter().length == 2)
return 'translate(' + ((i-1)*w + i*w + (i*(sectionWidth - 2*w))) + ',0)';
else
return 'translate(' + ((i-0.5)*w + i*(sectionWidth - w)) + ',0)';
The first case handles multiple bars case while the second one handles single bar case.
lawry's solution works. Also if using interactive guidelines, you need to shift the interactive line to match the new scale. Modify:
if(useInteractiveGuideline){
interactiveLayer
.width(availableWidth)
.height(availableHeight)
.margin({left:margin.left, top:margin.top})
.svgContainer(container)
.xScale(x);
wrap.select(".nv-interactive").call(interactiveLayer);
//ADD THIS LINE
wrap.select(".nv-interactiveGuideLine")
.attr('transform', 'translate(' + rbcOffset +', ' + 0 + ')' +
'scale(' + ((availableWidth - rbcOffset*2)/availableWidth) + ', 1)');
}
in multiChart.js.

Resources