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.
Related
My problem is simple to explain but I am having real trouble implementing a solution. I am trying to animate a circle along a path on a D3 map. The twist here is that I would like to use one of Mike Bostock's spinny globes (i.e. 3D map).
In time, I would like to add other paths to the globe and to use these for my animations. For now, I would simply like to animate the circles along the border of Russia (i.e. along the path of the Russia polygon coordinates)
I have built a jsfiddle to get traction on this and you can see all my code. Unfortunately I cannot get it to work, and am hoping you can help me out. My jsfiddle: http://jsfiddle.net/Guill84/xqmevpjg/7/
I think my key difficulty is (a) actually referencing the Russia path, and I think I am not getting it right at the moment, and (b) making sure that the interpolation is calculated properly (i.e. that the animation is dynamically linked to the globe, and not just 'layered on top'). The code that is supposed to do that is as follows:
setTimeout(function(){
var path = d3.select("path#Russia"),
startPoint = pathStartPoint(path);
marker.attr("r", 7)
.attr("transform", "translate(" + startPoint + ")");
transition();
//Get path start point for placing marker
function pathStartPoint(path) {
var d = path.attr("d"),
dsplitted = d.split(" ");
return dsplitted[1].split(",");
}
function transition() {
marker.transition()
.duration(7500)
.attrTween("transform", translateAlong(path.node()))
.each("end", transition);// infinite loop
}
function translateAlong(path) {
var l = path.getTotalLength();
return function(i) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";//Move marker
}
}
}
I'd be hugely grateful for any help.
For the first part of your question, one way to select the path is to add an id to id :
d3.json("http://mbostock.github.io/d3/talk/20111018/world-countries.json", function(collection) {
feature = svg.selectAll("path")
.data(collection.features)
.enter().append("svg:path")
.attr("d", clip)
.attr("id", function(d) { return d.properties.name; }) ;
and then select the path like that :
var path = d3.select("#Russia").node()
Then you can select the first point with :
path.getPointAtLength(0)
See this updated fiddle : http://jsfiddle.net/xqmevpjg/11/
I have a zoom event handler on my tree graph like so:
d3.select("#"+canvasId+" svg")
.call(d3.behavior.zoom()
.scaleExtent([0.05, 5])
.on("zoom", zoom));
Which calls the zoom function which handles the translation bounding logic:
function zoom() {
console.log(d3.event.translate[0]);
var wcanvas = $("#"+canvasId+" svg").width();
var hcanvas = $("#"+canvasId+" svg").height();
var displayedWidth = w*scale;
var scale = d3.event.scale;
var h = d3.select("#"+canvasId+" svg g").node().getBBox().height*scale;
var w = d3.select("#"+canvasId+" svg g").node().getBBox().width*scale;
var padding = 100;
var translation = d3.event.translate;
var tbound = -(h-hcanvas)-padding;
var bbound = padding;
var lbound = -(w-wcanvas)-padding;
var rbound = padding;
// limit translation to thresholds
translation = [
Math.max(Math.min(translation[0], rbound), lbound),
Math.max(Math.min(translation[1], bbound), tbound)
];
console.log("Width: "+w*scale+" || Height: "+h*scale+" /// "+"Left: "+translation[0]+" || Top: "+translation[1]);
d3.select("#"+canvasId+" svg g")
.attr("transform", "translate(" + translation + ")" +" scale(" + scale + ")");
console.log(d3.select("#"+canvasId+" svg g")[0]);
}
However, translations beyond the bounds cause the d3.event.translate values to increase. The result is that even if the translation is not causing the graph to move as it has reached its limit for translation, the value for the translation within successive events can continue to increase.
The result is that say I drag the graph far to the left, even though it will stop moving past a certain point, because the value within the events continues to increase, I would then have to drag it a long way back right before it actually begins to move right again.
Is there a good way to prevent this behaviour?
Okay I worked it out. The trick is to set the translation for the d3.behaviour.zoom so that successive zoom pans start at the bounded translation rather than with the additional panning that didn't actually give any movement.
To do this, we declare the zoom behaviour as a separate variable and add it to our zoomable element:
var zoomBehaviour = d3.behavior.zoom()
.scaleExtent([0.05, 5])
.on("zoom", zoom)
d3.select("#"+canvasId+" svg")
.call(zoomBehaviour);
Then we set the translation of this zoomBehaviour to our bounded translation in the zoom function:
function zoom() {
...
translation = [
Math.max(Math.min(translation[0], rbound), lbound),
Math.max(Math.min(translation[1], bbound), tbound)
];
zoomBehaviour.translate(translation);
d3.select("#"+canvasId+" svg g")
.attr("transform", "translate(" + translation + ")" +" scale(" + scale + ")");
}
I want to calculate text with maximum pixel size beforehand and then allocate the maximum width size to all other texts. getBBox gives me below error:
TypeError: Object [ TEXT ] has no method 'getBBox'
and getComputedTextLength() also does not work.
Both these methods give pixel size after it is written to page. I am creating chart on server side i.e. nodejs and I want to calculate pixel size before writing it to page. Is there any method for this?
Edit:
Adding code
chart.selectAll("svg:text")
.data(d3Data)
.enter().append("svg:text")
.attr("class", "d3LegendText")
.attr("transform", function(d, i) {
textLength = d.seriesName.length*6;
if (prev_legend_x + textLength + legend_padding_x > width) {
legend_y = legend_y + 15;
legend_x = 7;
} else {
legend_x = prev_legend_x;
}
prev_legend_x = legend_x + textLength + legend_padding_x;
return "translate(" + legend_x + "," + legend_y + ")"; })
.text(function(d, i) {
console.log(this.getBBox().width);
return d.seriesName;
})
You can ingnore variables in transform. I have written it to calculate pixel size depending on number of characters in text. But I want exact width of text.
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/
I'm trying to give a polygon - drawn with d3 - smooth edges using the d3.svg.line().interpolate() option but I get strange looking results.
I receive the polygon data from the nokia HERE api as world coordinate data in the form [lat1, long1, alt1, lat2, long2, alt2 ...] So in the routingCallback function - which is called when the response is in - I first refine it so it looks like this [[lat1, long1], [lat2, long2] ...]. In d3.svg.line() I then use this array of coordinates to calculate the pixel positions. Im using Leaflet to draw the polygon on a map so I use the map.latLngToLayerPoint() function to do that. The actual drawing of the polygon happens in reset() which is called from the routingCallback immediately after the data is available and every time the map gets zoomed
var map = new L.Map("map", {"center": [52.515, 13.38], zoom: 12})
.addLayer(new L.TileLayer('http://{s}.tile.cloudmade.com/---account key---/120322/256/{z}/{x}/{y}.png'));
map.on("viewreset", reset);
var svg = d3.select(map.getPanes().overlayPane).append("svg"),
g = svg.append("g").attr("class", "leaflet-zoom-hide group-element"),
bounds = [[],[]],
polygon,
refinedData,
line = d3.svg.line()
.x(function(d) {
var location = L.latLng(d[0], d[1]),
point = map.latLngToLayerPoint(location);
return point.x;
})
.y(function(d) {
var location = L.latLng(d[0], d[1]),
point = map.latLngToLayerPoint(location);
return point.y;
})
.interpolate("cardinal"),
routingCallback = function(observedRouter, key, value) {
if(value == "finished") {
var rawData = observedRouter.calculateIsolineResponse.isolines[0].asArray(),
refinedData = [];
for(var i = 2; i < rawData.length; i += 3) {
var lon = rawData[i-1],
lat = rawData[i-2];
refinedData.push([lat, lon]);
}
if(polygon)
polygon.remove();
polygon = g
.data([refinedData])
.append("path")
.style("stroke", "#000")
.style("fill", "none")
.attr("class", "isoline");
reset();
}
if(value == "failed") {
console.log(observedRouter.getErrorCause());
}
};
getIsolineData = function(isoline) {
return data;
};
function reset() {
var xExtent = d3.extent(refinedData, function(d) {
var location = L.latLng(d[0], d[1]);
var point = map.latLngToLayerPoint(location);
return point.x;
});
var yExtent = d3.extent(refinedData, function(d) {
var location = L.latLng(d[0], d[1]);
var point = map.latLngToLayerPoint(location);
return point.y;
});
bounds[0][0] = xExtent[0];
bounds[0][1] = yExtent[0];
bounds[1][0] = xExtent[1];
bounds[1][1] = yExtent[1];
var topLeft = bounds[0],
bottomRight = bounds[1];
svg .attr("width", bottomRight[0] - topLeft[0])
.attr("height", bottomRight[1] - topLeft[1])
.style("left", topLeft[0] + "px")
.style("top", topLeft[1] + "px");
g .attr("transform", "translate(" + -topLeft[0] + "," + -topLeft[1] + ")");
polygon.attr("d", line);
}
I expect this to produce smooth edges but instead I get a small loop at every corner. The red overlay is the same polygon without interpolation. There are only points at the corners. No points added inbetween.
Does it have something to do with the order of the points (clockwise/counter clockwise)? I tried to rearrange the points but nothing seemed to happen.
The only way I can recreate the pattern you're getting is if I add every vertex to the path twice. That wouldn't be noticeable with a linear interpolation, but causes the loops when the program tries to connect points smoothly.
http://fiddle.jshell.net/weuLs/
Edit:
Taking a closer look at your code, it looks like the problem is in your calculateIsolineResponse function; I don't see that name in the Leaflet API so I assume it's custom code. You'll need to debug that to figure out why you're duplicating points.
If you can't change that code, the simple solution would be to run your points array through a filter which removes the duplicated points:
refinedData = refinedData.filter(function(d,i,a){
return ( (!i) || (d[0] != a[i-1][0]) || (d[1] != a[i-1][1]) );
});
That filter will return true if either it's the first point in the array, or if either the lat or lon value is different from the previous point. Duplicated points will return false and be filtered out of the array.