D3 not rendering map data in JSON file - d3.js

I am working with D3 JS libraries to display maps. Loading the U.S. states map is working fine, but if I attempt to load county maps for individual states, it isn't working properly. The file I am using for all states is found at https://raw.githubusercontent.com/d3/d3.github.com/master/us-10m.v1.json. The state map I am currently using is https://raw.githubusercontent.com/deldersveld/topojson/master/countries/us-states/MI-26-michigan-counties.json, though other states have the same behavior.
Using the Chrome developer tools, I noticed that the states (the one that works) renders with one "path" element with one "d" element underneath it for the entire map:
<path d="M558.8236946374037,348.30360060633L559.3534721355569,348.2802219377176L567.3201262115587,347.9178525742255L571.2684679430778,347.7775805625511L572.0381446856777,347.7542018939387L577.0160540079474…
The counties render one "g" element with multiple "path" elements:
<g class="counties"><path d="M-86.2371554117112,44.517643343110294L-85.81834245759008,44.51265840274941L-85.82187674412275,44.16371257748768L-86.04276965241448,44.16620504766812L-86.38912973261591,44.178667398570326L-86.35201972402291,44.22851680217914L-86.26896399050521,44.344416665569646L-86.25305970110821,44.40049724462957L-86.24952541457554,44.48274876058412Z"></path><path d="M-84.13072063824123,42.42521462663013L-83.66596195919543,42.43144580208123L-83.55286479015007…
The D3 code I use to render them is the same, just pointing to the different data sources. This one points to the (working) states code:
d3.json("us-10m.v1.json", function (error, us) {
svg.append("g")
.attr("class", "counties")
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter()
.append("path")
.attr("d", path).append("svg:title");
The files themselves seem to be identically formatted, with (of course) different data, so I am at a loss as to why the large differences in the way the svg code is generated. The two differences I see are that the U.S. states file has a "bbox" element that the state files lack, and the Michigan file has "properties" elements inside of the geometries collection.

You problem is the opposite of this one. The US topojson is already projected (to a 960x600 pixel area) - the underlying coordinates in the topojson represent pixel coordinates. Maps that use pre-projected geographic features don't use d3 geographic projections. If drawing with a path generator, this looks like:
var path = d3.geoPath(); // or
var path = d3.geoPath().projection(null);
However, the Michigan data contains latitude/longitude pairs. If we convert your Michigan topojson to geojson (so we have human readable coordinates), we get values such as: [-87.86292721673836,45.35386708864823]. If we use a null projection and treat these as pixel values, the features will be to the left of the viewable SVG as the x values are negative. So we need a projection for our path:
var path = d3.geoPath().projection(someProjection);
I'm not sure what projection you would want - the pre-projected US data uses an Albers projection, so if we wanted to replicate this and scale and center Michigan, we could use:
var projection = d3.geoAlbers()
.fitSize([width,height],geojsonObject); // width/height of SVG/canvas
var path = d3.geoPath().projection(projection);
With an Albers it is tailored to the US, if using fitSize or fitExtent on other parts of the world, results may not be as expected, projection rotation must be set to account for different locations.
There are other ways that can be used to center/scale the map but this is covered in many questions/answers. But, to display geographic data, you need to project it from spherical coordinates to planar, and any d3 geoProjection can do this.

Related

D3 polygon projection is wrong

I have a working world projection using D3 geoOrthographic and topoJSON. It rotates and everything.
I wanted to place a hexagon shape at a coordinate and projected according to its place on the globe, which works... except my shape is really weird. I get a 5-sided shape, like one of the points is just missing, that rotates properly with the globe.
And then also a circle around the edge of the globe that does not rotate.
I have a function that throws out hexagon coordinates, I've tried with several scales and offsets, always the exact same behavior.
let hex = svgOrbit.append("path")
.datum({"type":"GeometryCollection","geometries":[{"type":"Polygon","coordinates":[[[6.732,6],[5,7],[3.268,6],[3.268,4],[5,3],[6.732,4]]]}]})
.attr("d", myGeoOrthographicProjection);
The circle looks like that no matter how I rotate, the trying-to-hexagon orients as desired sans that missing point.
The path does show a d attr with these two separate polygons.
I just plain don't understand what's happening here. There aren't even any weird numbers, like a zero or NaN or anything in the coordinates. The entire planet projects correctly, but a hexagon throws it for a loop?
The outer circle indicates that you have an inverted polygon: you are drawing a feature of the world minus the intended feature. As d3 uses spherical math in calculating projections, winding order matters, as opposed to most geographic tools which treat spherical coordinates as Cartesian (even when projecting). The first map below in red shows this by applying a fill.
The missing point is a bit odd, normally D3 won't render invalid geojson syntax and it won't throw an error or warning in not rendering anything. The issue here is that the last point in your coordinate array should be the first coordinate. I've forgotten where in the spec this is, and haven't looked as to why D3 renders it like this at all. When attempting to take a look at your geojson at geojson.io I noticed it didn't render at all with the missing end point.
I've rewound the coordinates (lazily with .reverse()) and added the extra point in the map on the right.
let hex = {"type":"GeometryCollection","geometries":[{"type":"Polygon","coordinates":[[[6.732,6],[5,7],[3.268,6],[3.268,4],[5,3],[6.732,4]]]}]};
let hex2 = {"type":"GeometryCollection","geometries":[{"type":"Polygon","coordinates":[[[6.732,6],[5,7],[3.268,6],[3.268,4],[5,3],[6.732,4],[6.732,6]].reverse()]}]};
let projection = d3.geoOrthographic().scale(125).translate([125,125]);
let path = d3.geoPath(projection);
let svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 250);
svg
.append("path")
.datum(hex)
.attr("d", path)
.attr("fill", "crimson");
svg.append("g")
.attr("transform","translate(250,0)")
.append("path")
.datum(hex2)
.attr("d", path)
.attr("fill","steelblue");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

Let's make a TopoJSON map and view it with D3.js

I made the following TopoJSON file: https://gofile.io/d/CKBGhF
I want to view it in my browser with a basic D3.js script. From https://bost.ocks.org/mike/map/ I found a small tutorial about the script. But because my map is made from a vector converted into TopoJSON it has no real coordinates. With QGIS I saved it first as a GeoJSON and with mapshaper I saved it as TopoJSON.
How should I view my map? Are the coordinates or scaling completely wrong? Or is my TopoJSON not good?
My HTML/Javascript code (I only changed the filename):
<!DOCTYPE html>
<meta charset="utf-8">
<style>
/* CSS goes here. */
</style>
<body>
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script>
var width = 960,
height = 1160;
var projection = d3.geo.mercator()
.scale(500)
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
d3.json("topojson_mapshaper.json", function(error, uk) {
svg.append("path")
.datum(topojson.feature(uk, uk.objects.subunits))
.attr("d", path);
});
</script>
In my response I'm upgrading all code to d3v5 due to the inclusion of new features since d3v4 that aid with drawing this data (such as d3.geoIdentity, projection.fitSize). Since D3 v4 there are some changes to D3 namespace (d3.geo.path and d3.geo.projectionName are now d3.geoPath and d3.geoProjectionName). And in D3v5, d3.json now returns promise.
There are a few things at play here.
Topojson.feature
First, D3 only draws geojson objects with d3.geoPath, D3 does not draw topojson directly. So your data, while stored as topojson, is converted to geojson here:
topojson.feature(uk, uk.objects.subunits)
But, your topojson data uk, doesn't have a subunits property that contains features. You instead have a sub unit property called test:
...463908,4.10162]},"objects":{"test":{"type":"GeometryCollection"...
Normally platforms such as mapshaper apply the filename of the source file as the sub property name, so I'm guessing your source file, exported from QGIS, was test.geojson or something to that effect.
If we log:
console.log(topojson.feature(uk, uk.objects.test))
We see valid geojson. Now that we have geojson, we can draw it.
Coordinates
As for coordinate systems, topojson preserves the original coordinate system by default. When converting back to geojson, your coordinates will be the same as originally. So this statement "it has no real coordinates" isn't true unless your source data has no real coordinates.
However, you can project or reproject points from the command line, or with mapshaper, while producing topojson. It appears you have applied a projection on your points because when I convert your topojson to geojson, I see coordinates that look like pixel values (units that are unlikely to be the original coordinates exported from QGIS).
If using projected coordinates (Cartesian points, not lat long pairs, whether units are meters or pixels), we cannot use a D3 geoProjection: these take 3D points and project them to a plane.
Since your values look like pixel values, we can pass your data directly to a null projection:
var path = d3.geoPath()
Or, more explicitly:
var path = d3.geoPath(null);
var path = d3.geoPath().projection(null);
In D3v3 and earlier, this needs to be explicitly set
This applies no transform to the coordinates in the geojson. It treats each geojson coordinate as a pixel coordinate and draws your features accordingly (example)
However, that option isn't useful if the projected extent of our features doesn't match the SVG/Canvas extent. Instead, we could use a geoTransform or geoIdentity to apply an appropriate transform.
The geoIdentity option is the easiest as it provides the convenient fitSize method that lets us automagically size the geojson to our SVG/Canvas dimensions (fitExtent allows specification of a margin, while fitSize assumes no margin). All of these options can be passed to d3.geoPath as a projection (example, full screen).
The last example should show you how to draw the data. You mostly have line strings, which will make filling features difficult, unless you only wish to show borders.
Further Reading
Based on previous experience, there are a few related questions that come to mind:
Preprojected Geometry vs Projecting on the Fly
This question addresses possible quesions on pre-projected geometry as it appears you have preprojected your geometry for a screen size. This introduces alignment challenges, the trade off is quicker rendering time.
Scaling a Map to Fit SVG
This question deals with preprojected geometry and fitting the features to the screen. It speaks more in depth to fitSize, fitExtent, and geoTransform.

Map not rendering, but data is correct

I am a newbie, attempting to copy someone else's choropleth code and use my data.
My data matches their data structure and I can get the correct data to console.log() and my tooltips display correct data.
But my map is just a big block of colour with no paths.
Here is a codepen, but this is the code that renders the map features:
var path = d3.geo.path();
svg.append("g")
.attr("class", "county")
.selectAll("path")
.data(topojson.feature(us, us.objects.counties).features)
.enter().append("path")
.attr("d", path)
.style("fill", function (d) {
return color(pairFipsWithId[d.id]);
})
The result looks like this:
⚠️This question and answer both use d3v3 - d3v4+ geo paths are slightly different, see this question/answer for d3v4+.
If you are making a D3 choropleth, you should be looking for a recent version of D3 - d3v3 is a bit dated now. However, as v3 has some differences, this isn't quite a duplicate of this question, but the problem is the same:
The example map you use has unprojected geographic data
Your geographic data is projected.
In other words, your example's geographic data uses a 3 dimensional coordinate system measured in degrees latitude and longitude, while your geographic data uses a coordinate system where the units are pixels. But, you don't specify this change and D3 does not know to correct for it.
In D3v3, the default projection for a d3.geo.path() is d3.geo.albersUsa(), all D3 projections assume your data is recorded in latitudes and longitudes, otherwise we would need to specify an additional parameter to projections to indicate what coordinate space the input data uses.
A sign that your data is already projected can come from mapshaper.org, if you drag your topojson into the window your map is upside down. Why? Geographic coordinates increase as one moves north (typically up) while pixel coordinates increase as one moves down. This is your data in mapshaper.org:
We cannot "unproject" the data as we don't know what projection was used to create it. But we could assign a null projection to the geo.path:
var path = d3.geo.path().projection(null)
The null projection simply takes each coordinate in the data and converts it to a pixel coordinate with no transform - input values are treated as pixel values. As this map was designed for a web map it doesn't need any scaling or centering (depending on the size of your SVG).
Here's an updated codepen.
In d3v4+, the default projection of a geoPath is a null projection off the bat, it also comes with selection.raise() and selection.lower() methods that can simplify your code a bit, as well as better options for fitting any dataset to a SVG/Canvas of a given size. Here's a d3v4 codePen with a few modifications, d3v5 requires a bit more in updating d3.json

Fit D3 map in SVG

In this jsfiddle I have a D3 map that I took from here, but I'm trying to fit it in an svg that is half the original size. For that, I changed:
var width = 480;
var height = 300;
.....
var path = d3.geoPath(d3.geoIdentity().translate([width/2, height/2]).scale(height*.5));
But it's not working. How to make the map fit the svg?
D3's geoIdentity exposes almost all the standard methods of a d3 projection (off the top of my head, only rotation is not possible as the identity assumes cartesian data). Most importantly here, it exposes the fitSize and fitExtent methods. These methods set translate and scale based on the coordinate extent of displayed geojson data and the pixel extent of the svg/canvas.
To scale your features with a geo identity you can use:
d3.geoIdentity().fitSize([width,height],geojsonObject)
Note that an array of geojson features won't work, but a geojson feature collection or any individual feature/geometry object works too. The width and height are of the svg/canvas.
If you want to apply a margin, you can use:
d3.geoIdentity().fitExtent([[margin,margin],[width-margin,height-margin]],geojsonObject)
The margin doesn't need to be uniform, the format is [[left,top],[right,bottom]],geojsonObject
If using fitSize or fitExtent there is no need to set center, translate or scale manually and setting these afterwards will recenter or rescale the map.

d3 mercator, geo, and path methods

I am following this tutorial on d3:
In it, I see this code:
var projection = d3.geo.mercator()
.scale(500)
.translate([width / 2, height / 2]);
And likewise the path generator:
var path = d3.geo.path()
.projection(projection);
What are these methods doing exactly? Is there good documentation on these d3 methods? On on the d3 docs. it says:
"# d3.geo.mercator()
"The spherical Mercator projection is commonly used by tiled mapping
libraries (such as OpenLayers and Leaflet). For an example displaying
raster tiles with the Mercator projection, see the d3.geo.tile plugin.
It is conformal; however, it introduces severe area distortion at
world scale and thus is not recommended for choropleths."
So is d3.geo.mercator just a kind of map design?
The map projection just converts points from one system (sphere/ellipsoid based latitude/longitudes) into another system (2d Cartesian plane with x/y values).
Mercator is one (very common) way of doing that. For more about map projections, take a look at http://en.wikipedia.org/wiki/Map_projection.
With the code you've posted, it sets up a Mercator projection, that when passed a [long, lat] point it will return an [x, y] point that corresponds to the x and y position that can be drawn on an svg or canvas. In this case it will be centered at [width/2, height/2].
The path generator is some d3 "magic" that converts a list of points into an svg path string. svg has it's own "language" for paths, you can find out more at http://www.w3.org/TR/SVG/paths.html, but that does get pretty technical.
Since svg talks in pixel co-ordinates, and most geo data is referenced to lat/long, the projection function allows you to easily convert from one to the other and back again.
Since drawing a path on a map is a very common activity, d3 includes the path generator that is projection "aware" and will automatically apply the projection specified to whatever data you pass to the path generator, which will result in the pixel co-ordinates getting returned, which will then be converted into the path "language" mentioned above, which can then be displayed on an svg element.

Resources