I have a question about D3 cartography. I am working on a little project and I am new to D3.
I have started out from this example: http://bl.ocks.org/mbostock/5914438 Instead of the showing the state-mesh, I would like to show circles on the map in certain locations (lon/lat). I am currently facing a problem that the circles are not on the correct spots on the map. I suspect the problem lies in the special projection that Mike uses. He uses a 1x1 square projection. Probably this is necessary for displaying the tiles. When I project the coordinates, the values are all between -1 and 1. I thought I could fix it by multiplying it width the height and width but it didn't work. Below is my code (snippet does not run because it is missing a file). Thanks for the assistance!
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
margin: 0;
}
path {
fill: none;
stroke: red;
stroke-linejoin: round;
stroke-width: 1.5px;
}
circle {
fill: #fff;
fill-opacity: 0.4;
stroke: #111;
}
</style>
<svg>
</svg>
<script src="//d3js.org/d3.v4.min.js"></script>
<script src="//d3js.org/d3-tile.v0.0.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script>
var pi = Math.PI,
tau = 2 * pi;
var width = Math.max(960, window.innerWidth),
height = Math.max(500, window.innerHeight);
// Initialize the projection to fit the world in a 1×1 square centered at the origin.
var projection = d3.geoMercator()
.scale(1 / tau)
.translate([0, 0]);
var path = d3.geoPath()
.projection(projection);
var tile = d3.tile()
.size([width, height]);
var zoom = d3.zoom()
.scaleExtent([1 << 9, 1 << 23])
.on("zoom", zoomed);
var svg = d3.select("svg")
.attr("width", width)
.attr("height", height);
var raster = svg.append("g");
var vector = svg.append("path");
var circle = svg.append("g")
d3.json("/data/flyingsites/AD.json", function(error, flyingsites) {
if (error) console.log(error);
// Compute the projected initial center.
var center = projection([6.2, 45.8]);//45,809718, 6,252314
// Apply a zoom transform equivalent to projection.{scale,translate,center}.
svg
.call(zoom)
.call(zoom.transform, d3.zoomIdentity
.translate(width / 2, height / 2)
.scale(1 << 12)
.translate(-center[0], -center[1]));
//add flying sites
circle.selectAll("circle")
.data(flyingsites.features)
.enter().append("circle")
.attr('r',5)
.attr('cx',function(d) { return projection(d.geometry.coordinates)[0]*width})
.attr('cy',function(d) { return projection(d.geometry.coordinates)[1]*height})
.style('fill','red')
//console.log(flyingsites.features);
//console.log(circle);
});
function zoomed() {
var transform = d3.event.transform;
var tiles = tile
.scale(transform.k)
.translate([transform.x, transform.y])
();
vector
.attr("transform", transform)
.style("stroke-width", 1 / transform.k);
circle
.attr("transform", "translate(" + transform.x + "," + transform.y + ")");
var image = raster
.attr("transform", stringify(tiles.scale, tiles.translate))
.selectAll("image")
.data(tiles, function(d) { return d; });
image.exit().remove();
image.enter().append("image")
.attr("xlink:href", function(d) { return "http://" + "abc"[d[1] % 3] + ".tile.openstreetmap.org/" + d[2] + "/" + d[0] + "/" + d[1] + ".png"; })
.attr("x", function(d) { return d[0] * 256; })
.attr("y", function(d) { return d[1] * 256; })
.attr("width", 256)
.attr("height", 256);
}
function stringify(scale, translate) {
var k = scale / 256, r = scale % 1 ? Number : Math.round;
return "translate(" + r(translate[0] * scale) + "," + r(translate[1] * scale) + ") scale(" + k + ")";
}
</script>
The approach you are taking won't work, for one, it doesn't consider the scale (just translate). This is critical as d3-tile uses geometric zooming - it applies a zoom transform (scale and translate) to all the vector elements (not the tiles), this is why the projection projects everything to a one pixel square area and never changes with the zoom.
To solve this, place your circles the same as the example places (and sizes) the polygons:
vector
.attr("d", path(topojson.mesh(us, us.objects.counties)));
circle
.attr("cx", projection(coord)[0])
.attr("cy", projection(coord)[1])
.attr("r", 5/(1<<12));
Both of these position features the same way: with the projection only, projecting to a one pixel square. The zoom applies the transform to cover the whole svg. Also, since we are scaling that one pixel to fit the svg, we want the radius to be scaled appropriately too.
Now we can apply a transform to the circles the same as the polygons:
circle
.attr("transform", transform);
Of course, we could scale the radius down each zoom too, using the zoom k to modify the size of the circle:
var pi = Math.PI,
tau = 2 * pi;
var width = Math.max(960, window.innerWidth),
height = Math.max(500, window.innerHeight);
// Initialize the projection to fit the world in a 1×1 square centered at the origin.
var projection = d3.geoMercator()
.scale(1 / tau)
.translate([0, 0]);
var path = d3.geoPath()
.projection(projection);
var tile = d3.tile()
.size([width, height]);
var zoom = d3.zoom()
.scaleExtent([1 << 11, 1 << 14])
.on("zoom", zoomed);
var svg = d3.select("svg")
.attr("width", width)
.attr("height", height);
var raster = svg.append("g");
var vector = svg.append("circle");
// Compute the projected initial center.
var center = projection([-98.5, 39.5]);
// Apply a zoom transform equivalent to projection.{scale,translate,center}.
svg
.call(zoom)
.call(zoom.transform, d3.zoomIdentity
.translate(width / 2, height / 2)
.scale(1 << 12)
.translate(-center[0], -center[1]));
var coord = [-100,40]
vector
.attr("cx", projection(coord)[0])
.attr("cy", projection(coord)[1])
.attr("r", 5/(1<<12));
function zoomed() {
var transform = d3.event.transform;
var tiles = tile
.scale(transform.k)
.translate([transform.x, transform.y])
();
vector
.attr("transform", transform)
.attr("r", 5/transform.k);
var image = raster
.attr("transform", stringify(tiles.scale, tiles.translate))
.selectAll("image")
.data(tiles, function(d) { return d; });
image.exit().remove();
image.enter().append("image")
.attr("xlink:href", function(d) { return "http://" + "abc"[d[1] % 3] + ".tile.openstreetmap.org/" + d[2] + "/" + d[0] + "/" + d[1] + ".png"; })
.attr("x", function(d) { return d[0] * 256; })
.attr("y", function(d) { return d[1] * 256; })
.attr("width", 256)
.attr("height", 256);
}
function stringify(scale, translate) {
var k = scale / 256, r = scale % 1 ? Number : Math.round;
return "translate(" + r(translate[0] * scale) + "," + r(translate[1] * scale) + ") scale(" + k + ")";
}
<svg></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-tile.v0.0.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
Ultimately d3-tile can be a bit confusing to start with because you are using quite a few coordinate systems (tile, zoom, pixel, projected, geographic), and normally aren't projecting the entire map to a 1 pixel square.