d3 transitioning from bar to pie and back - animation

LIVE DEMO
So I have this notion that all single axis data should be allowed to be displayed in all the basic ways; and at the very least from a pie to a bar. Ideally this would be an animated transition, but thats were the difficulty comes in.
Getting a pie chart to work is easy enough, as is getting a bar chart. Here is what I have so far:
# fields
width = 750
height = width/2
margin = 20
radius = (height-(margin*2))/2
# helpers
pie = d3.layout.pie().value (d) -> d
arc = d3.svg.arc()
.outerRadius(radius)
.innerRadius(radius/4)
x = d3.scale.linear().domain([0, 100]).range [0, width]
$http.get('/Classification_Top_10_by_Count.json').success (data) ->
percents = (parseFloat item.Percent for item in data).sort d3.ascending
svg = d3.select('#svgStage').append('svg')
.attr('width', width+(margin*2))
.attr('height', height+(margin*2))
svg.data([percents])
g = svg.append('g')
.attr('transform', "translate(#{radius},#{radius})")
paths = g.selectAll 'path'
paths.data(pie).enter().append('path')
.attr('d', arc)
toBars = ->
g.selectAll('path').transition().duration(2000)
.attr 'd', (d, index) ->
# this is over complex because I was playing with it.
cord =
tl : [0, index*20]
tr : [d.value*20, index*20]
br : [d.value*20, index*20-20]
bl : [0, index*20-20]
oCord = [
cord.tl
cord.tr
cord.br
cord.bl
]
"M #{oCord[0][0]}, #{oCord[0][2]}
A 0, 0 0 0, 0 #{oCord[1][0]}, #{oCord[1][3]}
L #{oCord[2][0]}, #{oCord[2][4]}
A 0, 0 0 0, 0 #{oCord[3][0]}, #{oCord[3][5]}
Z"
Obviously for this to work its got to be path element to path element, and the transition is working now. Problem is it looks like crap. The moment it starts it looks garbled, until it over and becomes decent bar chart.
I've been looking at this : http://d3-example.herokuapp.com/examples/showreel/showreel.html
Which demonstrates a bar transitioning to a donut in much the way I would like. Looking at the source code, this is accomplished through a custom tween. (view source line 518)
Now I'm in over my head. What is going on here? How can I get this transition to work? Has anyone else out there dealt with this problem?
UPDATE
Just to be clear, below illustrations the intention of my transition abit more clearly.
Bounty clarity. I added a bounty to this question because I need an explanation of what was going wrong. Superboggly did that, so he got the bounty. However Amit Aviv's approach is superior, so I accept his answer as the most correct. I have also +1ed both.

Here is my take: http://jsfiddle.net/amitaviv99/x6RWs/42/
My approach was to approximate both the arcs & bars using cubic bezier curves, with the exact same number of control points. The code is somewhat complicated, and need some work. But the result is quite smooth.
Here is an excerpt (SO requires..)
var bezierArc = function(radiusIn, radiusOut, startAngle, endAngle){
var arcIn = makeCompArc(radiusIn, startAngle, endAngle);
var arcIOut = makeCompArc(radiusOut, startAngle, endAngle);
var lines = makeBezierDoubleLine(radiusIn, radiusOut, startAngle, endAngle);
var path = [arcIn, lines[0], arcOut, lines[1]].join(' ');
return path;
}

D3 does a pretty good job of interpolating between paths, but it was having trouble with your original before and after path so instead of taking over the whole tweening process myself I thought maybe we could come up with better paths to make the job easier for D3. My result.
The first thing is to look at the svg arc path element. It basically goes like this:
A rx,ry a f1,f2 x,y
you can read the details here. This will draw an arc from wherever you are (previous final coordinate) to the coordinates x,y. But the things to focus on are that the first two numbers are the implied ellipse's radii and the last part before the end coordinates, that I've marked f1,f2, are flags and so not interpolate-able.
So the main weirdness in the transition from your code is because you are trying to interpolate between
A rx,ry, 0 0,1
A 0,0 0 0,0
You will immediately see a smoother transition if you set your end-path to A0,0 0 0,1 in the one case.
To make the pieces fit together a bit better I animated the pie's inner radius so that the segments looked more like the bars but curved, then I let D3 figure out the curve-to-bar transition but without switching the arc flag. Then you want the bars to have flat ends. The path will have a flatter arc if you increase your implied ellipses radii! So I simply used 100,100. My final transition-to path for the bars looks like:
"M " + oCord[0][0] + "," + oCord[0][1] +
"A100,100 0 0,1 " + oCord[1][0] + "," + oCord[1][1] +
"L " + oCord[2][0] + "," + oCord[2][1] +
"A100,100 0 0,0 " + oCord[3][0] + "," + oCord[3][1] +
"Z";
Then To actually, properly, flatten the endpoints I have a second transition (they run serially) to zero the Arc segments of the path. I suspect there is a better way to do this kind of cleanup with D3 transitions, but a transition with duration 0 also works.
To get the reverse to work nicely I set the paths to the flattened-arc-curves from above. Having the large radius and correct flags means the D3-computed transition back to the doughnut chart works well. Then I simply animate the inner radius back out.

Related

Graph ~ axis alignment issue

I'm currently working on a quite basic graph using 2 ordinal axes. X axis shows 4 categories, Y axis shows 3. For some reason, the plotted circles don't align with the plotted axes.
An example can be seen at http://jsfiddle.net/SrdY6/. Problem seems to be translation-related, but the only translation in there is applied to the large containing <g> element:
var lunchgraph = svg.append("g")
.attr("class", "lunchgraph")
.attr("transform", "translate(" + lunchmargin.left + "," + lunchmargin.top + ")");
I've been looking at this for some time now, but can't spot where things go wrong... Anyone with more insight?
Nothing like putting a question out there and risking public shame, only to find out the answer within minutes after posting.
For ordinal axes configured with rangeBands or rangeRoundBands, the scale function returns the lower value of the given input. To have the plot align with the exact categorical labels, you need to add half of the rangeBand to the calculated coordinate.
So: no problem with the translations or anything, but with the computation of cx and cy coordinates for placing the circles in the graph.
Correct code:
.attr("cx", function(d) { return x(d.label) + x.rangeBand()/2 ;} )
.attr("cy", function(d) { return y(d.sqid) + y.rangeBand()/2 ; } )

D3 force layout: Finding relative center based on current visible view

I have a d3.js graph that is a forced layout design. I have allowed for users to zoom in and out of the graph with bounds set so they can't zoom in past 1 and can't zoom out past 0.1. Right now, when I plot values on the graph, I automatically send them to the center of the graph (based on the height and width of the SVG container). This works fine until I zoom out then zoom in to some where else and plot a new node. The new node will end up back at the original center and not my new relative center.
How I scale when zooming right now:
function onZoom() {
graph.attr("transform", "translate(" + zoom.translate() + ")" + " scale(" + zoom.scale() + ")");
}
I was unable to find any calls to get the current visible coordinates of the graph, but even with those, how would I use them to calculate the relative center of the graph if my SVG graph size always remains static?
I know this post is very old but I found it useful. Below is the update for d3 v5.
var el = d3.select('#canvas').node().getBoundingClientRect();
var z = d3.zoomTransform(svg.node());
var w = el.width;
var h = el.height;
var center = {
x: (z.x / z.k * -1) + (w / z.k * 0.5),
y: (z.y / z.k * -1) + (h / z.k * 0.5)
};
One thing of note, however... is that I found I also needed to divide the pan x/y by the scale factor z.k. Which, you did not do in your formula.
For simple geometric zoom, it's fairly straightforward to figure out the visible area from the visible area dimensions plus the translation and scale settings. Just remember that the translation setting is the position of the (0,0) origin relative to the top left corner of your display, so if translation is (-100,50), that means that top left corner is at (+100,-50) in your coordinate system. Likewise, if the scale is 2, that means that the visible area covers 1/2 as many units as the original width and height.
How to access the current transformation? graph.attr("transform") will give you the most recently set transform attribute string, but then you'll need to use regular expressions to access the numbers. Easier to query the zoom behaviour directly using zoom.translate() and zoom.scale().
With those together, you get
var viewCenter = [];
viewCenter[0] = (-1)*zoom.translate()[0] + (0.5) * ( width/zoom.scale() );
viewCenter[1] = (-1)*zoom.translate()[1] + (0.5) * ( height/zoom.scale() );
I.e., the position of the center of the visible area is the position of the top-left corner of the visible area, plus half the visible width and height.

Minor tweaks for histogram generated using d3

Following is the stripped down version is what I'm using to generate histograms using d3 and a bit of jQuery.http://bl.ocks.org/4611158
While most of it might seem right, I'm still confused regarding
Why there is no '14' in the x-axis as should have been for the given input in the above example? Instead 13 gets the ordinate of what should have been 14's
In my trials d3.layout.histogram() assigned negative(and hence non-plot table) widths when I try altering the output range of scale to some non-zero value. why is it so? what is the possible workaround?
My main motive to use ordinal scale was to make ticks centrally aligned below the bars, unlike what Mike used in his demo for histograms. I've also made the number of bins equal to the number of ticks in d3.layout.histogram() for the very same purpose. I'm sure there might be a better way around to code what I'm looking for
Also any ideas how to add a 'graph' of indicator lines like its been done in nvd3 visualization (light gray in background )that will make it more pleasing?
There is no 14 and there are two 8s on the x-axis. This is because the bins function will diligently divide the range = 14 - 1 = 13 into 14 intervals as per the API reference:
The bins may be specified as a number, in which case the range of values will be split
uniformly into the given number of bins. Or, bins may be an array of threshold values,
defining the bins; the specified array must contain the rightmost (upper) value, thus
specifying n + 1 values for n bins. ...
Before solving this issue, I am guessing that the second problem you are facing is that if the rangeDefault has negative values, then some values are not plotted. To fix that problem, being unaware of the exact need of it, I will start by removing the following:
rangeDefault[0] = 0; //All histograms start from 0 <-- REMOVED
Then to fix the first problem, use the second form of arguments for binsas shown here:
var bins = [];
for(var ii = settings.range[0], jj = 0; ii <= settings.range[1] + 1; ii++, jj++)
bins[jj] = ii;
var data = d3.layout.histogram()
.bins(bins)(settings.data);
I hope this addresses the primary queries.
Adding the light grey indicator lines is fairly easy, as shown here. The changes were:
vis.css
.y.axis line.tick { opacity: .3; }
vis.js
Moving the axis before the chart in the DOM because of how SVG is laid out affects its z-index:
var gEnter = svg.enter().append("svg").append("g");
gEnter.append("g").attr("class", "x axis");
gEnter.append("g").attr("class","y axis");
gEnter.append("g").attr("class", "bars");
And finally making the major tickSize on the y-axis -(width - margin.right - margin.left):
yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickSubdivide(true)
.tickPadding(5)
.ticks(10)
.tickSize(-(width - margin.right - margin.left), 2, 8);

How to scale and translate together?

I want to scale and translate D3 force graph, both at the same time. E.g. On clicking a button it shoud scale to 400% and then make itself center on the screen. This should all happen with a smooth animation effect.
//animate vis to visible area
vis.transition()
.duration(2000)
.attr("transform", "scale(" + someScaleValue + ")" + "center("0,0)");
Doing this, scaling works fine, but graph is not centered. It shifts towards right-bottom corner.
vis.transition()
.duration(2000)
.attr("transform", "scale(" + someScaleValue + ")");
Why is scale is getting reset to 100% when I translate it second time.
I also tried using:
vis.transition()
.duration(2000)
.attr("transform", "scale(" + scaleValue + ")" + "translate(0,0)");`
This is not working too. Please help me.
center(0,0) is not a valid transform-definition to be used with transform, as per the spec.
If you want translate(0, 0) to take the object to the center of the screen (usually the top-left corner of vis), then you might want to set viewBox of the outer svg element to be: "-width/2 -height/2 width height". This would set the co-ordinate system inside the svg element such that the center lies at (0, 0). Alternatively, you can use translate(width/2, height/2).
Also, each time you call .attr('transform', ...), you overwrite the old value of the transform attribute. This is the possible reason why you are losing the original scaling on translating. The best solution would be to put the vis element inside a g which has the scaling in the transform attribute which remains constant.

How to add a rectangle to specified axis in D3

I have a zoomable area plot done in D3, which works well. Now I am trying to add a rectangle to the specified location along x-axis in the middle of the plot. However, I can't seem to figure out how to do that. "rect" element is specified using absolute (x,y) of the plot and so when using zooms it stays in the same position.
So I was wondering if there is a way to tie "rect" to the axis when plotting, so that it benefits from all the zoom and translate behaviour or do I need to manually edit the x,y,width and length of the rectangle according to translation as well as figuring out where the corresponding x and y coordinates are on the graph? I am trying to use "rect" because it seems the most flexible element to use.
Thanks
Alex
I'm not sure how you are doing the zooming, but I am guessing you are changing the parameters of the scales you use with your axis? You should be able to use the same scales to place your rectangle.
If you are starting with plot coordinates then maybe using the invert function on the scale will help (available at least for quantitive scales), e.g. https://github.com/mbostock/d3/wiki/Quantitative-Scales#wiki-linear_invert
You should be able to take initial plot coordinates and invert them to determine data coordinates that can then move with changes in the scale.
If the scale is linear you can probably invert the length and width too, but you will have to compute offsets if your domain does not include 0. Easiest is to compute the rectangle's end points, something like:
var dataX0 = xScale.invert(rect.x);
var dataX1 = xScale.invert(rect.x + rect.width);
var dataWidth = dataX1 - dataX0;
If you have the data in axes coordinates already you should be able to do something like:
var rectData = [{x: 'April 1, 1999', y: 10000, width: 100, height:100}];
svg.selectAll('rect.boxy')
.data(rectData)
.enter().append('rect').classed('boxy', true)
.style('fill','black');
svg.selectAll('rect.boxy')
.attr('x', function(d) { return x(new Date(d.x));} )
.attr('y', function(d) { return y(d.y);})
.attr('width', function(d) { return d.width;} )
.attr('height', function(d) { return d.height;} );
Based on the example you shared where x and y (as functions) are the scales the axes are based on.

Resources