Curved geojson polygon edges on map projections - d3.js

I am trying to draw rectangles on different map projections using d3.js and geojson. The mapped coordinates seem right, however the edges appear curved in a strange way. I understand that this may have to do with the shortest path on the real Earth, but what I would like is that the edges follow the parallels/meridians graticule of the projection. Is there a way to do that? Can anyone help?
Example: Aitoff Projection
Example: Mercator
Here is the code I am using:
<!DOCTYPE html>
<html>
<head>
<title>World Map</title>
<meta charset="utf-8">
<script src="https://d3js.org/d3-geo-projection.v2.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<style>
path {
fill: red;
stroke: #000;
stroke-width: .1px;
}
.graticule {
fill: none;
stroke: #000;
stroke-width: .2px;
}
.foreground {
fill: none;
stroke: #333;
stroke-width: 1.2px;
}
</style>
</head>
<body>
<svg width="960" height="600"></svg>
<script>
const svg = d3.select("svg")
const myProjection = d3.geoMercator()
const path = d3.geoPath().projection(myProjection)
const graticule = d3.geoGraticule()
const geojson = {
"type": "FeatureCollection",
"features": [
{ "type": "Feature", "properties": {
"color": "blue"
},
"geometry": {
"type": "Polygon",
"coordinates": [[[-80.0, 50.0], [-20.0, 50.0], [-20.0, -10.0], [-80.0, -10.0], [-80.0, 50.0]]]
} }
]
}
function drawMap(err, world) {
if (err) throw err
svg.append("g")
.selectAll("path")
.data(topojson.feature(world, world.objects.land).features)
.enter().append("path")
.attr("d", path);
svg.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path);
svg.append("path")
.datum(graticule.outline)
.attr("class", "foreground")
.attr("d", path);
svg.append("g")
.selectAll("path")
.data(geojson.features)
.enter().append("path")
.attr("d", path)
}
d3.json("https://unpkg.com/world-atlas#1.1.4/world/50m.json", drawMap)
</script>
</body>
</html>

Your assumption is right: d3 uses great circle distances to draw lines: this means that any path between two points using a d3 geoProjection and geoPath follows the shortest real world path between those two points. This means:
that the same path between two points aligns with other geographic points and features no matter the projection
that the anti-meridian can be accounted for
and the resulting map more accurately depicts lines.
To draw straight lines and/or lines that follow parallels (meridians are the shortest path between two points that fall on them - so paths follow this already, assuming an unrotated graticule) there are a few possibilities.
The easiest solution is to use a cylindrical projection like a Mercator to create a custom geoTransform. d3.geoTransforms do not use spherical geometry, unlike d3.geoProjections, instead they use Cartesian data. Consequently they don't sample along lines to create curved lines: this is unecessary when working with Cartesian data. This allows us to use spherical geometry for the geojson vertices within the geoTransform while still keeping straight lines on the map:
var transform = d3.geoTransform({
point: function(x, y) {
var projection = d3.geoMercator();
this.stream.point(...projection([x,y]));
}
});
As seen below:
var projection = d3.geoMercator();
var transform = d3.geoTransform({
point: function(x, y) {
var projection = d3.geoMercator();
this.stream.point(...projection([x,y]));
}
});
var color = ["steelblue","orange"]
var geojson = {type:"LineString",coordinates:[[-160,60],[30,45]]};
var geojson2 = {type:"Polygon",coordinates:[[[-160,60,],[-80,60],[-100,30],[-160,60]]]}
var svg = d3.select("body")
.append("svg")
.attr("width",960)
.attr("height",500);
svg.selectAll(null)
.data([projection,transform])
.enter()
.append("path")
.attr("d", function(d) {
return d3.geoPath().projection(d)(geojson)
})
.attr("fill","none")
.attr("stroke",function(d,i) { return color[i]; } )
.attr("stroke-width",1);
svg.selectAll(null)
.data([projection,transform])
.enter()
.append("path")
.attr("d", function(d) {
return d3.geoPath().projection(d)(geojson2)
})
.attr("fill","none")
.attr("stroke",function(d,i) { return color[i]; } )
.attr("stroke-width",2);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
The orange lines use the transform, the blue lines use a plain Mercator
In some cases you could set the precision of the projection (which regulates the adaptive sampling) to some absurdly high number, this will work for some lines, but not others due to things like anti-meridian cutting:
var projection = d3.geoMercator().precision(1000000);
var transform = d3.geoTransform({
point: function(x, y) {
var projection = d3.geoMercator();
this.stream.point(...projection([x,y]));
}
});
var color = ["steelblue","orange"]
var geojson = {type:"LineString",coordinates:[[-160,60],[30,45]]};
var geojson2 = {type:"Polygon",coordinates:[[[-160,60,],[-80,60],[-100,30],[-160,60]]]}
var svg = d3.select("body")
.append("svg")
.attr("width",960)
.attr("height",500);
svg.selectAll(null)
.data([projection,transform])
.enter()
.append("path")
.attr("d", function(d) {
return d3.geoPath().projection(d)(geojson)
})
.attr("fill","none")
.attr("stroke",function(d,i) { return color[i]; } )
.attr("stroke-width",1);
svg.selectAll(null)
.data([projection,transform])
.enter()
.append("path")
.attr("d", function(d) {
return d3.geoPath().projection(d)(geojson2)
})
.attr("fill","none")
.attr("stroke",function(d,i) { return color[i]; } )
.attr("stroke-width",2);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Neither approach works if you want to draw lines that are aligned with parallels on a non-cylindrical projection. For a cylindrical projection parallels are straight. The above approaches will only create straight lines. If the parallels aren't projected straight, such as the Aitoff, the lines will not align with the graticule.
To have a line follow a parallel you will need to sample points along your paths because the projected parallels will not all be straight and parallels don't follow great circle distance. Therefore neither the default projection nor the method above will work in these instances.
When sampling you will need to treat the data as Cartesian - essentially using a cylindrical projection (Plate Carree) to have lines follow parallels.

Related

Can I project one state from "d3js.org/us-10m.v1.json" and not the whole nation?

From this code at (https://bl.ocks.org/mbostock/4090848) I can access us.objects.states, .nation, or .counties but I just want to know if I can say: us.objects.states.geometries.id(Missouri FIPS code is 29)
I just want to get the state of Missouri or is there a way to narrow this down.
Or if you can give help or direction on how to manipulate this line:
.data(topojson.feature(us, us.objects.states).features)
I've read the API reference on the topojson.feature function but I am still confused on how to use it.
<svg width="960" height="600"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<script>
var svg = d3.select("svg");
var path = d3.geoPath();
d3.json("https://d3js.org/us-10m.v1.json", function(error, us) {
if (error) throw error;
svg.append("g")
.attr("class", "states")
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("d", path);
svg.append("path")
.attr("class", "state-borders")
.attr("d", path(topojson.mesh(us, us.objects.states, function(a, b) { return a !== b; })));
});
</script>
The simplest change would be to filter for your chosen state(s):
.data(topojson.feature(us, us.objects.states).features.filter(function(d) {
return d.id == 29;
}))
We still return an array to .data() as required, and we draw the map, just with only the chosen state. (I've removed the mesh for borders below, the state appears just out of view, you'll need to scroll):
var svg = d3.select("svg");
var path = d3.geoPath();
d3.json("https://d3js.org/us-10m.v1.json", function(error, us) {
if (error) throw error;
svg.append("g")
.attr("class", "states")
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features.filter(function(d) {
return d.id == 29;
}))
.enter().append("path")
.attr("d", path);
});
<svg width="960" height="600"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
Which brings us to the next question, how to position the state. There are plenty of options and answers on SO on how to do so, but I'll note that this file is already projected and its coordinate system is in pixels, the file is designed for a 960x600 pixel map. This is why you haven't had to use a projection so far, there is no need for a transformation or projection. But unless we are happy with the empty space where all the other states would normally be drawn, we need to fix this.
The links in the text above go into more detail, but since we're here I'll just quickly demonstrate how to apply an identity projection and use the fitSize method to showcase Missouri (I just use selection.append() and .datum() as I'm just adding one path):
var svg = d3.select("svg");
var path = d3.geoPath();
d3.json("https://d3js.org/us-10m.v1.json", function(error, us) {
if (error) throw error;
var feature = topojson.feature(us, us.objects.states)
.features
.filter(function(d) { return d.id == 29; })[0]; // get a geojson object (not an array of objects)
var projection = d3.geoIdentity()
.fitSize([960,600],feature); // scale and translate the map so the feature is centered
path.projection(projection);
svg.append("path")
.datum(feature)
.attr("d", path); // draw the path.
});
<svg width="960" height="600"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>

D3.js v4 Force layout and fixed node anomaly

I have a D3.js V4 Force layout, comprising two nodes linked by a single edge. One node is fixed near top left, the other free to move.
When the layout is run, the non-fixed node starts in the middle of the layout, and moves away from the fixed node, as though repelled. It ends up in the opposite corner from the fixed node.
I would expect the free node to end up between the centre of the layout (where gravity pulls it) and the fixed node (pulled towards it by the link force). What am I missing, please?
var width = 240,
height = 150;
var nodes = [
{ // Larger node, fixed
fx: 20, fy: 20, r: 10
},
{ // Small node, free
r: 5
}];
var links = [{ // Link the two nodes
source: 0, target: 1
}
];
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height);
// Create simulation with layout-centring and link forces
var force = d3.forceSimulation()
.force("centre", d3.forceCenter(width / 2, height / 2))
.force("link", d3.forceLink())
.nodes(nodes);
force.force("link").links(links);
// Draw stuff
var link = svg.selectAll('.link')
.data(links)
.enter().append('line')
.attr('class', 'link');
var node = svg.selectAll('.node')
.data(nodes)
.enter().append('circle')
.attr('class', 'node')
force.on('tick', function() {
node.attr('r', function(d) {
return d.r;
})
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y;
});
link.attr('x1', function(d) {
return d.source.x;
})
.attr('y1', function(d) {
return d.source.y;
})
.attr('x2', function(d) {
return d.target.x;
})
.attr('y2', function(d) {
return d.target.y;
});
});
<script src="https://d3js.org/d3.v4.min.js"></script>
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<style>
.node {
fill: #f00;
stroke: #fff;
stroke-width: 2px;
}
.link {
stroke: #777;
stroke-width: 2px;
}
</style>
</head>
<body>
</body>
</html>
The Centering force will try to have the center of mass of all nodes in the coordinate given. As you have 2 nodes and 1 is fixed, the other will be symmetric to the fixed node.
d3 Centering force documentation
Centering
The centering force translates nodes uniformly so that the mean
position of all nodes (the center of mass if all nodes have equal
weight) is at the given position ⟨x,y⟩.

how to zoom only map and smiley could stay at same place and size

I having problem of zoom over map. The actual problem is when i zoom map, the location showing on map using smiley could also zoom but i don't want to zoom smiley. It could stay at same size and place. Sometime smiley get overlap so to avoid this i am trying to solve the above problem but i don't have idea how to transform attribute contains many things like images and text on map of d3.js. Please have a look at jsfiddle link and you can see that at japan 3 smiley get overlap and keep overlapped even after zooming map.
My JSfiddle link
my code is following:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
path {
stroke: white;
stroke-width: 0.25px;
fill: grey;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v0.min.js"></script>
<script>
var width = 960,
height = 500;
var data = [
{
"code":"TYO",
"city":"TOKYO",
"country":"JAPAN",
"lat":"35.68",
"lon":"139.76"
},
{
"code":"OSK",
"city":"Osaka",
"country":"JAPAN",
"lat":" 34.40",
"lon":"135.37"
},
{
"code":"HISH",
"city":"Hiroshima",
"country":"JAPAN",
"lat":"34.3853",
"lon":"132.4553"
},
{
"code":"BKK",
"city":"BANGKOK",
"country":"THAILAND",
"lat":"13.75",
"lon":"100.48"
},
{
"code":"DEL",
"city":"DELHI",
"country":"INDIA",
"lat":"29.01",
"lon":"77.38"
},
{
"code":"SEA",
"city":"SEATTLE",
"country":"USA",
"lat":"38.680632",
"lon":"-96.5001"
}
];
var projection = d3.geo.mercator()
.center([0, 5 ])
.scale(200)
.rotate([-180,0]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var path = d3.geo.path()
.projection(projection);
var g = svg.append("g");
// load and display the World
d3.json("world-110m2.json", function(error, topology) {
// load and display the cities
function drawMap(data){
var circle = g.selectAll("circle")
.data(data)
.enter()
.append("g")
circle.append("circle")
.attr("cx", function(d) {
return projection([d.lon, d.lat])[0];
})
.attr("cy", function(d) {
return projection([d.lon, d.lat])[1];
})
.attr("r", 5)
.style("fill", "red");
circle.append("image")
.attr("xlink:href", "http://fc08.deviantart.net/fs71/f/2013/354/8/7/blinking_smiley__animated__by_mondspeer-d6ylwn3.gif")//http://t2.gstatic.//com/images?q=tbn:ANd9GcT6fN48PEP2-z-JbutdhqfypsYdciYTAZEziHpBJZLAfM6rxqYX";})
.attr("class", "node")
.attr("x", function(d) {
return (projection([d.lon, d.lat])[0]) - 8;
})
.attr("y", function(d) {
return (projection([d.lon, d.lat])[1])-8;
})
.attr("width",20)
.attr("height",20)
//});
}
g.selectAll("path")
.data(topojson.object(topology, topology.objects.countries)
.geometries)
.enter()
.append("path")
.attr("d", path)
drawMap(data);
});
// zoom and pan
var zoom = d3.behavior.zoom()
.on("zoom",function() {
g.attr("transform","translate("+
d3.event.translate.join(",")+")scale("+d3.event.scale+")");
g.selectAll("circle")
.attr("d", path.projection(projection));
g.selectAll("path")
.attr("d", path.projection(projection));
});
svg.call(zoom)
</script>
</body>
</html>
Any body help me to zoom only map image not smiley
Implement semantic zooming :)
Try use this example to change your code :) :
Semantic zoom on map with circle showing capital
JSFIDDLE : http://jsfiddle.net/xf7222dg/2/
The code below shrinks the 'circles' depending on scale
var zoom = d3.behavior.zoom()
.on("zoom",function() {
g.attr("transform","translate("+
d3.event.translate.join(",")+")scale("+d3.event.scale+")");
g.selectAll("circle")
.attr("r", function(){
var self = d3.select(this);
var r = 8 / d3.event.scale; // set radius according to scale
self.style("stroke-width", r < 4 ? (r < 2 ? 0.5 : 1) : 2); // scale stroke-width
return r;
});
});
Here is it working with your smileys: http://jsfiddle.net/dmn0d11f/7/
You have to change the 'width' of the nodes (images) not the radius like with the circles. So select the nodes and instead of changing 'r' change 'width' :
g.selectAll(".node")
.attr("width", function(){
var self = d3.select(this);
var r = 28 / d3.event.scale; // set radius according to scale
self.style("stroke-width", r < 4 ? (r < 2 ? 0.5 : 1) : 2); // scale stroke-width
return r;
});

d3.js Fill area between to diagonals

I would to imitate the pink filled elemets in the image. I think those are two diagonals. But I dont know how to fill the area between them.
Any help, I would appreciate it too much!
This is the code that I have:
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src="d3.min.js"></script>
<style>
.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
</style>
<div class="container">
<h1 class="page-header">Chart</h1>
<div class="chart"></div>
</div>
<script>
var links = [{source: {x:0,y:0}, target: {x:200,y:200},x:0,y:0},
{source: {x:0,y:0}, target: {x:170,y:200},x:0,y:0}
]
var canvas = d3.select(".chart").append("svg")
.attr("width", 200)
.attr("height", 200)
.append("g");
var linksContainer = canvas.append("g").attr("class","linksContainer")
var diagonal = d3.svg.diagonal()
.source(function(d) { return {"x":d.source.x, "y":d.source.y}; })
.target(function(d) { return {"x":d.target.x, "y":d.target.y};})
.projection(function(d) { return [d.x, d.y]; });
var link = linksContainer.selectAll(".link")
.data(links)
.enter()
.append("path")
.attr("class", "link")
.attr("d", diagonal);
var line = linksContainer.append("line")
.attr("x1", 170)
.attr("y1", 200)
.attr("x2", 200)
.attr("y2", 200)
.attr("stroke-width", 1)
.attr("stroke", "black");
</script>
</body>
d3.svg.diagonal() produces a String representing an SVG path description. In your case, the two paths Strings are
var path1 = diagonal(links[0]);// "M0,0C0,100 200,100 200,200"
and
var path2 = diagonal(links[1]);// "M0,0C0,100 170,100 170,200"
Your code currently assigns each of them to the d attribute of a <path class=".link" d="...">.
Instead, you need to make just a single <path> with a filled (hence closed) path string that is a combination of those two strings.
var shape = path1 + path2;// But not quite...
It turns out that you can't simply concat those two strings. One thing you need to do is replace the M — which picks up and moves the pen to a new point (thus interrupting the closed-ness of the shape) — with an L, which draws a line to that new point:
var path2 = diagonal(links[1]).replace(/^M/, 'L');// "L0,0C0,100 170,100 170,200"
That gives you this:
var shape = path1 + path2;// "M0,0C0,100 170,100 170,200 L0,0C0,100 170,100 170,200"
However, the sub-path ...170,200 L0,0... incorrectly connects the end of path1 to the start of path2. To correct this, you need to reverse path2 by swapping between the link's start and end points:
var links = [ {source: {x:0,y:0}, target: {x:200,y:200},x:0,y:0},
// instead of {source: {x:0,y:0}, target: {x:170,y:200},x:0,y:0}]
{source: {x:170,y:200}, target: {x:0,y:0},x:0,y:0}]
Finally, this might not make a difference visually, but for correctness, you should close the path, using the Z directive:
var shape = path1 + path2 + 'Z';
// Apply `shape` to a single path element:
var path = linksContainer.append('path')
.attr("class", "link")
.attr("d", shape);
Here's a working fiddle.
The picture shown is actually a chord diagram layout.
You can learn how to use it here
https://github.com/mbostock/d3/wiki/Chord-Layout
Diagonals are different shapes that are used for tree diagrams

Issues with d3 zoomable map

I'm having an issue with a d3 zoomable map.
I'm loading the map from a previously built topojson file with a departmentsobject (the areas in the map) and a maternidadesobject (a few points in the map, initially rendered with crosses).
I'm using d3.behavior.zoom to implement the zoom behaviour, and I want it to be able to zoom using the mouse wheel and pan with drag. It works just fine with the map itself (the areas). However, the points in the map get shifted instantly to a wrong location at any zoom event. Also, the points' path is changed from crosses to circles somehow!
You can reproduce the issue and view the code here: http://bl.ocks.org/monsieurBelbo/5033491
Here is code:
<!DOCTYPE html>
<meta charset="utf-8">
<script src="http://d3js.org/d3.v3.js"></script>
<script src="topojson.v0.min.js"></script>
<html>
<style>
.background {
fill: none;
pointer-events: all;
}
.department {
fill: #aaa;
stroke: #fff;
stroke-width: 1.5px;
}
</style>
<body>
<script>
d3.json("santafe.json", function(error, theProvince) {
var width= 960, height= 500;
var svg = d3.select("body").append("svg");
var departments = topojson.object(theProvince, theProvince.objects.departments);
// The projection
var projection = d3.geo.mercator()
.scale(14000)
.center([-60.951,-31.2])
.translate([width / 2, height / 2]);
// The path
var path = d3.geo.path()
.projection(projection);
// Zoom behavior
var zoom = d3.behavior.zoom()
.translate(projection.translate())
.scaleExtent([height, Infinity])
.scale(projection.scale())
.on("zoom", function() {
projection.translate(d3.event.translate).scale(d3.event.scale)
map.selectAll("path.zoomable").attr("d", path);
});
// The map
var map = svg.append("g")
.classed("provinceMap", true)
.call(zoom);
map.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height);
// Departments
map.selectAll(".department")
.data(departments.geometries)
.enter().append("path")
.classed("department", true)
.classed("zoomable", true)
.attr("d", path);
// Places
map.selectAll(".place-label")
.data(topojson.object(theProvince, theProvince.objects.maternidades).geometries)
.enter().append("path")
.classed("place", true)
.classed("zoomable", true)
.attr("d", d3.svg.symbol().type("cross"))
.attr("transform", function(d) { return "translate(" + projection(d.coordinates.reverse()) + ")"; });
});
</script>
</body>
</html>
Any ideas?
Thanks!
UPDATE
Thanks to #enjalot 's suggestion, the issue was solved by re-translating the places on the zoom behaviour. Just add:
map.selectAll(".place").attr("transform", function(d) { return "translate(" + projection(d.coordinates) + ")"; });
to the zoom behavior. Check out a working version here: http://tributary.io/inlet/5095947

Resources