3

I'm trying to create a cartogram using cartogram.js and d3.js. I've used the examples found in the cartogram.js repo and here to put together a script that generates a world map inside an SVG using the d3.geo.mercator() projection and now I'm trying to distort the map using the cartogram.js library however I'm getting the following error:

d3.js:8756 Error: <path> attribute d: Expected number, "MNaN,NaNLNaN,NaNL…".
   (anonymous function) @ d3.js:8756
   tick @ d3.js:8956
   (anonymous function) @ d3.js:8936
   d3_timer_mark @ d3.js:2166
   d3_timer_step @ d3.js:2147

Here's my code I'm using to distort the map:

var dataLoaded = new Event("dataLoaded"),
svg = d3.select("svg"),
proj = d3.geo.mercator(),
path = d3.geo.path()
    .projection(proj),
countries = svg.append("g")
    .attr("id", "countries")
    .selectAll("path"),
carto = d3.cartogram()
    .projection(proj)
    .properties(function(d) {
        return d.properties
    }),
mapData = d3.map(),
geometries,
topology

function init() {
    d3.csv("data/data.csv", function(data) {
        data.forEach(function (d) {
            mapData.set(d.COUNTRY, d.VALUE)
        })
    })

    d3.json("data/world.json", function(data) {
        topology = data
        geometries = topology.objects.countries

        var features = carto.features(topology, geometries)

        countries = countries
            .data(features)
            .enter()
            .append("path")
            .attr("fill", function (e) {
                return "#000000"
            })
            .attr("d", path)

        document.dispatchEvent(dataLoaded)
    })
}

document.addEventListener("dataLoaded", function() {
    $("#container").css("visibility", "visible").hide().fadeIn("fast")
    $("header").css("visibility", "visible").hide().fadeIn("slow")

     carto.value(function(d) {
        return +mapData.get(d.properties.name)
     })

     countries.data(carto(topology, geometries).features)

     countries.transition()
        .duration(750)
        .attr("d", carto.path);
})

init()

and the CSV file containing the data I want to use to distort the map:

COUNTRY,VALUE
Afghanistan,90
Albania,390
Algeria,90
Andora,110
Angola,10
Antigua,2400
Argentina,320
Armenia,40
Australia,6600
Austria,1300
Axerbaijan,0
Bahamas,1900
Bahrain,90
Bangladesh,50
Barbados,8100
Belarus,20
Belgium,260
Belize,480
Benin,0
Bhutan,170
Bolivia,90
Bosnia,70
Botswana,110
Brazil,1300
Brunei,40
Bulgaria,3600
Burkina Faso,0
Burundi,0
Cabo Verde,0
Cambodia,720
Cameroon,10
Canada,4400
Central African Republic,0
Chad,10
Chile,320
China,1600
Combodia,0
Comoros,10
Congo,20
Costa Rica,2900
Cote d'Ivoire,0
Croatia,9900
Cuba,14800
Cyprus,8100
Czech Republic,70
Denmark,320
Dijbouti,0
Dominica,0
Dominican Republic,4400
Ecuador,90
Egypt,6600
El Salvador,10
Equatorial Guinea,0
Eritrea,10
Estonia,110
Ethiopia,70
Fiji,1900
Finland,720
France,2900
Gabon,10
Gambia,2400
Georgia,70
Germany,880
Ghana,210
Greece,14800
Grenada,720
Guatemala,40
Guinea,0
Guinea - Bissau,0
Guyana,50
Haiti,90
Honduras,110
Hungary,170
Iceland,8100
India,2900
Indonesia,390
Iran,390
Iraq,140
Ireland,1900
Israel,590
Italy,9900
Jamaica,6600
Japan,3600
Jordan,480
Kazakhstan,40
Kenya,1000
Kiribati,10
Kosovo,10
Kuwait,40
Kyrgyzstan,10
Laos,70
Latvia,110
Lebanon,70
Lesotho,0
Liberia,10
Libya,30
Liechtenstein,10
Lithuania,70
Luxembourg,50
Macedonia,70
Madagascar,0
Malawi,40
Malaysia,1300
Maldives,12100
Mali,40
Malta,12100
Marshall Islands,10
Mauritania,10
Mauritius,6600
Mexico,18100
Micronesia,20
Moldova,20
Monaco,590
Mongolia,110
Montenegro,880
Morocco,4400
Mozambique,90
Myanmar,90
Namibia,210
Nauru,10
Nepal,0
Netherlands,50
New Zealand,1900
Nicaragua,50
Niger,10
Nigeria,90
North Korea,390
Norway,1600
Oman,590
Pakistan,110
Palau,50
Palestine,10
Panama,210
Papua New Guinea,40
Paraguay,10
Peru,1000
Philippines,590
Poland,880
Portugal,12100
Qatar,210
Romania,320
Russia,480
Rwanda,20
Saint Kitts and Nevis,0
Saint Lucia,90
Saint Vincent and the Grenadines,0
Samoa,90
San Marino,70
Sao Tome and Principe,10
Saudi Arabia,110
Senegal,70
Serbia,50
Seychelles,1600
Sierra Leone,20
Singapore,880
Slovakia,70
Slovenia,390
Solomon Islands,10
Somalia,70
South Africa,1900
South Korea,140
South Sudan ,0
Spain,14800
Sri Lanka,3600
Sudan,20
Suriname,10
Sweden,720
Switzerland,1300
Syria,590
Taiwan,50
Tajikistan,10
Tanzania,260
Thailand,14800
Timor-Leste,0
Togo,10
Tonga,50
Trinidad and Tobago,140
Tunisia,4400
Turkey,9900
Turkmenistan,10
Tuvalu,30
Uganda,50
Ukraine,70
United Arab Emirates,20
United Kingdom,50
United States of America,3600
Uruguay,50
Uzbekistan,30
Vanuatu,30
Vatican City,30
Venezuela,170
Vietnam,2400
Yemen,20
Zambia,90
Zimbabwe,70

I don't have any experience using d3.js prior to this project so I would appreciate any feedback/guidance you can give me.

I'm using version 3.5.17 of d3, fyi.

Thanks.


UPDATE - 9/8/2016 15:22 BST

As per @Mark's suggestion, I've implemented d3-queue, although the problem still persists. If I've done anything wrong with this implementation, however, I'd be grateful for any insight anyone can give me! :)

var svg = d3.select("svg"),
proj = d3.geo.mercator(),
path = d3.geo.path()
    .projection(proj),
countries = svg.append("g")
    .attr("id", "countries")
    .selectAll("path"),
carto = d3.cartogram()
    .projection(proj)
    .properties(function(d) {
        return d.properties
    }),
queue = d3.queue()
    .defer(csv)
    .defer(json)
    .awaitAll(ready),
mapData = d3.map(),
geometries,
topology

function json(callback) {
    d3.json("data/world.json", function(data) {
        topology = data
        geometries = topology.objects.countries

        var features = carto.features(topology, geometries)

        countries = countries
            .data(features)
            .enter()
            .append("path")
            .attr("fill", function (e) {
                return "#000000"
            })
            .attr("d", path)

        callback()
    })
}

function csv(callback) {
    d3.csv("data/data.csv", function(data) {
        data.forEach(function (d) {
            mapData.set(d.COUNTRY, +d.VALUE)
        })

        callback()
    })
}

function ready() {
    $("#container").css("visibility", "visible").hide().fadeIn("fast")
    $("header").css("visibility", "visible").hide().fadeIn("slow")

    carto.value(function(d) {
        if (mapData.has(d.properties.name)) {
            return +mapData.get(d.properties.name)
        }
    })

    countries.data(carto(topology, geometries).features)

    countries.transition()
        .duration(750)
        .attr("d", carto.path);
}

UPDATE 2 - 9/8/2016 18:05 BST

Here is the latest version of the script on Plunker which can be used for testing, courtesy of @Mark: http://plnkr.co/edit/iK9EZSIfwIXjIEBHhlep?p=preview

It seems my initial error has been fixed although the resulting cartogram isn't displaying correctly.


UPDATE 3 - 10/8/2016 20:45 BST

@Mark's answer helped clarify a lot of my issues and I had a partially functioning cartogram as a result however to fix the issue detailed here, I regenerated my map file using the --stitch-poles false parameter and and after doing this I am once again receiving the following error:

d3.js:8756 Error: <path> attribute d: Expected number, "MNaN,NaNLNaN,NaNL…".

@Mark's initial fix for this error is still in place therefore I'm quite confused as to why this has resurfaced. You can see my latest code here and my new map topojson file here. Thanks again.

James Brooks
  • 33
  • 1
  • 1
  • 7
  • Where is `carto.path` defined? – Gerardo Furtado Aug 09 '16 at 10:18
  • 1
    @GerardoFurtado Inside cartogram.js `carto.path = d3.geo.path().projection(null);` – James Brooks Aug 09 '16 at 10:25
  • 1
    Both, `d3.csv()` and `d3.json()` are asynchronous. Calling them sequentially like you are doing in `init()` will still have them being executed in parallel giving you no control about the order in which these functions will return. My guess is, that the `dataLoaded` event is fired before `d3.csv()` returns, thus leaving `mapData` only sparsely populated when trying to access it the event handler. – altocumulus Aug 09 '16 at 11:42
  • @altocumulus I had thought this might be the problem prior to posting although the data is being loaded in time as proven by several `console.log()` statements, printing out the data at each stage of the script. – James Brooks Aug 09 '16 at 12:13
  • `data is being loaded in time as proven by several console.log() statements`, this is a classic race condition and only *sometimes* that'll be true. Also, `console.log` can sometimes "lie", if it points to an object reference, it'll show you the object as it is while you inspect, not what it was when you `console.log`ed it. I'd start by restructuring your code with [d3.queue](https://github.com/d3/d3-queue) or at least put a check for completion of both async functions before raising your event. – Mark Aug 09 '16 at 13:49
  • @Mark I've implemented d3.queue as you suggested to no avail - the error still persists. – James Brooks Aug 09 '16 at 14:15
  • @Mark I've updated my code above. – James Brooks Aug 09 '16 at 14:25
  • Trying to reproduce your code, where are getting your `world.json` from? I'm attemping to use [these files](https://github.com/mbostock/topojson/tree/master/examples) and it doesn't match up. The countries don't have a `name` property. – Mark Aug 09 '16 at 15:38
  • @Mark world.json is a topojson file that I generated using the command line tools. Here's a [link](https://gist.githubusercontent.com/brks/de43e82d3b68be95c5fa77e6934e4683/raw/27b79feca4cf128f0fc3e740f19a25a1999879af/world.json). – James Brooks Aug 09 '16 at 15:45
  • So the root of your problem is the `carto.value` accessor function. You don't return a value if `mapData` **does not have** the country name. But, fixing that, doesn't really make a cartogram. See my work [here](http://plnkr.co/edit/iK9EZSIfwIXjIEBHhlep?p=preview) – Mark Aug 09 '16 at 16:18
  • @Mark I had previously thought this and implemented a solution although the lack of a cartogram in the end result caused me to think there was something else wrong with my code. – James Brooks Aug 09 '16 at 16:31
  • Hmm, I'm stumped and unfortunately have to get back to my day job. I'll try and come back later to poke a little harder if no one else has any good ideas. You should probably update your question with my link above so people don't need to recreate from scratch. – Mark Aug 09 '16 at 16:38
  • @Mark Okay, thank you for all of your help so far! :) – James Brooks Aug 09 '16 at 17:14
  • @Mark I've added a new update, any chance you can lend a hand? – James Brooks Aug 11 '16 at 07:15

1 Answers1

1

Okay, I'm making progress. It turns out that after fixing your .value function the reason you don't get a catrogram is the your values are too disparate. Why this throws off cartogram.js, I'm unsure, but the problem can be easily solved by introducing a scale.

With your data:

s = d3.scale.linear().range([1,100]).domain(d3.extent(data, function(d){ return  +d.VALUE}));

And then in your .value accessor:

carto.value(function(d,i) {
  if (mapData.has(d.properties.name)) {
    return s(mapData.get(d.properties.name));
  } else {
    return 1;
  }
});

Alas, though, all your problems aren't fixed. It seems that countries that "wrap" the projection (ie Russia and Fiji) get distorted by the paths generated by cartogram.js. Here's a fix though, discussed at length here

Regardless of that, here's what we've got so far.

Mark
  • 106,305
  • 20
  • 172
  • 230
  • Thank you! I've created a new topojson file (found [here](https://gist.github.com/brks/e04f62bd9e96d65ca4a1b91913c6a889) based on that thread although I seem to be back where I started with the initial `NaN` error reappearing and I have no idea why as the only thing I changed was the json file for the map. – James Brooks Aug 10 '16 at 09:17
  • I should add that the cartogram was working before I switched the map file. – James Brooks Aug 10 '16 at 09:25
  • Here's an up to date Plunker to demonstrate: http://plnkr.co/edit/1kFV484rtIdSGLaMsdFb?p=preview – James Brooks Aug 10 '16 at 10:02
  • @JamesBrooks, your troubles lie in Antarctica. Debugging into `cartogram.js`, you'll find that on line 80 when it computes the `area` of Antarctica, it returns `NaN`, all sorts of math is then done on these `NaN`s resulting in all sorts of trouble. To prove this out, [I removed Antarctica and all is well](http://plnkr.co/edit/sK5bVweoUEbV4F47Oi79?p=preview). – Mark Aug 11 '16 at 14:20
  • Wow, I wouldn't have figured that out at all. Thank you! – James Brooks Aug 11 '16 at 14:45