Render GeoJSON with D3 on server: Serve PNG not GeoJSON

Jafar AzizJafar Aziz
4 min read

Background

I have project to show thumbnail of maps from a hundreds GeoJSON files, and this is not efficient if we send geoJSON data to our client (web apps in this case). GeoJSON has a bigger size than png image in small resolution, especially for GeoJSON that has some detailed information.

my app that show thumbnail from maps

We generate images from GeoJSON files on the server and send it to the client. This method makes more sense if you want to show a lot of maps without interaction, like thumbnail.

  • We don't need to use library like leaflet or mapbox to show thumbnail, just show it like any others image.
  • Serve PNG is more efficient than GeoJSON because for small resoulution png has smaller size than GeoJSON files.

💻 Coding

TL;DR Go to conclusion to get the source code and geojson example

To begin, first you need to create a new node.js project, and install following package

npm install d3 jsdom sharp

d3 and jsdom to render svg maps from geoJSON files on the server, and sharp to convert svg to png. I am settings this project to module in package.json so I can use import statements, or use require otherwise.

import sharp from 'sharp';
import { JSDOM } from 'jsdom';
import { promises as fs } from "fs"
import { select, geoPath, geoMercator } from 'd3';

const removeExtension = (str) => str.split('.').slice(0, -1).join('.')

const createGeoJSONImage = async (filename) => {
  const WIDTH = 256

  const HEIGHT = 256;

  const COLORS = ['#63e6be', '#38d9a9', '#20c997', '#12b886'];

  let geoJSON
  try {
    geoJSON = JSON.parse(await fs.readFile(filename, 'utf8'))
  } catch (error) {
    console.log(error)
    return 0
  }

  const window = (new JSDOM(undefined, { pretendToBeVisual: true })).window;

  window.d3 = select(window.document);

  const svg = window.d3.select('body')
    .append('div').attr('class', 'container')
    .append('svg')
    .attr('xmlns', 'http://www.w3.org/2000/svg')
    .attr('width', WIDTH)
    .attr('height', HEIGHT)
    .append('g')

  const projection = geoMercator().fitSize([WIDTH, HEIGHT], {
    "type": "FeatureCollection",
    "features": geoJSON.features
  });

  const geoGenerator = geoPath()
    .projection(projection)

  svg.selectAll('path')
    .data(geoJSON.features)
    .join("path")
    .attr("d", geoGenerator)
    .attr('fill', (d, i) => COLORS[i % 4])
    .attr('stroke', '#fff');


  await sharp(Buffer.from(window.d3.select('.container').html()))
    .png()
    .toFile(`${removeExtension(filename)}.png`)
}

Let's breakdown the code above. that code generate a choropleth from geoJSON. First of all we set jsdom to show d3 svg element. It's like we create a simple HTML dom, but this is programtically with jsDOM.

  // .... rest of code

  const window = (new JSDOM(undefined, { pretendToBeVisual: true })).window;

  window.d3 = select(window.document);

  const svg = window.d3.select('body')
    .append('div').attr('class', 'container')
    .append('svg')
    .attr('xmlns', 'http://www.w3.org/2000/svg')
    .attr('width', WIDTH)
    .attr('height', HEIGHT)
    .append('g')

  // .... rest of code

next step we show the geoJSON files to mercator projection using d3.js. You can use other projection, but you need to experiment by yourself to get better result.

  // .... rest of code

  const projection = geoMercator().fitSize([WIDTH, HEIGHT], {
    "type": "FeatureCollection",
    "features": geoJSON.features
  });

  const geoGenerator = geoPath()
    .projection(projection)

  // .... rest of code

Then we show our projection of GeoJSON files to svg dom we created before.

  // .... rest of code

  svg.selectAll('path')
    .data(geoJSON.features)
    .join("path")
    .attr("d", geoGenerator)
    .attr('fill', (d, i) => COLORS[i % 4])
    .attr('stroke', '#fff');

  // .... rest of code

In that code I am using simple rule .attr('fill', (d, i) => COLORS[i % 4]) to create add color for each features. You can create your function to make it more beautiful choropleth maps, e.g with some geojson properties (You can access geoJSON properties from d.properties.properties_name). The last step, we save svg dom to png using sharp.

To generate multiple GeoJSON files, just create script to run the function automatically. You can read all GeoJSON files from a folder then run the script.

Drawback

Sometimes you can't get best result when generating image from GeoJSON. In my case, D3 can't render some specific GeoJSON features that too small to render, and I must search and remove manually (using QGIS) that specific features.

failed to render geojson

In my example, I wanna make continent images from geoJSON. You can see, Affrica does not exist, so I edited to remove some fetures with QGIS and run the script again and I get the best result. The trickiest part is find the feature that cause this happen, sometimes I export to svg to find that features.

Conclusion

This method is useful for generating a lot of images from GeoJSON files. You can use it to another use case. You can get all the code and example from this link.

0
Subscribe to my newsletter

Read articles from Jafar Aziz directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jafar Aziz
Jafar Aziz