d3.js spreading labels for pie charts - javascript

I'm using d3.js - I have a pie chart here. The problem though is when the slices are small - the labels overlap. What is the best way of spreading out the labels.
http://jsfiddle.net/BxLHd/16/
Here is the code for the labels. I am curious - is it possible to mock a 3d pie chart with d3?
//draw labels
valueLabels = label_group.selectAll("text.value").data(filteredData)
valueLabels.enter().append("svg:text")
.attr("class", "value")
.attr("transform", function(d) {
return "translate(" + Math.cos(((d.startAngle+d.endAngle - Math.PI)/2)) * (that.r + that.textOffset) + "," + Math.sin((d.startAngle+d.endAngle - Math.PI)/2) * (that.r + that.textOffset) + ")";
})
.attr("dy", function(d){
if ((d.startAngle+d.endAngle)/2 > Math.PI/2 && (d.startAngle+d.endAngle)/2 < Math.PI*1.5 ) {
return 5;
} else {
return -7;
}
})
.attr("text-anchor", function(d){
if ( (d.startAngle+d.endAngle)/2 < Math.PI ){
return "beginning";
} else {
return "end";
}
}).text(function(d){
//if value is greater than threshold show percentage
if(d.value > threshold){
var percentage = (d.value/that.totalOctets)*100;
return percentage.toFixed(2)+"%";
}
});
valueLabels.transition().duration(this.tweenDuration).attrTween("transform", this.textTween);
valueLabels.exit().remove();

As #The Old County discovered, the previous answer I posted fails in firefox because it relies on the SVG method .getIntersectionList() to find conflicts, and that method hasn't been implemented yet in Firefox.
That just means we have to keep track of label positions and test for conflicts ourselves. With d3, the most efficient way to check for layout conflicts involves using a quadtree data structure to store positions, that way you don't have to check every label for overlap, just those in a similar area of the visualization.
The second part of the code from the previous answer gets replaced with:
/* check whether the default position
overlaps any other labels*/
var conflicts = [];
labelLayout.visit(function(node, x1, y1, x2, y2){
//recurse down the tree, adding any overlapping labels
//to the conflicts array
//node is the node in the quadtree,
//node.point is the value that we added to the tree
//x1,y1,x2,y2 are the bounds of the rectangle that
//this node covers
if ( (x1 > d.r + maxLabelWidth/2)
//left edge of node is to the right of right edge of label
||(x2 < d.l - maxLabelWidth/2)
//right edge of node is to the left of left edge of label
||(y1 > d.b + maxLabelHeight/2)
//top (minY) edge of node is greater than the bottom of label
||(y2 < d.t - maxLabelHeight/2 ) )
//bottom (maxY) edge of node is less than the top of label
return true; //don't bother visiting children or checking this node
var p = node.point;
var v = false, h = false;
if ( p ) { //p is defined, i.e., there is a value stored in this node
h = ( ((p.l > d.l) && (p.l <= d.r))
|| ((p.r > d.l) && (p.r <= d.r))
|| ((p.l < d.l)&&(p.r >=d.r) ) ); //horizontal conflict
v = ( ((p.t > d.t) && (p.t <= d.b))
|| ((p.b > d.t) && (p.b <= d.b))
|| ((p.t < d.t)&&(p.b >=d.b) ) ); //vertical conflict
if (h&&v)
conflicts.push(p); //add to conflict list
}
});
if (conflicts.length) {
console.log(d, " conflicts with ", conflicts);
var rightEdge = d3.max(conflicts, function(d2) {
return d2.r;
});
d.l = rightEdge;
d.x = d.l + bbox.width / 2 + 5;
d.r = d.l + bbox.width + 10;
}
else console.log("no conflicts for ", d);
/* add this label to the quadtree, so it will show up as a conflict
for future labels. */
labelLayout.add( d );
var maxLabelWidth = Math.max(maxLabelWidth, bbox.width+10);
var maxLabelHeight = Math.max(maxLabelHeight, bbox.height+10);
Note that I've changed the parameter names for the edges of the label to l/r/b/t (left/right/bottom/top) to keep everything logical in my mind.
Live fiddle here: http://jsfiddle.net/Qh9X5/1249/
An added benefit of doing it this way is that you can check for conflicts based on the final position of the labels, before actually setting the position. Which means that you can use transitions for moving the labels into position after figuring out the positions for all the labels.

Should be possible to do. How exactly you want to do it will depend on what you want to do with spacing out the labels. There is not, however, a built in way of doing this.
The main problem with the labels is that, in your example, they rely on the same data for positioning that you are using for the slices of your pie chart. If you want them to space out more like excel does (i.e. give them room), you'll have to get creative. The information you have is their starting position, their height, and their width.
A really fun (my definition of fun) way to go about solving this would be to create a stochastic solver for an optimal arrangement of labels. You could do this with an energy-based method. Define an energy function where energy increases based on two criteria: distance from start point and overlap with nearby labels. You can do simple gradient descent based on that energy criteria to find a locally optimal solution with regards to your total energy, which would result in your labels being as close as possible to their original points without a significant amount of overlap, and without pushing more points away from their original points.
How much overlap is tolerable would depend on the energy function you specify, which should be tunable to give a good looking distribution of points. Similarly, how much you're willing to budge on point closeness would depend on the shape of your energy increase function for distance from the original point. (A linear energy increase will result in closer points, but greater outliers. A quadratic or a cubic will have greater average distance, but smaller outliers.)
There might also be an analytical way of solving for the minima, but that would be harder. You could probably develop a heuristic for positioning things, which is probably what excel does, but that would be less fun.

One way to check for conflicts is to use the <svg> element's getIntersectionList() method. That method requires you to pass in an SVGRect object (which is different from a <rect> element!), such as the object returned by a graphical element's .getBBox() method.
With those two methods, you can figure out where a label is within the screen and if it overlaps anything. However, one complication is that the rectangle coordinates passed to getIntersectionList are interpretted within the root SVG's coordinates, while the coordinates returned by getBBox are in the local coordinate system. So you also need the method getCTM() (get cumulative transformation matrix) to convert between the two.
I started with the example from Lars Khottof that #TheOldCounty had posted in a comment, as it already included lines between the arc segments and the labels. I did a little re-organization to put the labels, lines and arc segments in separate <g> elements. That avoids strange overlaps (arcs drawn on top of pointer lines) on update, and it also makes it easy to define which elements we're worried about overlapping -- other labels only, not the pointer lines or arcs -- by passing the parent <g> element as the second parameter to getIntersectionList.
The labels are positioned one at a time using an each function, and they have to be actually positioned (i.e., the attribute set to its final value, no transitions) at the time the position is calculated, so that they are in place when getIntersectionList is called for the next label's default position.
The decision of where to move a label if it overlaps a previous label is a complex one, as #ckersch's answer outlines. I keep it simple and just move it to the right of all the overlapped elements. This could cause a problem at the top of the pie, where labels from the last segments could be moved so that they overlap labels from the first segments, but that's unlikely if the pie chart is sorted by segment size.
Here's the key code:
labels.text(function (d) {
// Set the text *first*, so we can query the size
// of the label with .getBBox()
return d.value;
})
.each(function (d, i) {
// Move all calculations into the each function.
// Position values are stored in the data object
// so can be accessed later when drawing the line
/* calculate the position of the center marker */
var a = (d.startAngle + d.endAngle) / 2 ;
//trig functions adjusted to use the angle relative
//to the "12 o'clock" vector:
d.cx = Math.sin(a) * (that.radius - 75);
d.cy = -Math.cos(a) * (that.radius - 75);
/* calculate the default position for the label,
so that the middle of the label is centered in the arc*/
var bbox = this.getBBox();
//bbox.width and bbox.height will
//describe the size of the label text
var labelRadius = that.radius - 20;
d.x = Math.sin(a) * (labelRadius);
d.sx = d.x - bbox.width / 2 - 2;
d.ox = d.x + bbox.width / 2 + 2;
d.y = -Math.cos(a) * (that.radius - 20);
d.sy = d.oy = d.y + 5;
/* check whether the default position
overlaps any other labels*/
//adjust the bbox according to the default position
//AND the transform in effect
var matrix = this.getCTM();
bbox.x = d.x + matrix.e;
bbox.y = d.y + matrix.f;
var conflicts = this.ownerSVGElement
.getIntersectionList(bbox, this.parentNode);
/* clear conflicts */
if (conflicts.length) {
console.log("Conflict for ", d.data, conflicts);
var maxX = d3.max(conflicts, function(node) {
var bb = node.getBBox();
return bb.x + bb.width;
})
d.x = maxX + 13;
d.sx = d.x - bbox.width / 2 - 2;
d.ox = d.x + bbox.width / 2 + 2;
}
/* position this label, so it will show up as a conflict
for future labels. (Unfortunately, you can't use transitions.) */
d3.select(this)
.attr("x", function (d) {
return d.x;
})
.attr("y", function (d) {
return d.y;
});
});
And here's the working fiddle: http://jsfiddle.net/Qh9X5/1237/

Related

Accessibility: d3 brush/zoom can get focus and be controlled with keyboard

Any hints how to control d3 brush/zoom with keyboard:
1. Ability to focus on brush control
2. Ability to change brush area using keyboard
Is it supported out of the box?
Update: Apparently there is no out of the box solution (hope d3 will provide it at some point). It means that a custom solution will depend on visualization/scenario. Posting actual UX and requirements and will provide a solution for this particular case.
In order to meet accessibility requirements the task was to modify below chart control to be able to zoom/brush using keyboard. This includes: 1) being able to set focus; 2) being able to control using left and right arrow keys.
I'm going to use this bl.ock as a reference. I believe it is the source of your image.
Zoom and Brush Function Comparison
We are interested in a couple things in this block, the code for zooming and the code for brushing:
function brushed() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
var s = d3.event.selection || x2.range();
x.domain(s.map(x2.invert, x2));
focus.select(".area").attr("d", area);
focus.select(".axis--x").call(xAxis);
svg.select(".zoom").call(zoom.transform, d3.zoomIdentity
.scale(width / (s[1] - s[0]))
.translate(-s[0], 0));
}
function zoomed() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
var t = d3.event.transform;
x.domain(t.rescaleX(x2).domain());
focus.select(".area").attr("d", area);
focus.select(".axis--x").call(xAxis);
context.select(".brush").call(brush.move, x.range().map(t.invertX, t));
Both functions:
check to see if the main body of the function should be executed
set a new x scale domain
update the area and axis
The differences are important:
The brush function updates the scale using d3.zoomIdentity, it must do this as it needs to update the zoom function to reflect the current zoom scale and transform.
The zoom function manually sets the brush, it must do this because the brush needs to be updated.
Zoom In and Brush "In" Without Events
To control this by the keyboard, it is probably easier to use the brushed() function as a template. This is because the current zoom transform can be tricky to retrieve whereas it is relatively easy to spoof a change in the brush.
In the brushed function the value in d3.event.selection is an array containing the range of values contained by the brush (values in the range, not the domain). It is an array of the minimum and maximum range values in the reference/context scale, x2, covered by the brush. This is the only thing we need to update both zoom and brush.
To zoom in, we can take the focus x scale's domain and find the domain's minimum and maximum values. Then we can re-set the focus x scale's domain to be slightly smaller, zooming in effectively. The code below converts the domain to a range, and reduces that range before converting it back into a domain - this is unneeded but follows the brushed() function more closely and means not having to deal with dates.
var xMin = x2(x.domain()[0]);
var xMax = x2(x.domain()[1]);
var currentDifference = Math.abs(xMin-xMax);
xMin += currentDifference / 2 / 3 // increase the minimum value of the domain
xMax -= currentDifference / 2 / 3 // decrease the maximum value of the domain
x.domain([xMin,xMax].map(x2.invert, x2));
We can also set the zoom scale like so:
var identity = d3.zoomIdentity
.scale(width/ (xMax - xMin))
We also want to modify the zoom's transform so we are zooming in to the center of the previous larger domain. The following is just a reproduction of the code used in the example block, but with clearer names for the sake of illustration:
var identity = d3.zoomIdentity
.scale(width/ (xMax - xMin))
.translate(-xMin, 0);
If we use the brushed function as a template, we might end up with:
var xMin = x2(x.domain()[0]); // minimum value in x range currently
var xMax = x2(x.domain()[1]); // maximum value in x range currently
var currentDifference = Math.abs(xMax-xMin); // center point of range
xMin += currentDifference / 2 / 3 // reduce the distance between center point and end points
xMax -= currentDifference / 2 / 3
x.domain([xMin,xMax].map(x2.invert, x2)); // convert the range to a domain
focus.select(".area").attr("d", area); // redraw the chart
focus.select(".axis--x").call(xAxis); // redraw the axis
var identity = d3.zoomIdentity
.scale(width/ (xMax - xMin))
.translate(-xMin, 0); // update the zoom factor
context.select(".brush").call(brush.move, x.range().map(identity.invertX, identity)); // update the brush
svg.select(".zoom").call(zoom.transform, identity); // apply the zoom factor
This will zoom the focus area in to an area centered in the current domain's center. The domain will shrink by one third with the code above, but that can be changed to match your needs.
The only real differences compared to the original brushed function are that we are:
manually computing the brush extent
updating the brush with the method used in the zoomed function.
That is it.
Other Operations
You can zoom out by expanding rather than shrinking the domain, just switch the sign when defining the new end points:
xMin -= currentDifference / 2 / 3
xMax += currentDifference / 2 / 3
Moving left would look like:
xMin -= currentDifference / 2 / 3
xMax -= currentDifference / 2 / 3
And moving right naturally would be the opposite.
Adding the Keyboard
Now all you have to do is set up a listener to listen for key strikes:
d3.select("body")
.on("keypress", function() {
if (d3.event.key == "a") {
// one of zoom in/out/pan
}
else if (d3.event.key == "b" {
//...
}
});
Putting it All Together
I've assembled a block that shows it all together, I've used asdw for key inputs:
a: pan left
d: pan right
w: zoom in
s: zoom out.
One last note: I've included a check to make sure that the new domain is in bounds: we don't want to zoom beyond the domain of our data.
Here's the example.
Since SVG 2.0 is not here yet and in SVG 1.0 focusable elements are not supported I ended up using <a xlink:href="#"> trick to get a focus on left/right ticks. Also decided that getting a focus for the whole brush element is not necessarily because proper range can be achieved by moving left/right ticks.
private createResizeTick(resizeClass: string, id: string, brushTickClass: string, tickIndex: number, bottom: number) {
let self = this;
// +++++++++++++++++++ NEW CODE +++++++++++++++++++
let aElement = this._xBrushElement.selectAll(resizeClass)
.append('a')
.attr('id', id)
.attr('xlink:href', '#')
.on('keydown', () => {
if ((<KeyboardEvent>d3.event).keyCode !== 37 && (<KeyboardEvent>d3.event).keyCode !== 39) {
return;
}
// A function which adjusts brush's domain (specific to data model)
self.brushKeyMove((<KeyboardEvent>d3.event).keyCode, tickIndex);
})
.on('keyup', () => {
if ((<KeyboardEvent>d3.event).keyCode !== 37 && (<KeyboardEvent>d3.event).keyCode !== 39) {
return;
}
self.brushOnEnd(); // A function which already processes native onBrushEnd event
document.getElementById(id).focus();
});
// --------------- END OF NEW CODE ---------------
aElement.append('text')
.attr('class', 'brushtick ' + brushTickClass)
.attr('transform', 'translate(0,' + bottom + ')')
.attr('x', 0)
.attr('y', 6)
.attr('dy', '0.35em');
}
Here is the result:

How to rotate d3.js nodes around a foci?

I've been using force layout as a sort of physic's engine for board game i'm making, and it's been working pretty well. However, I've been trying to figure out if it is possible to rotate nodes around a specific foci. Consider this codepen. I would like to make the 3 green nodes in the codepen rotate around the foci in a uniform fashion. In the tick() function I do the following:
var k = .1 * e.alpha;
// Push nodes toward their designated focus.
nodes.forEach(function(o, i) {
o.y += (foci[o.id].y - o.y) * k;
o.x += (foci[o.id].x - o.x) * k;
});
In the same way that I push nodes toward a foci, I'd like to make all nodes designated to a foci rotate around said foci. Is there any way to accomplish this by manipulating the o.y and o.x variables within the tick() function? I've tried to manually set the x and y values using this formula however I think possibly the charge and gravity of the force layout are messing it up. Any ideas?
I know i'm using force layout for something it's not quite intended to do, but any help would be appreciated.
I have messed around with your code to get a basic movement around a point.
I changed the foci var to an object which is just two points :
foci = {
x: 300,
y: 100
};
Ive added to the data you have to give each node a start point :
nodes.push({
id: 0,
x:20,
y:30
});
nodes.push({
id: 0,
x:40,
y:60
});
nodes.push({
id: 0,
x:80,
y:10
});
I have added an angle to each node so you can use these independently later:
.attr("cx", function(d) {
d.angle = 0; //added
return d.x;
})
And changed the tick so each node moves around the focal point. As said before I added an angle as these points will move around different circles with different sized radius as they will be different distances from the foci point. If you use one angle then all the nodes will move ontop of each other which is pointless :
Formula for point on a circle :
//c = centre point, r = radius, a = angle
x = cx + r * cos(a)
y = cy + r * sin(a)
Use this in tick :
var radius = 100; //made up radius
node
.attr("cx", function(d) {
if(d.angle>(2*Math.PI)){ restart at full circle
d.angle=0;
}
d.x = foci.x + radius *Math.cos(d.angle) //move x
return d.x;
})
.attr("cy", function(d) {
d.y = foci.y + radius *Math.sin(d.angle) //move y
return d.y;
});
Updated fiddle : https://jsfiddle.net/reko91/yg0rs4xc/7/
This should be simple to implement to change from circle movement to elliptical :))
Looking at this again, this only moves around half way. This is due to the tick function only lasting a couple of seconds. If you click one of the nodes, it will continue around the circle. If you want this to happen continuously, you'll have to set up a timer function so it runs around the circle non stop, but that should be easily implemented.
Instead of tick function just make another function with the timer inside, call it on load and it will run continuously :)

How to calculate (SVG) X/Y coordinates out of translation and rotation (in JS/D3.js)?

The following function draws small circles in the middle of some arcs. I want to draw lines from these circles to other elements.
Thats why I need to get the cx/cy values of the circles (after they have been rotated).
var drawSmallCircles = function(arcs){
var d=arcs;
var arcRadius=d[0].outer;
var svg = d3.select("svg").append("g")
.attr("transform", "translate(" + width / 4 + "," + height / 2 + ")");
var smallCircles = svg.selectAll("circle").data(d).enter().append("circle")
.attr("fill","black")
.attr("cx",0)
.attr("cy",-arcRadius)
.attr("r",4)
.attr("transform", function(d) {
return "rotate(" + (((d.startAngle+d.endAngle)/2) * (180/Math.PI)) + ")";
});
}
Best would be if someone could show me a function which gets the Arc-Radius and an Angle and returns (cx/cy). I would pre-calculate and store (cx/cy) in the "arcs-objects" and draw the circles and the lines out of those values.
The "translate" transformation is not really my problem.
Thank you!
A friend of mine helped me with the following calculations:
new_x= ( width/4
+Math.cos((d.startAngle+d.endAngle)/2 ) * (0 )
-Math.sin((d.startAngle+d.endAngle)/2 ) * (-arcRadius) );
new_y= ( height/2
+Math.sin((d.startAngle+d.endAngle)/2 ) * (0 )
+Math.cos((d.startAngle+d.endAngle)/2 ) * (-arcRadius) );
In this case, your first transform is applied to the g element, which contains a circle element that has a cy attribute and a rotation transform.
The first transform sets the origin of your group, the second rotates the circle around the origin at a distance of cy. Since your cy attribute is negative, the y-position relative to the origin will be given by -r*Math.cos(theta), while the x-position will be given by r*Math.sin(theta), where theta is the rotation angle in radians and r is the radial distance from the transformed origin to the center of the circle (arcRadius in your code).

D3 Tree Layout - Custom Vertical Layout when children exceed more than a certain number

I'm trying to use the D3 Tree Layout to create a family tree of sorts and one of the things I noticed is that when I have many children nodes, it would stretch out horizontally across the screen. Ideally I would like a more vertical layout for these nodes so that people don't have to scroll across the screen and can just keep looking down the tree.
Here is what I currently see:
Now it might not be that bad, but if I had say 20 children, it would span across the whole screen and that is something I kind of want to avoid.
I have seen questions like this but this doesn't help me because I want a specific layout and not simply a resize... I have large nodes and they begin to collide with one another if I try to dynamically resize the tree -- shrinking the tree does not do me any good. I specifically need a different layout for situations where there are more than a certain number of children.
Here is kind of what I was envisioning/hoping for. Notice the root does not make this format because it has only 4 children. Ideally I want it so that if a parent has 5 or more children, it would result in the layout below. If the root had 5 children, it would result in this layout and the layout should simply stretch out vertically if users wanted to see the root's grandchildren (the A, B, C... nodes). If necessary I can get a diagram of that going:
I found a semi-similar question regarding custom children layouts and he said he had to play around with the actual d3js code. I kind of want to avoid this so I am hoping to find out if this is possible with d3js as it is right now and, if so, how to go about it? I don't need a complete answer, but a snippet of code proving that this is possible would be very helpful.
If necessary I can upload a JSFiddle for people to play around with.
Check out this fiddle:
http://jsfiddle.net/dyXzu/
I took the sample code from http://bl.ocks.org/mbostock/4339083 and made some modifications. Note that in the example, x and y are switched when drawing so the layout appears as a vertical tree.
The important thing I did was modifying the depth calculator:
Original:
// Normalize for fixed-depth.
nodes.forEach(function(d) { d.y = d.depth * 180; });
Fixed:
// Normalize for fixed-depth.
nodes.forEach(function (d) {
d.y = d.depth * 180;
if (d.parent != null) {
d.x = d.parent.x - (d.parent.children.length-1)*30/2
+ (d.parent.children.indexOf(d))*30;
}
// if the node has too many children, go in and fix their positions to two columns.
if (d.children != null && d.children.length > 4) {
d.children.forEach(function (d, i) {
d.y = (d.depth * 180 + i % 2 * 100);
d.x = d.parent.x - (d.parent.children.length-1)*30/4
+ (d.parent.children.indexOf(d))*30/2 - i % 2 * 15;
});
}
});
Basically, I manually calculate the position of each node, overriding d3's default node positioning. Note that now there's no auto-scaling for x. You could probably figure this out manually by first going through and counting open nodes (d.children is not null if they exist, d._children stores the nodes when they are closed), and then adding up the total x.
Nodes with children in the two-column layout look a little funky, but changing the line-drawing method should improve things.
You could dynamically count the children and adjust the width/height of the DOM element accordingly. I would recommend counting the maximum number of children in any level, which can be calculated using the depth property provided by d3.tree.nodes(), or just by recursively going down the tree. Modifying some code which was in a previous SO answer, you could have something like this:
var levelWidth = [1];
var childCount = function (level, n) {
if (n.children && n.children.length > 0) {
if (levelWidth.length <= level + 1) levelWidth.push(0);
levelWidth[level + 1] += n.children.length;
n.children.forEach(function (d) {
childCount(level + 1, d);
});
}
};
childCount(0, root);
var newHeight = d3.max(levelWidth) * 40;
tree = tree.size([Math.max(newHeight, 660), w]);
document.getElementById("#divID").setAttribute("height", Math.max(newHeight, 660) + 50);
Or using the nodes() method.
var nodes = tree.nodes(root), arr = [];
for(i = 0; i < nodes.length; i++)
arr[nodes[i].depth]++;
var max = 0;
for(i = 0; i <nodes.length; i++)
if(arr[i] > max)
max = arr[i];
$('#elementOfChoice').css("height", max*40);
var newHeight = max * 40;
tree = tree.size([Math.max(newHeight, 660), w]);

Scaling geographic shapes to a similar size in D3

I'm using D3's world-countries.json file to create a mercator map of world countries, which I'll then bind to some data for a non-contiguous cartogram. Alas, the much larger sizes of Canada, the U.S., Australia, etc. mean that one unit for those countries is the spatial equivalent of several units for, say, Malta.
What I think I need to do is normalize the geojson shapes, such that Canada and Malta are the same size when starting out.
Any idea how I'd do that?
Thanks!
Update: I've tried explicitly setting the width and height of all the paths to a small integer, but that seems to just get overridden by the transform later. Code follows:
// Our projection.
var xy = d3.geo.mercator(),
path = d3.geo.path().projection(xy);
var states = d3.select("body")
.append("svg")
.append("g")
.attr("id", "states");
function by_number() {
function compute_by_number(collection, countries) {
//update
var shapes = states
.selectAll("path")
.data(collection.features, function(d){ return d.properties.name; });
//enter
shapes.enter().append("path")
.attr("d", path)
.attr("width", 5) //Trying to set width here; seems to have no effect.
.attr("height", 5) //Trying to set height here; seems to have no effect.
.attr("transform", function(d) { //This works.
var centroid = path.centroid(d),
x = centroid[0],
y = centroid[1];
return "translate(" + x + "," + y + ")"
+ "scale(" + Math.sqrt(countries[d.properties.name] || 0) + ")"
+ "translate(" + -x + "," + -y + ")";
})
.append("title")
.text(function(d) { return d.properties.name; });
//exit
}
d3.text("../data/country_totals.csv", function(csvtext){
var data = d3.csv.parse(csvtext);
var countries = [];
for (var i = 0; i < data.length; i++) {
var countryName = data[i].country.charAt(0).toUpperCase() + data[i].country.slice(1).toLowerCase();
countries[countryName] = data[i].total;
}
if (typeof window.country_json === "undefined") {
d3.json("../data/world-countries.json", function(collection) {
window.country_json = collection;
compute_by_number(collection, countries);
});
} else {
collection = window.country_json;
compute_by_number(collection, countries);
}
});
} //end by_number
by_number();
You might be able to use the helper function I posted here: https://gist.github.com/1756257
This scales a projection to fit a given GeoJSON object into a given bounding box. One advantage of scaling the projection, rather than using a transform to scale the whole path, is that strokes can be consistent across maps.
Another, simpler option might be to:
Project the paths;
Use path.getBBox() to get the bounding box for each (.getBBox() is a native SVG function, not a D3 method)
Set a transform on the path, similar to how you do it now, to scale and translate the path to fit your bounding box.
This is a bit simpler, as it doesn't involve projections, but you'll need to scale the stroke by the inverse (1/scale) to keep them consistent (and therefore you won't be able to set stroke values with CSS). It also requires actually rendering the path first, then scaling it - this might affect performance for complex geometries.

Categories