d3.geo.path rectangle wrapping the wrong way - d3.js

This simple geojson rectangle is displayed correctly with some geojson viewers, I get a rectangle as expected. But when I do it with d3, the rectangle seems to wrap around.
var polygonData = {
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[ -100, -20 ],
[ -100, 20 ],
[ 100, 20 ],
[ 100, -20 ],
[ -100, -20 ]
]
]
},
"properties": {}
};
var width = 1000;
var height = 500;
var projection = d3.geo.equirectangular()
.scale(100)
.translate([width / 2, height / 2])
.rotate([0, 0])
.center([0, 0])
.precision(0);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("body").append("svg")
.attr({
width: width,
height: height
});
svg.append('path')
.datum(polygonData)
.attr({
d: path,
fill: 'orange',
opacity: 0.5
});
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<style>
</style>
</head>
<body>
</body>
Here is what I get with a geojson viewer:
But here's what I get with the above d3 code:
Reversing the winding order just fills the opposite shapes, it doesn't fix the problem. I guess its an antimeridian cutting issue. One fix is to add some intermediate points to force the path to not wrap around, but I would need to be able to automate this solution with more complex paths.
Any idea how I can use this geojson with d3 and force it to show it like other geojson viewers, as a simple rectangle across the map?

I don't think, there is anything to blame on D3; in my understanding it's those other GeoJSON viewers which are in error. As a human being living on a more or less planar surface one is easily tricked into believing that a polygon with four corners having carefully chosen coordinates like yours should look like a rectangle. On a sufficiently small scale and given a suitable projection this even holds true for spherical geometry. But as your points are almost half the globe apart, this won't be the case.
To shed some light on this, I used an orthographic projection to display some geographic features along with your polygon:
From this view it becomes apparent that the line along the meridian is the first edge connecting points [-100,-20] and [-100,20]. From that point [-100,20] somewhere in Mexico to the northwest is the great arc, i.e. the shortest connection, to the next point [100,20] half way around the globe. The path is then similarly closed around the southern hemisphere. Thus, the outline of the polygon is the shortest path on the globe's surface connecting all of its points in the given order.
Although your polygon is determined by its coordinates, its look will depend on the projection in use. Here is another view of the same polygon using a mercator projection:

Related

D3.js v7: interpolate translate/scale with new position?

I have a graph of several nodes and edges. I used a layout algorithm (elkjs) to calculate the position of the nodes and edges. The graph is fairly large, with 268 nodes and 276 edges, such that:
svg width: 800, height: 600; graph width: 1844, height: 3007
As such, I had to do the requisite math to calculate pan offsets and scaling so it would fully fit, centered in the viewport:
translate(225.228, 15) scale(0.1896)
I programmatically transition it into place over 2500ms - it works fine and fits nicely.
But then I wish to switch to a subgraph, picking a node to see only it and its descendants. This particular example subgraph is near the bottom of the graph, so the starting position of its y-values are relatively large. After I call the layout algorithm again to get the new positions, the repositioned graph is smaller:
svg width: 800, height: 600; graph width: 874, height: 459
I then do the join/enter/update/exit thing to update the positions over a duration (and remove the other elements not in the subgraph), and I also do the math again for pan offsets and scaling:
translate(20, 100.4348) scale(0.8696)
Here's the problem: while it does end up in the right place, it pans off-screen before panning back on-screen:
I think I see why: the re-positioning transition has the same duration (2500ms) as the panning/scaling, and something about that all combined has that undesirable effect. I'm able to keep the subgraph on-screen by making the positioning happen faster:
rects
.transition().duration(1500) // instead of 2500
.attr('x', (d) => d.x ?? 0)
.attr('y', (d) => d.y ?? 0)
but that's of course not a universal solution, and it kind of moves around haphazardly. I'd rather have a way to interpolate the re-positioning with the translate so it smoothly grows and pans to be centered without going off-screen. Is this possible? I'm aware of d3.interpolate and scaleExtent/translateExtent but am stumped on how to use them - so far I haven't figured out how to account for both repositioning and translate/scale at the same time.
Other relevant bits of code:
<svg
style={{ borderStyle: "Solid", borderWidth: "1px" }}
width="800"
height="600"
onClick={onClickHandler} <!-- this just switches to the subgraph -->
>
<g />
</svg>
const gr = d3.select('g');
const rects = gr.selectAll<SVGSVGElement, ElkNode>('rect').data(elkGraph.children, (d) => d.id);
// update, enter, exit logic omitted; see above for position update
// offset and scale logic omitted
gr.transition().duration(2500)
.attr('transform', `translate(${xCenterOffset}, ${yCenterOffset}) scale(${scaleFactor})`)

d3's fitSize distorted by New Zealand geojson

I'm trying to expand this example to include Asia plus Oceania (specifically Australia and New Zealand): https://bl.ocks.org/aholachek/700f930820f2704a957c070173327789
So I replaced the json data in the example with Asia + Oceania from https://geojson-maps.ash.ms/ and trimmed out all the tiny pacific islands. When I do this I see the entire map skewed to the right (as below). Whereas by simply removing New Zealand, the map will scale appropriately to the entire canvas.
The relevant area of the code seems to be...
const projection = d3.geoMercator()
// d3's 'fitSize' magically sizes and positions the map for you
.fitSize([width, height], data);
But I can't find any documentation explaining why fitSize might be getting tripped up on New Zealand (and I need to have New Zealand included in the final visualisation).
I'm adding an answer as it might not be possible to remove features to align the map as intended - say for a map of the Bering Sea or Pacific Rim. Or, you might want to include those tiny NZ islands.
By default, most D3 projections are centered at 0°N,0°E, with the anti-meridian at 180°W/E. This means any feature split by the anti-meridian will likely end up on two sides of the map. FitSize/fitExtent will then scale and translate the maps so that both sides of the map are visible, likely with a large empty space in between. As you noted, your features bridge the anti-meridian, so fitSize/fitExtent don't work as needed.
Both projection.fitSize and projection.fitExtent are convenience methods for setting the projection projection.scale and projection.translate. Both scale and translate modify projected coordinates - all they can do is pan and scale the projected data. Consequently fitSize and fitExtent, nor translate or center, modify the antimeridian.
There are two other useful projection methods: projection.center() and projection.rotate(). Projection.center translates the map in geographic coordinates. The geographic coordinate specified by .center() and the pixel coordinate specified by .translate() will align in the map. But, projection.rotate() will apply a rotation to the map prior to projection, this will shift the anti-meridian.
projection.rotate takes an array with two (or three values), the first represents the longitude, the second the latitude. Generally you'll want to leave latitude at 0 - this will change the appearance of the Mercator projection otherwise. Changing the longitude will not alter a Mercator (longitude and projected x values have a linear relationship). In your case, rotating the world 180 degrees would make the prime meridian the anti-meridian, and this would ensure the projection's anti-meridian doesn't intersect your features, which means that fitSize and fitExtent will work as needed and your features will not be on split across two far sides of the map:
var width = 480;
var height = 480;
// feature crossing anti-meridian (180W/E), ensure proper winding direction.
var data = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
150.8203125,
-28.92163128242129
],
[
-121.0625,
-30.145127183376115
],
[
-121.765625,
-60.586967342258674
],
[
145.8984375,
-57.70414723434192
],
[
150.8203125,
-28.92163128242129
]
]
]
}
}
]
}
var svg = d3.select("svg");
var g = svg.append("g");
var projection = d3.geoMercator().rotate([180,0]);
var path = d3.geoPath(projection);
d3.json("https://d3js.org/world-110m.v1.json").then(function(world) {
// Draw the world.
let countries = topojson.feature(world, world.objects.countries).features;
projection.fitSize([width,height],data)
let features = g.selectAll("path")
.data(countries)
.enter()
.append("path")
.attr("d", path)
.style("stroke-width",1);
g.append("path")
.datum(data)
.attr("d",path)
.attr("fill","none")
.attr("stroke","black")
.attr("stroke-width",1);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<svg width="480" height="480"></svg>
apologies - I figured out the issue - there were tiny islands (invisible on my laptop screen) associated in the NZ geodata that were mapped to left of the image. Closing

Drawing state borders with d3 not working as expected

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.

How to add a rectangle to specified axis in D3

I have a zoomable area plot done in D3, which works well. Now I am trying to add a rectangle to the specified location along x-axis in the middle of the plot. However, I can't seem to figure out how to do that. "rect" element is specified using absolute (x,y) of the plot and so when using zooms it stays in the same position.
So I was wondering if there is a way to tie "rect" to the axis when plotting, so that it benefits from all the zoom and translate behaviour or do I need to manually edit the x,y,width and length of the rectangle according to translation as well as figuring out where the corresponding x and y coordinates are on the graph? I am trying to use "rect" because it seems the most flexible element to use.
Thanks
Alex
I'm not sure how you are doing the zooming, but I am guessing you are changing the parameters of the scales you use with your axis? You should be able to use the same scales to place your rectangle.
If you are starting with plot coordinates then maybe using the invert function on the scale will help (available at least for quantitive scales), e.g. https://github.com/mbostock/d3/wiki/Quantitative-Scales#wiki-linear_invert
You should be able to take initial plot coordinates and invert them to determine data coordinates that can then move with changes in the scale.
If the scale is linear you can probably invert the length and width too, but you will have to compute offsets if your domain does not include 0. Easiest is to compute the rectangle's end points, something like:
var dataX0 = xScale.invert(rect.x);
var dataX1 = xScale.invert(rect.x + rect.width);
var dataWidth = dataX1 - dataX0;
If you have the data in axes coordinates already you should be able to do something like:
var rectData = [{x: 'April 1, 1999', y: 10000, width: 100, height:100}];
svg.selectAll('rect.boxy')
.data(rectData)
.enter().append('rect').classed('boxy', true)
.style('fill','black');
svg.selectAll('rect.boxy')
.attr('x', function(d) { return x(new Date(d.x));} )
.attr('y', function(d) { return y(d.y);})
.attr('width', function(d) { return d.width;} )
.attr('height', function(d) { return d.height;} );
Based on the example you shared where x and y (as functions) are the scales the axes are based on.

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