----------------------------------------------------------
Leaflet map with d3.js objects that scale with the map
The first example we’ll look at will project a leaflet.js map on the screen with a d3.js object (in this case a simple rectangle) onto the map.
The rectangle will be bound to a set of geographic coordinates so that as the map is panned and zoomed the rectangle will shrink and grow. For example the following diagram shows a rectangle (made with d3.js ) superimposed over a leaflet.js map;
Rectangular d3 area on leaflet map |
If we then zoom in…
Zoomed rectangular d3 area on leaflet map |
…the rectangle zooms in as well.
This may not sound terribly exciting and if you’re familiar with Leaflet you will know that it is possible to draw polygons onto a map using only leaflet’s built in functions. However, the real strength of this application of vector data comes when making the d3.js content interactive which is more difficult with leaflet.js.
For an excellent example of this please visit Mike Bostock’s tutorial where he demonstrates superimposing a map of the United States separated by state (which react individually to the mouse being hovered over them). My following explanation is a humble derivation of his code.
Speaking of code, here is a full listing of the code that we will be using;
<!DOCTYPE html>
<html>
<head>
<title>
Leaflet and D3 Map</title>
<meta
charset=
"utf-8"
/>
<link
rel=
"stylesheet"
href=
"http://cdn.leafletjs.com/leaflet-0.7/leaflet.css"
/>
</head>
<body>
<div
id=
"map"
style=
"width: 600px; height: 400px"
></div>
<script
src=
"http://d3js.org/d3.v3.min.js"
></script>
<script
src=
"http://cdn.leafletjs.com/leaflet-0.7/leaflet.js"
>
</script>
<script>
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
:
'© '
+
mapLink
+
' Contributors'
,
maxZoom
:
18
,
}).
addTo
(
map
);
// Add an SVG element to Leaflet’s overlay pane
var
svg
=
d3
.
select
(
map
.
getPanes
().
overlayPane
).
append
(
"svg"
),
g
=
svg
.
append
(
"g"
).
attr
(
"class"
,
"leaflet-zoom-hide"
);
d3
.
json
(
"rectangle.json"
,
function
(
geoShape
)
{
// create a d3.geo.path to convert GeoJSON to SVG
var
transform
=
d3
.
geo
.
transform
({
point
:
projectPoint
}),
path
=
d3
.
geo
.
path
().
projection
(
transform
);
// create path elements for each of the features
d3_features
=
g
.
selectAll
(
"path"
)
.
data
(
geoShape
.
features
)
.
enter
().
append
(
"path"
);
map
.
on
(
"viewreset"
,
reset
);
reset
();
// fit the SVG element to leaflet's map layer
function
reset
()
{
bounds
=
path
.
bounds
(
geoShape
);
var
topLeft
=
bounds
[
0
],
bottomRight
=
bounds
[
1
];
svg
.
attr
(
"width"
,
bottomRight
[
0
]
-
topLeft
[
0
])
.
attr
(
"height"
,
bottomRight
[
1
]
-
topLeft
[
1
])
.
style
(
"left"
,
topLeft
[
0
]
+
"px"
)
.
style
(
"top"
,
topLeft
[
1
]
+
"px"
);
g
.
attr
(
"transform"
,
"translate("
+
-
topLeft
[
0
]
+
","
+
-
topLeft
[
1
]
+
")"
);
// initialize the path data
d3_features
.
attr
(
"d"
,
path
)
.
style
(
"fill-opacity"
,
0.7
)
.
attr
(
'fill'
,
'blue'
);
}
// Use Leaflet to implement a D3 geometric transformation.
function
projectPoint
(
x
,
y
)
{
var
point
=
map
.
latLngToLayerPoint
(
new
L
.
LatLng
(
y
,
x
));
this
.
stream
.
point
(
point
.
x
,
point
.
y
);
}
})
</script>
</body>
</html>
There is also an associated json data file (called
rectangle.json
) that has the following contents;{
"type": "FeatureCollection",
"features": [ {
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [ [
[ 174.78, -41.29 ],
[ 174.79, -41.29 ],
[ 174.79, -41.28 ],
[ 174.78, -41.28 ],
[ 174.78, -41.29 ]
] ]
}
}
]
}
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-combined.html’ and ‘rectangle.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, 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>
Leaflet and D3 Map</title>
<meta
charset=
"utf-8"
/>
<link
rel=
"stylesheet"
href=
"http://cdn.leafletjs.com/leaflet-0.7/leaflet.css"
/>
</head>
<body>
<div
id=
"map"
style=
"width: 600px; height: 400px"
></div>
<script
src=
"http://d3js.org/d3.v3.min.js"
></script>
<script
src=
"http://cdn.leafletjs.com/leaflet-0.7/leaflet.js"
>
</script>
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
<style>
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
:
'© '
+
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.
The first part of that involves making sure that Leaflet and D3 are synchronised in the view that they’re projecting. This synchronisation needs to occur in zooming and panning so we add an SVG element to Leaflet’s
overlayPlane
var
svg
=
d3
.
select
(
map
.
getPanes
().
overlayPane
).
append
(
"svg"
),
g
=
svg
.
append
(
"g"
).
attr
(
"class"
,
"leaflet-zoom-hide"
);
Then we add a
g
element that ensures that the SVG element and the Leaflet layer have the same common point of reference. Otherwise when they zoomed and panned it could be offset. The leaflet-zoom-hide
affects the presentation of the map when zooming. Without it the underlying map zooms to a new size, but the d3.js elements remain as they are until the zoom effect has taken place and then they adjust. It still works fine, but it ‘looks’ wrong.
Then we load our data file with the line…
d3
.
json
(
"rectangle.json"
,
function
(
geoShape
)
{
This is pretty standard fare for d3.js but it’s worth being mindful that while the type of data file is
.json
this is a GeoJSON file and they have particular features (literally) that allow them to do their magic. There is a good explanation of how they are structured at geojson.org for those who are unfamiliar with the differences.
Using our data we need to ensure that it is correctly transformed from our latitude/longitude coordinates as supplied to coordinates on the screen. We do this by implementing d3’s geographic transformation features (d3.geo).
var
transform
=
d3
.
geo
.
transform
({
point
:
projectPoint
}),
path
=
d3
.
geo
.
path
().
projection
(
transform
);
Here the path that we want to create in SVG is generated from the points that are supplied from the data file which are converted by the function
projectPoint
This function (which is placed at the end of the file) takes our latitude and longitudes and transforms them to screen (layer) coordinates.function
projectPoint
(
x
,
y
)
{
var
point
=
map
.
latLngToLayerPoint
(
new
L
.
LatLng
(
y
,
x
));
this
.
stream
.
point
(
point
.
x
,
point
.
y
);
}
With the transformations now all taken care of we can generate our path in the traditional d3.js way and append it to our
g
group.d3_features
=
g
.
selectAll
(
"path"
)
.
data
(
geoShape
.
features
)
.
enter
().
append
(
"path"
);
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"
,
reset
);
reset
();
Obviously when our view changes we call the function
reset
. It’s the job of the reset
function to ensure that whatever the leaflet layer does, the SVG (d3.js) layer follows;function
reset
()
{
bounds
=
path
.
bounds
(
geoShape
);
var
topLeft
=
bounds
[
0
],
bottomRight
=
bounds
[
1
];
svg
.
attr
(
"width"
,
bottomRight
[
0
]
-
topLeft
[
0
])
.
attr
(
"height"
,
bottomRight
[
1
]
-
topLeft
[
1
])
.
style
(
"left"
,
topLeft
[
0
]
+
"px"
)
.
style
(
"top"
,
topLeft
[
1
]
+
"px"
);
g
.
attr
(
"transform"
,
"translate("
+
-
topLeft
[
0
]
+
","
+
-
topLeft
[
1
]
+
")"
);
// initialize the path data
d3_features
.
attr
(
"d"
,
path
)
.
style
(
"fill-opacity"
,
0.7
)
.
attr
(
'fill'
,
'blue'
);
}
It does this by establishing the
topLeft
and bottomRight
corners of the desired area and then it applies thewidth
, height
, top
and bottom
attributes to the svg element and translates the g
element to the right spot. Last, but not least it redraws the path.
The end result being a fine combination of leaflet.js map and d3.js element;
Rectangular d3 area on leaflet map |
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 :-)).
This is really helpful! Thanks so much for writing.
ReplyDeleteThanks so much for this info. It saved me a ton of time!!!
ReplyDelete