The d3.geo.path has a null projection because the TopoJSON is already projected, so it can be displayed as-is. I'm trying to plot data in the form of [longitude, latitude] on a map.
Below is what my code basically looks like:
var width, height, path, svg;
width = 960;
height = 600;
path = d3.geo.path().projection(null);
svg = d3.select('.viz').append('svg')
.attr('width', width)
.attr('height', height);
d3.json("counties.json", function(error, us) {
svg.append('path')
.datum(topojson.mesh(us))
.attr('d', path);
});
svg.selectAll('.pin')
.data(ds) // i.e. ds = {[12.521, 15.312], [616.122,-31.160]}
.enter().append('circle', '.pin')
.attr('r', 3)
.attr('transform', function (d) {
return 'translate(' + path([
d.longitude,
d.latitude
]) + ')';
});
I debugged through this, and I do get the data fine. But, I am getting an error that "path([d.longitude, d.latitude])" is undefined. "d" exists with longitude and latitude values. "path" also exists. I think it's because the projection is null.
What can I do to make this work?
------- EDIT -------
I followed the recommendation by Ben Lyall to remove the use of "path" in the selectAll statement as well as moving the selectAll statement inside the .json(). I also noticed I put in an incorrect example for ds, so I modified the comment. Here is my updated code.
This displays the map fine with no console errors, but I still don't see any circles in the map itself.
var width, height, path, svg;
width = 960;
height = 600;
path = d3.geo.path().projection(null);
svg = d3.select('.viz').append('svg')
.attr('width', width)
.attr('height', height);
d3.json("counties.json", function(error, us) {
svg.append('path')
.datum(topojson.mesh(us))
.attr('d', path);
svg.selectAll('.pin')
.data(ds) // i.e. ds = [{longitude: 12.521, latitude: 15.312}, {longitude: 616.122, latitude: -31.160}]
.enter().append('circle', '.pin')
.attr('r', 3)
.attr('transform', function (d) {
return 'translate(' +
d.longitude + ',' + d.latitude +
')';
});
});
------- EDIT -------
The solution was a combination of Ben Lyall's proposed solution as well as taking into account the projection already done on the map for the .pins. Since the projection in code is null, I had to create a new one that matches the projection for the map and then use that when performing the transform on the .pin's.
Below is the solution:
var width, height, path, projection, svg;
width = 960;
height = 600;
path = d3.geo.path().projection(null);
projection = d3.geo.albersUsa().scale(1280).translate([width/2, height/2]);
svg = d3.select('.viz').append('svg')
.attr('width', width)
.attr('height', height);
d3.json("counties.json", function(error, us) {
svg.append('path')
.datum(topojson.mesh(us))
.attr('d', path);
svg.selectAll('.pin')
.data(ds)
.enter().append('circle', '.pin')
.attr('r', 3)
.attr('transform', function (d) {
return 'translate(' +
projection([d.longitude, d.latitude]) +
')';
});
});
When you're positioning your .pin elements, why are you using path in your translate? You don't want to create a path, and the d.latitude and d.longitude values should already be in pixel co-ordinates, since they're projected already, so you should be able to use them directly.
Note: you probably also want that part of your code inside the d3.json handler, rather than outside, since it'll run asynchronously before your data is set to anything (this is probably the actual issue with your code, rather than using path incorrectly).
It's a little hard to confirm without an example to fork, but try this:
var width, height, path, svg;
width = 960;
height = 600;
path = d3.geo.path().projection(null);
svg = d3.select('.viz').append('svg')
.attr('width', width)
.attr('height', height);
d3.json("counties.json", function(error, us) {
svg.append('path')
.datum(topojson.mesh(us))
.attr('d', path);
svg.selectAll('.pin')
.data(ds) // i.e. ds = {[12.521, 15.312], [616.122, -31.160]}
.enter().append('circle', '.pin')
.attr('r', 3)
.attr('transform', function (d) {
return 'translate(' + d.longitude + "," + d.latitude + ')';
});
});
Related
I am creating a sunburst for big data. To make it more readable, I need to assign different color for each node (ideally different shades of the same color for every subtree).
I've already tried with :
d3.scaleSequential()
d3.scale.ordinal()
d3.scale.category20c()
I think it can work but I am not sure where to put it exactly. For the moment it works only with one color for every subtree.
var width = 500;
var height = 500;
var radius = Math.min(width, height) / 2;
var color = d3.scaleSequential().domain([1,10]).interpolator(d3.interpolateViridis);
var g = d3.select('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
var partition = d3.partition() //.layout
.size([2 * Math.PI, radius]);
d3.json("file:///c:\\Users\\c1972519\\Desktop\\Stage\\tests_diagrams\\figure_4.8_ex3\\data2.json", function(error, nodeData){
if (error) throw error;
var root = d3.hierarchy(nodeData)
.sum(function(d){
return d.size;
});
partition(root);
var arc = d3.arc()
.startAngle(function(d) { return d.x0; })
.endAngle(function(d) { return d.x1; })
.innerRadius(function(d) { return d.y0; })
.outerRadius(function(d) { return d.y1; });
var arcs = g.selectAll('g')
.data(root.descendants())
.enter()
.append('g')
.attr("class", "node")
.append('path')
.attr("display", function (d) { return d.depth ? null : "none"; })
.attr("d", arc)
.style('stroke', '#fff')
.style("fill", function(d){return color(d)});
}
So I would like to have different shade on every subtree to make it more readable.
Anyone have an idea?
can you try with scaleLinear.
var x = d3.scaleLinear([10, 130], [0, 960]);
or
var color = d3.scaleLinear([10, 100], ["brown", "steelblue"]);
Example:
https://bl.ocks.org/starcalibre/6cccfa843ed254aa0a0d
Documentation:
https://github.com/d3/d3-scale/blob/master/README.md#scaleLinear
Linear Scales
d3.scaleLinear([[domain, ]range]) <>
Constructs a new continuous scale with the specified domain and range, the default interpolator and clamping disabled. If either domain or range are not specified, each defaults to [0, 1]. Linear scales are a good default choice for continuous quantitative data because they preserve proportional differences. Each range value y can be expressed as a function of the domain value x: y = mx + b.
I am trying to make a map in D3 of the 12 provinces of the Netherlands and color them based on some mock data in an external JSON file:
[{"_id":"Groningen","value":52},
{"_id":"Friesland","value":18},
{"_id":"Drenthe","value":87},
{"_id":"Overijssel","value":93},
{"_id":"Flevoland","value":60},
{"_id":"Gelderland","value":7},
{"_id":"Utrecht","value":26},
{"_id":"Noord-Holland","value":52},
{"_id":"Zuid-Holland","value":72},
{"_id":"Zeeland","value":41},
{"_id":"Noord-Brabant","value":78},
{"_id":"Limburg","value":19}]
This is the code I have thus far. It successfully generates them map but right now colors each province as a function of the length of the province name (which is silly ofcourse):
var width = 960,
height = 500,
centered;
// Define color scale
var color = d3.scaleLinear()
.domain([1, 20])
.clamp(true)
.range(['steelblue', "yellow"]);
var projection = d3.geoMercator()
.scale(5500)
.center([6, 52.1])
.translate([width / 2, height / 2]);
var path = d3.geoPath()
.projection(projection);
// Set svg width & height
var svg = d3.select('svg')
.attr('width', width)
.attr('height', height);
// Add background
svg.append('rect')
.attr('class', 'background')
.attr('width', width)
.attr('height', height)
var g = svg.append('g');
var effectLayer = g.append('g')
.classed('effect-layer', true);
var mapLayer = g.append('g')
.classed('map-layer', true);
// Load map data
d3.json('data/provincesNL2.json', function(error, mapData) {
var features = mapData.features;
// Draw each province as a path
mapLayer.selectAll('path')
.data(features)
.enter().append('path')
.attr('d', path)
.attr('vector-effect', 'non-scaling-stroke')
.attr('fill', fillFn)
});
// Get province name
function nameFn(d){
return d && d.properties ? d.properties.PROVINCIE : null;
}
// Get province name length
function nameLength(d){
var n = nameFn(d);
return n ? n.length : 0;
}
// Get province color
function fillFn(d){
return color(nameLength(d));
};
I just do not know how to implement the values from the JSON file to represent the color of each province along the color scale. I am not to experienced with javascript and I thought this shouldnt be too hard but I just can not figure it out. Many thanks for any help!
You should specify the domain for you colour scale using min and max value of your data object.
var colour = d3.scaleLinear()
.domain([
d3.min(dataObject, function(d) { return d.value }),
d3.max(dataObject, function(d) { return d.value })
])
.clamp(true)
.range(['steelblue', "yellow"]);
In the callback function for fill attribute, you should find (you can do it various ways, depend on data structure) the value for current provinces and pass it to colour function.
.attr("fill", function(d, i) {
var value = // here you should get for current data item ;
return colour(value); // use this value for colour scale
})
Check this example for Netherlands map (I modified your data a bit, to simplify the example and hardcoded topojson object).
There are a few ways you can do this, one would be to use d3.map. This allows you to look up values in the map with map.get(key).
The general set up looks like:
var lookup = d3.map(array, function(d) { return d.key; })
Where array is your data array, and key is some property of each item in the data array that can be used as an identifier. To lookup a value, you can use:
lookup.get(value)
Where value is an identifying value/key - this will return the entire object that was in the data array. For your data this might look like:
var lookup = d3.map(data, function(d) { return d._id; });
lookup.get("Drenthe") // {"_id":"Drenthe","value":87}
To color the map you can get the identifying value/key from the element's datum. Here's a working example of d3.map used to color your data:
var color = d3.scaleLinear()
.domain([0,100])
.range(["orange", "steelblue"])
var data = [{"_id":"Groningen","value":52},
{"_id":"Friesland","value":18},
{"_id":"Drenthe","value":87},
{"_id":"Overijssel","value":93},
{"_id":"Flevoland","value":60},
{"_id":"Gelderland","value":7},
{"_id":"Utrecht","value":26},
{"_id":"Noord-Holland","value":52},
{"_id":"Zuid-Holland","value":72},
{"_id":"Zeeland","value":41},
{"_id":"Noord-Brabant","value":78},
{"_id":"Limburg","value":19}];
var lookup = d3.map(data, function(d) { return d._id; });
// test a value:
console.log(lookup.get("Drenthe").value);
var svg = d3.select("body")
.append("svg")
.attr("width",400)
.attr("height",100);
var squares = svg.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("fill",function(d) { return color(lookup.get(d._id).value) })
.attr("x", function(d,i) { return i * 20; })
.attr("y", 40)
.attr("width",16)
.attr("height",16);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
I am trying to draw the states of India map (with disputed territories ) by D3 Map. I successfully generate the topojson file which looks good in http://www.mapshaper.org/
And the json link is https://dl.dropboxusercontent.com/s/wk9qd47wn1nhsjm/dddtopo.json
But I failed to draw the map. The link http://jsfiddle.net/sEFjd/47/ is how I did under jsfiddle.
var topoJsonUrl = "https://dl.dropboxusercontent.com/s/wk9qd47wn1nhsjm/dddtopo.json";
var width = 360,
height = 360;
d3.json(topoJsonUrl, function(error, mx) {
var projection = d3.geo.mercator();
// create the path
var path = d3.geo.path().projection(projection);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
console.log(mx);
var geoPaths = svg.append("g")
.attr("class", "municipalities")
.selectAll("path")
.data(topojson.feature(mx, mx.objects.india).features);
geoPaths.enter().append("path")
.attr("d", path);
var p= svg.append("path")
.datum(topojson.mesh(mx, mx.objects.india))
.attr("d", path)
.attr("class", "state-boundary");
geoPaths.style("fill", function(d) {
return Math.random() > 0.5 ?'gray' : 'blue';
});
});
The code works well with other countries(Germany, Mexico) Not sure why it does not work this time. Any help will be very appreciated.
I have a topojson which contains state's paths. I want the user to be able to hover over a state and the state to appear in a different svg. So far, I've tried to extract the geometry out of the topojson (d.geometry , d.geometry.coordinates etc) But I'm not able to do it.
Maybe I need to draw a polygon out of that, but some states are of type "Polygon" and some of them are of type "MultiPolgyon".
Any ideas/suggestions?
Edit : Here's my code
var svg = d3.select("#india-map")
.append("svg")
.attr("width",width).attr("preserveAspectRatio", "xMidYMid")
.attr("viewBox", "0 0 " + width + " " + height)
.attr("height", height)
var stateSvg = d3.select("#state-map")
.append("svg")
.append("g")
.attr("height", height)
.attr("width", width);
var g = svg.append("g");
var projection = d3.geo.mercator()
.center([86, 27])
.scale(1200);
var path = d3.geo.path().projection(projection);
var pc_geojson = topojson.feature(pc, pc.objects.india_pc_2014);
var st_geojson = topojson.feature(state_json, state_json.objects.india_state_2014);
g.selectAll(".pc")
.data(pc_geojson.features)
.enter().append("path")
.attr("class", "pc")
.attr("d", path)
.attr("id", function(d){ return d.properties.Constituency;})
.attr("fill", "orange")
.on("click", function(d){
drawCons(d);
});
function drawCons(d){
stateSvg.selectAll(".pc2")
.data(d)
.enter().append("path")
.attr("class","pc2")
.attr("d", path)
}
.data() expects to be given an array of objects to be matched against the selection. You're passing a single object, so it doesn't work. You can either use .datum(d) or .data([d]) to make it work.
Quick and dirty demo here.
I have a very basic D3 SVG which essentially consists of a couple arcs.
No matter what I use (attr, attrTween, and call) I cannot seem to get the datum via the first argument of the callback--it is always coming back null (I presume it's some kind of parse error, even though the path renders correctly?)
I might be overlooking something basic as I am relatively new to the library...
var el = $('#graph'),
width = 280,
height = 280,
twoPi = Math.PI * 2,
total = 0;
var arc = d3.svg.arc()
.startAngle(0)
.innerRadius(110)
.outerRadius(130),
svg = d3.select('#graph').append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"),
meter = svg.append('g').attr('class', 'progress');
/* Add Meter Background */
meter.append('path')
.attr('class', 'background')
.attr('d', arc.endAngle(twoPi))
.attr('transform', 'rotate(180)');
/* Add in Icon */
meter.append('text')
.attr('text-anchor', 'middle')
.attr('class', 'fa fa-user')
.attr('y',30)
.text('')
/* Create Meter Progress */
var percentage = 0.4,
foreground = meter.append('path').attr('class', 'foreground')
.attr('transform', 'rotate(180)')
.attr('d', arc.endAngle(twoPi*percentage)),
setAngle = function(transition, newAngle) {
transition.attrTween('d', function(d,v,i) {
console.log(d,v,i)
});
/*transition.attrTween('d', function(d) { console.log(this)
var interpolate = d3.interpolate(d.endAngle, newAngle);
return function(t) { d.endAngle = interpolate(t); return arc(d); };
});*/
};
setTimeout(function() {
percentage = 0.8;
foreground.transition().call(setAngle, percentage*twoPi);
},2000);
It's this block of code that seems to be problematic:
transition.attrTween('d', function(d,v,i) {
console.log(d,v,i)
});
Returning:
undefined 0 "M7.959941299845452e-15,-130A130,130 0 0,1 76.4120827980215,105.17220926874317L64.65637775217205,88.99186938124421A110,110 0 0,0 6.735334946023075e-15,-110Z"
I tried using the interpolator to parse the i value as a string since I cannot seem to acquire "d," however that had a parsing error returning a d attribute with multiple NaN.
This all seems very strange seeing as it's a simple path calculated from an arc???
The first argument of basically all callbacks in D3 (d here) is the data element that is bound to the DOM element you're operating on. In your case, no data is bound to anything and therefore d is undefined.
I've updated your jsfiddle here to animate the transition and be more like the pie chart examples. The percentage to show is bound to the path as the datum. Then all you need to do is bind new data and create the tween in the same way as for any of the pie chart examples:
meter.select("path.foreground").datum(percentage)
.transition().delay(2000).duration(750)
.attrTween('d', function(d) {
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
return arc.endAngle(twoPi*interpolate(t))();
};
});