I have a json file which contain multiple paths
Json file
{
"paths": {
"path1": {
"name": "PathName1",
"path": "m 411.42,469.19 -0.98 ... z",
},
"path2": {
"name": "PathName2",
"path": "m 396.03,243.46 1.48,0.11 0,0 -0.01,0.47 -0.95,1.45 ... z",
}
}
}
And I would like to create an svg from this Json file using d3, but I don't know how to use my paths
var svg = d3.select('#map').append("svg")
.attr("id", "svg")
.attr("width", 500)
.attr("height", 500);
d3.json("urlToJsonFile", function(req, data) {
// how can I show my paths ?
}
If you have several paths, and want to draw them all, you might want to parse your JSON data into a collection to use data()/enter()/exit() joins (or process them in a loop or use some other optimisation technique).
To use the data in the JSON structure you provided you can path objects, bind each "path" object as "datum", and pass the path data to its "d" attribute. This will draw one of your paths:
svg.append("path")
.attr("class", "path1")
.datum(data.paths.path1)
.attr("d", function(d) { return d.path});
Try it out: JSFiddle
Related
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 )
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]);
});
I'm trying to display a map using D3. I have the same map as a geoJSON file and a topoJSON file. When I load in the geoJSON file, it renders on the page and the <path> tag gets filled with d=.... But when I change the url to get the topoJSON file, the <path> tag remains empty, even though I'm leaving the rest of the code untouched. The svg still renders with the topoJSON request, but nothing appears in it. Any thoughts on what might be going on?
My code:
var width = 550;
var height = 570;
var arizonaProjection = d3.geoMercator()
.center([-111.6602, 34.2744])
.scale(4500)
.translate([width/2, height/2]);
var path = d3.geoPath()
.projection(arizonaProjection);
var svg = d3.select("#map").append("svg")
.attr("height", height)
.attr("width", width);
d3.json("geojson/Arizona.geojson", function(error, Arizona) {
svg.append("path")
.attr("d", path(Arizona));
console.log(path);
});
The file geojson/Arizona.geojson is stored in a different directory of my local server, as is the topojson file at topojson/Arizona.json.
Sample of the topoJSON:
"transform":{
"scale":[0.00003998538143372804,0.000031941344085415994],
"translate":[-114.81659,31.33218]
},
"objects":{
"Arizona_88_to_89":{
"type":"GeometryCollection",
"geometries":[
{
"arcs":[[0,1,2,3,4,5]],
"type":"Polygon",
"properties":{
"startcong":"88",
"district":"1",
"statename":"Arizona",
"member":{
"88":{"7845":{"party":"Republican","name":"Rhodes, John Jacob","district":"1"}},
"89":{"7845":{"party":"Republican","name":"Rhodes, John Jacob","district":"1"}}
},
"endcong":"89",
"id":"004088089001"
}
},{
"arcs":[[6,-5,7,-3,8,-1,9,10,11,12]],
"type":"Polygon",
"properties":{
"startcong":"88",
"district":"2",
"statename":"Arizona",
"member":{
"88":{"10566":{"party":"Democrat","name":"Udall, Morris K.","district":"2"}},
"89":{"10566":{"party":"Democrat","name":"Udall, Morris K.","district":"2"}}
},
"endcong":"89",
"id":"004088089002"
}
},{
"arcs":[[-12,13,-10,-6,-7,14]],
"type":"Polygon",
"properties":{
"startcong":"88",
"district":"3",
"statename":"Arizona",
"member":{
"88":{"10623":{"party":"Democrat","name":"Senner, Georg F., Jr.","district":"3"}},
"89":{"10623":{"party":"Democrat","name":"Senner, Georg F., Jr.","district":"3"}}
},
"endcong":"89",
"id":"004088089003"
}
}
]
}
Sample of the geoJSON:
{"type": "FeatureCollection", "features": [{"geometry": {"type": "MultiPolygon", "coordinates": [[[[-112.75515, 33.99991], [-112.75073, 33.99984], [-112.75034, 33.99992], [-112.74655, 33.99991], [-112.74509, 33.9999], [-112.7442, 33.9999], [-112.74395, 33.9999], [-112.74346, 33.99977], [-112.74331, 33.99973], [-112.74262, 33.99955],
A d3 geopath takes a geojson object, not a topology/topojson object. To use topojson with a d3 geoPath, you must first convert it back to geojson. You can do this quite easily with topojson.js:
var featureCollection = topojson.feature(Arizona, Arizona.objects.counties)
Of course you can get the features as an array with:
var features = topojson.feature(Arizona, Arizona.objects.counties).features
The above assumes that topology.objects contains a property for counties, you'll have to take a look at your topojson to find out if counties is correct or not (I'm guessing you might be showing counties). If you used a tool such as mapshaper.org, the property name may be the same as the original file name.
I have build a connection by using d3. The codes show the data and method of connection:
var places = {
TYO: [139.76, 35.68],
BKK: [100.48, 13.75],
BER: [13.40, 52.52],
NYC: [-74.00, 40.71],
};
var connections = {
CONN1: [places.TYO, places.BKK],
CONN2: [places.BER, places.NYC],
};
...
svg.append("path")
.datum({type: "LineString", coordinates: connections.CONN1})
.attr("class", "route")
.attr("d", path);
svg.append("path")
.datum({type: "LineString", coordinates: connections.CONN2})
.attr("class", "route")
.attr("d", path);
You can see my codes, that I use the two identical methods to build two connections. That is not good to build more connections.
I am wondering, if there is a loop function to interpret the connections by using data "connections" directly? I mean, I could get information for data "connections" and use them directly to build connections.
I have thought some ways, such as .datum({type: "LineString", function(d,i) {
return coordinates: connections[i];});. But it does not work.
Could you please tell me some way to solve it? Thanks.
Generally when you want to append many features in d3, you want to use an array not an object. With an array you can use a d3 enter selection which will then allow you to build as many features as you need (if sticking to an object, note that connections[0] is not what you are looking for, connections["conn1"] is).
Instead, use a data structure like:
var connections = [
[places.TYO, places.NYC],
[places.BKK, places.BER],
...
]
If you must have identifying or other properties for each datapoint use something like:
var connections = [
{points:[places.TYO, places.NYC],id: 1,...},
{points:[places.BKK, places.BER],id: 2,...},
...
]
For these set ups you can build your lines as follows:
paths = svg.selectAll(".connection")
.data(connections)
.enter()
.append("path")
.attr("class","connection")
.attr('d', function(d) {
return path ({
type:"LineString",
coordinates: d
});
})
See here. Or:
paths = svg.selectAll(".connection")
.data(connections)
.enter()
.append("path")
.attr("class","connection")
.attr('d', function(d) {
return path ({
type:"LineString",
coordinates: d.points
});
})
Alternatively, you can use a data set up like:
var connections = [
{target:"TYO", source:"NYC"},
{target:"BKK", source: "BER"},
...
]
paths = svg.selectAll(".connection")
.data(connections)
.enter()
.append("path")
.attr("class","connection")
.attr('d', function(d) {
return path ({
type:"LineString",
coordinates: [ places[d.source],places[d.target] ]
});
})
See here.
If selecting elements that don't yet exist, using these lines
d3.select("...")
.data(data)
.enter()
.append("path")
will append a path for each item in the data array - this means that d3 generally avoids the use of for loops as the desired behavior is baked right into d3 itself.
Doing a project in information visualization and wants to draw multiple lines from airport to airport.
Managed to get it working with great arcs, but since there is multiple flights to and from the same airport, I want to have different radiuses on the lines. Is this possible in d3?
EDIT: Here is the current code:
this.formatedflightdata = {type: "FeatureCollection", features: formatFlightData(this.flightdata)};
console.log(this.formatedflightdata);
var line = this.g.selectAll(".arc")
.data(this.formatedflightdata.features);
line.enter().append("path")
.attr("class", "arc")
.attr("fill", "none")
.attr("stroke", "red")
.attr("stroke-width", "2px")
.attr("stroke-linecap", "round")
.attr("opacity", "1")
.attr("d", this.path)
.on("click", function(d) {
console.log("Clicked line!")
});
function formatFlightData(array) {
var data = [];
array.map(function (d, i) {
var feature = {
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [
[d.origlong, d.origlat],
[d.destlong, d.destlat]]
},
"properties": {
"origin": d.ORIGIN,
"destination": d.DEST,
"dayOfMonth": d.DAY_OF_MONTH,
"flightDate": d.FL_DATE,
"carrier": d.CARRIER,
"distance": d.DISTANCE
}
};
data.push(feature);
});
return data;
}
Current result
Yes, it is possible. This is more of a "raw" svg question than a d3 question.
d3 is creating an svg path element. See https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths for more info.
Basically, you can construct your path to have any type of curve that you like. Specific to your example, you may ? want to insert additional elements to the array representing control points. Then, in the .attr("d", portion, construct strings with the appropriate curves.
Untested example:
attr("d", this.path[0] + ""Q 12 17 " + this.path[1])
The "Q 12 17" is trying to make a quadratic curve, with (12,17) as an arbitrarily chosen control point.
I'm having trouble working with json data in D3. The file is read properly, judging from the fact that it appears when I console.log, and seems to be formatted right based on the way all the examples I found. But, when I try to do a nested selection using .data(function(json_data){return json_data.accessibility;}) I get: "cannot read property 'length' of undefined".
My function:
//load scenario json data
d3.json("./SupportTool/scenario1result.json", function(error, json_data){
if(error) {return console.warn(error)};
console.log(json_data); //works
// add main SVG block
var svg = d3.select(d3id)
.append('svg')
.attr('width', 300)
.attr('height', 75)
.attr('id', 'svgblock');
// add an SVG group element for each scenario
var series = svg.selectAll('g.series')
.data(d3.keys(json_data))
.enter()
.append('g')
.attr('class', 'series');
var circles = series.selectAll("circle")
.data(function(json_data){return json_data.accessibility;})
.enter()
.append("circle");
var circleAttributes = circles
.attr("cx", 20)
.attr("cy", 20)
.attr("r", 20)
.style("color","blue"); });
My json data:
{
"meta":[{"sc":"1"},{"stratid":"1"}],
"accessibility":[
{"pop400":"77"},{"pop800":"86"},{"jobs800":"78"},{"pop400tr":"41"},{"pop800tr":"69"},{"jobs800tr":"80"}
],
"housing":[
{"newcom":"0"},{"redev":"100"},
{"apt5":"6"},{"apt4":"65"},{"twn":"14"},{"sglf":"15"},
{"urb":"0"},{"urbhec":"0"}
],
"transport":[
{"walk":"55"},{"transit":"18"},{"auto":"27"},
{"vkt":"11000"},
{"kmtr":"502"},{"form":"grid"},
{"lanekm":"3250"},
{"ghgtr":"67"},{"ghgres":"75"}
],
"costs":[
{"roadcapbils":null,"roadcap":null},
{"transitcapbils":null,"transitcap":null},
{"watercapbils":null,"watercap":null},
{"firecapbils":null,"firecap":null},
{"reccapbils":null,"reccap":null},
{"educapbils":null,"educap":null}
],
"opcosts":[
{"roadopbils":null,"roadop":null},
{"transitoppbils":null,"transitop":null},
{"wateropbils":null,"waterop":null},
{"fireopbils":null,"fireop":null},
{"parksopbils":null,"parksop":null}
] }
The issue you are having comes from the fact that you are binding data to circles using a function of the data already bound to series:
var circles = series.selectAll("circle")
series already has data bound to it from .data(d3.keys(json_data)). Thus, when you log the objects being passed one at a time to the .data() for circles, you just get the keys of json_data, i.e.
["meta", "accessibility", "housing", "transport", "costs", "opcosts"]
Since this is a list of Strings, they do not have any property called accessibility, hence your error.
My guess is that you are trying to append circles for each item in json_data.accessibility, which your code will do if you just replace that line with
.data(json_data.accessibility)
which will pass
[{"pop400":"77"},{"pop800":"86"},{"jobs800":"78"},{"pop400tr":"41"},{"pop800tr":"69"},{"jobs800tr":"80"}]
to data. This code works on my machine, and draws six circles on the page.
One final note is that you should be careful with your variable names. In the function you pass to data, you are redefining json_data as a local variable in that function, which means you can't access your actual JSON data in that function.