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 to onEachFeature 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 both style and onEachFeature 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 = red
  • weight: 5 - Border width = 5px
  • fillColor: "yellow" - Fill color = yellow
  • fillOpacity: 0.2 - Fill opacity = 20% opaque

The resulting map example-08-01.html is shown on Figure 8.1.

FIGURE 8.1: example-08-01.html (Click to view this example on its own)

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.

FIGURE 8.2: example-08-02.html (Click to view this example on its own)

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.

Setting symobology in QGIS, with automatically determined color scale breaks

FIGURE 8.3: Setting symobology in QGIS, with automatically determined color scale breaks

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.

Sequential, diverging and qualitative ColorBrewer scales, using the maximal number of colors available in each scale

FIGURE 8.4: Sequential, diverging and qualitative ColorBrewer scales, using the maximal number of colors available in each scale

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.

FIGURE 8.5: example-08-03.html (Click to view this example on its own)

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 processed
  • layer - 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.

FIGURE 8.6: example-08-04.html (Click to view this example on its own)

  • It is more convenient to view long numbers with a comma formatting. For example, 25,298 is easier to read than 25298
  • 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.

FIGURE 8.7: example-08-05.html (Click to view this example on its own)

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.

FIGURE 8.8: example-08-06.html (Click to view this example on its own)

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.

FIGURE 8.9: example-08-07.html (Click to view this example on its own)

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.

FIGURE 8.10: example-08-08.html (Click to view this example on its own)

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 provided towns_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 and onEachFeature, use the following pointToLayer option to display circle markers, instead of markers, so that the radius styling property is enabled -
pointToLayer: function(geoJsonPoint, latlng) {
    return L.circleMarker(latlng);
}
<code>solution-08.html</code>

FIGURE 8.11: solution-08.html

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.