CartoDB: Handling infowindows for overlapping features

CartoDB is a great service for generating online maps and is particularly useful if you’re working with big data that might not render quickly using APIs such as Leaflet and Google Maps. For details on CartoDB we did a write-up of the nice functionality here. In working with CartoDB layers, though, one of the challenges we’ve faced is how to generate infowindows for overlapping features.

On a recent project we were working with water system polygons — a messy geographic file with many, many overlapping features (see below). We wanted to allow users to click on a location and see all of the water systems serving that location — not just information for the polygon which would be the default. Below we show pseudo-code for how we used CartoDB spatial analysis functionality to identify all features at a clicked location and render details about them in a tabbed infowindow. Here is what the final map and infowindows look like.

Click on a Water System To See InfoWindow

Pseudo-Code Showing How the InfoWindow Handler Was Constructed

In order to create info windows like the ones you see above we took advantage of CartoDB’s backend spatial database powered by PostgreSQL/PostGIS. We start with the code to add the layer (note that ‘USER’ and ‘DATA’ would be your username and data file name). This code adds a CartoDB layer and sets up the infowindow function to call our customized callinfowindow function into which we feed the lat/long and the ID of the feature clicked (our ID is kml_key). We then parse the data using, in part, underscore.js and create the tabbed info windows.

Add CartoDB layer

  cartodb.createLayer(map, '', {
    query: 'SELECT * FROM {{table_name}} ORDER BY thearea DESC'

    .on('done', function(layer) {

      map.overlayMapTypes.setAt(1, layer);
      var sublayer = layer.getSubLayer(0);

      layers.SystemLyr = sublayer
      var infowindow = sublayer.infowindow

      infowindow.set('template', function(data) {

        var clickPosLatLng = this.model.get('latlng');
        var fields = this.model.get('content').fields;

        if (fields && fields[0].type !== 'loading') {

          var obj = _.find(fields, function(obj) {
            return obj.title == 'kml_key'

          callinfowindow(clickPosLatLng, obj)

      }); // end infowindow set

    }); // end on.done for the water systems

InfoWindow Function

Now when a user actually clicks on the map this triggers a call to our callinfowindow function. In CartoDB a user click can give you the ID of the feature clicked and the latitude and longitude of the point clicked. We used these to identify the top polygon by ID. We used PostGIS' function ST_Contains to identify the underlying polygons that 'contain' the clicked point.

function callinfowindow(clickPosLatLng, obj) {

//Query: select the water systems where the ID (kml_key) equals that of the clicked
//polygon OR the polygon contains the clicked point.

  var q = "SELECT kml_key,pwsname, pwsid, edited, locadd, locstreet, loccity, phone, population, connection, \
pws_class FROM service_areas WHERE (kml_key=" + obj + "OR (ST_Contains(service_areas.the_geom, \
ST_GeomFromText('POINT(" + clickPosLatLng[1] + " " + clickPosLatLng[0] + ")', 4326))))"

    url: "" + q,
    dataType: "jsonp",
    success: function(data) {

      var dat = data.rows

      // pull out the fields of interest and order the results
      var kml_key = _.pluck(dat, 'kml_key');
      var topsystemindx = _.indexOf(kml_key, obj)
      var topsystem = [dat[topsystemindx]]
      dat.splice(topsystemindx, 1)
      dat = topsystem.concat(dat)

      var pwsname = _.pluck(dat, 'pwsname');
      var pwsid = _.pluck(dat, 'pwsid');

      var tabHead = 'Water System(s) Serving<br>this Location';


      // Set up the tabs so we have a tab for each overalpping water system
      // infohtml.push('<div id="tabs">')

      for (var i = 0; i < pwsname.length; i++) {

        var tabID = "System" + (i + 1);

        tabhtml.push(' <li><a href="#' + tabID + '">' + tabID + '</a></li>');

        // if field is null leave blank
        address[i] = address[i] || '';
        address2[i] = address2[i] || '';

		//create the pop-ups
        infohtml.push('<div id="' + tabID + '"><ul>' +
          '<li><span class="strong">' + pwsname[i].toProperCase() + '</span></li>' +
          '<li><span class="light">ID:</span> ' + pwsid[i].toProperCase() + '</li>' +
          '<li><span class="light">Last Edited:</span> ' + edit[i].toProperCase() + '</li>' +
          '<li><span class="light">Address:</span> ' + address[i].toProperCase() + '</li>' +

      var infohtml = infohtml.join('')
      var tabhtml = tabhtml.join('')

      if ($('#tabs').length) {

      $('.systeminfo').append("<div id='tabs'></div>")
      //$('#tabs').html('<h4>' + tabHead + '</h4><ul>' + tabhtml + '</ul>' + infohtml)
      $('#tabs').html('<ul><h4>' + tabHead + '</h4>' + tabhtml + '</ul>' + infohtml)
      $('.systeminfo .close').css('visibility', 'visible');

  }); // end get data for infowindow


7 responses

    • I really appreciate you reporting this! This seems to be related to a change in how CartoDB names its visualizations and should be fixed here. You might need to do a browser hard refresh.

  1. Excellent job!
    but I think is there any code missing, isn’t it?, something relative to the template

    Could you help us?, thanks in advance

      • Sorry, I’m new at Carto, and tried to reproduce the window for my demo.
        It may be my ignorance, but I Cannot make run the code in the last lines:

        $(‘#tabs’).html(” + tabHead + ” + tabhtml + ” + infohtml)
        $(‘.systeminfo .close’).css(‘visibility’, ‘visible’)

        I guessed there were some css missing before to mount the tabs.
        Thanks for your attention

        • Perhaps you’re getting this error because we left off a piece of the code! Try adding these 2 lines of code after the line: var pwsid = _.pluck(dat, 'pwsid')

          var infohtml = [];
          var tabhtml = [];

Leave a Reply

Your email address will not be published. Required fields are marked *