post Leaflet recipe: Hover events on features and polygons

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://leaflet.cloudmade.com/dist/leaflet.css" />
    <!--[if lte IE 8]><link rel="stylesheet" href="http://leaflet.cloudmade.com/dist/leaflet.ie.css"  /><![endif]-->
    <script src="http://leaflet.cloudmade.com/dist/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.
        featureLayer.on("featureparse", function (e){
            // All we're doing for now is loading the default style. 
            // But stay tuned.
            e.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.
        featureLayer.addGeoJSON(boundaries);
        // 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.

featureLayer.on("featureparse", function (e){
    // Load the default style. 
    e.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.
    })(e.layer, e.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.

Submit a comment

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