Displaying a map using D3.js projection - javascript

I'm trying to display India's map using D3.js and this geoJSON file. When I run the following HTML file in the browser, the map doesn't get generated, with no error on the console.
I suspect this has something to do with projection because when I remove the projection from the path variable, I do get a tiny map at the top of the svg. I tried Mercator, Albers and other projections but nothing seems to work.
Code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>India</title>
<script src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script type="text/javascript">
//Width and height
var w = 500;
var h = 500;
var projection = d3.geoMercator()
.translate([w/2, h/2])
.scale(500);
//Define default path generator
var path = d3.geoPath()
.projection(projection);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
var rectangle = svg.append("rect")
.attr("height", h).attr("width", w)
.attr("fill", "transparent")
.attr("stroke", "black")
.attr("stroke-width", 1);
//Load in GeoJSON data
d3.json("india.json", function(json) {
//Bind data and create one path per GeoJSON feature
svg.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d", path)
});
</script>
</body>
</html>

When I use your code, changing only the geojson of India to a geojson of the world, I get this:
The map is focused at [0,0], off the east coast of Afrcia. This is the default focus point of most projections in d3. This is also why you don't see anything in your viewport and don't see any errors.
If you set your center, to say (off the top of my head) [80,25] you'll be much more centered on India:
var projection = d3.geoMercator()
.center([80,25])
.translate([w/2, h/2])
.scale(800);
There are different ways to center a Mercator map, including using a rotation of the projection. Likewise, different map projections may be centered using different parameters or methods.
Using your code with with this modification (.center([80,25])) and a little bit of zooming in (.scale(800)) gets me:
A more precise method would be to find the centroid of India (online certainly) as the centering point rather than my guestimate.
Whenever you can't see what you had hoped to, check the DOM to see if it was drawn (but is only off screen) or zoom out to see if you are looking at the wrong place.
Why would the map project without a projection? Because the lat long pairs are interpreted as pixel locations on the svg, which is why the map is tiny when you don't define a projection for the geoPath.

Related

How can I offset the initial starting position of a d3 zoom?

I'm trying to zoom in on an SVG element after having translated it's Matrix, but when I start the initial zoom, the position of the SVG resets to 0,0. I'd like the zoom and pan to start from the moved position on page load.
<svg id="#svg">
<g id="#mainGrid>
....a bunch of SVG
</g>
<svg>
<script>
var winCenterV = $(window).height()/2;
var winCenterH = $(window).width()/2;
//set mainGrid to the center of window using snap.svg
var mainGrid = Snap.select("#mainGrid");
var myMatrix = new Snap.Matrix();
myMatrix.scale(1,1);
myMatrix.translate(winCenterH,winCenterV);
myMatrix.rotate(0);
mainGrid.transform(myMatrix);
//d3 zoom
var svgElement = d3.select("svg");
var gridELement = d3.select("#mainGrid");
svgElement.call(d3.zoom()
.scaleExtent([1 / 2, 4])
.on("zoom", zoomed));
function zoomed() {
gridELement.attr("transform", d3.event.transform);
}
</script>
the "mainGrid" SVG element zooms and pans fine, but it snaps back to it's 0,0 position (top left of browser) during the first zoom mouse click or wheel scroll instead of zooming and panning from the transformed location as set by myMatrix. How can I get the d3.event.transform to start zooming from this offset?
Problem:
The d3 zoom behavior does not track or know about what transforms you've applied to an element. It is initialized with scale equal to 1 and translate equal to 0,0. So, when you use
gridELement.attr("transform", d3.event.transform);
in your zoomed function, you are zooming relative to this initial scale and translate.
Rationale
Not every d3 zoom manipulates the transform of the SVG element it is called on: you may want to have zoom interaction everywhere on a plot, but only zoom the plot area and not the margins. Many d3 zooms don't manipulate any SVG transform: they may use semantic zooming, or the zoom may be altering a canvas rather than SVG, etc. As such, a d3-zoom behavior is independent of any SVG transorm attribute on the element(s) it is called on.
Solution
If your zooming by manipulating the transform attribute of an SVG element, let D3 zoom do all of the manipulation - no need to manipulate it manually and have a zoom, this is how the zoom gets out of step.
So,
You can programmatically trigger a zoom event prior to rendering anything. The easiest way to do so is to use:
selection.call(zoom.transform, d3.zoomIdentity.translate(x,y).scale(k));
This line triggers a zoom event. We can use this prior to rendering anything and rely on the zoomed function to set the initial value of the SVG transform attribute. All subsequent zooms will be relative to this and we can proceed as normal:
var svg = d3.select("svg");
var g = svg.append("g");
// define a zoom behavior:
var zoom = d3.zoom()
.on("zoom", zoomed);
// call the zoom:
svg.call(zoom)
// trigger tha initial zoom with an initial transform.
svg.call(zoom.transform,d3.zoomIdentity.scale(20).translate(-100,-100));
// draw the visualization:
var rect = g.append("rect")
.attr("width", 5)
.attr("height", 5)
.attr("x", 100)
.attr("y", 100);
// zoomed function:
// zoom as normal.
function zoomed() {
g.attr("transform",d3.event.transform);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="300"></svg>
The above snippet applies an initial zoom (scale 20, translate -100,-100) and uses this as the starting point for the zoom interaction. The comment should help explain the process.
// It appears that you're moving #mainGrid and not #svg. So when you call zoom from svgElement it is zooming from it's 0,0 position.
// Or it may because you don't have pound sign in d3.select
var svgElement = d3.select("#svg");
// Try changing svgElement to gridElement
gridELement.call(d3.zoom()
.scaleExtent([1 / 2, 4])
.on("zoom", zoomed));

Zoom causes graph to go out of the plotting area bounds

I'm having an issue with my graph. When I zoom, the line goes over the edge of the canvas area and over the x/y axis. I tried adding a clippath but that doesn't seem to work. If I inspect the DOM in the debugger I can see the clippath rectangle is position exactly where it needs to be.
//The canvasGroup is the area between the axis
var clipGroup = canvasGroup.append("clipPath")
.attr("id", "canvasGroup-clipPath");
var clipRect = clipGroup.append("rect")
.attr("width", width)
.attr("height", height);
//Function that does the zooming
function doZoom()
{
paths.forEach(function(path)
{
path.attr("transform", d3.event.transform);
});
gX.call(xAxis.scale(d3.event.transform.rescaleX(xScale)));
gY.call(yAxis.scale(d3.event.transform.rescaleY(yScale)));
}
var zoom = d3.zoom()
.scaleExtent([1, 5])
.translateExtent([[0, 0], [width, height]])
.on("zoom", doZoom);
//Register the event listener
canvasGroup.call(zoom);
//now loop over the data sets and plot each one
//For brevity, I'm skipping the loop and only showing the call to create the line
var path = canvasGroup.append("path")
.attr("clip-path", "url(#canvasGroup-clipPath)")
.attr("fill", "steelblue")
.attr("class", "line")
.attr("id", lineId + "-line")
.style("stroke", lineColor)
.style("stroke-width", 1)
.style("fill", "none");
paths.push(path);
path.attr("d", function(d) { return plotline(i)});
Here is how it looks.
Before zoom:
After zoom:
The problem is caused by setting the clipping path on the path representing your data. When applying the clipping path the browser needs to decide which coordinate system should be used for the contents of <clipPath> element. This is controlled by the clipPathUnits attribute which defaults to userSpaceOnUse:
userSpaceOnUse
The contents of the <clippath> element represent values in the current user coordinate system in place at the time when the <clippath> element is referenced (i.e., the user coordinate system for the element referencing the <clippath> element via the clip-path attribute).
When transforming the path in your doZoom() function, you are actually establishing a new coordinate system for the path. And, apart from drawing the path itself, this coordinate system will be used to compute the position and dimension of the clipping path. Thus, by transforming the path according to the zoom behavior, you are applying the same transformation to the clipping path whereby moving it away from the desired position.
Although it is tempting to set the clipPathUnits attribute to its other valid value objectBoundingBox, this is most likely not what you want for this case as it further complicates matters. When setting this value the positions and lengths of the <clipPath>'s contents need to be specified as fractions of the bounding box!
Knowing all this, there is a much easier solution to it! You just need to apply the clip-path to an element which will not be transformed during zooming ,e.g. a parent group. Given the incomplete code you provided, this might very well work by setting the clipping path to canvasGroup:
// Set the clipping path on the parent group.
canvasGroup.attr("clip-path", "url(#canvasGroup-clipPath)")
// Append the path to the group as before.
var path = canvasGroup.append("path")
.attr("fill", "steelblue")
.attr("class", "line")
// ...

D3 Initial Zoom with default mercator projection scale

I'm having an issue specific to scale behavior with a default mercator projection scale value, in my case a pretty ridiculous value (75000), since somehow it was the only way I was able to get my TOPOJson map to be project as anything other than a dot.
I created a projection of a map and want to add zoom and pan. My functions work, all except for the default zoom value. As soon as you scroll to zoom, the value jumps back to the default projection scale value, rather than scaling based on the mouse wheel. I have a feeling that the reason is because in order to get the map to display I needed to make my projection scale value really high (75000).
So when the page loads, the zoom looks fine, like this... but when you try to pan or zoom in or out one mouse wheel click, it jumps to this
I tried to limit the code to just the projection and zoom logic bellow.
<body>
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<div id = "map" style = "float : left; min-width:320px;width :50%;height : 600px;"></div>
<script>
var width = 900,
height = 800;
var projection = d3.geo.mercator()
.center([-73.94, 40.70])
.scale(75000)
.translate([(width/2), (height/2)]);
var svg = d3.select("#map").append("svg")
.attr("viewBox", "0 0 900 800")
.attr("preserveAspectRatio", "xMidYMid meet");
var path = d3.geo.path()
.projection(projection);
var g = svg.append("g")
.attr("transform", "translate(-793.0489643729818,-630.1796382734433)scale(3.01487286910262)");
d3.json("ny.json", function(error, ny) {
g.append("path")
.attr("id", "boroughs")
.datum(topojson.feature(ny, ny.objects.boroughs))
.attr("d", path);
// zoom and pan
var zoom = d3.behavior.zoom()
.on("zoom",function() {
g.attr("transform","translate("+
d3.event.translate.join(",")+")scale("+d3.event.scale+")");
g.selectAll("path")
.attr("d", path.projection(projection));
});
svg.call(zoom)
});
I believe your issue is that you set initial scaling and translate values for your g and then reset them on zoom. For scale, for example:
You set a scale for the g to 3.01 to start:
var g = svg.append("g")
.attr("transform", "translate(-793.0489643729818,-630.1796382734433)scale(3.01487286910262)");
However, the first time you zoom with d3.event, it'll return 0.607 or 1.647, (zoom out, zoom in) both are zoomed out relative to 3.01:
g.attr("transform","translate("+
d3.event.translate.join(",")+")scale("+d3.event.scale+")");
This creates the jump you are seeing, as both scale and translate are reset and thus independent of your initial values. I think the translate value would be ok but you call it on the svg and not the g on which you specify the initial translate.
In any event, it is probably easier to not set an initial transform on your g, instead using the projection to do this (instead of a two step center and zoom with both a projection and a transform). That way you can leave all your code unchanged, save for removing the line setting the initial g transform and a modification to your projection.

Geojson map not showing up

I'm struggling with creating a map. I am simply using one of the new york times geojsons ("census_tracts_2010.geojson") from here ( https://github.com/dwillis/nyc-maps ).
I'd appreciate it if someone could look at my code below, and let me know why no map is showing up for me, especially I have no errors. If something is wrong, then it's probably in the last two lines.
STEP 1 - CONVERT GEOJSON TO TOPOJSON
ran this in terminal
geo2topo census_tracts_2010.geojson > nyc2.json
STEP 2
created index.html
(inspired by https://bost.ocks.org/mike/map/)
<!DOCTYPE html>
<meta charset="utf-8">
<style>
</style>
<body>
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script>
var width = 960,
height = 1160;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
d3.json("nyc2.json", function(error, uk) {
if (error) return console.error(error);
console.log(uk.objects)
console.log(uk.objects.census_tracts_2010)
svg.append("path")
.datum(topojson.feature(uk, uk.objects.census_tracts_2010))
.attr("d", d3.geo.path().projection(d3.geo.albersUsa()));
});
</script>
My output:Plain white webpage
Updated code:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
</style>
<body>
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script>
var width = 500,
height = 500;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
d3.json("census_tracts_2010.geojson", function(error, uk) {
if (error) return console.error(error);
var subunits = topojson.feature(uk, uk.objects.nyct2010);
var projection = d3.geo.albers()
.center([0,40.7])
.rotate([-74,0])
.translate([width/2,height/2])
.scale(65000);
var path = d3.geo.path()
.projection(projection);
svg.append("path")
.datum(subunits)
.attr("d", path);
});
</script>
Data Source
First, your referenced file is already a topopjson. You can contrast the differences with a regular geojson in this file (from the same repository).
The most visible difference is that geojson uses recognizable coordinates while topojson uses arcs and what might look like arbitrary coordinates. Topojsons also finish with scale and translate values.
The Problem
Why does your map not appear? Well that could be because of issues in topojsoning a file that is already a topojson, but a more likely option - and one that touches on your other question - is that you are not focusing your map on your area of interest. This appears to be the the issue in your previous question as well.
You are using a geo.albersUsa projection - this is by default focused on the entire continental US (it's a composite projection so it includes space for Alaska and Hawaii).
Changing your code only to use the topojson you reference (census_tracts_2010) I got:
Your map is displaying properly - or at least as coded - but the entire area of interest looks like it could be a small insect that hit the screen to fast.
AlbersUSA vs Albers
You will need to modify your map projection parameters but if you want to keep the AlbersUSA projection you will not be able to center or rotate, instead use a plain Albers projection. The AlbersUsa is intended for the entire country and I don't believe it has centering or rotation methods.
Setting Map Parameters
To set an Albers projection you'll want to know the center of latitude and longitude of your area of interest. Let's say about 40.7N and 74 W - I used Google Earth to generalize and then adjusted until I got a pleasant result.
Generally for an Albers you also want to know your standard parallels; however, in d3 the default parallels are for the US. Yes, they could be made more specific for your projection (by choosing two parallels that intersect the upper and lower portions of your area of interest), but I'll leave them out in this answer.
The general pattern for an Albers projection in D3 is:
var projection = d3.geo.albers()
.center([0,y])
.rotate([-x,0])
.parallels([a,b]) // don't worry about this in this instance
.translate([width/2,height/2])
.scale(k);
Using the center coordinates above and a few attempts to get the scale down I got this:
Using:
var projection = d3.geo.albers()
.center([0,40.7])
.rotate([74,0])
.translate([width/2,height/2])
.scale(65000);
Note: I've modified your svg dimensions to something more appropriate to the shape of your area of interest (as opposed to the reference map dimensions in the demonstration creating a map of the UK). My dimensions are: 500 x 500.
A relatively more detailed explanation of an Albers projection's parameters is in this answer.

d3.js, is it possible to append hyperlinks to GeoJSON file?

I have started to play with d3.js and json recently and after not finding answer in other threads I state my question directly here.
I wonder if its possible to accomplish following task with d3 library:
I have working example of loaded and projected geojson file made according to Scott Murray's book - Interactive Data Visualization (https://github.com/alignedleft/d3-book)
Here is my code (from book):
<html>
<head>
<title>Zoom/pan map example</title>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
</html>
<body>
<script type="text/javascript">
//Width and height
var w = 500;
var h = 300;
//Define map projection
var projection = d3.geo.albersUsa()
.translate([w/2, h/2])
.scale([500]);
//Define path generator
var path = d3.geo.path()
.projection(projection);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
//Load in GeoJSON data
d3.json("us-states.json", function(json) {
//Bind data and create one path per GeoJSON feature
svg.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d", path)
.style("fill", "steelblue");
});
</script>
<body>
</html>
json file from chapter 12 - https://github.com/alignedleft/d3-book/tree/master/chapter_12
Now,
I want - let's say - polygon representing state Washington (top left corner) to be clickable, "holding" hyperlink. In other words - is it possible to append hyperlink to that polygon ?
Second thing - is it possible to insert text (state numbers) into polygons so that text will remain inside polygons and not crossing its borders ?
I hope its clear and I would be thankful if someone skilled enough could provide solution for this problem.
Svg has a <a> tag you can insert dinamically:
http://www.w3schools.com/svg/svg_text.asp.
This is probably the way to go

Categories