Monday, 3 March 2014

Leaflet map with d3.js elements that are overlaid on a map

The following post is a portion of the D3 Tips and Tricks book which is free to download. To use this post in context, consider it with the others in the blog or just download the the book as a pdf / epub or mobi .
----------------------------------------------------------

Leaflet map with d3.js elements that are overlaid on a map

The next example of a combination of d3.js and leaflet.js is one where we want to have an element overlaid on our map at a specific location, but have it remain a specific size over the map. For example, here we will display 5 circles which are centred at specific geographic locations.
d3.js circles fixed in geographic location on leaflet map but constant size
When we zoom out of the map, those circles remain over the geographic location, but the same size on the screen.
Zoomed d3.js circles fixed in geographic location on leaflet map but constant size
You may (justifiably) ask yourself why we would want to do this with d3.js when Leaflet could do the same job with a marker? The answer is that as cool as leaflet.js’s markers are, d3 elements have a wider range of features that make their use advantageous in some situations. For instance if you want to animate or rotate the icons or dynamically adjust some of their attributes, d3.js would have a greater scope for adjustments.
The following code draws circles at geographic locations;
<!DOCTYPE html>
<html>
<head>
 <title>d3.js with leaflet.js</title>

    <link 
        rel="stylesheet" 
        href="http://cdn.leafletjs.com/leaflet-0.7/leaflet.css"
    />
    <script src="http://d3js.org/d3.v3.min.js"></script>

    <script
        src="http://cdn.leafletjs.com/leaflet-0.7/leaflet.js">
    </script>
    
</head>
<body>

 <div id="map" style="width: 600px; height: 400px"></div>

 <script type="text/javascript">
 
        var map = L.map('map').setView([-41.2858, 174.7868], 13);
        mapLink = 
            '<a href="http://openstreetmap.org">OpenStreetMap</a>';
        L.tileLayer(
            'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            attribution: '&copy; ' + mapLink + ' Contributors',
            maxZoom: 18,
            }).addTo(map);
    
 // Initialize the SVG layer
 map._initPathRoot()    

 // We pick up the SVG from the map object
 var svg = d3.select("#map").select("svg"),
 g = svg.append("g");
 
 d3.json("circles.json", function(collection) {
  // Add a LatLng object to each item in the dataset
  collection.objects.forEach(function(d) {
   d.LatLng = new L.LatLng(d.circle.coordinates[0],
      d.circle.coordinates[1])
  })
  
  var feature = g.selectAll("circle")
   .data(collection.objects)
   .enter().append("circle")
   .style("stroke", "black")  
   .style("opacity", .6) 
   .style("fill", "red")
   .attr("r", 20);  
  
  map.on("viewreset", update);
  update();

  function update() {
   feature.attr("transform", 
   function(d) { 
       return "translate("+ 
    map.latLngToLayerPoint(d.LatLng).x +","+ 
    map.latLngToLayerPoint(d.LatLng).y +")";
       }
   )
  }
 })    
</script>
</body>
</html> 
There is also an associated json data file (called circles.json) that has the following contents;
{"objects":[
{"circle":{"coordinates":[-41.28,174.77]}},
{"circle":{"coordinates":[-41.29,174.76]}},
{"circle":{"coordinates":[-41.30,174.79]}},
{"circle":{"coordinates":[-41.27,174.80]}},
{"circle":{"coordinates":[-41.29,174.78]}}
]}
The full code and a live example are available online at bl.ocks.org or GitHub. They are also available as the files ‘leaflet-d3-linked.html’ and ‘circles.json’ as a separate download with D3 Tips and Tricks. A a copy of all the files that appear in the book can be downloaded (in a zip file) when you download the book from Leanpub
While I will explain the code below, as with the previous example (which is similar, but different) please be aware that I will gloss over some of the simpler sections that are covered in other sections of either books and will instead focus on the portions that are important to understand the combination of d3 and leaflet.
Our code begins by setting up the html document in a fairly standard way.
<!DOCTYPE html>
<html>
<head>
 <title>d3.js with leaflet.js</title>

    <link 
        rel="stylesheet" 
        href="http://cdn.leafletjs.com/leaflet-0.7/leaflet.css"
    />
    <script src="http://d3js.org/d3.v3.min.js"></script>

    <script
        src="http://cdn.leafletjs.com/leaflet-0.7/leaflet.js">
    </script>
    
</head>
<body>

 <div id="map" style="width: 600px; height: 400px"></div>
Here we’re getting some css styling and loading our leaflet.js / d3.js libraries. The only configuration item is where we set up the size of the map (in the <div> section and as part of the map div).
Then we break into the JavaScript code. The first thing we do is to project our Leaflet map;
    var map = L.map('map').setView([-41.2858, 174.7868], 13);
    mapLink = 
        '<a href="http://openstreetmap.org">OpenStreetMap</a>';
    L.tileLayer(
        'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '&copy; ' + mapLink + ' Contributors',
        maxZoom: 18,
        }).addTo(map);
This is exactly the same as we have done in any of the simple map explanations in Leaflet Tips and Tricks and in this case we are using the OpenStreetMap tiles.
Then we start on the d3.js part of the code.
Firstly the Leaflet map is initiated as SVG using map._initPathRoot().
 // Initialize the SVG layer
 map._initPathRoot()    

 // We pick up the SVG from the map object
 var svg = d3.select("#map").select("svg"),
 g = svg.append("g");
Then we select the svg layer and append a g element to give a common reference point g = svg.append("g").
Then we load the json file with the coordinates for the circles;
 d3.json("circles.json", function(collection) {
Then for each of the coordinates in the objects section of the json data we declare a new latitude / longitude pair from the associated coordinates;
 collection.objects.forEach(function(d) {
        d.LatLng = new L.LatLng(d.circle.coordinates[0],
                                d.circle.coordinates[1])
 })
Then we use a simple d3.js routine to add and place our circles based on the coordinates of each of ourobjects.
  var feature = g.selectAll("circle")
   .data(collection.objects)
   .enter().append("circle")
   .style("stroke", "black")  
   .style("opacity", .6) 
   .style("fill", "red")
   .attr("r", 20); 
We declare each as a feature and add a bit of styling just to make them stand out.
The last ‘main’ part of our JavaScript makes sure that when our view of what we’re looking at changes (we zoom or pan) that our d3 elements change as well;
  map.on("viewreset", update);
  update();
Obviously when our view changes we call the function update. It’s the job of the update function to ensure that whenever the leaflet layer moves, the SVG layer with the d3.js elements follows and the points that designate the locations of those objects move appropriately;
 function update() {
  feature.attr("transform", 
  function(d) { 
   return "translate("+ 
    map.latLngToLayerPoint(d.LatLng).x +","+ 
    map.latLngToLayerPoint(d.LatLng).y +")";
   }
  )
 }
Here we are using the transform function on each feature to adjust the coordinates on our LatLngcoordinates. We only need to adjust our coordinates since the size, shape, rotation and any other attribute or style is dictated by the objects themselves.
And there we have it!
d3.js circles fixed in geographic location on leaflet map but constant size


The description above (and heaps of other stuff) is in the D3 Tips and Tricks book that can be downloaded for free (or donate if you really want to :-)).

2 comments:

  1. On inspection I am able to point circles. But they are not visible. I tried changing the z-index but it doesn't help.

    ReplyDelete
    Replies
    1. Interesting. Compare your code with the example here http://blockbuilder.org/d3noob/9267535 and see where the difference lies.

      Delete