Drawing map with d3js from openstreetmap geojson - d3.js

Hy
I'm trying to draw an svg with d3.v3.js from geojson. I fetch the geojson from openstreetmap(my test data: http://pastebin.com/4GQne42i) and try to render it to svg.
My JS code:
var path, vis, xy, jdata;
xy = d3.geo.mercator().translate([0, 0]).scale(200);
path = d3.geo.path().projection(xy);
vis = d3.select("body").append("svg").attr("width", 960).attr("height", 600);
//22.json is the name of the file which contains the geojson data
d3.json("22.json", function(error, json) {
jdata = json;
if(error!=null)
console.log(error);
return vis.append("svg:g")
.selectAll("path")
.data(json.coordinates)
.enter().append("path")
.attr("d", path);
});
And somehow my svg result is this:
<svg width="960" height="600">
<g>
<path></path>
</g>
</svg>
I know the projection is not good, but I think the svg should have nodes.
What is the problem with my code? Would you post a correct solution?

The first problem is with your data join:
vis.append("g")
.selectAll("path")
.data(json.coordinates)
.enter().append("path")
.attr("d", path);
This would mean you want one path element for each element in the json.coordinates array. Since your test data is a polygon, that would mean one path element for the exterior ring, and then perhaps multiple other path elements for any interior holes, if your data has them. I expect you just want a single path element for the entire polygon.
The second problem is that you’re not passing valid GeoJSON to the d3.geo.path instance. Because the data in your data join is json.coordinates, you’re just passing an array of coordinates directly to path, when you need to pass a GeoJSON geometry object or a feature. (See the GeoJSON specification.)
Fortunately both of these problems are easy to fix by eliminating the data join and rendering the full polygon. To add just one path element, just call selection.append:
vis.append("path")
.datum(json)
.attr("d", path);
Your projection will probably need adjusting (translate and scale), too. You might find the project to bounding box example useful here.

Do you really need to do it with D3?
I would suggest to go with more map oriented libraries like:
polymaps
Leaflet
Leaflet vector layer has support for GeoJSON and its size is quite compact.
Open Layers is also an option but it's size is quite big.
Here is an example how I have used Leaflet + GeoJSON to display suburb shape http://www.geolocation.ws/s/6798/en

Related

Rendering in the background of a dc.js chart with renderlet

I use dc.js for showing the results of multiple classification algorithms. More specifically, I want to show a precision recall chart (each point corresponds to a result of a classification system).
I already used a dc.js scatter chart for this which works fine.
Additionally I would like to have a d3 contour in the background of the chart which shows the F-measure.
This is already implemented. The only issue is that the contour part is in the foreground and not in the background of the chart.
Please have a look at the jsfiddle for a full example.
Two questions are still open for me because I'm not a dc.js or d3 expert:
Is there a way to put the contour in the background or the symbols(cycles) of the scatter chart in the foreground (I already tried it with the help of this stackoverflow question but with no success)
I used the 'g.brush' selector to get the area of the inner chart. This works fine as long as the brushing is turned on. Is the selector a good way to go or are there better alternatives (which may also work if brushing is switched off).
In my example I put the contour part in the upper left corner to see if it works but I also provide the code (currently uncommented) to increase the width and height of the contour to the correct size.
chart
.on('renderlet', function (chart) {
var innerChart = chart.select('g.brush');
var width = 300, height=300;
//getting the correct width, height
//var innerChartBoundingRect = innerChart.node().getBoundingClientRect();
//var width = innerChartBoundingRect.width, height=innerChartBoundingRect.height;
[contours, color] = generateFmeasureContours(width,height, 1);
innerChart
.selectAll("path")
.data(contours)
.enter()
.append("path")
.attr("d", d3.geoPath())
.attr("fill", d => color(d.value));
var symbols = chart.chartBodyG().selectAll('path.symbol');
symbols.moveToFront();
});
jsfiddle
Putting something in the background is a general purpose SVG skill.
SVG renders everything in the order it is declared, from back to front, so the key is to put your content syntactically before everything else in the chart.
I recommend encapsulating it in an svg <g> element, and to get the order right you can use d3-selection's insert method and the :first-child CSS selector instead of append:
.on('pretransition', function (chart) {
// add contour layer to back (beginning of svg) only if it doesn't exist
var contourLayer = chart.g().selectAll('g.contour-layer').data([0]);
contourLayer = contourLayer
.enter().insert('g', ':first-child')
.attr('class', 'contour-layer')
.attr('transform', 'translate(' + [chart.margins().left,chart.margins().top].join(',') + ')')
.merge(contourLayer);
A few more points on this implementation:
use dc's pretransition event because it happens immediately after rendering and redrawing (whereas renderlet waits for transitions to complete)
the pattern .data([0]).enter() adds the element only if it doesn't exist. (It binds a 1-element array; it doesn't matter what that element is.) This matters because the event handler will get called on every redraw and we don't want to keep adding layers.
we give our layer the distinct class name contour-layer so that we can identify it, and so the add-once pattern works
contourLayer = contourLayer.enter().insert(...)...merge(contourLayer) is another common D3 pattern to insert stuff and merge it back into the selection so that we treat insertion and modification the same later on. This would probably be simpler with the newer selection.join method but tbh I haven't tried that yet.
(I think there may also have been some improvements in ordering that might be easier than insert, but again, I'm going with what I know works.)
finally, we fetch the upper-left offset from the margin mixin
Next, we can retrieve the width and height of the actual chart body using
(sigh, undocumented) methods from dc.marginMixin:
var width = chart.effectiveWidth(), height = chart.effectiveHeight();
And we don't need to move dots to front or any of that; the rest of your code is as before except we use this new layer instead of drawing to the brushing layer:
contourLayer
.selectAll("path")
.data(contours)
.enter()
.append("path")
.attr("d", d3.geoPath())
.attr("fill", d => color(d.value));
Fork of your fiddle.
Again, if you'd like to collaborate on getting a contour example into dc.js, that would be awesome!

Multiple maps with d3.js: change values of scale and center

I’m building a (d3 v4) cartographic visualization which allows the user to switch between many datasets (json files) and two different regions (administrative units of a country and smaller administrative units into its capital city). Actually the switch from one to another dataset on the initial country level works well, through buttons and jquery.
Problem: it’s a bit less convincing when switching to a map/dataset about the capital city, as the projection is initially set for the whole country and consequently the user has to zoom many times to visualize properly the map of the capital city. I would like to change the values of .scale and .center when calling the projection but after several trials I haven’t found how to do it.
As I only have two different regions to show, my intuition was to set first values of scale and center and to change them to other values (I know the values of .scale and .center I would like to use in both cases) when the user switches to a map of the capital city through a function. Is there any possibility to switch easily these values? Do you have any suggestion to solve this problem?
As I load the json file path into a function when the user clicks on the button to switch to another dataset, I was trying to load the value of scale the same way but I’m probably doing wrong. It seems that the part of the code about the projection can't be put in a function?
Thanks for your help!
Small part of my code:
var width = 1100, height = 770;
var projection = d3.geoConicConformal()
.scale(19000) // value I would like to which when the region changes
.center([4.45, 50.53]) // value I would like to which when the region changes
.translate([width/2,height/2]);
var svg = d3.select( "#mapcontainer" )
.append( "svg" )
.attr("width", width)
.attr("height", height)
.style("border", "solid 1px black");
var path = d3.geoPath()
.projection(projection);
var color, jsonfile, legendtext;
function load (jsonfile, legendtext, color) {
d3.selectAll(".currentmap").remove() ;
d3.json(jsonfile, function(error, belgique) {
g.selectAll("path")
.data(belgique.features)
.enter()
.append("path")
.attr("d", path)
.style("stroke", "#fff")
.attr( "class", "currentmap")
.style("fill", function(d) {
var value = d.properties.DATA;
if (value) {return color(value);}
else {return "rgb(250,110,110)"}
});
})
};
//one of the following function for each map
function BGQprovinces() {
jsonfile = "ATLAS/NewGeoJson/bgq-data1-provinces.json";
legendText [= …];
color = d3.scaleOrdinal()
.domain( […])
.range([…]);
load(jsonfile, legendtext, color) ;
};
;
There area few approaches to accomplish this.
fitSize and fitExtent
One is to modify the projection scale and translate as opposed to scale and center. This is nearly the same operation, but translate pans the projected plane and center will pan the unprojected plane. To do so you need to use projection.fitSize([width,height],geojsonObject), or projection.fitExtent([[x0,y0],[x1,y1]],geojsonObject). The latter will allow margins of say, the first coordinate provided is the top left and the second coordinate provided is the bottom right of a bounding box in which the feature will be constrained.
d3.json(jsonfile, function(error, belgique) {
projection.fitSize([width,height], belgique);
// now draw as you would:
d3.selectAll(".currentmap").remove() ;
g.selectAll("path")
.data(belgique.features)
.enter()
.append("path")
.attr("d", path)
...
Note that for showing all of a country you need to have a feature that shows the whole country or a feature collection that shows all the parts of a country. You cannot use an array with fitSize or fitExtent, if you have an array of features, you can create a feature collection by using:
var featureCollection = {"type":"featureCollection","features":featureArray}
For your case, I'd suggest using fitSize or fitExtent.
centroid
If you really wanted to modify the center attribute as opposed to translate, or perhaps you want to change the rotation (a more likely outcome for conic conformals in many parts of the world, Belgium should be fine), then you need the geographic coordinates of the center. One way of a handful to do this is to get the centroid of a feature from path.geoCentroid:
var centroid = path.geoCentroid(geojsonObject);
Then use that to set the projection parameters to rotate:
projection.rotate([ -centroid[0],-centroid[1] ])
projection.center([0,0])
or to center:
projection.rotate([0,0])
projection.center(centroid)
Or a combination of both (depending on map projection type). Now you can apply fitSize or fitExtent, the feature is in the middle already, but now we can set the scale. The reason I suggest this as a potential answer is because not all projections, concic projections in particular, will give desired results by modifying only scale along with translate and/or center.
Of course for conic projections, you may need to find a way to set the parallels as well, but I'll leave that for another answer if it ever comes up.

TopoJSON map not rendering properly using D3

I've downloaded this map of Great Britain and Ireland from Highcharts and converted from GeoJSON to TopoJSON format (topojson -o input output) but it just renders as a black box. I'm following the famous 'Let's make a map' tutorial:
var container = d3.select( "#container" );
var margin = 20,
width = parseInt(container.style( "width" )),
height = parseInt(container.style( "height" ));
var projection = d3.geo.mercator()
.scale(50);
var path = d3.geo.path()
.projection(projection);
var svg = container.append("svg")
.attr("width", width)
.attr("height", height);
d3.json("http://www.lenart.pl/assets/codepen/gb_all_ireland_topo.json", function(error, gb) {
svg.append("path")
.datum(topojson.feature(gb, gb.objects.gb_all_ireland_geo))
.attr("d", path);
});
http://codepen.io/znak/pen/rVYbNB
I've previewed my TopoJSON file with http://jsoneditoronline.org and found the relevant object containing geometries (I think) but it doesn't work.
My ultimate goal is to visualise an interactive (administrative) map of GB & I. Thanks.
I believe the first spot of bother is the download from Highcharts--it seems to be in a projection that is not wgs84. So the coordinates given are not latitudes and longitudes. If you look at the beginning of highcharts.com/mapdata/custom/gb-all-ireland.geo.json you'll see a few "crs" keys, and mention of "EPSG:7405".
You can convert to wgs84 several ways, including:
ogr2ogr -f geojson eire_4326.geojson gb-all-ireland.geo.json -t_srs epsg:4326 -s_srs epsg:7405
You could then convert to topojson and pick up where you left off.
It's also important to realize that small changes in the scale and center values of the d3 projections can have big impacts.
If you're not tied to Highcharts, it's probably better to get data that's already in wgs84, like http://www.naturalearthdata.com/ or even use a more comprehensive library and data like mapsense https://developer.mapsense.co/tileViewer/. (Disclosure: I work at mapsense.)

Venue/Indoor Map using D3.js and Geojson

I have created geojson file which contains all the features of 1st floor of a shopping mall. I got that venue map projected using d3.js with different colors but only some parts not the complete map. Below is the script code and link to the geojson file. Also please note that i have not converted this geojson into topojson and used Qgis to draw the maps and c#.net to convert the geometry data to geojson objects. Can anyone please check my json and my d3.js code? Do I need to use any other projections?
https://www.dropbox.com/s/8pu2s0yamfkd89p/JSONfromDB_8Feb2014.json
$(document).ready(function () {
parseResultShopDetails();
});
function parseResultShopDetails() {
var width = 600, height = 300;
var svg = d3.select("#map").append("svg")
.attr("width", width)
.attr("height", height);
var projection = d3.geo.mercator()
.scale(30)
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
d3.json("http://localhost:1209/data/JSONfromDB_8Feb2014.json", function (error, jsonData) {
var color1 = d3.scale.category10();
svg.selectAll("path")
.data(jsonData.features)
.enter()
.append("path")
.attr("d", path)
.attr("text", function (d, i) { return "js"; })
.attr("fill", function (d, i) { return color1(i); });
});
}
It looks like the d3 mapping tools really fall apart if you try to use coordinates other than longitude and latitude.
I tried creating a "null" projection that just returns the input values, but the negative numbers and numbers greater than 360 were still getting wrapped by d3 before passing to the projection function. That avoids the trig errors from the Mercator projection, and it creates interesting art, but not the floor plan you were hoping for:
var projection = d3.geo.projection(function(λ, φ) {
return [ λ, φ ];
});
http://fiddle.jshell.net/rR2hG/1/
However, all is not lost. The second image in that example is created by just passing the array of coordinates as the points of <polygon> elements. I think that's closer to what you wanted. So you'll need to do a little more work to grab the points from the data file but you can definitely visualize them just as an array of coordinates.
svg2.selectAll("polygon")
.data(jsonData.features)
.enter()
.append("polygon")
.attr("points", function(d){ return d3.merge(d.geometry.coordinates);})
.attr("fill", function (d, i) {
return color1(i);
});
The only other suggestion is to write a script to convert your geoJSON file to geographic units. They don't have to be actual latitude and longitude of a particular place (you could still have the map centered on a reference point of your choice), but the scale has to be in degrees not feet or meters or whatever you are using.
D3's mapping projections are designed to transform 3D earth coordinates into 2D browser coordinates, so they are not that great at transforming local coordinates like the ones you've got. And as Amelia outlines your putting in coordinates that are outside of what's expected.
You'd be better off doing one of two things; creating a geometry stream based on 2 linear scales as outlined in this google groups discussion; or using d3's path generators.
To creating a 2D path generator is straightforward in d3 something like this will work:
var shops = d3.svg.line()
.interpolate("linear")
.x(function(d) {
return xScale(d.x);
})
.y(function(d) {
return yScale(d.y);
})
The real trick here is accessing the 'right' part of your json object. If you look into the geojson structure you see that there is a geometry part as well as an properties part. You need to dig through to pull out the coordinates and then pass them to the pavement generator. In this case it would be:
d.geometry.coordinates
which would obviously need to be referenced correctly.
Note that the method outlined here isn't going to work if you have complex geometries such as multi-polygons, you'll need to do quite a bit more work. If that's what you've got you'll want to create a custom geometry stream.
Now putting all of that together here's a working example of you're json.

Why do GeoJSON features appear like a negative photo of the features themselves?

I have a pretty standard code thats reads a GeoJSON file and renders its features using D3.js. It works fairly well except with this file: https://github.com/regiskuckaertz/d3/blob/master/circonscriptions.json
The file doesn't look weird or anything, in fact you can preview it on GitHub or geojsonlint.com. However, D3 draws paths that look like the features were used as a clipping mask, i.e. all the shapes are negatives of the features themselves. The code is pretty standard though:
var proj = d3.geo.mercator()
.scale(25000)
.center([6.08642578125,49.777716951563754])
.rotate([-.6, -.2, 0]);
var path = d3.geo.path().projection(proj);
function ready(error, luxembourg) {
svg
.selectAll("path")
.data(luxembourg.features)
.enter().append("path")
.attr("d", path)
.attr("class", function(d) { return quantize(rateById.get(d.properties.name)); })
}
You can have a look here: http://jsfiddle.net/QWZXd/
The same code works with another file, which comes from the same source.
For some reason, the points in these polygons are in reverse order - they ought to be clockwise, but are defined as counterclockwise, and d3 follows the right-hand rule for polygon interpretation.
To fix, reverse the points, either in the file or in JS:
luxembourg.features.forEach(function(feature) {
feature.geometry.coordinates[0].reverse();
});
Fixed fiddle: http://jsfiddle.net/nrabinowitz/QWZXd/1/

Resources