post Leaflet recipe: Hover events on features and polygons

A newer version of Leaflet offers a dedicated API you should consider as an alternative to the method described here. —Ben, Nov. 10, 2012

Leaflet is an exciting tool for making interactive maps. It's free. It's simple. It's easy to write. It isn't tied to any one set of background tiles. It works in all the major browsers. It's under active development.

It even has good documentation. But one thing it doesn't show you how to create is hover effects. Say you've got some shapes or points on your map and you want something to happen when the user rolls her mouse over them. After getting some help from GitHubber patmisch, here's how I made it work.

Getting started

The city of Los Angeles recently redrew its City Council districts. Below is a Leaflet map displaying the boundaries from the redistricting commission's final recommendation. I downloaded the lines as a shapefile from the commission's Web site and converted them into GeoJSON format using Quantum GIS.

This kind of map can be accomplished by following the GeoJSON instructions Leaflet already provides. One hundred percent of the code is below. I've annotated it to provide some clues to what's happening, but this is the baseline for making a static map run and I'm going to assume you get how it works. If you get stuck up, fire off a question in the comments or spend some time with Leaflet's tutorial for beginners.

<!DOCTYPE html>
<html>
<head>
    <title>Leaflet GeoJSON example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <!-- All the stuff you need to install from Leaflet -->
    <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.4/leaflet.css" />
    <!--[if lte IE 8]><link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.4/leaflet.ie.css"  /><![endif]-->
    <script src="http://cdn.leafletjs.com/leaflet-0.4/leaflet.js"></script>
    <!-- My external GeoJSON file with the City Council boundaries in it -->
    <script src="http://s3-us-west-1.amazonaws.com/palewire/leaflet-hover/citycouncil.geojson"></script>
</head>
<body style="margin:0; padding:0;">
    <!-- The <div> where we're put the map -->
    <div id="map" style="width: 100%; height: 350px;"></div>
    <script type="text/javascript">
        // Initialize the map object
        var map = new L.Map('map', {
            // Some basic options to keep the map still and prevent 
            // the user from zooming and such.
            scrollWheelZoom: false,
            touchZoom: false,
            doubleClickZoom: false,
            zoomControl: false,
            dragging: false
        });
        // Prep the background tile layer graciously provided by stamen.com
        var stamenUrl = 'http://{s}.tile.stamen.com/toner/{z}/{x}/{y}.png';
        var stamenAttribution = 'Map tiles by <a href="http://stamen.com">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://creativecommons.org/licenses/by-sa/3.0">CC BY SA</a>.';
        var stamenLayer = new L.TileLayer(stamenUrl, {maxZoom: 18, attribution: stamenAttribution});
        // Set the center on our city of angels
        var center = new L.LatLng(34.0, -118.4);
        map.setView(center, 9);
        // Load the background tiles
        map.addLayer(stamenLayer);
        // Create an empty layer where we will load the polygons
        var featureLayer = new L.GeoJSON();
        // Set a default style for out the polygons will appear
        var defaultStyle = {
            color: "#2262CC",
            weight: 2,
            opacity: 0.6,
            fillOpacity: 0.1,
            fillColor: "#2262CC"
        };
        // Define what happens to each polygon just before it is loaded on to
        // the map. This is Leaflet's special way of goofing around with your
        // data, setting styles and regulating user interactions.
        var onEachFeature = function(feature, layer) {
            // All we're doing for now is loading the default style. 
            // But stay tuned.
            layer.setStyle(defaultStyle);
        };
        // Add the GeoJSON to the layer. `boundaries` is defined in the external
        // GeoJSON file that I've loaded in the <head> of this HTML document.
        var featureLayer = L.geoJson(boundaries, {
            // And link up the function to run when loading each feature
            onEachFeature: onEachFeature
        });
        // Finally, add the layer to the map.
        map.addLayer(featureLayer);
    </script>
</body>
</html>

Hooking up hovers

Our goal is to make the same map as above, except with polygons that light up on hover and a popup that appears with metadata about the selected council district. Something like this.

First add jQuery to the head of the page, so we can easily make the popup when the time comes.

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>

Then create a second style set that describes how to brighten up the polygon.

var highlightStyle = {
    color: '#2262CC', 
    weight: 3,
    opacity: 0.6,
    fillOpacity: 0.65,
    fillColor: '#2262CC'
};

Now the real work. Look back at how we used the "featureparse" event above to assign the default style to each polygon. You can assign events in the same way, turning them on one at a time as Leaflet loops through your GeoJSON features and adds them to the layer. The only trick is to bind your events using a self-invoking function that gets the scope right and lines things up as they loop by.

Watch as it happens below. The wrapper function passes in the layer and JSON metadata, which are then assigned as the function is executed before each step of the loop is complete.

var onEachFeature = function(feature, layer) {
    // Load the default style. 
    layer.setStyle(defaultStyle);
    // Create a self-invoking function that passes in the layer
    // and the properties associated with this particular record.
    (function(layer, properties) {
      // Create a mouseover event
      layer.on("mouseover", function (e) {
        // Change the style to the highlighted version
        layer.setStyle(highlightStyle);
        // Create a popup with a unique ID linked to this record
        var popup = $("<div></div>", {
            id: "popup-" + properties.DISTRICT,
            css: {
                position: "absolute",
                bottom: "85px",
                left: "50px",
                zIndex: 1002,
                backgroundColor: "white",
                padding: "8px",
                border: "1px solid #ccc"
            }
        });
        // Insert a headline into that popup
        var hed = $("<div></div>", {
            text: "District " + properties.DISTRICT + ": " + properties.REPRESENTATIVE,
            css: {fontSize: "16px", marginBottom: "3px"}
        }).appendTo(popup);
        // Add the popup to the map
        popup.appendTo("#map");
      });
      // Create a mouseout event that undoes the mouseover changes
      layer.on("mouseout", function (e) {
        // Start by reverting the style back
        layer.setStyle(defaultStyle); 
        // And then destroying the popup
        $("#popup-" + properties.DISTRICT).remove();
      });
      // Close the "anonymous" wrapper function, and call it while passing
      // in the variables necessary to make the events work the way we want.
    })(layer, feature.properties);
};

That's it. If this doesn't make sense, or I've made a mistake, please leave a comment below. You can find the source code for the complete working demo here.

Comments

nick on 2012.03.27
Pretty cool! Is it possible to add CSS3 animations to add a fade in/out effect to the highlight on hover? What is the maximum size of GeoJSON, that can be loaded? Will it freeze if there is thousands of features? Does Leaflet have a loading strategy like in OpenLayers? (fixed, bbox, refresh), so I can send requests to the WFS server to get geojson partially?
palewire on 2012.03.27
I personally have not used any of those features, but that doesn't mean they are not possible. You should look at the code published on GitHub and talk with the maintainer. As far as volume, Leaflet is limited by the browser in the same way any JavaScript library would be. As browsers improve, these libraries can handle more and more. I don't know any precise figures, but ask on GitHub and I bet somebody can answer you.
mike on 2012.03.27
Nice, Ben! One quick tip: switch your Toner URL to use “png” instead of “jpg” for smaller tiles and less re-rendering. I’ll probably make this a requirement soon, with a redirect.
palewire on 2012.03.28
Thanks, mike. I've made that change above.
daniel on 2012.04.05
Great post, already coming in handy. To nick's question, I get some jerkiness (in chrome on a mapbook pro) whenever I'm trying to drag around in a viewport with > 100 or so points. When the number of points gets into the thousands, no matter how widely scattered they are it just can't keep up. That situation, however, is the perfect to use MapBox or CloudMade to host a tile layer with the points/polygons coming across as part of the map.
aboutaaron on 2012.05.17
Hey Ben, Great tutorial. I remember we talked about this when I was at the LA Times building back in February. I'm already beginning to see how this is a great way to work with city and state data. Two questions: 1. How'd you go about converting the .shp into .geojson in QGIS? I couldn't find the appropriate export button. I used `ogr2ogr` to convert mine, but ended up losing all the county and representative data in the JSON. 2. When do you guys decided to use maps like this versus .svg graphics a la http://graphics.latimes.com/2012-election-electoral-map/ ? Thanks!
carlvlewis on 2012.05.26
Thanks for the tutorial! Works great. One question, though: Would you or anyone else know how to go about styling the layer so that each geometry has its own color styling? I'm trying to use this method to create a heat map, so I need to variate the color of each element.
valery on 2012.06.11
Great tutorial. My question is: how to put popup for every district above this district?
valery on 2012.06.12
I solved my issue, using latLngToLayerPoint method of the map map.latLngToLayerPoint(layer._latlng) May be somebody will also find this useful.
teamcritter on 2012.06.28
Great tutorial! Has anybody figured out how to make the popup follow the mouse cursor? I can't seem to get the x and y position of the polygons in order to pass the values to the popup DIV.
rune on 2012.10.23
Thanks Ben, great example. I found your tutorial when I was looking why I couldn't get 'mouseout' event working in the leaflet example. If I copy your code locally it works but if I copy the code from http://leaflet.cloudmade.com/examples/choropleth-example.html 'mouseout' doesn't get triggered. Any ideas?
palewire on 2012.10.23
Rune, your comment is the first time I've seen this new Leaflet tutorial. It appears they have introduced some new tricks to help people have hover effects. Great idea! Though I havn't worked with it yet, so I'm unaware of any sticking points or little details. I'll check it out.
db on 2012.12.05
Hi!, your tutorial is great, but I have a question, how I can put a "hover" over the districts, showing the name of the district and to give me clikc display a pop-up in the bottom, as your example, I read but without success, I can not implement both things at once .... =C Thanks!

Submit a comment

:
Email:
:
en
968
© 2013 palewire . colophon . rss feeds . powered by django . hosted on an openstack in the rackspace cloud