I am trying to create a custom map projections in D3 using Observable, following the approach given in the d3-geo Docs. Essentially, you pass a function that takes in (lambda, phi) and returns [x, y]. I am aware that an excellent selection of projections are already available in D3 - the desire here is to be able to construct the projection myself using an Observable notebook.

For example, here is the way to replicate the Mercator projection (which matches the equations given on the Wikipedia page, with the exception of scaling using a value for the Earth's radius (which is not needed here - see below):

var mercator = d3.geoProjection(function(x, y) {
  return [x, Math.log(Math.tan(Math.PI / 4 + y / 2))];

The principle works fine in Observable, as is demonstrated by this Equirectangular Map (the simplest possible implementation). Here is the (working) projection function:

projection = d3.geoProjection(function(lambda, phi) {
  return [lambda, phi];

However, if you get much more complicated than that then the map seems to simply disappear. An example of this can be seen with, this Mercator Map, which implements the above Mercator function:

projection = d3.geoProjection(function(lambda, phi) {
  return [lambda, Math.log(Math.tan(Math.PI / 4 + phi / 2))];

I would assume that this was an issue with scaling or something, but the d3-geo docs state that:

The project function does not need to scale or translate the point, as these are applied automatically by projection.scale, projection.translate, and projection.center. Likewise, the project function does not need to perform any spherical rotation, as projection.rotate is applied prior to projection.

Interestingly - it does work if you implement the same in JavaScript:

<!DOCTYPE html>
        <meta charset="utf-8">
        <!-- imports -->
        <script src="https://cdn.jsdelivr.net/npm/d3@4"></script>
        <script src="https://cdn.jsdelivr.net/npm/d3-geo@3"></script>
        <script src="https://cdn.jsdelivr.net/npm/d3-geo-projection@4"></script>
        <script src="https://d3js.org/topojson.v1.min.js"></script>

        <!-- style for the map -->
            .land {
                fill: #eee;
                stroke: #444;
                stroke-width: .5px;
            .graticule  {
                stroke: #ccc;
                stroke-opacity: .5;
                fill: none;

        <!-- this is where the map goes -->
        <div id="map-container"></div>

        <!-- build the map -->

            // set dimensions
            const width = 900, height = 900;

            // projection
            const projection = d3.geoProjection(function(lambda, phi) {
                return [lambda, Math.log(Math.tan(Math.PI / 4 + phi / 2))];

            // make the projection into a path generator
            const path = d3.geoPath().projection(projection);

            // create svg object
            const svg = d3.select("#map-container").append("svg")
                .attr("width", width)
                .attr("height", height);

            // load map data
            d3.json("https://raw.githubusercontent.com/d3/d3.github.com/master/world-50m.v1.json", function(error, world) {
                if (error) throw error;

                // add countries
                    .datum(topojson.feature(world, world.objects.land))
                    .attr("class", "land")
                    .attr("d", path);

                // add graticule
                    .attr("class", "graticule")
                    .attr("clip-path", "url(#clip)")
                    .attr("d", path);

Does anyone have any idea what I am doing wrong?

  • Are you converting lambda and phi to radians? Also, I think you should multiple lambda and the result of the 'y' equation by the sphere's radius or ellipsoid's semimajor axis that you want to use.
    – mkennedy
    Nov 20 at 16:35
  • Thanks for the good suggestions @mkennedy- unfortunately d3 does the conversion to radians before passing to the function (I double checked that this is the case). With respect to the radius, this doesn’t seem make a difference as it all gets re-scaled by D3 anyway when it is drawn to the SVG.
    – jonnyhuck
    Nov 21 at 10:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.