Fit D3 map in SVG - d3.js

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.

Related

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

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.

D3.js -- Equirectangular Projection Sizing Woes

all.
I'm working on a project that involves overlaying a map of the state of Kansas (that includes county borders, and some other geographic details) on top of a static, equirectangular base map that I've designed in Adobe Illustrator.
I'm having no problems generating the equirectangular projection. But when it comes to sizing the projection so that it fits exactly on top of the static base map, I can't seem to figure it out.
I'm guessing the secret lies within these functions.
var projection = d3.geo.equirectangular()
.scale(1)
.translate([0, 0]);
If the total size of my static base map image is, say, 1000px by 300px, and the outline of Kentucky in which the projection must fit is 530px by 230px, how can I tweak the scale/transform and other functions to resize the projection?
Here is a link to a proof-of-concept illustrating what I'm trying to accomplish.
http://i.imgur.com/acRk6RF.png
The background image is obviously fixed in width/height. The transparent outline of Kansas that's overlaid would be generated by D3, but would have to be tweaked to fit the map.
Any help would be appreciated!

sync d3.js map and leaflet map

In one application I have both d3 map and leaflet map, I follow Mike's code to sync them(http://bost.ocks.org/mike/leaflet/) and draw svg points on leaflet map. The problem is that some points being cutoff at the edge of SVG container, and other SVG elements added later on (an animated pulse in this case) will only partially shown. I wonder if there's anyway to expand the SVG container based on the features (points) bound.
a working demo is here: http://xqin1.github.io/usschools/usac_school_stat.html, to reproduce the issue:
1. select the 'Number of Students' slider bar to 5300-6545, the two points on Leaftlet map only half shown.
2. click the first table row, map will zoom to the point, but the animated pulse being cut off.
Any suggestions are highly appreciated.
thanks
xiaoming
I had this problem as well, and found a solution. Referencing mbostock's sample code you just need to add a little bit of padding to the projected bounding box that is calculated each time reset is called:
var bottomLeft = project(bounds[0]),
topRight = project(bounds[1]);
Insert something like this (just after the above declarations):
var padding = 25; // In pixels, choose large enough to prevent edge clipping
// of your largest element
bottomLeft = [bottomLeft[0]-padding, bottomLeft[1]+padding]
topRight = [topRight[0]+padding, topRight[1]-padding]

Resources