I'm interested in tweaking the radius of the circles on the circle pack layout. For that I need to know how the original radius is calculated.
By reading the d3.js source code for pack layout it seems the default radius function is simply Math.sqrt of value for each node. But that is not really the case because I modified the D3.js original circle pack example adding a .radius(function(d){return Math.sqrt(d);}) and as you can see at bl.ocks.org/ecerulm/f0a36710e3 the radius of the circles are not the same.
The d3.layout.pack() uses Math.sqrt as radius function. But pack.nodes will apply a scale transform d3_layout_packTransform(node, x, y, k) to make the whole circle pack chart to fit if radius wasn't explicitly set. That is why if you apply you own function (even if its radius(Math.sqrt)) you will need to apply your own scaling after if you want to get the same result as with implicit radius.
In the example below I explicitly set Math.sqrt as the radius function and then scale afterward to fit [diameter,diameter] with my own function pack_transform since d3_layout_packTranform is not accesible:
var pack = d3.layout.pack()
.value(function(d) { return d.size; })
.radius(Math.sqrt)
.size([diameter - 4, diameter - 4]);
var packnodes = pack.nodes(root);
var packroot = packnodes[0];
var w = diameter, h = diameter;
function pack_transform(node, k) {
function inner_transform(node,cx,cy,k) {
var children = node.children;
node.x = cx + k * (node.x-cx);
node.y = cy + k * ( node.y-cy);
node.r *= k;
if (children) {
var i = -1, n = children.length;
while (++i < n) inner_transform(children[i],cx,cy, k);
}
}
return inner_transform(node,node.x,node.y,k);
}
pack_transform(packroot, 1 / Math.max(2 * packroot.r / w, 2 * packroot.r / h));
Related
I use this as reference: https://bl.ocks.org/iamkevinv/0a24e9126cd2fa6b283c6f2d774b69a2
Adjusted some syntax to fit for version 5
Scale works, Translate looks like it works too because if I change the value, it zooms on different place..
But the problem is it doesn't zoom on the correct place I clicked.
I think this doesn't get to the place correctly because I use d3.geoMercator().fitSize([width, height], geoJSONFeatures) instead:
var bounds = path.bounds(d),
dx = bounds[1][0] - bounds[0][0],
dy = bounds[1][1] - bounds[0][1],
x = (bounds[0][0] + bounds[1][0]) / 2,
y = (bounds[0][1] + bounds[1][1]) / 2,
scale = Math.max(1, Math.min(8, 0.9 / Math.max(dx / width, dy / height))),
translate = [width / 2 - scale * x, height / 2 - scale * y];
Already tried to change the values to fit mine but failed, I can't get it.
Here is my projection:
var width = 500;
var height = 600;
d3.json("/regions50mtopo.json")
.then((geoJSON) => {
var geoJSONFeatures = topojson.feature(geoJSON, geoJSON.objects["Regions.50m"]);
// My Projection
var projection = d3.geoMercator().fitSize([width, height], geoJSONFeatures);
...
Any help, guide or reference?
Note: I'm mapping different country and fitSize(...) solves the
problem easily to fit on my svg that's why I can't use the same as in
the reference link I provided.
Found an answer: https://bl.ocks.org/veltman/77679636739ea2fc6f0be1b4473cf03a
centered = centered !== d && d;
var paths = svg.selectAll("path")
.classed("active", d => d === centered);
// Starting translate/scale
var t0 = projection.translate(),
s0 = projection.scale();
// Re-fit to destination
projection.fitSize([960, 500], centered || states);
// Create interpolators
var interpolateTranslate = d3.interpolate(t0, projection.translate()),
interpolateScale = d3.interpolate(s0, projection.scale());
var interpolator = function(t) {
projection.scale(interpolateScale(t))
.translate(interpolateTranslate(t));
paths.attr("d", path);
};
d3.transition()
.duration(750)
.tween("projection", function() {
return interpolator;
});
Exactly what I'm looking for. It works now as expected.
But maybe somebody also have suggestions on how to optimise it, because as the author said too, it feels slow and "laggy" when zooming in/out.
The title pretty much says it all. I'm looking for a convenient way to generate a geoJSON polygon defining an ellipse similar to d3-geo's d3.geoCircle()(); I want to use this GeoJSON ellipse with d3-geo. To clarify with and example, Cesium has this capability with a simple function allowing you to create an ellipse like so:
var ellipse = new Cesium.EllipseGeometry({
center : Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883),
semiMajorAxis : 500000.0,
semiMinorAxis : 300000.0,
rotation : Cesium.Math.toRadians(60.0)
});
If that function returned GeoJSON I'd be set. What's the best way to generate a GeoJSON polygon defining an ellipse?
D3 doesn't offer anything that can really help here. Vanilla javascript can achieve this fairly easily. First let's create a geojson ellipse in Cartesian coordinate space. After, we can use the haversine formula to draw the ellipse.
Create a geojson ellipse in Cartesian coordinate space.
This is pretty straightforward, the method I'm using is to calculate the radius of the ellipse at a given angle. Using these polar coordinates we can stitch together an ellipse. The formula for the radius of an ellipse at a given point can be found pretty easily, I used this source, which gives us:
So, we can easily iterate through a series of angles, calculate the radius at that angle, and then translate this polar coordinate into a Cartesian coordinate. Perhaps something like:
function createEllipse(a,b,x=0,y=0,rotation=0) {
rotation = rotation / 180 * Math.PI;
var n = n = Math.ceil(36 * (Math.max(a/b,b/a))); // n sampling angles, more for more elongated ellipses
var coords = [];
for (var i = 0; i <= n; i++) {
// get the current angle
var θ = Math.PI*2/n*i + rotation;
// get the radius at that angle
var r = a * b / Math.sqrt(a*a*Math.sin(θ)*Math.sin(θ) + b*b*Math.cos(θ)*Math.cos(θ));
// get the x,y coordinate that marks the ellipse at this angle
x1 = x + Math.cos(θ-rotation) * r;
y1 = y + Math.sin(θ-rotation) * r;
coords.push([x1,y1]);
}
// return a geojson object:
return { "type":"Polygon", "coordinates":[coords] };
}
Note: a/b: axes (in pixels), x/y: center (in pixels), rotation: rotation in degrees
Here's that in a quick snippet:
var geojson = createEllipse(250,50,200,200,45);
var svg = d3.select("body")
.append("svg")
.attr("width",600)
.attr("height",500);
var path = d3.geoPath();
svg.append("path")
.datum(geojson)
.attr("d",path);
function createEllipse(a,b,x=0,y=0,rotation=0) {
rotation = rotation / 180 * Math.PI;
var n = n = Math.ceil(36 * (Math.max(a/b,b/a))); // n sample angles
var coords = [];
for (var i = 0; i <= n; i++) {
// get the current angle
var θ = Math.PI*2/n*i + rotation;
// get the radius at that angle
var r = a * b / Math.sqrt(a*a*Math.sin(θ)*Math.sin(θ) + b*b*Math.cos(θ)*Math.cos(θ));
// get the x,y coordinate that marks the ellipse at this angle
x1 = x + Math.cos(θ-rotation) * r;
y1 = y + Math.sin(θ-rotation) * r;
coords.push([x1,y1]);
}
// return a geojson object:
return { "type":"Polygon", "coordinates":[coords] };
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
Apply the haversine formula.
One of the best resources on the haversine and related functions I know of is at Moveable Type Scripts. The formula I have came from there a few years back and has had a few cosmetic modifications. I'm not going to break down the formula here, as the linked reference should be useful.
So, rather than calculating the Cartesian coordinates, we can take the polar coordinate and use the angle as bearing and the radius as distance in the haversine formula, which should be relatively trivial.
This could look like:
function createEllipse(a,b,x=0,y=0,rotation=0) {
var k = Math.ceil(36 * (Math.max(a/b,b/a))); // sample angles
var coords = [];
for (var i = 0; i <= k; i++) {
// get the current angle
var angle = Math.PI*2 / k * i + rotation
// get the radius at that angle
var r = a * b / Math.sqrt(a*a*Math.sin(angle)*Math.sin(angle) + b*b*Math.cos(angle)*Math.cos(angle));
coords.push(getLatLong([x,y],angle,r));
}
return { "type":"Polygon", "coordinates":[coords] };
}
function getLatLong(center,angle,radius) {
var rEarth = 6371000; // meters
x0 = center[0] * Math.PI / 180; // convert to radians.
y0 = center[1] * Math.PI / 180;
var y1 = Math.asin( Math.sin(y0)*Math.cos(radius/rEarth) + Math.cos(y0)*Math.sin(radius/rEarth)*Math.cos(angle) );
var x1 = x0 + Math.atan2(Math.sin(angle)*Math.sin(radius/rEarth)*Math.cos(y0), Math.cos(radius/rEarth)-Math.sin(y0)*Math.sin(y1));
y1 = y1 * 180 / Math.PI;
x1 = x1 * 180 / Math.PI;
return [x1,y1];
}
// Create & Render the geojson:
var geojson = createEllipse(500000,1000000,50,70); // a,b in meters, x,y, rotation in degrees.
var geojson2 = createEllipse(500000,1000000)
var svg = d3.select("body")
.append("svg")
.attr("width",600)
.attr("height",400);
var g = svg.append("g");
var projection = d3.geoMercator().translate([300,200]).scale(600/Math.PI/2);
var path = d3.geoPath().projection(projection);
g.selectAll("path")
.data([geojson,geojson2])
.enter().append("path")
.attr("d", path);
g.selectAll("circle")
.data([[50,70],[0,0]])
.enter().append("circle")
.attr("cx", function(d) { return projection(d)[0] })
.attr("cy", function(d) { return projection(d)[1] })
.attr("r", 4)
.attr("fill","orange");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
Note: a/b axes in meters, x,y,rotation in degrees
That's a pretty boring demonstration, perhaps this simple demonstration is better:
The formula I'm using assumes a earth is a sphere, not an ellipsoid, this can lead to errors in distance of up to 0.3%. However, depending on map scale, this will often be less than the stroke width.
I might have to try and make a particularly visually challenging version of a tissot's indicatrix with this
Snippets use default parameter values that are not compatible with IE, example block offers IE support
I'm using a d3 attrTween to translate a circle over a path smoothly, similar to this example and as shown in the picture below:
The circle's transition is defined here:
function transition() {
circle.transition()
.duration(2051)
.ease("linear")
.attrTween("transform", translateAlong(path.node()))
}
And the attribute tween is shown here:
function translateAlong(path) {
var l = path.getTotalLength();
return function (d, i, a) {
return function (t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";
};
};
}
This works well thanks to the SVG method getPointAtLength, which allows us to retrieve coordinates at different lengths of the path. However, I need a different kind of behavior and I've been unable to come up with a solution so far.
I need the circle to animate along the path, but at a steady horizontal speed. Meaning that the circle ought to take as much time to navigate this slice:
As it does with this slice:
Because both slices encompass the same width. On a low level, what I need is to be able to translate any X coordinate with its corresponding Y coordinate along the path. I've looked at all the SVG path methods and I haven't found anything particularly useful here. I'm hoping there's some way in D3 to feed an X coordinate to a d3 line and retrieve its corresponding Y coordinate.
Here's a JSFiddle working as described above. I'd really appreciate any help I can get on this. Thanks!
I ended up creating a lookup array for all my points along the line using getPointAtLength:
var lookup = [];
var granularity = 1000;
var l = path.node().getTotalLength();
for(var i = 1; i <= granularity; i++) {
var p = path.node().getPointAtLength(l * (i/granularity))
lookup.push({
x: p.x,
y: p.y
})
}
Once I had all those points in my lookup table, I used a bisector in my translate tween:
var xBisect = d3.bisector(function(d) { return d.x; }).left;
function translateAlong(path) {
var l = path.getTotalLength();
return function (d, i, a) {
return function (t) {
var index = xBisect(lookup, l * t);
var p = lookup[index];
return "translate(" + p.x + "," + p.y + ")";
};
};
}
And it works as expected! Yahoo!
Fiddle
I'm working on a tag visualization where tags transition between different force-directed layouts.
I had few issues figuring out how to transition from a bubble chart to a node chart, but I'm a bit stuck as to how to get the charts to transition into a word cloud. My difficulties largely stem from my inexperience at writing custom clustering/collision detection functions.
I declare the forces as globals and then stop and start them when the user clicks a button:
var force1 = d3.layout.force()
.size([width, height])
.charge(0)
.gravity(0.02)
.on("tick", ticka);
//layout for node chart
var force2 = d3.layout.force()
.size([width, height])
.charge(-50)
.gravity(0.005)
.linkDistance(120)
.on("tick", tickb);
//layout for bubble chart
var force3 = d3.layout.force()
.size([width, height])
.charge(0)
.gravity(0.02)
.on("tick", tickc);
Relevant node/link functions are added to the force when the function that draws the nodes is called (as data changes according to a slider value).
The code for creating node data is as follows:
nodes = splicedCounts.map(function(d, e) {
var choice;
var i = 0,
r = d[1],
d = { count: d[1],
sentiment: d[2]/d[1],
cluster: i,
radius: radScale(r),
name: d[0],
index: e,
x: Math.cos(i / m * 2 * Math.PI) * 200 + width / 2 + Math.random(),
y: Math.sin(i / m * 2 * Math.PI) * 200 + height / 2 + Math.random()
};
if (!clusters[i] || (r > clusters[i].radius))
clusters[i] = d;
return d;
});
In order to keep this question relatively brief, the code I use for drawing the bubble chart is derivative of this example: http://bl.ocks.org/mbostock/7881887 and the code for drawing the node chart are similarly generic (I am happy to provide this code if it would help to solve my issue).
This is where my issue comes in:
I found this nice example for collision detection between rectangles and incorporated it into my code. However, since I'm using SVG text and the font size changes on transition, I opted to estimate the text size/bounding box size based on text-length and radius.
The entire "tick" functions for the word chart are below.
function tickc(e) {
node = nodeGroup.selectAll(".node");
var nodeText = nodeGroup.selectAll(".node text");
node.each(cluster(5 * e.alpha * e.alpha));
var k = e.alpha;
nodeText.each(function(a, i) {
var compWidth = d3.select(this).attr("bWidth");
var compHeight = d3.select(this).attr("bHeight");
nodes.slice(i + 1).forEach(function(b) {
// console.log(a);
var lineWidthA = a["name"].length * a["radius"]/2.5;
var lineHeightA = a["radius"]/0.9;
var lineWidthB = b["name"].length * b["radius"]/2.5;
var lineHeightB = b["radius"]/0.9;
dx = (a.x - b.x)
dy = (a.y - b.y)
adx = Math.abs(dx)
ady = Math.abs(dy)
mdx = (1 + 0.07) * (lineWidthA + lineWidthB)/2
mdy = (1 + 0.07) * (lineHeightA + lineHeightB)/2
if (adx < mdx && ady < mdy) {
l = Math.sqrt(dx * dx + dy * dy)
lx = (adx - mdx) / l * k
ly = (ady - mdy) / l * k
// choose the direction with less overlap
if (lx > ly && ly > 0)
lx = 0;
else if (ly > lx && lx > 0)
ly = 0;
dx *= lx
dy *= ly
a.x -= dx
a.y -= dy
b.x += dx
b.y += dy
}
});
});
node.select("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
node.select("text")
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; });
}
// Move d to be adjacent to the cluster node.
function cluster2(alpha) {
return function(d) {
var cluster = clusters[d.cluster];
if (cluster === d) return;
var x = d.x - cluster.x,
y = d.y - cluster.y,
l = Math.sqrt(x * x + y * y),
r = (d["name"].length * d["radius"]) + (cluster["name"].length * cluster["radius"]);
};
}
I was unsure of how to conclude the clustering function so as to move the nodes appropriately. I tried to adapt the standard cluster function, i.e.
// Move d to be adjacent to the cluster node.
function cluster(alpha) {
return function(d) {
var cluster = clusters[d.cluster];
if (cluster === d) return;
var x = d.x - cluster.x,
y = d.y - cluster.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + cluster.radius;
if (l != r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
cluster.x += x;
cluster.y += y;
}
};
}
to be more similar to the aforementioned rectangular cluster force layout but without luck (I'm afraid I no longer have copies of my exact attempts).
I'm afraid I can't attach images due to my lack of reputation but I can try to find a way to provide them if it would help. The overlap problem with the word cloud is minor (most words resolve into adjacent but not touching positions) but, if possible, I'd like it to resolve as perfectly as the bubble chart. I'm pretty sure that these issues arose from a.) the unfinished cluster function and b.) my hack at using text length and radius to estimate text size rather than proper bounding box coords, but I'm not sure exactly how to fix these things.
I'd recommend using the d3-cloud package which should do a lot of what you need. If not, then at least it's a good starting point https://github.com/jasondavies/d3-cloud
The way it seems to work is by calculating a bounds for each word and then resolving collisions between those bounds. You can see that here
I am rendering a raster map using d3.geo.tile, like in this example here (the raster images don't work on that page, so I cloned it over here). Unlike this example I have thousands of points to show, so I am rendering them using HTML5 Canvas instead of SVG. I positioned the canvas directly over the map.
The points are rendering correctly, and I am able to pan the map. However, if I zoom in the points are not translated to the proper coordinates.
Here is the function that draws these points:
function set_scales() {
var translate = zoom.sub_regions.translate(),
scale = zoom.sub_regions.scale(),
width = sub_region.get('width'),
height = sub_region.get('height');
var x1 = -translate[0]+width/2;
var y1 = -translate[1]+height/2;
var x2 = width + x1;
var y2 = height + y1;
var x = d3.scale.linear().domain([x1, x2]).range([0, width]);
var y = d3.scale.linear().domain([y1, y2]).range([0, height ]);
draw_canvas();
function draw_canvas() {
sub_region.var.context.clearRect(0, 0, width, height);
var data = sub_region.get('points');
if (!data) {
return;
}
var i = -1, n = data.length, d, cx, cy;
var canvas = sub_region.get('context');
canvas.fillStyle = '#0A00FF';
canvas.beginPath();
while (++i < n) {
d = data[i];
cx = x(d[0]);
cy = y(d[1]);
canvas.moveTo(cx, cy);
canvas.arc(cx, cy, 1, 0, 2 * Math.PI);
}
canvas.fill()
}
}
I know that I should be using the scale variable in there somehow, but I can't figure out how. The scale at which the points are positioned properly is 4096. I tried making a variable called zoom_factor setting it to 4096/scale, and multiplying the x1, x2, y1, and y2 coordinates by it, but that didn't work. Perhaps I didn't do it correctly.
I took some screenshots:
This is before the zoom (points render correctly):
This is after the zoom (points translated to incorrect coordinates):
If you want to see it in all its broken glory go here and click on the United States, then select "Stations."
I could really use some help on this one; been running in circles for days!