DMrFrost
DMrFrost

Reputation: 911

Access topojson object to change map country properties in d3.js

I am building a web application to display different trends and stats between countries in the world. With d3, I am able to load the topojson file and project the world map.

  var countryStatistics = [];
  var pathList = [];

function visualize(){

var margin = {top: 100, left: 100, right: 100, bottom:100},
    height = 800 - margin.top - margin.bottom, 
    width = 1200 - margin.left - margin.right;

 //create svg
var svg = d3.select("#map")
      .append("svg")
      .attr("height", height + margin.top + margin.bottom)
      .attr("width", width + margin.left + margin.right)
      .append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

  //load topojson file
d3.queue()
  .defer(d3.json, "world110m.json")
  .await(ready)  

var projection = d3.geoMercator()
  .translate([ width / 2, height / 2 ])
  .scale(180)

  //pass path lines to projection
var path = d3.geoPath()
.projection(projection);

function ready (error, data){
  console.log(data);

    //we pull the countries data out of the loaded json object
  countries = topojson.feature(data, data.objects.countries).features

    //select the country data, draw the lines, call mouseover event to change fill color
  svg.selectAll(".country")
    .data(countries)
    .enter().append("path")
    .attr("class", "country")
    .attr("d", path)
    .on('mouseover', function(d) {
      d3.select(this).classed("hovered", true)
        //this function matches the id property in topojson country, to an id in (see below)
      let country = matchPath(this.__data__.id);
      console.log(country)
    })
    .on('mouseout', function(d) {
      d3.select(this).classed("hovered", false)
    })
      //here I push the country data into a global array just to have access and experimentalism. 
    for (var i = 0; i < countries.length; i++) {
      pathList.push(countries[i]);
    } 
  }
};

The matchPath() function is used to allow me to match the path data to countryStatistics for display when a certain country is mouseovered.

function matchPath(pathId){
    //to see id property of country currently being hovered over
  console.log("pathID:" + pathId)
    //loop through all countryStatistics and return the country with matching id number
  for(var i = 0; i < countryStatistics.length; i++){
    if(pathId == countryStatistics[i].idTopo){
      return countryStatistics[i];
    }
  }
}

The Problem: This works, but only in one direction. I can reach my statistical data from each topojson path... but I can't reach and manipulate individual paths based on the data.

What I would like to happen, is to have a button that can select a certain property from countryStatistics, and build a domain/range scale and set a color gradient based on the data values. The step I am stuck on is getting the stat data and path data interfacing.

Two potential solutions I see,

1:There is a way to connect the topo path data to the statistical data during the render, I could call a function to redraw sgv...

2:I build a new object that contains all of the path data and statistical data. In this case can I just pull out the topojson.objects.countries data and ignore the rest?

How should I achieve this? Any pointers, next step will be appreciated.

(where I am at with this project... http://conspiracytime.com/globeApp )

Upvotes: 0

Views: 1680

Answers (1)

luissevillano
luissevillano

Reputation: 122

TopoJSON is a really powerfull tool. It has its own CLI (command line interface) to generate your own TopoJSON files.

That CLI allows you to create a unique file with the topology and the data you want to merge with.

Even though it is in its v3.0.2 the first versión looks the clear one to me. This is an example of how you can merge a csv file with a json through a common id attribute.

# Simplified versión from https://bl.ocks.org/luissevillano/c7690adccf39bafe583f72b044e407e8
# note is using TopoJSON CLI v1
topojson \
  -e data.csv \
  --id-property CUSEC,cod \
  -p population=+t1_1,area=+Shape_area \ 
  -o cv.json \
  -- cv/shapes/census.json

There is a data.csv file with a cod column and census.json file with a property named CUSEC. - Using the --id-property you can specify which attributes will be used in the merge. - With the property -p you can create new properties on the fly.

This is the solid solution where you use one unique file (with one unique request) with the whole data. This best scenario is not always possible so another solution could be the next one.

Getting back to JavaScript, you can create a new variable accessible through the attribute in common the following way. Having your data the format:

// countryStatistics
{
  "idTopo": "004",
  "country": "Afghanistan",
  "countryCode": "afg",
  // ..
},

And your TopoJSON file the structure:

{"type":"Polygon","arcs":[[0,1,2,3,4,5]],"id":"004"},
{"type":"MultiPolygon","arcs":[[[6,7,8,9]],[[10,11,12]]],"id":"024"} // ...



A common solution to this type of situation is to create an Array variable accessible by that idTopo:

var dataById = [];
countryStatistics.forEach(function(d) {
    dataById[d.idTopo] = d;
});

Then, that variable will have the next structure:

[
    004:{
      "idTopo": "004",
      "country": "Afghanistan",
      "countryCode": "afg",
      //...
  },
    008: {
      //...
    }
]

From here, you can access the properties through its idTopo attribute, like this:

dataById['004'] // {"idTopo":"004","country":"Afghanistan","countryCode":"afg" ...}

You can decide to iterate through the Topo data and add these properties to each feature:

var countries = topojson
  .feature(data, data.objects.countries)
  .features.map(function(d) {
    d.properties = dataById[d.id];
    return d
  });

Or use this array whenever you need it

// ...
.on("mouseover", function(d) {
  d3.select(this).classed("hovered", true);
  console.log(dataById[d.id]);
});

Upvotes: 1

Related Questions