Chapter 8 Symbology and Interactivity
Last updated: 2018-11-11 20:44:59
8.1 Introduction
Styling of map features lets us convey quantitative or qualitative information (how many residents are in that polygon?) or emphasis for drawing viewer attention (where is the border of the particular polygon of interest?). The way that aesthetic properties of map features are associated with underlying data or meaning is collectively known as map symbology, an essential concept of mapping in general, and web mapping in particular.
In the last part of Chapter 7 we learned about how GeoJSON layers can be added on a web map, whether from an object defined in the JavaScript environment (Section 7.4), a local file (Section 7.7.2) or a remote file (Section 7.7.3). As for style, however, all of the layers’ features were drawn the same way. That is because all feature were set with the same default settings for their various aesthetic properties, such as fill color, line width, etc.
In addition to map symbology, web maps usually also express interactive behavior to further enhance user experience and convey even more information. Interactive behavior is what really sets interactive maps apart from static maps, such as those printed on paper or contained in image files and PDF documents. For example, an interactive map may have controls for turning overlaid layers on or off, switching between different base maps, displaying popups with textual or multimedia content for each clicked feature, highlighting elements on mouse hover, and so on.
In this Chapter, we concentrate on defining map symbology along with a corresponding legend, by setting the style of our GeoJSON layers displayed on the map, and interactive behavior, by setting custom popups, dynamic styling and dynamic information boxes.
8.2 L.geoJSON
options
As shown in Section 7.4, the L.geoJSON
function accepts a GeoJSON object and turns it into a GeoJSON layer which can be displayed on a Leaflet map. Additionally, the L.geoJSON
can also accept a number of options which we have not used yet. These options allow for styling, filtering, attaching event listeners or popups, and otherwise controlling the display and functionality of the individual features comprising the GeoJSON layer.
In this Chapter we use the following two options to enhance our GeoJSON layers -
style
- Determines layer style. As shown below,style
can be either an object to style all features the same way (Section 8.3), or a function to style features based on their properties (Section 8.4)onEachFeature
- A function that gets called on each feature before adding it to a GeoJSON layer. The function passed toonEachFeature
can be used to add specific popups (Section 8.5) or event listeners (Section 8.8.1) to each feature
When using both options, the L.GeoJSON
function call may look like this -
L.geoJSON(geojson, {style: ..., onEachFeature: ...}).addTo(map);
Where -
geojson
is the GeoJSON object{style: ..., onEachFeature: ...}
is the options object, in this case specifying bothstyle
andonEachFeature
options
The ...
parts in the above expression are to be replaced with an object or a function (see below), depending on how we want to define the appearance and behavior of the GeoJSON layer.
In addition to style
and onEachFeature
, in the exercise of this Chapter, and in Section 12.4.5, we will use a third option of L.geoJSON
called pointToLayer
. The pointToLayer
option determines the way that GeoJSON geometries are translated to visual layers. For example the pointToLayer
option can be used to determine whether a GeoJSON "point"
feature should be displayed as a marker (the default), a circle marker, or a circle.
8.3 Constant style
The simplest way to style our GeoJSON layer is to set constant aesthetic properties for all of the features it contains. As always, we only need to set those specific properties where we would like to override the default appearance.
For example, let’s start with example lesson-07-01.html
from section 7.4. In that example, we used the following expression to add the GeoJSON object named states
to our map -
L.geoJSON(states).addTo(map);
This sets the default Leaflet style for polygons: blue border and semi-transparent blue fill (Figure 7.4). To override some of the default settings, the above expression can be replaced with this one -
L.geoJSON(states, {
style: {
color: "red",
weight: 5,
fillColor: "yellow",
fillOpacity: 0.2
}
}).addTo(map);
Note that we are passing an object of options to the L.geoJSON
function. The object contains one property named style
, for which the value is also an object of style options. In this example we use four style options -
color: "red"
- Border color = redweight: 5
- Border width =5px
fillColor: "yellow"
- Fill color = yellowfillOpacity: 0.2
- Fill opacity = 20% opaque
The resulting map example-08-01.html
is shown on Figure 8.1.
The options being set in this example - border width, border and fill color and opacity levels - are the most commonly used ones. Other options include line cap and join styles (lineCap
, lineJoin
) and line dash types (dashArray
, dashOffset
). The full list of styling options can be found in the path options section in the Leaflet documentation.
8.4 Varying style
8.4.1 States example
In case we want the style to vary according to the properties of each feature, we need to set the style
property to a function, instead of an object as shown above.
For example, our GeoJSON layer has an attribute named party
, with two possible values of "Republican"
and "Democrat"
. To color the state polygons according to party
, we need to write a function to determine the feature style according to the feature properties. The function takes an argument feature
, which refers the current GeoJSON feature, and returns the style
object for that particular feature.
In the following example, the styling function returns an object with four properties. Three out the four properties are set to constant values (color
, weight
, fillOpacity
). The fourth property (fillColor
) is variable. The fillColor
property is dependent on the party
attribute of the current feature (feature.properties.party
), through the party_color
function (see below).
function party_style(feature) {
return {
color: "black",
weight: 1,
fillColor: party_color(feature.properties.party),
fillOpacity: 0.7
}
}
The separately defined internal function party_color
takes a party name p
and returns a color name, which goes on to set the fillColor
. The function uses the if...else
conditional which we learned about in Section 3.9.1.
function party_color(p) {
if(p == "Republican") return "red"; else
if(p == "Democrat") return "blue"; else
return "grey";
}
The code of the party_color
function means that if the value of p
is "Republican"
the function returns "red"
, and if the value of p
is "Democrat"
the function returns "blue"
. If any other value is encountered the function returns the default color "grey"
.
Finally, we can use the party_style
function as the style
option when creating the states
layer -
L.geoJSON(states, {style: party_style}).addTo(map);
The resulting map example-08-02.html
is shown on Figure 8.2. Note that polygon fill color is variable: one of the polygons is red while the other is blue.
8.4.2 Towns example
As another example, we will modify the style of polygons in the Israel towns example-07-03.html
from Section 7.7.2. The towns layer in the towns.geojson
GeoJSON file contains an attribute named pop_2015
, containing the population size estimate per town in 2015. We will use the pop_2015
attribute to set a fill color reflecting the population size of each town.
First, we need to define a function which can accept an attribute value and return a color, just like the party_color
function from the previous example. Since the pop_2015
attribute is a continuous variable, our function will operate using breaks to divide the continuous distribution into categories. Each of the categories will be associated with a different color. The break points, along with the respective colors, comprise the layer symbology.
There are several commonly used methods for determining the appropriate breaks when classifying a continuous distribution into a set of discrete categories. The simplest method, appropriate when the distribution of values is roughly uniform, is to use a linear scale. For example, if our values are between 0 and 1000, break points for a linear scale with five colors may be defines as follows -
[200, 400, 600, 800]
Note that there are just three intervals between these values (200-400, 400-600 and 600-800), with the 0-200 and 800-1000 ranges contributing the remaining two intervals. To emphasize this fact we could explicitly specify the distribution minimum/maximum (which are 0 and 1000), or just +/- Infinity
-
[-Infinity, 200, 400, 600, 800, Infinity]
When the distribution of the variable of interest is skewed, a linear scale may be inappropriate, as it would lead to some of the colors being much more common then others. As a result, the map may have little color diversity and appear uniformly colored. One solution is to use a logarithmic scale, such as -
[1, 10, 100, 1000]
The logarithmic scale gives increasingly detailed emphasis on one side of the distribution, by inducing increasingly narrower categories (compare 0-1 with 100-1000).
Yet another common way of dealing with non-uniform distributions is using quantiles. Quantiles are breakpoints that distribute our set of values into groups which contain equal counts of cases. For example, using quantiles we may divide our distribution into four equal groups containing a quarter of all values each (0%-25%-50%-75%-100%). There are advantages and disadvantages to quantile classification. The major advantage is that all colors are equally represented, which makes it easy to detect ordinal spatial patterns in our data. The downside is that category ranges can be widely different, in a non-systematic manner, which means that different color “steps” on the map are not consistently related to quantitative differences. In the present example, we are going to use quantiles to determine color breaks.
How can we determine the quantile break points for a given variable? There are many ways to do that in various software and programming languages, including in JavaScript.
One example of an external software to calculate quantiles is QGIS. Once the layer of interest is loaded in QGIS, we need to set its symbology choosing the Quantile (Equal Count) for Mode, as shown on Figure 8.3. We also need to choose how many quantiles we want. For example, choosing 5 means that our values are divided into 5 equally sized groups, each containing 20% of the observations (i.e. 0%-20%-40%-60%-80%-100%). The breaks will be automatically calculated and displayed.
For the sake of simplicity we will hereby provide pre-calculated quantiles. The four breaks between 5 quantiles of the pop_2015
variable are given in the following array -
[399, 642, 933.2, 2089.8]
Once we figured out the break points, we can write a color-selection function such as the one hereby named getColor
. The function accepts the (population size) value d
and determines the appropriate color for that value. Since we have 5 different cases rather than just two, the function uses a hierarchical set of four conditionals, as follows -
function getColor(d) {
if(d > 2089.8) return "..."; else
if(d > 933.2) return "..."; else
if(d > 642) return "..."; else
if(d > 399) return "..."; else
return "...";
}
What’s missing in the body of the above function ("..."
) are the color definitions. As discussed in Section 2.7.1, there are several ways to specify colors with CSS. The same methods are supported in Leaflet, too. For example, we can use RBG or RGBA values, HEX codes, or color names.
How can we pick the right set of five colors for our color scale? It may be tempting to pick the colors manually, but this may lead to an inconsistent color gradient. It is therefore best to use automatic tools and resources rather than try to pick the colors ourselves. There are many resources on the web and in software packages available for the job. One of the best and most well-known ones is colorbrewer2.org. The colorbrewer2.org website provides a collection of carefully selected color scales specifically designed for cartography by Cynthia Brewer (Figure 8.4). Conveniently, you can choose the color palette, and how many colors to pick from it, then export the HEX codes of all colors in several formats, including a JavaScript array.
Note that colorbrewer2.org provides three types of scales (Figure 8.4) -
- Sequential
- Diverging
- Qualitative.
For our population data we will use a sequantial scale, which is appropriate since the data are continuous and have a single direction (low-high). Diverging scales are appropriate for continuous variables that diverge in two directions around some kind of “middle” (low-mid-high). For example, rate of population growth can be mapped with a diverging scale, where positive and negative growth are shown with different increasingly dark colors (e.g. red and blue), while values around zero are shown in white. Qualitative scales are appropriate for categorical variables that have no inherent order, mapped to an easily distinguishable set of colors. For instance, land cover categories (e.g. built area, forests, water bodies, etc.) can be mapped to a qualitative scale. Note that it makes sense to choose intuitively interpretable colors whenever possible, e.g. built areas = grey, water bodies = blue, forest = green, etc.
To export color codes from colorbrewer2.org -
- Go to http://colorbrewer2.org
- Select the number of data classes
- Pick the nature of your data, i.e. scale type (sequential, diverging or qualitative)
- Select a color scheme
- Click export and copy the contents of the text box named JavaScript
For example, taking 5 classes from the fourh (from left) sequential color scheme, the colorbrewer2.org website gives us the following set of color codes -
["#fef0d9", "#fdcc8a", "#fc8d59", "#e34a33", "#b30000"]
This is an expression for creating a JavaScript array, which we can copy and paste directly into our JavaScript code.
By the way, colorbrewer2.org contains a JavaScript code file named colorbrewer.js
, with the complete set of color palette definitions as JavaScript arrays. You can include the file in your website or map to use the ColorBrewer scales.
Now that we have the five color codes, let’s insert them into the getColor
function definition -
function getColor(d) {
if(d > 2089.8) return "#b30000"; else
if(d > 933.2) return "#e34a33"; else
if(d > 642) return "#fc8d59"; else
if(d > 399) return "#fdcc8a"; else
return "#fef0d9";
}
One disadvantage of the above getColor
function definition is that it uses a rigid set of if...else
conditionals, which will be inconvenient to modify in case we decide to change the number of break points and colors. A more general version of the function could operate on an array of break points and an array of colors. Then, instead of if...else
conditionals the function could use a for
loop. The loop goes over the breaks array to detect between which pair of breaks our value is situated, and returns the appropriate color.
Here is an example of such an alternative definition -
var breaks =
[-Infinity, 399, 642, 933.2, 2089.8, Infinity];
var colors =
["#fef0d9", "#fdcc8a", "#fc8d59", "#e34a33", "#b30000"];
function getColor(d) {
for(var i=0; i < breaks.length; i++) {
if(d >= breaks[i] && d < breaks[i+1]) {
return colors[i];
}
}
}
In this version, it is easier to modify the color scale - we just need to replace the breaks and color arrays in the first two expressions.
Like in the states example above (example-08-02.html
), the getColor
function is wrapped into another function named style
which is responsible for setting all of the styling options where we override the default.
function style(feature) {
return {
fillColor: getColor(feature.properties.pop_2015),
weight: 0.5,
opacity: 1,
color: "black",
fillOpacity: 0.7
};
}
This time, we have four constant properties (weight
, opacity
, color
and fillOpacity
) and one variable property (fillColor
).
Finally, we need to pass the style
function to the GeoJSON style
option when adding the towns layer on the map -
$.getJSON("data/towns.geojson", function(data) {
L.geoJSON(data, {style: style}).addTo(map);
});
The resulting map example-08-03.html
is shown on Figure 8.5. The towns polygons are now filled with one of the above five colors from our chosen scale, according to the getColor
function.
There is a lot of theory and considerations behind choosing a color scale, which we barely scratched the surface of. While our main focus is on the technical process of defining and applying the scale in a web map, the reader is referred to textbooks on Cartography (Dent, Torguson, and Hodler 2008) or Data Visualization (Tufte 2001) for more information on the theoretical considerations of choosing a color scale for particular types of data and display purposes.
8.5 Constructing popups from data
In Section 6.7, we introduced the .bindPopup
method for adding popups to simple shapes, such as the description popup for the line between the Aranne Library and the Geography Department in example-06-06.html
(Figure 6.11). We could do the same with a with a GeoJSON layer to bind the same popup to all features. However, it usually doesn’t make much sense to add the same popup to all of the features. Instead, we usually want to add specific popups per feature, where each popup conveys information on the respective feature where it is binded. To bind specific features, we use an option of the L.geoJSON
object called onEachFeature
. The onEachFeature
option applies a function on each feature when that feature is added to the map. We can use the function to add a popup with specific content, based on the feature properties.
The onEachFeature
option should comprise a function invoked on each feature when it is being processed into a layer on the map. Importantly, the function we supply to onEachFeature
has two parameters -
feature
- Referring to the current feature of the GeoJSON object being processedlayer
- Referring to the layer being added on the map
For example, the function code can utilize feature.properties
to access the properties of the GeoJSON object, then run the layer.bindPopup
method to add a corresponding popup on the layer for that specific feature.
The code below sets the onEachFeature
option for our towns
GeoJSON layer, and runs a function that binds a specific popup to each GeoJSON feature.
$.getJSON("data/towns.geojson", function(data) {
geojson = L.geoJSON(data, {
onEachFeature: function(feature, layer) {
layer.bindPopup(feature.properties.name_eng);
},
style: style
}).addTo(map);
});
Note how the L.geoJSON
function gets an options object with two properties: the onEachFeature
property contains an anonymous function defined inside the object, while the style
property is defined separately as a named function (incidentally having the same name style
).
One more thing introduced in this code section is that the reference to the GeoJSON layer is assigned to a variable, hereby named geojson
. This will be useful in subsequent examples in this Chapter (see below), where we will use the reference to the GeoJSON layer to execute its methods. Since we are using the geojson
variable, we should define it with an expression such as the following one at the beginning of our script -
var geojson;
The above code section binds a simple popup with just the town name, using the contents of the name_eng
attribute. However, as we have seen in Section 6.7, we can construct more complicated popup content by concatenating several strings along with HTML tags. For example, we can include both the town name and the town population size inside the popup of each town.
Practically, we replace the expression used above -
layer.bindPopup(feature.properties.name_eng);
With this one -
layer.bindPopup(
'<div class="popup">' +
feature.properties.name_eng + "<br><b>" +
feature.properties.pop_2015 + "</b></div>"
);
The popups now show both the town name name_eng
and the town population size pop_2015
. The latter is shown in bold font using the <b>
tag.
Additionally, the entire popup content is encompassed in a <div>
element with class="popup"
. The reason for doing this is to be able to apply CSS rules for styling popup content. In this case, we use the text-align
property to make popup text centered -
.popup {
text-align: center;
}
The resulting map example-08-04.html
, now with both variable styling and specific popups per feature, is shown on Figure 8.6. The popup for the Tel-Aviv polygon is opened for demonstration.
- It is more convenient to view long numbers with a comma formatting. For example,
25,298
is easier to read than25298
- Use a JavaScript function, such as the following one taken from a StackOverflow post, to format the
pop_2015
values when adding them to the popup
function formatNumber (num) {
return num
.toString()
.replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,")
}
8.6 Adding a legend
8.6.1 Using L.control
In Section 6.8 we used the L.control
function to add a map description. Here we use the same technique to create a legend for our map.
The workflow to create a legend involves creating a custom control with L.control
, populating it with HTML that represents the legend components, and styling it with CSS so that the contents appear properly on screen. The following code section adds a legend to the towns population map from the above example-08-04.html
.
var legend = L.control({position: "topright"});
legend.onAdd = function(map) {
var div = L.DomUtil.create("div", "legend");
div.innerHTML = '<b>Population in 2015</b><br>' +
'by Town<br>' +
'<small>Persons/Town</small><br>' +
'<i style="background-color: #b30000">' +
'</i>2090+<br>' +
'<i style="background-color: #e34a33">' +
'</i>933 - 2090<br>' +
'<i style="background-color: #fc8d59">' +
'</i>642 - 933<br>' +
'<i style="background-color: #fdcc8a">' +
'</i>399 - 642<br>' +
'<i style="background-color: #fef0d9">' +
'</i>0 - 399<br>';
return div;
};
legend.addTo(map);
So, what did we do here? First, we created an instance of a custom control object, calling it legend
. We used the position
option to tell it to locate in the top right of our map. Next, we used the onAdd
method of the control to run a function when the legend is added. That function creates a new <div>
in the DOM, giving it a class of legend
. This will let us use CSS to style the entire legend with the .legend
selector. We then populate the newly created <div>
with HTML by using the .innerHTML
method, like we already did in Section 6.8.
Most of the HTML code should be familiar from Chapter 1. One element type which we have not seen yet is <small>
, used to create relatively smaller text, which is convenient for displaying the units of measurement -
<small>Persons/Town</small>
What may seem strange is that we use the <i>
(italic text) to represent our legend icons. The <i>
elements are of use thanks to the fact they are colored using inline CSS (Section 2.6.1) with the background-color
property. The five <i>
elements then reflect the colors and ranges match our color scale classification.
<i style="background-color: #a50f15"></i>
After the HTML is appended, the <div>
element is returned with return div;
. Lastly, the legend is added on the map using the .addTo
method.
It is important to note that, in the above code, the legend is manually generated and does not depend on any layer or symbology shown on the map. It is up to us to make sure the labels and colors indeed correspond to the ones we styled the layer with. Again, a more general approach is to generate the legend programatically, e.g. using a for
loop going through the same arrays of breaks and color as the ones used in determining map symbology (Section 8.4.2 above). Here is an alternative version of the legend definition using a for
loop, using the breaks
and colors
arrays we defined in Section 8.4.2 -
legend.onAdd = function(map) {
var div = L.DomUtil.create("div", "legend");
div.innerHTML = '<b>Population density</b><br>' +
'by Town<br>' +
'<small>Persons/km<sup>2</sup></small><br>';
for(var i = 0; i < breaks.length-1; i++) {
div.innerHTML +=
'<i style="background-color: ' +
colors[i] + '"></i>' +
breaks[i] + ' - ' +
breaks[i+1] + '<br>';
}
return div;
};
8.7 Using CSS to style the legend
We need to use CSS to give the right placement and appearance to the newly created legend. The following CSS code will style our legend.
.legend {
padding: 6px 8px;
background-color: rgba(255,255,255,0.8);
box-shadow: 0 0 15px rgba(0,0,0,0.2);
border-radius: 5px;
}
.legend i {
width: 18px;
height: 18px;
float: left;
margin-right: 8px;
opacity: 0.7;
}
First, we set properties for the legend as a whole, referring to .legend
. We set a line height, padding, background color, box shadow, and border radius. Next we set our legend symbol <i>
dimensions and set them to float: left;
so that the the symbols will be aligned into columns.
The final map example-08-05.html
with the legend is shown on Figure 8.7.
8.8 Dynamic style
8.8.1 Styling in response to events
In the previous examples we learned how to add a constant style (same for all features) and a variable style (dependent on feature properties, such as political party or population size). In both cases, however, the style was determined on page load and remained the same regardless of subsequent user interaction with the map. In this section, we will see how styling can also be made dynamic. This means the style can change while the user interacts with the page. For example, a specific feature can be highlighted when the user places the mouse cursor above it. Dynamic styling can greatly enhance user experience and is one of the distinctive features of interactive web maps.
To achieve dynamic styling based on mouse hover, we need to add event listeners to each feature in our layer. The event listener function will then modify the feature appearance in response to specific events. For example, the event listener can respond to "mouseover"
and "mouseout"
events by changing the respective feature style to “highlighted” and “normal” state, respectively.
Specific event listeners per feature can be binded when loading the GeoJSON layer using the onEachFeature
option, similarly to the way we used it to bind specific popups (Section 8.5 above). Inside the onEachFeature
function we can use the .on
method of layer
to add the event listeners. Our code for loading the GeoJSON layer then takes the following form, where highlightFeature
and resetHighlight
, as their name suggests, are functions for highlighting and resetting feature style.
$.getJSON("data/towns.geojson", function(data) {
geojson = L.geoJSON(data, {
style: style,
onEachFeature: function(feature, layer) {
layer
.on("mouseover", highlightFeature)
.on("mouseout", resetHighlight)
}
}).addTo(map);
});
The above code means that whenever we enter or leave a GeoJSON feature with the mouse, the highlightFeature
or resetHighlight
function will be executed, respectively. These functions (defined below) will be responsible for changing the feature style to “highlighted” or “normal”, respectively.
How can we make sure we highlight the specific feature which triggered the event, i.e. the one we enter with the mouse, rather than any other feature? This brings us back to the event object, introduced in Section 4.10 and further discussed in Section 6.9. For example, in Section 6.9, we used the map event object property named .latlng
to obtain and display the clicked map coordinates within a popup. In the present case, we use yet another property of the event object called .target
, to get the reference to the page element which triggered the event, i.e. a reference to the hovered feature.
To understand what exactly does the .target
property contain when referring to a GeoJSON layer, we can manually create a reference to an individual feature in our layer as follows -
geojson._layers[100];
where 100
is the ID of the feature. The ID values are arbitrary numbers, automatically generated by Leaflet. If necessary, we can always set our own IDs when the layer is created, again using the onEachFeature
option (see Section 8.8.3).
The reference to a given GeoJSON feature, such as geojson._layers[100]
, contains numerous useful methods. Importantly, it has the following two methods that are useful for dynamic styling -
.setStyle
- Changes the style of the respective feature.bringToFront
- Moves the feature to the front, in terms of display order
In addition, the GeoJSON layer itself has a method named .resetStyle
, which accepts a feature reference and resets the feature style back to the original GeoJSON style.
Using these three methods, together with the .target
property of the event object, our highlightFeature
and resetHighlight
functions take the following form -
function highlightFeature(e) {
var currentlayer = e.target;
currentlayer.setStyle(highlightStyle);
currentlayer.bringToFront();
}
function resetHighlight(e) {
geojson.resetStyle(e.target);
}
The highlightStyle
object contains the “highlighted” style definition, separately defined below. This object implies that the highlighted feature border becomes wider and its border color becomes yellow. The fill color of the highlighted feature also becomes more transparent, since in default style (see above) we used an opacity of 0.7
while in the highlighted style we set opacity to 0.5
-
var highlightStyle = {
weight: 5,
color: "yellow",
fillOpacity: 0.5
}
How do the highlightFeature
and resetHighlight
functions work in conjugation with the highlightStyle
setting? First of all, both functions accept the event object parameter, which means the executed code can be customized to the particular properties of the event. Indeed, both functions use the .target
property of the event object, which is a reference to the particular feature that triggered the event, i.e. the feature we are entering or leaving with the mouse cursor. Using the .target
property is crucial for dynamic styling, to determine the specific feature we wish to highlight or to “reset”.
On mouse enter, the highlightFeature
function uses the .setStyle
method of the feature reference to set highlighted style (defined in highlightStyle
) on the target feature. Then, the referenced feature is brought to front using the .bringToFront
method, so that its borders - which are now highlighted - will not be obstructed by any other layers on the map.
On mouse leave, the resetHighlight
function resets the styling by using the .resetStyle
method of the geojson
layer, using the feature to reset e.target
as its argument. This reverts the particular feature style back to the default one.
The resulting map example-08-06.html
is shown on Figure 8.8.
8.8.2 Dynamic control contents
In addition to visually emphasizing the hovered feature, we may also want some other things to happen on our web page. For example, we can have a dynamically updated information box, where relevant information about the feature is being shown. This can be considered as an alternative to popups, with the advantage that the user does not need to click on each feature they wants to get information on.
In the following example-08-07.html
, we are going to add an information box displaying the name and population size of the currently hovered town (similarly to the information that what was shown in the popups).
The same technique used above to add a legend (Section 8.6) can be used to initialize the information box -
var info = L.control({position: "topright"});
info.onAdd = function(map) {
var div = L.DomUtil.create("div", "info");
div.innerHTML =
'<h4>Towns in Israel</h4>' +
'<p id="currentTown"></p>';
return div;
};
info.addTo(map);
Note that, initially, the information box contains just a heading and an empty paragraph with id="currentTown"
. The paragraph will be dynamically updated using an event listener.
We need to add some CSS code to make the information box look nicer, just like we did with the legend -
.info {
padding: 6px 8px;
font: 14px/16px Arial, Helvetica, sans-serif;
background: rgba(255,255,255,0.8);
box-shadow: 0 0 15px rgba(0,0,0,0.2);
border-radius: 5px;
width: 10em;
}
.info h4 {
margin: 0 0 5px;
color: #777;
}
.info #currentTown {
margin: 6px 0;
}
As discussed in Section 8.8.1 above, the .target
property of the event object is a reference to the specific feature being hovered with the mouse pointer. For example, we used the .setStyle
and the .bringToFront
methods of the currently hovered feature to highlight it on mouse hover. Another property of that reference called .feature
contains the specific GeoJSON feature the mouse pointer intersects, along with all of its properties.
The modified highlightFeature
function (below) uses the properties
of the GeoJSON feature to capture the name name_eng
and population size value pop_2015
of the currently hovered town. These two values are used to update the text message in the information box, using the .html
method applied on the empty paragraph -
function highlightFeature(e) {
var currentlayer = e.target;
currentlayer.setStyle(highlightStyle);
currentlayer.bringToFront();
$("#currentTown").html(
currentlayer.feature.properties.name_eng + "<br>" +
currentlayer.feature.properties.pop_2015 + " people"
);
}
Accordingly, the new resetHighlight
function now needs to clear the text message when the mouse cursor leaves the feature, by setting the paragraph content to an empty string ""
-
function resetHighlight(e) {
geojson.resetStyle(e.target);
$("#currentTown").html("");
}
The resulting map example-08-07.html
, with both the dynamic styling and the information box, is shown on Figure 8.9.
Check out the Leaflet Interactive Choropleth Map tutorial for a walk-through of another example for setting symbology and interactive behavior in a Leaflet map.
8.8.3 Linked views
There is an infinite amount of interactive behaviors that can be incorporated with web maps, which we do not have the time to cover. The important take-home message from the last example is that, using JavaScript / JQuery, the spatial entities displayed on our web map can be linked with other elements so that the map responds to user interaction in various ways. Moreover, the interaction is not necessarily limited to the map itself: we can associate the map with other elements on our page outside of the map <div>
. This leads us to the idea of linked views.
Linked views is one of the most important concepts in interactive data visualization. The term refers to the situation when the same data are shown from different points of view or in different ways, while synchronizing user actions across all of these views. With web mapping, this usually means that in addition to a web map our page contains one or more other panes, displaying information the same spatial features in different ways: tables, graphs, lists, and so on. User selection on the map is reflected on the other panes, such as filtering the tables, highlighting data points on graphs, and so on, and the other way around.
The following example-08-08.html
(Figure 8.10) implements a linked web map and list. Whenever a polygon of a given town is hovered on the map, the polygon itself as well as the corresponding entry in the towns list are highlighted. Similarly, whenever a town name is hovered on the list, the list item itself as well as the corresponding polygon are highlighted.
This is mostly accomplished with methods we already covered in previous Chapters, except for several techniques. The key to the association between the list and the GeoJSON layer is that both the list items and the GeoJSON features are assigned with corresponding IDs when loading the GeoJSON layer -
function onEachFeature(feature, layer) {
var town = feature.properties.town;
var name_eng = feature.properties.name_eng;
$("#townslist").append(
'<li data-value="' + town + '">' + name_eng + '</li>'
);
layer._leaflet_id = town;
layer
.on("mouseover", function(e) {
var hovered_feature = e.target;
hovered_feature.setStyle(highlightStyle);
hovered_feature.bringToFront();
$(
"li[data-value='" +
hovered_feature._leaflet_id +
"']"
)
.addClass("highlight")[0]
.scrollIntoView();
})
.on("mouseout", function(e) {
var hovered_feature = e.target;
geojson.resetStyle(hovered_feature);
$(
"li[data-value='" +
hovered_feature._leaflet_id +
"']"
)
.removeClass("highlight");
});
}
In the above function, executed on each GeoJSON feature, the following expression assigns the town ID to each newly appended list item -
$("#townslist").append(
'<li data-value="' + town + '">' + name_eng + '</li>'
);
The ID is stored in the data-value
attribute of the <li>
element. The data-value
attribute is a generic attribute for storing associated data inside an HTML element.
Correspondingly, the following expression assigns the same ID to the Leaflet layer feature -
layer._leaflet_id = town;
While the GeoJSON layer is being loaded, the following code section attaches event listeners for "mouseover"
and "mouseout"
on each feature -
layer
.on("mouseover", function(e) {
var hovered_feature = e.target;
hovered_feature.setStyle(highlightStyle);
hovered_feature.bringToFront();
$(
"li[data-value='" +
hovered_feature._leaflet_id +
"']"
)
.addClass("highlight")[0]
.scrollIntoView();
})
.on("mouseout", function(e) {
var hovered_feature = e.target;
geojson.resetStyle(hovered_feature);
$(
"li[data-value='" +
hovered_feature._leaflet_id +
"']"
)
.removeClass("highlight");
});
The event listeners take care of setting or resetting the polygon style and the list item style. Note that the targeted <li>
element corresponding to the hovered polygon is detected using the ._leaflet_id
property of the hovered_feature
, which was assigned on GeoJSON layer load as shown above.
Once the layer is loaded, event listeners are binded to the list items too, with the following code section where "#townslist li"
targets all <li>
elements -
$("#townslist li")
.on("mouseenter", function(e) {
var hovered_item = e.target;
var hovered_id = $(hovered_item).data("value");
$(hovered_item).addClass("highlight");
geojson
.getLayer(hovered_id)
.setStyle(highlightStyle);
})
.on("mouseout", function(e) {
var hovered_item = e.target;
var hovered_id = $(hovered_item).data("value");
geojson.resetStyle(geojson.getLayer(hovered_id));
$(hovered_item).removeClass("highlight");
});
These event listeners mirror the previous ones, meaning that now hovering on the list, rather than the layer, is the triggering event. This time, we determine the hovered ID using the .data("value")
method, which returns the data-value
attribute of the hovered <li>
element.
This may seem like a lot of work for a fairly simple effect, as we had to explicitly define each and every detail of the interactive behavior in our web map. However, consider the fact that using the presented methods there is complete freedom to build any type of innovative interactive behavior.
The final result example-08-08.html
is shown on Figure 8.10.
8.9 Exercise
- Create a map of town population size, where circle markers are shown instead of polygons, with circle area being proportional to population size
- To get a layer of town point locations, you can calculate the centroids of the
towns.geojson
layer in GIS software such as QGIS, or use the providedtowns_pnt.geojson
file- Circle area should be visualized proportional to population size, therefore circle radius should be proportional to square root of population; you can use the
Math.sqrt
function to calculate square root of population size per town- In addition to
style
andonEachFeature
, use the followingpointToLayer
option to display circle markers, instead of markers, so that theradius
styling property is enabled -
pointToLayer: function(geoJsonPoint, latlng) {
return L.circleMarker(latlng);
}
References
Dent, Borden D, Jeffrey S Torguson, and Thomas W Hodler. 2008. Cartography: Thematic Map Design. 6th ed. WCB/McGraw-Hill Boston.
Tufte, Edward. 2001. The Quantitative Display of Information. 2nd ed. Graphics Press, Cheshire, Connecticut.