D3 Topojson Circle with Radius Scaled in Miles - javascript

(assuming existing projection/topojson)
What I'm trying to do is create a circle at a point ([long,lat]) of radius (r) in miles. I know there is a d3.geo function for this, but after some consideration I don't think it will be very compatible with my particular application.
So now I'm looking for using a native svg circle solution, where cx and cy are the lat and long, and r is the radius in miles. I know the cx and cy, but I don't know how to make sure the r is say 15 miles. So the main thing is how to make sure the radius is scaled in miles when drawn in pixel space. There must be someway to use the projection function to set the appropriate scale for the radius. But I haven't seen this in practice.
Also I should point out that my projection is dynamic, depending on user events the projection (including scale) can change. So I'm not sure if that will have bearing on how circles are scaled within the context of an existing projection, but I thought I would disclose that to be on the safe side.

Why not use the built-in circle generator d3.geoCircle()?
Returns a new GeoJSON geometry object of type “Polygon” approximating a circle on the surface of a sphere, with the current center, radius and precision. Any arguments are passed to the accessors.
The only task left to you is to calculate the radius of the circle in degrees. Because earth is not a perfect sphere this can become quite challenge of its own. But for many applications an approximation will suffice. Taking just the mean radius of 3,958 mi into account, the calculations can be written as:
var EARTH_RADIUS = 3959; // mean radius in miles
var radiusMi = 5; // radius to be drawn in miles
var radiusDeg = radiusMi / EARTH_RADIUS * 90; // radius in degrees for circle generator
This can then be passed to the circle generator:
var circle = d3.geoCircle().radius(radiusDeg);
Finally, the circle generator is used to pass its output via data binding to an appropriate path generator taking into account the projection:
svg.append("path")
.datum(circle)
.attr("d", path);
Have a look at this Block which features circles of 50 miles radius each at various positions around the globe. The circle generator in combination with the projection will take control of the correct sizing and the correct appearance of the circle.
D3 v3
If you are still stuck to D3 v3 the example works as well. However, you need to adjust the names accordingly:
d3.geo.circle ↦ d3.geoCircle
In addition to that, some of the circle generator's methods have been renamed:
circle.origin() ↦ circle.center()
circle.angle() ↦ circle.radius()
Applying those adjustments to my above linked Block, this works for v3 just as well: v3 Block.
This approach gets to play to its strengths when it comes to unusual projections having severe distortions. Just by changing the projection in the Block to d3.geoGnomonic() this becomes easily visible. The following screenshot from the updated Block still shows the same circles as above having a radius of 50 miles each:

Related

Can d3 voronoi map work well with any other projection besides geoAlbers?

I'm learning voronoi map from the example link
https://bl.ocks.org/mbostock/7608400
But it doesn't work when I wanna change the projection to mercator.Why?
This is the only code I change:
var projection = d3.geoMercator()
.translate([width / 2, height / 2])
.scale(1280);
Yes, a voronoi should work with any projection - it uses the projected points in svg coordinate space. If you see points, you can make a voronoi.
The issue is you need to modify the parameters of the projection. An Albers projection requires two secant lines or parallels where the ellipsoid of the earth intersects the cone of the projection (or alternatively, one tangent line or parallel). Rarely are these set to the equator, so D3 has default settings of an Albers projection to be suited to and centered on the US as it doesn't make much sense to leave all the defaults to zero.
Most other projections in D3 have their default settings resulting in a map centered at [0,0], which is the prime meridian and the equator, just off the coast of Africa. Consequently, that's where you are looking when you set the projection as you have - no visible points, no visible voronoi (other than perhaps the periphery.
If you were to set your scale to be much smaller, say 170, you would see the your map, but all the features would be in the top left corner:
Instead of just zooming out, we can use the default centering coordinates of the Albers to center the Mercator:
projection.center([-96,39])
If you are too zoomed in, lower the scale value, and increase it if you are zoomed out too far. Because Mercator's distortion of size gets pretty bad near the poles, you will likely need to scale out.
Here's what I get with the example's code and this projection, zooming out a bit, but depending on if you want Alaska or not, you might want to zoom in or out:
var projection = d3.geoMercator()
.translate([width / 2, height / 2])
.scale(600)
.center([-96,39])

Why does D3 not render TopoJSON correctly?

I'm trying to render a map of Switzerland with D3.js and TopoJSON. The underlying JSON can be found here. First I tried to follow this tutorial and after I couldn't render anything looking remotely like a map, I found this question on here with a link to a working example. From where I took this code I'm currently using:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<style>
path {
fill: #ccc;
}
</style>
</head>
<body>
<h1>topojson simplified Switzerland</h1>
<script>
var width = window.innerWidth,
height = window.innerHeight;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var projection = d3.geo.mercator()
.scale(500)
.translate([-900, 0]);
var path = d3.geo.path()
.projection(projection);
d3.json("ch-cantons.json", function(error, topology) {
if (error) throw error;
svg.selectAll("path")
.data(topojson.feature(topology, topology.objects.cantons).features).enter().append("path").attr("d", path);
});
</script>
</body>
</html>
Since the JSON looks ok (checked on some online platforms that renders it properly after copy/pasting) and there is only little code that could go wrong, I assume the error is in the projection parameters. Fiddled around a bit couldn't make it work. So any help would be very much appreciated!
You are right, the error is in the projection.
But, the error depends on if your data is projected or unprojected (lat long pairs).
Unprojected Data
If you have data that is in WGS84 - that is to say lat long pairs, then you have this problem:
Using your projection, but changing only the data source I get something like this (I shaved off the empty ocean on the right):
To center a Mercator properly, you need to know the center coordinate of your area of interest. This generally can be fairly general, for Switzerland I might try 47N 8.25E.
Once you have this coordinate you need to place it in the middle. One way is to rotate on the x axis and center on the y:
var projection = d3.geo.mercator()
.scale(3500)
.rotate([-8.25,0])
.center([0,47])
.translate([width/2,height/2])
Note that the x rotation is negative, you are spinning the globe underneath the projection.
The other option is to rotate on both x and y, this is likely the preferred option as you approach the poles - as Mercator distortion becomes unworkable at high latitudes. This approach would look like:
var projection = d3.geo.mercator()
.scale(4000)
.rotate([-8.25,-47])
.center([0,0])
.translate([width/2,height/2])
Note again the negative rotation values. The second option requires a higher scale value as this method essentially treats Switzerland as though it were at zero,zero on a Mercator projection - and along the equator land sizes are minimized.
Using the second of these projections, I get:
So you'll have to dial in the scale a bit, but now you should be able to see your data (assuming your data is in proper lat long pairs).
Projected Data
Based on the comment below, which includes a linked json file, we can see that this is your problem.
There are two potential solutions to this:
Convert the data to lat long pairs
Use a geoTransform
Option one is the easiest, you'll unproject the data - which requires knowing the current projection. In GIS software this will generally be projecting it as WGS84, which is arguably not really a projection but a datum. Once you have your lat long pairs, you follow the steps above for unprojected data.
Option two skips a d3.geoProjection altogether. Instead, we'll create a transform function that will convert the projected coordinates to the desired SVG coordinates.
A geo projection looks like:
function scale (scaleFactor) {
return d3.geo.transform({
point: function(x, y) {
this.stream.point(x * scaleFactor, (y * scaleFactor);
}
});
}
And is used like a projection:
var path = d3.geo.path().projection(scale(0.1));
It simply takes a stream of x,y coordinates that are already cartesian and transforms them in a specified manner.
To translate the map so it is centered you'll need to know the center coordinate. You can find this with path.bounds:
var bounds = path.bounds(features);
var centerX = (bounds[0][0] + bounds[1][0])/2;
var centerY = (bounds[0][1] + bounds[1][1])/2;
Path.bounds returns the top left corner and bottom right corner of a feature. This is the key part, you can make an autoscaling function, there are plenty of examples out there, but I like manually scaling often. If the map is centered, this is easy. For your map, your geoTransform might look like:
function scale (scaleFactor,cx,cy,width,height) {
return d3.geo.transform({
point: function(x, y) {
this.stream.point((x-cx) * scaleFactor + width/2, (y-cy) * scaleFactor +height/2);
}
});
}
Here cx and cy refer to the middle of your feature and the width and height refer to the width and height of the svg element - we don't want features clustered at SVG point [0,0].
Altogether, that gives us something like (with a scale factor of 0.002):
Here's an updated JSbin: http://jsbin.com/wolamuzeze/edit?html,output
Keep in mind that scale is dependent on window size as your width/height are relative to window size in your case. This might be best addressed with automatically setting the zoom level, though this can create problems if you have labels (for example).
This answer might help as well: Scaling d3 v4 map to fit SVG (or at all)

Lambert conic conformal projection in d3

I'm trying to project a set of points ([long, lat] tuples) on top of an SVG map of my home country Austria:
https://commons.wikimedia.org/wiki/File:Austria-geographic_map-blank.svg
The description of the SVG file on Wikimedia gives a projection name and the bounds of the map:
Lambert Conformal Conic, WGS84 datum
Geographic limits of the map:
West: 17.2° W
East: 9.3° W
North: 49.2° N
South: 46.0° N
Naive as I am, I thought this information would be enough to create the right kind of projection with D3.
This is what I tried first:
let gcc = d3.geoConicConformal()
.fitSize([width, height], bbox)
Where bbox is a GeoJSON polygon representing the boundaries of the map as given above.
Unfortunately the result is not the correct projection:
Now, from reading the D3 docs I can guess that I have to specify more parameters for the projection, e.g. two standard parallels. Unfortunately I have no idea what they are, and trying various values around the western and eastern boundaries of the map didn't work. I assume they can be derived from what I know about the map though, or maybe not?
Secondly, what confuses me is that the projection is not just wrongly rotated but incorrectly scaled as well -- I thought using .fitSize would take care of that.
Can anybody give me any pointers on correctly setting up a Lambert conic conformal projection?
Fitsize will translate and scale the map properly, however, with a conic projection you need to rotate, and as you noted, set the parallels.
Parallels:
There is a documented Austria Lambert Conic Conformal map projection, its specifications can be found here or here. The parallels that likely are correct are in this case are [46,49], though the map you are using could be a custom projection.
Rotation
Now you need to rotate the map, along the x axis by longitude. Conic maps are generally rotated along the x axis and centered on the y axis (see my answer here here for a graphical explanation of why (and how parallels change your projection)).
Rotation moves the world so that your area of interest is aligned properly, such that the central meridian of the map is vertical in your map. Based on the projection specifications noted above, the central meridian should be at 13 degrees, 20 minutes (13.333 degrees), though there is a small disagreement between the two references. Rotation along the x axis is set at the negative of this value.
Using these parameters:
d3.geoConicConformal()
.parallels([46,49])
.rotate([-13.333,0])
.fitSize([width,height],bbox)
I managed a pretty good fit with my very downsampled goto world topojson:
It is possible that the svg image uses parameters that differ slightly from the posted parameters (rounding, typos) or that the parallels are custom selected; however, this should be a fairly tight fit.

How to Draw line in 3D rectangle based on x,y and z?

I would like draw 3D points represented in image to 3D rectangle. Any idea how could I represent these in x,y and z axis
Here projection type is orthographic.
Thanks
Okay. Let's look at a simple example of what you are trying to accomplish it, and why this is such a complicated problem.
First, lets look a some projection functions. You need a way to mathematically describe how to transform a 3D (or higher dimensional) point into a 2D space (your monitor), or a projection.
The simpiest to understand is a very simple dimetric projection. Something like:
x' = x + z/2;
y' = y + z/4;
What does this mean? Well, x' is you x coordinate 2D projection: for every unit you move backwards in space, the projection will move that point half that many units to the right. And y' represents that same projection for your y coordinate: for every unit you move backwards in space, the projection will move that point a quarter unit up.
So a point at [0,0,0] will get projected to a 2d point of [0,0]. A point at [0,0,4] will get projected to a 2d point of [2,1].
Implemented in JavaScript, it would look something like this:
// Dimetric projection functions
var dimetricTx = function(x,y,z) { return x + z/2; };
var dimetricTy = function(x,y,z) { return y + z/4; };
Once you have these projection functions -- or ways to translate from 3D space into 2D space -- you can use them to start draw your image. A simple example of that using js canvas. First, some context stuff:
var c = document.getElementById("cnvs");
var ctx = c.getContext("2d");
Now, lets make a little helper to draw a 3D point:
var drawPoint = (function(ctx,tx,ty, size) {
return function(p) {
size = size || 3;
// Draw "point"
ctx.save();
ctx.fillStyle="#f00";
ctx.translate(tx.apply(undefined, p), ty.apply(undefined,p));
ctx.beginPath();
ctx.arc(0,0,size,0,Math.PI*2);
ctx.fill();
ctx.restore();
};
})(ctx,dimetricTx,dimetricTy);
This is pretty simple function, we are injecting the canvas context as ctx, as well as our tx and ty functions, which in this case our the dimetric functions we saw earlier.
And now a polygon drawer:
var drawPoly = (function(ctx,tx,ty) {
return function() {
var args = Array.prototype.slice.call(arguments, 0);
// Begin the path
ctx.beginPath();
// Move to the first point
var p = args.pop();
if(p) {
ctx.moveTo(tx.apply(undefined, p), ty.apply(undefined, p));
}
// Draw to the next point
while((p = args.pop()) !== undefined) {
ctx.lineTo(tx.apply(undefined, p), ty.apply(undefined, p));
}
ctx.closePath();
ctx.stroke();
};
})(ctx, dimetricTx, dimetricTy);
With those two functions, you could effectively draw the kind of graph you are looking for. For example:
// The array of points
var points = [
// [x,y,z]
[20,30,40],
[100,70,110],
[30,30,75]
];
(function(width, height, depth, points) {
var c = document.getElementById("cnvs");
var ctx = c.getContext("2d");
// Set some context
ctx.save();
ctx.scale(1,-1);
ctx.translate(0,-c.height);
ctx.save();
// Move our graph
ctx.translate(100,20);
// Draw the "container"
ctx.strokeStyle="#999";
drawPoly([0,0,depth],[0,height,depth],[width,height,depth],[width,0,depth]);
drawPoly([0,0,0],[0,0,depth],[0,height,depth],[0,height,0]);
drawPoly([width,0,0],[width,0,depth],[width,height,depth],[width,height,0]);
drawPoly([0,0,0],[0,height,0],[width,height,0],[width,0,0]);
ctx.stroke();
// Draw the points
for(var i=0;i<points.length;i++) {
drawPoint(points[i]);
}
})(150,100,150,points);
However, you should now be able to start to see some of the complexity of your actual question emerge. Namely, you asked about rotation, in this example we are using an extremely simple projection (our dimetric projection) which doesn't take much other than an oversimplified relationship between depth and its influences on x,y position. As the projections become more complex, you need to know more about your relationship/orientation in 3D space in order to create a reasonable 2D projection.
A working example of the above code can be found here. The example also includes isometric projection functions that can be swapped out for the dimetric ones to see how that changes the way the graph looks. It also does some different visualization stuff that I didn't include here, like drawing "shadows" to help "visualize" the actual orientation -- the limitations of 3D to 2D projections.
It's complicated, and even a superficial discussion is kind of beyond the scope of this stackoverflow. I recommend you read more into the mathematics behind 3D, there are plenty of resources, both online and in print form. Once you have a more solid understanding of the basics of how the math works then return here if you have a specific implementation question about it.
What you want to do is impossible to do using the method you've stated - this is because a box - when rotated in 3 dimensions won't look anything like that diagram of yours. It will also vary based on the type of projection you need. You can, however get started using three.js which is a 3D drawing library for Javascript.
Hope this helps.
How to Draw 3D Rectangle?
posted in: Parallelogram | updated on: 14 Sep, 2012
To sketch 3 - Dimensional Rectangle means we are dealing with the figures which are different from 2 – D figures, which would need 3 axes to represent them. So, how to draw 3D rectangle?
To start with, first make two lines, one vertical and another horizontal in the middle of the paper such that they represent a “t” letter of English. This is what we need to draw for temporary use and will be removed later after the construction of the 3 – D rectangle is complete. Next we draw a Square whose measure of each side is 1 inch. Square must be perfect in Geometry so that 90 degree angles that are formed at respective corners are exact in measure. Now starting from upper right corner of the square we draw a line segment that will be stretched to a measure of 2 inches in the direction at an angle of 45 degrees. Similarly, we repeat the procedure by drawing another Line Segment from the upper left corner of the square and stretching it to 2 inches length in the direction at an angle of 45 degrees. These 2 line segments are considered to be the diagonals with respect to the horizontal line that we drew temporarily in starting. Also these lines will be parallel to each other. Next we draw a line that joins the end Point of these two diagonals.
Next starting from the very right of the 2 inch diagonal end point, draw a line of measure 1 inch that is supposed to be perpendicular to the temporary horizontal line. Next we need to join the lower left corner of the square with end point of the last 1’’ line we drew in 4th step and finally we get our 3 - D rectangular. Now we can erase our initial “t”. This 3- D rectangle resembles a Cuboid.

Drawing Curves on Surfaces in WebGL

Well, the title pretty much states it. I want to be able to draw a curve on a surface in Web GL. So for example, I'd like to draw a parabola on the surface of a sphere.
x = cos(theta)sin(phi); y = sin(theta)sin(phi); z = cos(phi).
If you make theta = theta(t) and phi = phi(t), you can draw curves on the surface.
I guess lines on shapes is what I need. Anyone know if that's possible in Web GL?
A parabola is the set of point of the plane that have the same distance from a line and a point (called focus). The point here is what do you mean by "line" on a sphere. Remember that a parabola extends to infinity, bu you can't do that on a sphere, unless you define some particular metric on it.
Anyway, you gave use a parametrization of the sphere, in terms on theta and phi. That's good. If you want to define a curve on the surface, you should have a bind between theta and phi, for example
phi = theta ^ 2
would draw something that could be defined as a "parabola" in some way, i.e. the projection on the sphere, given by the parametrization, of its representation on a plane.
Are you looking for help with how to do this in WebGL? In this case, take a look at this example
http://dl.dropbox.com/u/17612367/OpenGL%20to%20WebGL/example%202.3.1%20-%20line%20graph/code/index.html
you would basically load the positions into a vector and draw it using drawArrays with LINELOOP or something... See this cheatsheet for arguments or google the drawArrays function for more info:
http://www.nihilogic.dk/labs/webgl_cheat_sheet/WebGL_Cheat_Sheet.pdf
Good Luck!

Categories