Rotate Pie Label in dc.js Pie Chart - javascript

Let's say I have the following code in dc.js to create a pie chart:
var chart = dc.pieChart("#test");
d3.csv("morley.csv", function(error, experiments) {
var ndx = crossfilter(experiments),
runDimension = ndx.dimension(function(d) {return "run-"+d.Run;})
speedSumGroup = runDimension.group().reduceSum(function(d) {return d.Speed * d.Run;});
chart
.width(768)
.height(480)
.slicesCap(4)
.innerRadius(100)
.dimension(runDimension)
.group(speedSumGroup)
.legend(dc.legend())
// workaround for #703: not enough data is accessible through .label() to display percentages
.on('pretransition', function(chart) {
chart.selectAll('text.pie-slice').text(function(d) {
return d.data.key + ' ' + dc.utils.printSingleValue((d.endAngle - d.startAngle) / (2*Math.PI) * 100) + '%';
})
});
chart.render();
});
What I want to do is rotate the label, but when I do so, all of the labels translate to the center of the pie.
chart.renderlet(function (chart) {
chart.selectAll('text.pie-slice')
.attr('transform', 'rotate(315)');
});
Is there a way to rotate the labels without changing their position on the graph?

The problem is that you're replacing the transform attribute for these elements, which is currently used to "translate" the labels in position.
Since it's hard to dig into the calculations used here, I think the best approach is to pull the existing transform attribute and modify it, like this:
chart.on('renderlet', function (chart) {
chart.selectAll('text.pie-slice')
.attr('transform', function(d) {
var translate = d3.select(this).attr('transform');
var ang = ((d.startAngle + d.endAngle) / 2 * 180 / Math.PI)%360;
if(ang<180) ang -= 90; else ang += 90;
return translate + ' rotate(' + ang + ')';
});
});
For my own entertainment, I've also rotated the labels using the start and end angles of the pie slices.
It's unfortunate you can't do this as a pretransition event and avoid the "jump". It will just get overwritten by the animations. Doing this properly would require some changes to dc.js - file an issue if you're interested.

addition to Gordon's answer, for all pieCharts to have this feature; i changed labelPosition function in dc.js.
return 'translate(' + centroid + ')';
to
var ang = ((d.startAngle + d.endAngle) / 2 * 180 / Math.PI)%360;
if(ang<180) ang -= 90; else ang += 90;
return 'translate(' + centroid + ') rotate(' + ang +')';

Related

SVG path doesn't render correctly in D3 force network graph

I have no idea what is happening here, but when I draw my network diagram, it ends up like this :
Notice the blue lines to the right. I have a zooming ability, and when I zoom, the blue paths on the right disappear.
My code base is huge, so I'll try get a codePen together of an example to see if I can recreate it. But I used this as a guideline for creating curved links :
https://bl.ocks.org/mbostock/4600693
This is when I hit the issue.
Some code for the network creation :
Data
var bilinks = [];
edges.forEach(function (d) {
var s = d.source;
var t = d.target;
var i = {};
edges.push({
source: s,
target: i
}, {
source: i,
target: t
});
nodes.push(i);
bilinks.push({
source: s,
target: t,
middleNode: i
});
});
Path creation :
linkEnter
.append('path')
.attr('id', function (d, i) {
return d.id
})
.attr('class', 'network-path')
.attr('stroke', function (d) {
return colour(d.color);
})
.attr('stroke-width', 1)
.attr('fill', 'none')
.on('click', function (d) {
console.log(d);
})
Perhaps there is a similar question out there, but I'm not sure what to search for.
By the way, thie blue lines on the right are not selectable with the developer selector tool. I'm not sure how it would, but looks similar to when you have a loose monitor connection, I'm really not sure.
Added :
So, I've hidden the nodes, and gone into the elements area. Hovered over the paths you see above, and as you can see, the boundary is only small. When I hide the content in the blue box, the bunch of paths to the right disappear. When I unhide the elements, they return. I can not select the paths to the right via the select tool in dev tools.
EDIT
Tick functionality, drawing the path :
link.selectAll('path').attr('d', function (d) {
// ----
// Total difference in x and y from source to target
var diffX = d.target.x - d.source.x;
var diffY = d.target.y - d.source.y;
// Length of path from center of source node to center of target node
var pathLength = Math.sqrt((diffX * diffX) + (diffY * diffY));
// x and y distances from center to outside edge of target node
var offsetX = (diffX * nodeSize) / pathLength;
var offsetY = (diffY * nodeSize) / pathLength;
// return "M" + d.source.x + "," + d.source.y + "L" + (d.target.x - offsetX) + "," + (d.target.y - offsetY);
var thisPath = 'M' + d.source.x + ',' + d.source.y +
'S' + d.middleNode.x + ',' + d.middleNode.y +
' ' + (d.target.x - offsetX) + ',' + (d.target.y - offsetY);
return thisPath;
});
Here is a codePen of the Bostock example : https://codepen.io/anon/pen/ePJbKZ
If you drag one of the nodes ontop of the other, you should be able to see the issue.
The problem is the rendering of the Cubic Bezier splines when the points are co-linear.
If you set the d3.forceManyBody() to a strength of -1 the effect is more visible.
It looks like it is a render problem (rounding error) in the erasing of these Cubic Bezier splines. If you drag a node over the ghost lines they disappear because this part of the SVG is re-rendered.
Choosing a different spline type Q or L (straight line) does not have this erase problem.

How is data parsed in this 3D piechart?

I'm trying to grasp how the functions in Donut3D.js -> http://plnkr.co/edit/g5kgAPCHMlFWKjljUc3j?p=preview handle the inserted data:
Above all, where is it set that the data's startAngle is set at 0 degrees?
I want to change it to 45º, then to 135º, 225º and 315º (look at the image above).
I've located this function:
Donut3D.draw = function(id, data, x /*center x*/, y/*center y*/,
rx/*radius x*/, ry/*radius y*/, h/*height*/, ir/*inner radius*/){
var _data = d3.layout.pie().sort(null).value(function(d) {return d.value;})(data);
var slices = d3.select("#"+id).append("g").attr("transform", "translate(" + x + "," + y + ")")
.attr("class", "slices");
slices.selectAll(".innerSlice").data(_data).enter().append("path").attr("class", "innerSlice")
.style("fill", function(d) {
return d3.hsl(d.data.color).darker(0.7); })
.attr("d",function(d){
return pieInner(d, rx+0.5,ry+0.5, h, ir);})
.each(function(d){this._current=d;});
slices.selectAll(".topSlice").data(_data).enter().append("path").attr("class", "topSlice")
.style("fill", function(d) {
return d.data.color; })
.style("stroke", function(d) {
return d.data.color; })
.attr("d",function(d){
return pieTop(d, rx, ry, ir);})
.each(function(d){this._current=d;});
slices.selectAll(".outerSlice").data(_data).enter().append("path").attr("class", "outerSlice")
.style("fill", function(d) {
return d3.hsl(d.data.color).darker(0.7); })
.attr("d",function(d){
return pieOuter(d, rx-.5,ry-.5, h);})
.each(function(d){this._current=d;});
slices.selectAll(".percent").data(_data).enter().append("text").attr("class", "percent")
.attr("x",function(d){
return 0.6*rx*Math.cos(0.5*(d.startAngle+d.endAngle));})
.attr("y",function(d){
return 0.6*ry*Math.sin(0.5*(d.startAngle+d.endAngle));})
.text(getPercent).each(function(d){this._current=d;});
}
and tried to insert an arc such as :
var arc = d3.svg.arc().outerRadius(r)
.startAngle(function(d) { return d.startAngle + Math.PI/2; })
.endAngle(function(d) { return d.endAngle + Math.PI/2; });
but it doesn't produce the desired effects.
EDIT 1
The first answer helped in rotating the inner pie, by changing:
var _data = d3.layout.pie().sort(null).value(function(d) {
return d.value;
})(data);
to
var _data = d3.layout.pie()
.startAngle(45*Math.PI/180)
.endAngle(405*Math.PI/180).sort(null).value(function(d) {
return d.value;
})(data);
the problem is that now the outer pie gets broken -> http://plnkr.co/edit/g5kgAPCHMlFWKjljUc3j?p=preview
I guess the solution has something to do with the function function pieOuter(d, rx, ry, h ) and the two startAngle and endAngle variables, but they work in apparently unpredictable ways.
Thank you
I know that Pie Charts are bad, especially if in 3D; but this work
is part of my thesis where my job is actually demonstrate how
PieCharts are Bad! I want to rotate this PieChart in order to show how
if the 3D pie Slice is positioned at the top the data shows as less
important, or more important if positioned at the bottom. So a 'Evil
Journalist' could alter the visual perception of data by simply
inclinating and rotating the PieChart!
Here's a corrected function which allows rotation.
First, modify function signature to include rotate variable:
Donut3D.draw = function(id, data, x /*center x*/ , y /*center y*/ ,
rx /*radius x*/ , ry /*radius y*/ , h /*height*/ , ir /*inner radius*/, rotate /* start angle for first slice IN DEGREES */ ) {
In the draw function, modify angles. Instead of screwing with pie angles, I'd do it to the data directly:
_data.forEach(function(d,i){
d.startAngle += rotate * Math.PI/180; //<-- convert to radians
d.endAngle += rotate * Math.PI/180;
});
Then you need to correct the pieOuter function to fix the drawing artifacts:
function pieOuter(d, rx, ry, h) {
var startAngle = d.startAngle,
endAngle = d.endAngle;
var sx = rx * Math.cos(startAngle),
sy = ry * Math.sin(startAngle),
ex = rx * Math.cos(endAngle),
ey = ry * Math.sin(endAngle);
// both the start and end y values are above
// the middle of the pie, don't bother drawing anything
if (ey < 0 && sy < 0)
return "M0,0";
// the end is above the pie, fix the points
if (ey < 0){
ey = 0;
ex = -rx;
}
// the beginning is above the pie, fix the points.
if (sy < 0){
sy = 0;
sx = rx;
}
var ret = [];
ret.push("M", sx, h + sy, "A", rx, ry, "0 0 1", ex, h + ey, "L", ex, ey, "A", rx, ry, "0 0 0", sx, sy, "z");
return ret.join(" ");
}
Here's the full code
Changing the default start angle
Donut3D users d3's pie layout function here, which has a default startAngle of 0.
If you want to change the start angle, you should modify donut3d.js.
In the first place, you should certainly avoid to use 3d pie/donut charts, if you care about usability and readability of your visualizations - explained here.
Fixing bottom corner layout
The endAngle you are using is not correct, causing the "light blue" slice to overlap the "blue" one. Should be 405 (i.e. 45 + 360) instead of 415.
var _data = d3.layout.pie()
.startAngle(45*Math.PI/180)
.endAngle(405*Math.PI/180)
Then, the "pieOuter" angles calculation should be updated to behave correctly. The arc which doesn't work is the one where endAngle > 2 * PI, and the angle computation should be updated for it.
This does the trick (don't ask me why):
// fix right-side outer shape
if (d.endAngle > 2 * Math.PI) {
startAngle = Math.PI / 120
endAngle = Math.PI/4
}
demo: http://plnkr.co/edit/wmPnS9XVyQcrNu4WLa0D?p=preview

how to implement zoom in d3 graph of force directed layout and position the nodes

When I was trying out d3 force directed layout, I came across a challenge where
I want to zoom this svg. But Its quite tough for me to integrate.I want to remove the scrollers and put zoom for the graph.
http://nylen.tv/d3-process-map/graph.php
I want something like this which i can zoom,
http://cpettitt.github.io/project/dagre-d3/latest/demo/tcp-state-diagram.html
Below is the code where i integrate the graph in svg,
graph.svg = d3.select('#graph').append('svg')
.attr('width' , graph.width + graph.margin.left + graph.margin.right+500)
.attr('height', graph.height + graph.margin.top + graph.margin.bottom)
.append('g')
.attr('transform', 'translate(' + graph.margin.left + ',' + graph.margin.top + ')');
The Second link has something like this which implements zoom,
var svg = d3.select("svg"),
inner = svg.select("g");
// Set up zoom support
var zoom = d3.behavior.zoom().on("zoom", function() {
);
inner.attr("transform", "translate(" + d3.event.translate + ")" +
"scale(" + d3.event.scale + ")");
});
svg.call(zoom
Below is the code I inspected from the link you provided(http://nylen.tv/d3-process-map/graph.php) from a file called script.js, it is not minified :)
obj.positionConstraints.push({
weight : c.weight,
x : c.x * graph.width,
y : c.y * graph.height
});
They are manually calculating the x & y positions as shown above. Their tick function has the following code:
for (var name in graph.data) {
var obj = graph.data[name];
obj.positionConstraints.forEach(function(c) {
var w = c.weight * e.alpha;
if (!isNaN(c.x)) {
obj.x = (c.x * w + obj.x * (1 - w));
}
if (!isNaN(c.y)) {
obj.y = (c.y * w + obj.y * (1 - w));
}
});
}

How to accurately zoom d3 maps which have already been translated

I have a map which has been translated to make it fit on the canvas properly.
I'm trying to implement a way to zoom it and it does work, but it moves away from center when you zoom in, rather than centering on the mouse or even the canvas.
This is my code:
function map(data, total_views) {
var xy = d3.geo.mercator().scale(4350),
path = d3.geo.path().projection(xy),
transX = -320,
transY = 648,
init = true;
var quantize = d3.scale.quantize()
.domain([0, total_views*2/Object.keys(data).length])
.range(d3.range(15).map(function(i) { return "map-colour-" + i; }));
var map = d3.select("#map")
.append("svg:g")
.attr("id", "gb-regions")
.attr("transform","translate("+transX+","+transY+")")
.call(d3.behavior.zoom().on("zoom", redraw));
d3.json(url_prefix + "map/regions.json", function(json) {
d3.select("#regions")
.selectAll("path")
.data(json.features)
.enter().append("svg:path")
.attr("d", path)
.attr("class", function(d) { return quantize(data[d.properties.fips]); });
});
function redraw() {
var trans = d3.event.translate;
var scale = d3.event.scale;
if (init) {
trans[0] += transX;
trans[1] += transY;
init = false;
}
console.log(trans);
map.attr("transform", "translate(" + trans + ")" + " scale(" + scale + ")");
}
}
I've found that adding the initial translation to the new translation (trans) works for the first zoom, but for all subsequent zooms it makes it worse. Any ideas?
Here's a comprehensive starting-point: semantic zooming of force directed graph in d3
And this example helped me specifically (just rip out all the minimap stuff to make it simpler): http://codepen.io/billdwhite/pen/lCAdi?editors=001
var zoomHandler = function(newScale) {
if (!zoomEnabled) { return; }
if (d3.event) {
scale = d3.event.scale;
} else {
scale = newScale;
}
if (dragEnabled) {
var tbound = -height * scale,
bbound = height * scale,
lbound = -width * scale,
rbound = width * scale;
// limit translation to thresholds
translation = d3.event ? d3.event.translate : [0, 0];
translation = [
Math.max(Math.min(translation[0], rbound), lbound),
Math.max(Math.min(translation[1], bbound), tbound)
];
}
d3.select(".panCanvas, .panCanvas .bg")
.attr("transform", "translate(" + translation + ")" + " scale(" + scale + ")");
minimap.scale(scale).render();
}; // startoff zoomed in a bit to show pan/zoom rectangle
Though I had to tweak that function a fair bit to get it working for my case, but the idea is there. Here's part of mine. (E.range(min,max,value) just limits value to be within the min/max. The changes are mostly because I'm treating 0,0 as the center of the screen in this case.
// limit translation to thresholds
var offw = width/2*scale;
var offh = height/2*scale;
var sw = width*scale/2 - zoomPadding;
var sh = height*scale/2- zoomPadding;
translate = d3.event ? d3.event.translate : [0, 0];
translate = [
E.range(-sw,(width+sw), translate[0]+offw),
E.range(-sh,(height+sh), translate[1]+offh)
];
}
var ts = [translate[0], translate[1]];
var msvg = [scale, 0, 0, scale, ts[0], ts[1]];

Fire zoom event (or simulate zoom event)

When I zoom with the mouse, the following function attached to myZoom will be executed:
myZoom.on('zoom', function() {
someElement.attr('transform', 'translate(' + d3.event.translate[0] + ',' + d3.event.translate[1] + ') scale(' + d3.event.scale + ')');
....
// redraw axes, which should stay where they are at.
....
}
To simulate zoom without mouse or some other pointing device, I can just change the value of the attribute 'transform' above. Easy.
But problem is in this function I actually redraw axes, whose scale is automatically recalculated. Refer to this official documentation from d3:
zoom.y([y])
Specifies an y-scale whose domain should be automatically adjusted
when zooming. If not specified, returns the current y-scale, which
defaults to null. If the scale's domain is modified programmatically,
it should be reassigned to the zoom behaviour.
I need to zoom programmatically (maybe with zoom button). How can I fire zoom event, so that scale of my axes is automatically recalculated?
Programmatic zoom seems to be a daunting task in the D3 library because the D3 zooming is closely tied to the mouse events. A common instance of programmatic zooming is zooming in or out with a slider control. Surprisingly, I couldn't find a single working example of how to make D3 zooming work with a slider control. After investing some time and effort I developed this working demo which can be found here D3SliderZoom. The key point is to change the transform attribute of a "<g>" SVGElement embedded in an "<svg>" element using the scale value thrown by the slider.
function zoomWithSlider(scale) {
var svg = d3.select("body").select("svg");
var container = svg.select("g");
var h = svg.attr("height"), w = svg.attr("width");
// Note: works only on the <g> element and not on the <svg> element
// which is a common mistake
container.attr("transform",
"translate(" + w/2 + ", " + h/2 + ") " +
"scale(" + scale + ") " +
"translate(" + (-w/2) + ", " + (-h/2) + ")");
}
This method then has to be invoked from the change event of the slider as shown below.
$(function() {
$( "#slider-vertical" ).slider({
orientation: "vertical",
range: "min",
min: 1000,
max: 10000,
value: 1000,
slide: function( event, ui ) {
zoomWithSlider(ui.value/1000);
}
});
});
This solution is much more elegant than generating pseudo-mouse scroll event.
I ended up calculating new domain for a new zoom level by myself. With this new domain I could redraw two y-axes. For someone, who has same problem, I post my code. It's very specific to my project, so it might be hard to understand. Just for your interest.
wr.zoomSim = function(sNew) {
var s = wr.zoom.scale(),
tx = wr.zoom.translate()[0],
ty = wr.zoom.translate()[1],
sReal = sNew / s,
dtx = wr.width / 2 * (1 - sReal),
dty = wr.height / 2 * (1 - sReal),
txNew = sReal * tx + dtx,
tyNew = sReal * ty + dty,
a = wr.scaleYBZoom.domain()[0],
b = wr.scaleYBZoom.domain()[1],
c = wr.scaleYBZoom.range()[0],
d = wr.scaleYBZoom.range()[1],
r = (b-a)/(d-c);
wr.scaleYBZoom.domain([a + r * ( (c - dty) / sReal - c), a + r * ( (d - dty) / sReal - c)]);
wr.zoom.scale(sNew);
wr.zoom.translate([txNew, tyNew]);
wr.svg2.select('g#bar')
.attr('transform', 'translate(' + txNew + ',' + tyNew + ') scale(' + sNew + ')');
wr.svg2.select('g#axisl')
.call(d3.svg.axis().scale(wr.scaleYBZoom).orient('left'))
.selectAll('line.tick')
.attr('x2', wr.width - wr.bar.left - wr.bar.right + 2 * wr.padding);
wr.svg2.select('g#axisr')
.call(d3.svg.axis().scale(wr.scaleYBZoom).orient('right'))
.selectAll('line')
.remove();
};
Unfortunately #Debasis's answer didn't work for me, because I wanted to achieve this with a zoom behavior which I was already using with my force layout. After two days of desperation I finally found the solution in this thread:
https://groups.google.com/forum/#!msg/d3-js/qu4lX5mpvWY/MnnRMLz_cnUJ
function programmaticZoom ($svg, $zoomContainer, zoomBehavior, factor) {
var width = $svg.attr('width');
var height = $svg.attr('height');
var newScale = zoomBehavior.scale() * factor;
var newX = (zoomBehavior.translate()[0] - width / 2) * factor + width / 2;
var newY = (zoomBehavior.translate()[1] - height / 2) * factor + height / 2;
zoomBehavior
.scale(newScale)
.translate([newX,newY])
.event($zoomContainer);
}
I did this by making use of zoomListener. Worked well in simple steps for me:
Define zoomListener:
var zoomListener = d3.behavior.zoom();
Call the zoom listener
d3.select(the-element-that-you-need-zoom-on).call(zoomListener);
Decide your zoom step. I took steps of 0.1. (Use 1.1 for zoom-in and 0.9 for zoom-out)
Multiply with the current zoom scale
var newScale = zoomListener.scale() * step;
Set the new scale value
zoomListener.scale(newScale);

Categories