Just lately I’ve been working with map information to create interactive visualisations. When working with maps it’s widespread to obtain information as GeoJSON, a JSON format for encoding geographic options, which specifies the kind of geometry and co-ordinates for the options we need to show on a map. Javascript mapping libraries corresponding to Mapbox GL are designed to devour GeoJSON to render options on a canvas. I’m pretty accustomed to utilizing GeoJSON on this means — for instance, rendering geographic areas as completely different colored polygons overlaid on a map to indicate various values for various areas.
However for a latest mission I made a decision to take a unique strategy. Mapbox GL is a good library that gives a variety of helpful options out of the field, like zooming and panning. It’s additionally fairly hefty so far as libraries go, weighing in at 1.4MB un-minified. This mission didn’t require any superior map performance, nonetheless, and solely required the map show to be centred on a specific space, with interactive options on hover.
Within the pursuits of minimising the JS payload for customers, it made sense to render the map as a static SVG, with solely minimal JS wanted for interactivity. That meant I wanted to transform the GeoJSON information I had been supplied with to a static SVG file. In case you end up in an identical place, I’m going to indicate you ways to do that utilizing D3.js. There’s a pre-prepared instance on Codepen, in case you need to skip straight to the code.
Fetching the information
We’ll use the Fetch API to fetch some hosted GeoJSON, which has the .json
suffix. I’ve uploaded an instance file to Codepen, which has a restrict of 5MB for file property. In an actual mission, the GeoJSON file is perhaps a lot greater.
We’ll use the json()
response technique to parse our response information, identical to another JSON response, then we’ll log it to the console. We should always see our parsed information.
const geojsonUrl = 'https://property.codepen.io/85648/map-example.json'
fetch(geojsonUrl)
.then((response) => response.json())
.then((information) => console.log(information))
Relying on our geographic information, our GeoJSON might take completely different codecs. In my case, the information I need to show is a FeatureCollection
, consisting of a number of geographic areas. Alternatively you might need a single Function
, or geographic space, and that might include a number of polygons.
Right here is an instance of a quite simple GeoJSON characteristic. The geometry
sort is Level
, which implies it pinpoints a selected location — helpful in case you’re including a marker to a map, as an example. For drawing geographic areas, the geometry
sort will seemingly be Polygon
or MultiPolygon
.
{
"sort": "Function",
"geometry": {
"sort": "Level",
"coordinates": [125.6, 10.1]
},
"properties": {
"title": "Instance location"
}
}
Creating the SVG
Earlier than rendering our information as an SVG path, we first have to create an empty SVG factor. We might do that in HTML:
<svg width="600" peak="400" viewBox="0 0 600 400"></svg>
Nevertheless, since we’re going to be utilizing D3 anyway, let’s create the SVG in Javascript, the D3 means. That makes it easy to set our SVG dimensions as variables, which we’ll use once more shortly.
It additionally means we will wait till after the browser has efficiently fetched our information and parsed the response earlier than rendering the SVG — and offers us the choice of exhibiting a useful error message to customers in case our request fails.
We’ll set the SVG dimensions as a variable.
const dimensions = {
width: 600,
peak: 400,
}
Then we’ll use D3’s choose()
technique to pick out a component to which to append our SVG. This could possibly be the <physique>
, or another factor. On this case, we’ll use a <div>
with an ID of wrapper
.
We’ll append an SVG factor, then set the width, peak and viewBox attributes.
fetch(geojsonUrl)
.then((response) => response.json())
.then((information) => {
d3.choose('#wrapper')
.append('svg')
.attr('width', dimensions.width)
.attr('peak', dimensions.peak)
.attr('viewBox', `0 0 ${dimensions.width} ${dimensions.peak}`)
})
Changing GeoJSON to an SVG path
Now we’re going to make use of D3’s geographic path generator to generate SVG path strings from our information. We’ll append a path
factor to the SVG and set the d
attribute (the directions for a way to attract the road) from our information. The geoPath()
perform can render a path from a single characteristic or geometry, or from a number of options mixed right into a FeatureCollection
. If now we have a single characteristic we will create a path generator, and name it with information:
const path = d3.geoPath()
d3.choose('#wrapper')
.append('svg')
.attr('width', dimensions.width)
.attr('peak', dimensions.peak)
.attr('viewBox', `0 0 ${dimensions.width} ${dimensions.peak}`)
.append('path')
.attr('d', path(information))
If our information consists of a FeatureCollection
, we’d as a substitute have to render a number of paths. We strategy this barely otherwise, by binding the dataset to our SVG, then appending a path for every of the options within the FeatureCollection
.
d3.choose('#wrapper')
.append('svg')
.attr('width', dimensions.width)
.attr('peak', dimensions.peak)
.attr('viewBox', `0 0 ${dimensions.width} ${dimensions.peak}`)
.selectAll()
.information(information.options)
.be part of('path')
.attr('d', path)
This could create paths from our information. It additionally works if our information incorporates “MultiPolygons” — multidimensional arrays of polygons. Word, we might alternatively use solely the geometry
information from our options as a substitute of your entire Function
object.
Projection and scaling
Though inspecting the SVG factor within the browser may present that we’ve rendered some SVG paths, it’s seemingly they’ll presently be invisible to the viewer. That’s as a result of we haven’t but scaled them to our SVG viewbox, so they could be rendered off-canvas. We have to inform D3 the right way to mission our map parts onto the out there house.
For this we’ll rework the projection, by calling geoIdentity(), utilizing the fitSize()
technique to scale it to our SVG bounding field. Our revised projection is handed in as an argument to d3.geoPath()
, overriding the default projection.
const projection = d3
.geoIdentity()
.fitSize([dimensions.width, dimensions.height], information)
const path = d3.geoPath(projection)
This assumes the highest left SVG co-ordinates must be [0, 0] — in any other case you need to use fitExtent()
which permits us to specify all corners of the bounding field.
Flipping the trail
Now our paths ought to render visibly. However you may discover there’s yet one more difficulty: they’re upside-down. Watch out as a result of this may not be completely apparent with unfamiliar paths. Nevertheless it was definitely noticeable with a map of the UK!
The explanation for that is that customary spatial reference programs deal with the y axis as pointing upwards from 0, whereas within the SVG co-ordinate system the y axis factors downwards, with 0 on the high. Fortunately D3 supplies a technique for reflecting our projection within the y dimension.
const projection = d3
.geoIdentity()
.reflectY(true)
.fitSize([dimensions.width, dimensions.height], information)
const path = d3.geoPath(projection)
Outcome
Try the Codepen demo under to see this in motion. You possibly can change the geojsonUrl
variable with your individual GeoJSON information URL to create an SVG from your individual information.
See the Pen
GeoJSON to SVG by Michelle Barker (@michellebarker)
on CodePen.
As soon as I created this I used to be capable of copy the ensuing SVG code and save the static file to be used in my codebase.
Widespread points drawing paths from GeoJSON
When working with GeoJSON polygon information (notably with map libraries) I typically get an error alongside the traces of “Polygons and MultiPolygons ought to comply with the right-hand rule”. This tends to happen in GeoJSON validators, or when utilizing a library like Mapbox. (I didn’t have this difficulty with the above code.) This pertains to the GeoJSON specification relating to how polygons are “drawn”. It states that “A linear ring MUST comply with the right-hand rule with respect to the world it bounds, i.e. exterior rings are counterclockwise, and holes are clockwise.”
There are a few methods to repair this:
- Within the browser, by importing the file or pasting the code into the mapster-right-hand-rule-fixer instrument.
- Utilizing Mapbox’s rewind package deal,
Each of those will output the polygons within the right format.
Server-side era
After implementing this within the browser I obtained inquisitive about producing SVGs from GeoJSON at build-time. This didn’t take an excessive amount of work, and permits me to simply replace the SVG if the information adjustments.
We have to do that barely otherwise as there is no such thing as a DOM, so we will’t choose parts utilizing D3. However we will nonetheless generate the paths simply, append them to an SVG factor and write it to a file.
We are able to nonetheless use the geoPath()
and geoIdentity()
strategies as beforehand. This time, nonetheless, we’ll map over the options and return a HTML string. As well as the the d
attribute, I’m giving every path a singular ID primarily based on its properties, which will probably be helpful for interplay.
const projection = geoIdentity()
.reflectY(true)
.fitSize([dimensions.width, dimensions.height], information)
const path = geoPath(projection)
const paths = information.options.map((d) => {
return `<path id="${d.properties.title}" d="${path(d)}" />`
})
Then we simply have to append these paths to the SVG factor and write to a file utilizing Node JS’s writeFile() technique.
const fileData = `<svg width="${width}" peak="${peak}" viewBox="0 0 ${width} ${peak}">${paths.be part of('')}</svg>`
writeFile('./src/map-svg.svg', fileData)
Right here’s the complete code:
import { geoPath, geoIdentity } from 'd3'
import { writeFile } from 'node:fs/guarantees'
const geojsonUrl = 'https://property.codepen.io/85648/map-example.json'
const dimensions = {
width: 600,
peak: 300,
}
fetch(geojsonUrl)
.then((response) => response.json())
.then(async (information) => {
strive {
console.log('✨Producing SVG')
const { width, peak } = dimensions
const projection = geoIdentity()
.reflectY(true)
.fitSize([dimensions.width, dimensions.height], information)
const path = geoPath(projection)
const paths = information.options.map((d) => {
return `<path id="${d.properties.title}" d="${path(d)}" />`
})
const fileData = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" peak="${peak}" viewBox="0 0 ${width} ${peak}">
${paths.be part of('')}
</svg>`
await writeFile('./src/map-svg.svg', fileData)
console.log('Completed!')
} catch (error) {
console.error('Error writing file')
}
})
See the Github gist with this code