3

I'm having a problem drawing a map in D3.js

The code below works fine for some json files but on the data I need to use it's returning a string of errors similar to this:

Error: attribute d: Expected number, "….21478248741596,NaNL547.70469610…".

And displaying this image (which although pretty is not the map of England I was hoping for)

This is the code:

const width = 800
const height = 800

const svg = d3.select('svg')
  .append('g')
  .attr('class', 'map');

const projection = d3.geoMercator()

Promise.all([
  d3.json('https://raw.githubusercontent.com/joewretham/d3_map_project/main/Clinical_Commissioning_Groups__April_2019__Boundaries_EN_BUC.json')
]).then(
  d => ready(null, d[0], d[1])
);

function ready(error, data) {

  var fixed = data.features.map(function(feature) {
    return turf.rewind(feature, {
      reverse: true
    });
  });

  const path = d3.geoPath().projection(projection);

  projection.fitSize([width, height], {
    "type": "FeatureCollection",
    "features": fixed
  })

  svg.append('g')
    .attr('class', 'countries')
    .selectAll('path')
    .data(fixed)
    .enter().append('path')
    .attr('d', path)
    .style('fill', "teal")
    .style('stroke', 'white')
    .style('opacity', 0.8)
    .style('stroke-width', 0.3);
};
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src='https://npmcdn.com/@turf/turf/turf.min.js'></script>
<svg id="partitionSVG" width="800" height="800">"</svg>

Does anyone know what's happening here? Because the code works with other sets of data I know it's something to do with the coordinates and I'm guessing they need to be transformed in some way but I don't know enough about geographic data to know what to do.

Thanks for any suggestions

Joe

Ruben Helsloot
  • 12,582
  • 6
  • 26
  • 49
joe_w
  • 43
  • 3
  • Please turn your code into a runnable [mre], using stack snippets or JSFiddle/Codepen. It will help us understand the problem and will lead to better answers – Ruben Helsloot Nov 16 '20 at 10:31
  • Your JSON file has some uncommon projection. Normally, coordinates are given in lat/long, but there are many different projections out there - the most common is WGS84 (also called 4326, by it's ID). The current projection seems to be in metres or feet from a certain zero point. I don't know the current projection, if you find it out, you can transform the JSON to WGS84. – Ruben Helsloot Nov 16 '20 at 11:28
  • Hi Ruben - thanks for the response. You walked me to the solution - I went back to where I got the data from (https://geoportal.statistics.gov.uk/datasets/clinical-commissioning-groups-april-2019-boundaries-en-buc-1/geoservice) and I found that the API they have available has the coordinates in WGS84. Updated the fiddle - https://jsfiddle.net/joe_wretham/fm1ud063/13/. Thanks for your help – joe_w Nov 16 '20 at 11:44
  • Great! Happy to help! – Ruben Helsloot Nov 16 '20 at 11:51

1 Answers1

2

Your JSON file has some uncommon projection. Normally, coordinates are given in lat/long. However, because of many reasons, like the fact that the earth is a sphere and sphere projections are mathematically difficult, several different projections exist. The most common is WGS84 (also called 4326, by its ID), and uses lat/long format. But, it turns out several national systems exist. In your case, it is the UK national system OSGB36.

Using this package, I was able to transform the projection from OSGB36 to WGS84, which gives the correct result:

const width = 800
const height = 800

const svg = d3.select('svg')
  .append('g')
  .attr('class', 'map');

console.log(window);

const projection = d3.geoMercator()

Promise.all([
  d3.json('https://raw.githubusercontent.com/joewretham/d3_map_project/main/Clinical_Commissioning_Groups__April_2019__Boundaries_EN_BUC.json')
]).then(
  d => ready(null, d[0], d[1])
);

function ready(error, data) {
  data = returnExports.toWGS84(data);

  var fixed = data.features.map(function(feature) {
    return turf.rewind(feature, {
      reverse: true
    });
  });

  const path = d3.geoPath().projection(projection);

  projection.fitSize([width, height], {
    "type": "FeatureCollection",
    "features": fixed
  })

  svg.append('g')
    .attr('class', 'countries')
    .selectAll('path')
    .data(fixed)
    .enter().append('path')
    .attr('d', path)
    .style('fill', "teal")
    .style('stroke', 'white')
    .style('opacity', 0.8)
    .style('stroke-width', 0.3);
};
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src='https://npmcdn.com/@turf/turf/turf.min.js'></script>
<script src='https://unpkg.com/proj4@2.6.2/dist/proj4-src.js'></script>
<script src='https://unpkg.com/gbify-geojson@0.2.1/index.js'></script>
<svg id="partitionSVG" width="800" height="800">"</svg>

If you have access to the generation of the file, you can change the input file once, or you can just call this function whenever necessary.

Ruben Helsloot
  • 12,582
  • 6
  • 26
  • 49
  • Thanks Ruben - that is brilliant, you've fixed my problem and I've learned about projections – joe_w Nov 16 '20 at 11:49
  • 1
    *Technically* WGS84 isn't a projection: it's a 3D coordinate system, whereas the projected data the OP is 2D - as D3 projections project data, you need to unproject the data before passing through the D3 projection (as the answer does). However, you could also use `d3.geoIdentity.fitSize/fitExtent` to scale and pan the data properly, treating it as planar (though overlaying additional features would be difficult) rather than unprojecting and immediately re-projecting, this would also remove the need for `turf.rewind`. Nice answer regardless. – Andrew Reid Nov 17 '20 at 02:59
  • Ah good to know, thanks! – Ruben Helsloot Nov 17 '20 at 08:00