Skip to content

Instantly share code, notes, and snippets.

@milkbread
Created June 27, 2013 20:45
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save milkbread/5880252 to your computer and use it in GitHub Desktop.
Save milkbread/5880252 to your computer and use it in GitHub Desktop.
HTML: D3 Mapping Basics - example for the tutorial on digital-geography
<!DOCTYPE html>
<html>
<head>
<title>Testmap</title>
<meta charset="utf-8" />
<script src="http://cdn.leafletjs.com/leaflet-0.6.1/leaflet.js"></script>
<script src="http://d3js.org/d3.v3.min.js"></script>
<style>
@import url(http://cdn.leafletjs.com/leaflet-0.6.1/leaflet.css);
</style>
</head>
<body>
<div id="map" style="width: 300px; height: 300px"></div>
<script>
var map = L.map('map').setView([52.45,13.4], 9);
var toolserver = L.tileLayer('http://{s}.www.toolserver.org/tiles/bw-mapnik/{z}/{x}/{y}.png');
var stamen = L.tileLayer('http://{s}.tile.stamen.com/toner/{z}/{x}/{y}.png', {attribution: 'Add some attributes here!'}).addTo(map);
var baseLayers = {"stamen": stamen, "toolserver-mapnik":toolserver};
L.control.layers(baseLayers).addTo(map);</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>Testmap</title>
<meta charset="utf-8" />
<script src="http://cdn.leafletjs.com/leaflet-0.6.1/leaflet.js"></script>
<script src="http://d3js.org/d3.v3.min.js"></script>
<style>
@import url(http://cdn.leafletjs.com/leaflet-0.6.1/leaflet.css);
</style>
</head>
<body>
<div id="map" style="width: 960px; height: 300px"></div>
<script>
var map = L.map('map').setView([52.45,13.4], 9);
var toolserver = L.tileLayer('http://{s}.www.toolserver.org/tiles/bw-mapnik/{z}/{x}/{y}.png');
var stamen = L.tileLayer('http://{s}.tile.stamen.com/toner/{z}/{x}/{y}.png', {attribution: 'Add some attributes here!'}).addTo(map);
var baseLayers = {"stamen": stamen, "toolserver-mapnik":toolserver};
L.control.layers(baseLayers).addTo(map);
var centerPoint = L.circle([map.getCenter().lat, map.getCenter().lng], 200).addTo(map);
map.on("viewreset", showCenterInfo);
map.on("drag", showCenterInfo);
map.on("mousemove", setCurrentPosition);
var container = d3.select("body").append("div").attr("id","#container");
var svgContainer = container.append("svg").attr("width", 960).attr("height", 200);
var size = 20;
var zoomIn = svgContainer.append("rect")
.attr("x", 0)
.attr("y", size)
.attr("width", size)
.attr("height", size)
.style("fill","#0f0")
.on("mouseup",zoomInAction);
var zoomOut = svgContainer.append("rect")
.attr("x", size+10)
.attr("y", size)
.attr("width", size)
.attr("height", size)
.style("fill","#f00")
.on("mouseup",zoomOutAction);
var zoomText = svgContainer.append("text")
.text("Zoomlevel: "+map.getZoom())
.attr("x", (size * 2)+20)
.attr("y", size * 2)
.style("fill","#444")
.style("font-size","20");
function zoomOutAction(){
anotherFunction(-1);
}
function zoomInAction(){
anotherFunction(1);
}
function anotherFunction(value){
var newZoomlevel = map.getZoom() + value
map.setZoom(newZoomlevel)
zoomText.text("Zoomlevel: "+newZoomlevel)
}
var centerText = svgContainer.append("text")
.attr("x", 0)
.attr("y", size * 3)
.style("fill","#444")
.style("font-size","20");
//console.log(map.getBounds()._northEast, map.getBounds()._southWest)
var group = svgContainer.append('g');
var scaleLat = d3.scale.linear();
var scaleLng = d3.scale.linear();
var currentPos = svgContainer.append('text')
.style("fill","#444")
.style("font-size","20")
showCenterInfo();
function showCenterInfo(){
centerText.text("Center Lat: "+map.getCenter().lat.toFixed(4)+" Lng: "+map.getCenter().lng.toFixed(4))
centerPoint.setLatLng([map.getCenter().lat, map.getCenter().lng])
defineExtentVisualisation();
}
function defineExtentVisualisation(){
var rectangle = [];
rectangle.push([map.getBounds()._northEast.lat, map.getBounds()._northEast.lng])
rectangle.push([map.getBounds()._northEast.lat, map.getBounds()._southWest.lng])
rectangle.push([map.getBounds()._southWest.lat, map.getBounds()._southWest.lng])
rectangle.push([map.getBounds()._southWest.lat, map.getBounds()._northEast.lng])
//console.log(rectangle)
scaleLat.domain([map.getBounds()._northEast.lat,map.getBounds()._southWest.lat])
.range([size*4,size*7]);
scaleLng.domain([map.getBounds()._southWest.lng, map.getBounds()._northEast.lng])
.range([0,750]);
//console.log(scaleLat.invert(100))
//console.log(scaleLat(52.30))
group.selectAll('text').remove();
var coordText = group.selectAll('text').data(rectangle).enter().append("text").text("Hello")
coordText.attr("x", function(data,i){return scaleLng(data[1])})
.attr("y", function(data,i){return scaleLat(data[0])})
.style("fill","#444")
.style("font-size","20")
.text(function(data){return "Lat: "+data[0].toFixed(2) +" : Lng: "+ data [1].toFixed(2)});
}
function setCurrentPosition(mouse_data){
//console.log(mouse_data)
var coordinates = mouse_data.latlng;
//console.log(coordinates.lat,coordinates.lng);
currentPos.transition().duration(2000).ease('bounce').attr("x", scaleLng(coordinates.lng))
.attr("y", scaleLat(coordinates.lat))
.text("Lat: "+coordinates.lat.toFixed(2) +" : Lng: "+ coordinates.lng.toFixed(2));
}
</script>
</body>
</html>
Within this, 2nd basic D3 tutorial, I will show you how to combine D3 with Leaflet whereby each library can demonstrate its vantages!
These will be the main topics:
<ul>
<li><a href="#map">Set up a map using Leaflet</a>
<li><a href="#zoombar">Add an individual zoombar</a>
<li><a href="#center">Show the center of the map</a>
<li><a href="#extent">Show the extent of the map</a>
<li><a href="#scale">Get a 1st impression of d3.scale()</a>
</ul>
When you are not familiar with D3 or Leaflet I recommend you to apply these tutorials, previously:
<ul>
<li><span style="line-height: 16px"><a title="D3-Basics – All you need is a selection!" href="http://www.digital-geography.com/d3-basics-all-you-need-is-a-selection/" target="_blank">D3 Basics</a>
</span></li>
<li><a title="geoJSON with QGIS and leaflet: from data to map" href="http://www.digital-geography.com/geojson-with-qgis-and-leaflet-from-data-to-map/" target="_blank">geoJSON &amp; Leaflet</a></li>
</ul>
<a id="map"></a>
<h3>Set up a map using Leaflet</h3>
Let's begin by setting up an html-file containing a very simple map. How this can be done, was already described <a title="geoJSON with QGIS and leaflet: from data to map" href="http://www.digital-geography.com/geojson-with-qgis-and-leaflet-from-data-to-map/" target="_blank">by Riccardo</a> ... so I 'll use this example with a few changes:
<ul>
<li>without data overlay</li>
<li>set Leaflet to the latest version</li>
<li>add d3.js</li>
</ul>
[code language="HTML"]
Testmap&lt;meta charset="utf-8" /&gt;&lt;/pre&gt;&lt;style&gt;&lt;!-- @import url(http://cdn.leafletjs.com/leaflet-0.6.1/leaflet.css);--&gt;&lt;/style&gt;&lt;pre&gt;&lt;/pre&gt;&lt;div id="map" style="width: 960px; height: 300px;"&gt;&lt;/div&gt;&lt;pre&gt;&lt;script type="text/javascript"&gt;// &lt;![CDATA[ var map = L.map('map').setView([52.45,13.4], 9); var toolserver = L.tileLayer('http://{s}.www.toolserver.org/tiles/bw-mapnik/{z}/{x}/{y}.png');// ]]&gt;&lt;/script&gt;
[/code]
Basically, I'd like to have another layer! So we'll add another layer (from 'stamen') and add a layer switch:
[code language="JavaScript"]
var map = L.map('map').setView([52.45,13.4], 9);
var toolserver = L.tileLayer('http://{s}.www.toolserver.org/tiles/bw-mapnik/{z}/{x}/{y}.png');
var stamen = L.tileLayer('http://{s}.tile.stamen.com/toner/{z}/{x}/{y}.png', {attribution: 'Add some attributes here!'}).addTo(map);
var baseLayers = {"stamen": stamen, "toolserver-mapnik":toolserver};L.control.layers(baseLayers).addTo(map);
[/code]
Your 'simple' map example should look now something like this:
Quickview der Karte???!!!
<a id="zoombar"></a>
<h3>OK ... let's add a simple, individual zoombar to the map, using D3!</h3>
Begin, by adding a 'div'-element to the body. It acts like a container, so we'll name it 'container'...
[code language="JavaScript"]
var container = d3.select("body").append("div").attr("id","#container");
[/code]
... and the same for a 'svg-container' element, notice that the 'svg'-element needs extents!
[code language="JavaScript"]
var svgContainer = container.append("svg").attr("width", 960).attr("height", 200);
[/code]
Now, we add the steering elements and a textual indication of background information!
<ul>
<li><span style="line-height: 16px">Zoom in</span></li>
</ul>
[code language="JavaScript"]
var zoomIn = svgContainer.append("rect")
.attr("x", 0) .attr("y", size)
.attr("width", size)
.attr("height", size)
.style("fill","#0f0")
.on("mouseup",zoomInAction);
[/code]
<ul>
<li>Zoom out</li>
</ul>
[code language="JavaScript"]
var zoomOut = svgContainer.append("rect")
.attr("x", size+10)
.attr("y", size)
.attr("width", size)
.attr("height", size)
.style("fill","#f00")
.on("mouseup",zoomOutAction);[/code]
<ul>
<li>Text</li>
</ul>
[code language="JavaScript"]
var zoomText = svgContainer.append("text")
.text("Zoomlevel: "+map.getZoom())
.attr("x", (size * 2)+20)
.attr("y", size * 2)
.style("fill","#444")
.style("font-size","20");
[/code]
Maybe you have already realised … there are some variables that were not alocated so far. We have to define the ‘size’ value…
[code language="JavaScript"]var size = 20;[/code]
… and we need the functions for interaction.
[code language="JavaScript"]
function zoomOutAction(){
anotherFunction(-1);
}
function zoomInAction(){
anotherFunction(1);
}
function anotherFunction(value){
var newZoomlevel = map.getZoom() + value
map.setZoom(newZoomlevel)
zoomText.text("Zoomlevel: "+newZoomlevel)
}
[/code]
I prefer to have a function for each interaction (‘zoomOutAction’ &amp; ‘zoomInAction’) and one function that executes both interactions (‘anotherFunction’). You can see … when you click on the ‘zoomIn’-rectangle … the function ‘zoomInAction’ is executed … that executes the function ‘anotherFunction’ with the value ‘+1′ … what results in increase of the current zoomlevel of the map. Same principle but other values for ‘zoomOut’!
Cool ... not stylish but 'handmade' ;-)  Now you surely ask yourself: "Is there more information that we could display?"
Yes...a lot, just have a <a href="http://leafletjs.com/reference.html#map-get-methods" target="_blank">look into the documentation</a>. I will just show you two more examples on how to get and show them:
<ul>
<li><span style="line-height: 15.989583969116px">the center of the map</span></li>
</ul>
[code language="JavaScript"]map.getCenter()[/code]
<ul>
<li>the extent of the map</li>
</ul>
[code language="JavaScript"]map.getBounds()[/code]
<a id="center"></a>
<h3>Show the center of the map</h3>
First of all ... we need a text element, that will show us the information...
[code language="JavaScript"]
var centerText = svgContainer.append("text")
.attr("x", 0)
.attr("y", size * 3)
.style("fill","#444")
.style("font-size","20");[/code]
... and a function that does the action stuff...
[code language="JavaScript"]
function showCenterInfo(){
centerText.text("Center Lat: "+map.getCenter().lat.toFixed(4)+" Lng: "+map.getCenter().lng.toFixed(4))
}
[/code]
... we want to see the initial values, so call the function directly after you've initialised the 'text'-element 'centerText'.
[code language="JavaScript"]showCenterInfo();[/code]
Aha ... but this information is static ... hmm ... a 'button' to call the function would be a solution ... but ... an elegant solution would be to react on map interactions, right?
<strong><em>Let's do it!</em></strong>
Leaflet offers you some 'action listeners'  (<a href="http://leafletjs.com/reference.html#map-events" target="_blank">see documentation</a>) that let you know, when you have to update the relevant information. We have to update the 'centerText' (means: call the corresponding function 'showCenterInfo()') when the map was:
<ul>
<li><span style="line-height: 15.989583969116px">dragged</span></li>
</ul>
[code language="JavaScript"]map.on("drag", showCenterInfo);[/code]
<ul>
<li>zoomed in/out</li>
</ul>
[code language="JavaScript"]map.on("viewreset", showCenterInfo);[/code]
These lines can be added anywhere, but it is more clear to add it in the near of the 'map' definition.
***
One small addition ... let's add a tiny, cute center point, what about that? So...making this in D3 would be too early, as it is a little bit more complicated...hey...we still have Leaflet as option, and are ready to use it ;-) ... <a href="http://leafletjs.com/reference.html#circle" target="_blank">have a look a the documentation</a> ... and ...
[code language="JavaScript"]
L.circle([map.getCenter().lat, map.getCenter().lng], 200).addTo(map);
[/code]
...add this line to the function 'showCenterInfo()' ... try it and move the map ... ah, funny, isn't it ... but ... be a smart JavaScript developer, have a short look into your development window and analyze the point elements ... upps that's a lot!!!
I think a static center point is enough ... add the previous line to the near of the map definition and alocate it to a variable...
[code language="JavaScript"]
var centerPoint = L.circle([map.getCenter().lat, map.getCenter().lng], 200).addTo(map);
[/code]
...and change the code within that function dynamically, which is a kind of reset!
[code language="JavaScript"]
centerPoint.setLatLng([map.getCenter().lat, map.getCenter().lng])
[/code]
Nice...
<a id="extent"></a>
<h3>Show the extent of the map</h3>
<span style="font-size: 13px;line-height: 19px">Basically, this is just a repetion of the previous code, especially because the actualisation of the information is released by the same interactions. </span>
<strong><em>BUT...</em></strong>we're here to learn something...so let's stabilize our knowledge and use another method, we already got to know... <strong><em>d3.selectAll()</em></strong> ... do you remember?
As we are going to do a lot of selection stuff ... it could be advantagous to have a clean playground ... that is why I start by adding a <strong><em>Group</em></strong> ('g') to the 'svgContainer'...
[code language="JavaScript"]var group = svgContainer.append('g');[/code]
...within that group, we can make selections and no (already) existing element will make problems
Start slowly, by examining the available data...
[code language="JavaScript"]
console.log(map.getBounds()._northEast, map.getBounds()._southWest)
[/code]
...aha...2 Points...wait...
[code language="JavaScript"]
var rectangle = [];
rectangle.push([map.getBounds()._northEast.lat, map.getBounds()._northEast.lng])
rectangle.push([map.getBounds()._northEast.lat, map.getBounds()._southWest.lng])
rectangle.push([map.getBounds()._southWest.lat, map.getBounds()._southWest.lng])
rectangle.push([map.getBounds()._southWest.lat, map.getBounds()._northEast.lng])
console.log(rectangle)
[/code]
...4 Points...and an array...that reminds me on something...
[code language="JavaScript"]
var coordText = group.selectAll('text').data(rectangle).enter().append("text").text("Hello")
[/code]
...we have already seen that in the <a href="http://www.digital-geography.com/d3-basics-all-you-need-is-a-selection/" title="D3-Basics – All you need is a selection!" target="_blank">previous D3-Basics-Tutorial</a>, right?
Ok...go on and define the text elements...
[code language="JavaScript"]
coordText.attr("x", 0)
.attr("y", function(data,i){return size * (4+i)})
.style("fill","#444")
.style("font-size","20");
[/code]
...and check out how to change the text content directly by adding...
[code language="JavaScript"]
.text(function(data){return "Lat: "+data[0].toFixed(2) +" : Lng: "+ data [1].toFixed(2)});
[/code]
...to the chain!
Aha...hmm...maybe we can use the values directly to define the positions of the text elements...but therefore, we need a scale ... or to be accurate a 'wonderful'
<a id="scale"></a>
<h2><a href="https://github.com/mbostock/d3/wiki/Quantitative-Scales" target="_blank">d3.scale()</a></h2>
I'd like to use the coordinates of each extent-point to define the text-coordinates of each extent point. Let's project these values onto a 'field' of this dimension: height=80 width=760 ... have a look at the code...
[code language="JavaScript"]
var scaleLat = d3.scale.linear();
scaleLat.domain([map.getBounds()._northEast.lat,map.getBounds()._southWest.lat])
.range([size*4,size*7]);
[/code]
...this is the scale for the latitude ... you can define a 'domain' (your data) and a 'range' (the values I want to project them on) ... in result, I can scale between both values very, very simple!
For example ... get the corresponding latitude of the value '100':
[code language="JavaScript"]console.log(scaleLat.invert(100))[/code]
... or the value of the latitude '52.30':
[code language="JavaScript"]console.log(scaleLat(52.30))[/code]
Add a similar code for the longitude...
[code language="JavaScript"]
var scaleLng = d3.scale.linear();
scaleLng.domain([map.getBounds()._southWest.lng, map.getBounds()._northEast.lng])
.range([0,750]);
[/code]
Bring them to the action, by using them to change the coordinate definition of the text elements...
[code language="JavaScript"]
coordText.attr("x", function(data,i){return scaleLng(data[1])})
.attr("y", function(data,i){return scaleLat(data[0])})
[/code]
Yeah ... that's cool ... but I'd like to have it updated on any map action ... therefore we have to throw everything into a function ...
[code language="JavaScript"]
function defineExtentVisualisation(){
var rectangle = [];
rectangle.push([map.getBounds()._northEast.lat, map.getBounds()._northEast.lng])
rectangle.push([map.getBounds()._northEast.lat, map.getBounds()._southWest.lng])
rectangle.push([map.getBounds()._southWest.lat, map.getBounds()._southWest.lng])
rectangle.push([map.getBounds()._southWest.lat, map.getBounds()._northEast.lng])
scaleLat.domain([map.getBounds()._northEast.lat,map.getBounds()._southWest.lat])
.range([size*4,size*7]);
scaleLng.domain([map.getBounds()._southWest.lng, map.getBounds()._northEast.lng])
.range([0,750]);
group.selectAll('text').remove();
var coordText = group.selectAll('text').data(rectangle).enter().append("text").text("Hello")
coordText.attr("x", function(data,i){return scaleLng(data[1])})
.attr("y", function(data,i){return scaleLat(data[0])})
.style("fill","#444")
.style("font-size","20")
.text(function(data){return "Lat: "+data[0].toFixed(2) +" : Lng: "+ data [1].toFixed(2)});
}
[/code]
...did you notice ... as this function will always selectAll text elements and add new for each 'rectangle' element, we have to remove all existing before...that is why I added this line...
[code language="JavaScript"]group.selectAll('text').remove(); [/code]
...and we have to define the scales outside of the function, so I'll do that directly after adding the group to the svgContainer...
[code language="JavaScript"]
var group = svgContainer.append('g');
var scaleLat = d3.scale.linear();
var scaleLng = d3.scale.linear();
[/code]
...and finally we have to call the function within the existing function for map-interaction 'showCenterInfo'...
[code language="JavaScript"]defineExtentVisualisation(); [/code]
...that's it...now the info on the extent should be update each time a interaction with the map happens!
I think/hope, that you have now a good knowledge base for being able to play with D3 &amp; Leaflet on your own!
You've got no idea for something else? ... What about an additional information on the mouse position when you move it over the map?!? Here's what you'd need for that:
<ul>
<li>a 'text' element that will show the mouse coordinates --&gt; 'currentPos'</li>
<li>a Leaflet 'action listener' --&gt; 'mousemove'</li>
<li>a function that is called by the 'listener' --&gt; 'setCurrentPosition'</li>
</ul>
I've added this to <a href="http://bl.ocks.org/milkbread/5880252" target="_blank">the final html-file of this tutorial</a> ... I hope you've liked it :-)
In the next tutorial, I plan to show you how to use combined D3-Leaflet data-overlays, their advantage and some other cool stuff.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment