Drawing state borders with d3 not working as expected - d3.js

Here's an example of my problem. Eventually this is going to have county/zipcode info drawn on it.
I create a projection and a geo path:
var projection = d3.geo.albersUsa()
.scale(width * 1.3)
.translate([width / 2, height / 2]);
var path = d3.geo.path().projection(projection);
However when I try to add state borders... this happens:
#state-borders {
fill: none;
stroke: #333;
stroke-width: 1.5px;
stroke-linejoin: round;
stroke-linecap: round;
pointer-events: none;
}
svg.append('path')
.datum(topojson.mesh(states, states.objects.states, function(a, b) { return a !== b; }))
.attr('id', 'state-borders')
.attr('d', path);
I've looked at other examples and they all seem to be doing something similar, I can't figure out what I am doing wrong. Its almost like the stroke-livejoin isnt working at all.

Your geographic data is already projected; it does not need to be projected again. A projection takes unprojected points on a 3d globe and projects them to a 2d space, doing this on data that is already 2d won't work.
Your data doesn't comprise of 3d points (with a coordinate space measured in degrees), but rather has 2d points (with a coordinate space measured in pixels). So we can just use an identity transform as there is no need to modify/project/transform the coordinates. In d3v4+ we could use:
var path = d3.geoPath(); // default projection is an identity
But with d3v3, the default projection of a geo path is AlbersUsa. So we need to explicitly state we don't want a projection:
var path = d3.geo.path().projection(null);
This gives us a working fiddle.
That this file is pre-pojected is not immediately self evident. Generally a map that looks like a ball of yarn is a good indicator that you are projecting already projected data.
Now we may have new problems, working with pre-projected geometry can make sizing difficult, more so in d3v3 - this file is intended for a 960x600 viewport. To automatically scale and center a map based on its features see this quesion, several of the answers are meant for v3 of d3 (a,b). But this gets much easier with the fitSize method that you can use in d3v4+, as described in this answer. And for more of a discussion of working pre-projected geometry there is this question. If you intend on overlaying features on top of the preprojected geometry you are probably best to find an unprojected US topo/geojson to simplify things, as discussed here.

Related

get point coordination using path.getPointAtLength after rotation in d3 and svg

i want get some points from path after rotating it
i use below code to rotate path
let path= svg.append("path")
.attr("class","offset control")
.attr("d", lineFunction(offsetLineData))
.style("stroke-width", 0.5)
.style("stroke", "red")
.style("fill", "none")
.attr("transform","rotate(90,"+p.x+","+p.y+")")
.attr('transform-origin',"center")
then i want get end point and start point of path
path.node().getPointAtLength(0)
but it return coordination where it don't rotated
how can i get x and y of point after rotation
Think of a SVG as a tree of nested coordinate systems. Every element has its own coordinates, and is fitted into its parent with a rule how to re-calculate them. These rules can be explicit transform attributes, but also implicit combinations of width, height and viewBox ("fit box A in size B").
A transform attribute is considered to be the link to the parent element. That means, if you ask for a geometric property of an element, most of the times you get the value before the transform is applied. After it is applied would be asking for geometric values in the coordinate system of the parent.
Because of this complexity there are several SVG API functions to find out how these coordinate systems fit together. getPointAtLength() gets you coordinates before applying the transform attribute.
var localPoint = path.node().getPointAtLength(0)
First, you have to find out what the transform attribute does. This looks a bit complicated, partly because the attribute can be animated and can contain a list of functions, partly because the API is...well...:
// construct a neutral matrix
var localMatrix = path.node().viewportElement.createSVGMatrix()
var localTransformList = path.node().transform.baseVal
// is there at least one entry?
if (localTransformList.length) {
// consolidate multiple entries into one
localMatrix = localTransformList.consolidate().matrix
}
You can then apply the found transformation to the point with
var transformedPoint = localPoint.matrixTransform(localMatrix)
There are several functions hat will return a SVGMatrix to transform data from the coordinate system after application of the transform attribute (i. e. after the step above):
to ask for the transformation to the nearest viewport (in most cases the nearest parent <svg>) element: element.getCTM()
to ask for the transformation to screen pixels: element.getScreenCTM()
to ask for the transformation to an arbitrary element: element.getTransformToElement(...)

How can I add space between features in d3 geo projections

I have a simple d3 geojson projection here. I am using the following for my scaling...
var projection = d3.geo.albersUsa()
.translate([$scope.width / 2, $scope.height / 2]);
Is there a way to add a margin between the state "features" in this projection. So say I want 10px between the borders of every state.

How does D3's projection function work for paths and points?

My understanding of D3's projection functions are that they do the same thing as the scale functions. They map GPS coordinates to pixel coordinates. This is the projection I'm currently using. (I don't understand all the variables in detail but I fiddled around with them until the map showed up)
var projection = d3.geo.albers()
.center([-122.436269,37.798107])
.rotate([0, 0, 0])
.parallels([36, 38])
.scale(300000);
This draws the map fine:
.attr("d", d3.geo.path().projection(projection))
When I try to plot points though the numbers are crazy.
.attr("cx",function(d) {
return projection([d._lon,d._lat])[0];
})
.attr("cy",function(d) {
return projection([d._lon,d._lat])[1];
});
How do I properly do this?
Here are some examples of the points I'm getting through the projection function:
[5175.3799972560955, 1808.5108650794136]
[5158.315547249338, 1823.564395627589]
[5143.958532762888, 1831.9879789081751]
On a 1280x800 screen these are way off. Even if I scale them by dividing by 100, they'll still mostly just stack on top of each other. I have a 700*700 svg positioned using twitter bootstrap. Not sure how those are taken into account. I just assumed that if the map if fine, then the same projection should work for the points.

transformation issues when implementing fisheye distortion in a radial tree

Basically, I am attempting to apply the d3 fisheye distortion algorithm to a radial tree. I believe the issues I am encountering revolve around the fact that the coords being fed to the fisheye distortion are the coords computed by the d3.layout.tree. But the actual coords have been adjusted by the g transform. So, the coords resulting from the fisheye distortion need to be adjusted back to the g transform.
For example:
// re-setting the projection according to fisheye coords
diagonal.projection(function(d) { d.fisheye = fisheye(d); return [d.fisheye.y, d.fisheye.x / 180 * Math.PI]; })
I have been attempting this...here is the fiddle.
I am somewhat close...help is appreciated.
Following the direction I'd suggested in the comments, this is the result:
https://jsfiddle.net/xdk5ehcr/
Instead of using rotations and translations to position the nodes, I created two trigonometry-based functions to calculate horizontal and vertical position from the data (x,y) values, which are treated as polar coordinates.
Then I had to set the fisheye function to use my positioning functions as "accessor" functions instead of reading d.x and d.y directly. Unfortunately, the basic plug-in you were using for the fisheye didn't include a way to get and set x/y accessor functions, so I had to modify that too. I was surprised it wasn't already in the code; it's a standard functionality on most d3 layout objects.
(When I get github set up, I will have to make a pull request to add it in. I'll need to figure out how the fisheye scale/zoom function works, though -- I took that out of this example since you weren't using it.)
The positioning functions were as follows:
function getHPosition(d){
//calculate the transformed (Cartesian) position (H, V)
//(without fisheye effect)
//from the polar coordinates (x,y) where
//x is the angle
//y is the distance from (radius,radius)
//See http://www.engineeringtoolbox.com/converting-cartesian-polar-coordinates-d_1347.html
return (d.y)*Math.cos(d.x);
}
function getVPosition(d){
return (d.y)*Math.sin(d.x);
};
The functions are used to set the original position of the nodes and links, and then once the fisheye kicks in it uses these functions internally, returning the results (with distortion if appropriate) as d.fisheye.x and d.fisheye.y.
For example, for links that means the projection setting the d3.svg.diagonal function like this for initialization:
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [getHPosition(d), getVPosition(d)];
});
But like this for update:
diagonal.projection(function(d) {
d.fisheye = fisheye(d);
return [d.fisheye.x, d.fisheye.y];
});
There are a couple other little changes:
I simplified the dimensions of the plotting area a bit.
I added a background rectangle with pointer-events:all; so that the fisheye doesn't turn on and off as the mouse moves between nodes and empty background.
I didn't bother rotating the text (since the node groups are no longer rotating, it doesn't happen by default), but you could easily add in a rotate transformation on the individual text elements.
Finally, and this one stumped me for longer than I'd like to admit, the angles have to be in radians for the Javascript trig functions. Couldn't figure out why my layouts were so ugly, with overlapping lines. I thought it was something to do with switching between d3.svg.diagonal() and d3.svg.diagonal.radial() and spent a lot of time trying to do inverse-trig and all sorts of things...

d3js scale, transform and translate

I've created nycMap, a project that uses angularJS (MVC), yeoman (build), d3 (mapping) and geoJSON (geo data).
Everything works very nicely, but I did have to spend quite some time getting the right scale and translation. I was wondering how I can automatically figure out at what scale the map will show its best and what x and y values go into the translation?
'use strict';
japanAndCo2App.controller('MainCtrl', function($scope) {
function makeJapanAll(){
var path, vis, xy;
xy = d3.geo.mercator().scale(16000).translate([-5600,2200]);
path = d3.geo.path().projection(xy);
vis = d3.select("#japanAll").append("svg:svg").attr("width", 1024).attr("height", 700);
d3.json("data/JPN_geo4.json", function(json) {
return vis.append("svg:g")
.attr("class", "tracts")
.selectAll("path")
.data(json.features).enter()
.append("svg:path")
.attr("d", path)
.attr("fill",function(d,i){ return d.properties.color || "transparent"});
});
}
makeJapanAll();
});
(If you are interested in the code, it's all on github. The code for the map is in scripts/controllers/main.js which is the same as shown above.)
I've had the same problems. But it is very easy to do when you have a bounding box, which can be determined from the GeoJSON (like meetamit said), or while creating the GeoJson. And the width of the wanted SVG.
I'll start with the variables lattop, lonleft, lonright, width and height for the bounding box of the geojson and the dimensions of the image. I haven't yet occupied myself with calculating a good height from the difference in latutude. So the height is just estimated to be big enough to fit the image. The rest should be clear from the code:
var xym = d3.geo.mercator();
// Coordinates of Flanders
var lattop = 51.6;
var lonleft = 2.4;
var lonright = 7.7;
var width = 1500;
var height =1000;
// make the scale so that the difference of longitude is
// exactly the width of the image
var scale = 360*width/(lonright-lonleft);
xym.scale(scale);
// translate the origin of the map to [0,0] as a start,
// not to the now meaningless default of [480,250]
xym.translate([0,0]);
// check where your top left coordinate is projected
var trans = xym([lonleft,lattop]);
// translate your map in the negative direction of that result
xym.translate([-1*trans[0],-1*trans[1]]);
var path = d3.geo.path().projection(xym);
var svg = d3.select("body").append("svg").attr("width",width).attr("height",height);
Note, if you go over the date line (180 degrees), you will have to take the overflow into account.
Given this:
xy = d3.geo.mercator().scale(someScale).translate([0, 0]);
someScale is the pixel width of the entire world when projected using the mercator projection. So, if your json data had outlines for the whole world – spanning from lat/lng -180,90 to latLng 180,-90 – and if someScale was 1024, then the world would be drawn such that it exactly fits within a 1024x1024-pixel square. That's what you see on in this Google Maps view (well... sort of... not quite... read on...).
That's not enough though. When the world is drawn at 1024px, without any translation, lat/lng 0,0 (i.e. the "middle" of the world) will sit at the 0,0 pixel of the projected map (i.e. the top left). Under these conditions, the whole northern hemisphere and western hemisphere have negative x or y values, and therefore fall outside the drawn region. Also, under these conditions, the bottom right of the world (i.e. lat/lng -90, 180) would sit at the exact middle of the 1024x1024 square (i.e. at pixel 512,512).
So, in order to center the world in the square described here, you need to translate the map by half its width in the X and Y directions. I.e. you need
xy = d3.geo.mercator().scale(1024).translate([512, 512]);
That'll give you exactly the Google Map view I linked to.
If your json data only has part of the world (like, nyc or NY state) drawing it with this xy projection will render the outlines in the correct geographic position relative to the entire 1024x1024 world-spanning region. So it would appear rather small, with lots of whitespace.
The challenge is how to scale and translate the projection such that the area in question fills up the 1024x1024 square. And... so far I haven't answered this question, but I hope that this explanation points you in the right direction towards figuring out this math. I'll also try to continue the answer later, when I have more time. :/
There's an example here that gets the bounds of countries from geojson and then scales and translates the map to that country. The code is a bit ugly; there're however efforts to make this easier in the future (see this and this issue).

Resources