Reputation: 911
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
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