Search code examples
rshinyr-leaflet

Coloring clusters by markers inside


I would like to know, how to color the clusters depending on icon in it.

My Data :

remorque       time.stamp      lat      long geolocalisation maintenance temperature appairage
1        21/11/2017 10:36 48.86272 2.2875920          OnMouv        noir                      
2        21/11/2017 10:36 43.60776 1.4421606       StartMouv                   rouge          
3        21/11/2017 10:36 46.58619 0.3388710          OnMouv                   rouge          
4        21/11/2017 10:36 45.76695 3.0556216            Life                  orange          
5        21/11/2017 10:36 45.14555 1.4751652         EndMouv                             rouge
6        21/11/2017 10:36 46.81157 1.6936336            Life                  orange          
7        21/11/2017 10:36 47.36223 0.6751146          alerte                             rouge
8        21/11/2017 10:36 47.36032 1.7441244       StartMouv                                  
9        21/11/2017 10:36 48.85333 1.8215332       StartMouv                                  
10       21/11/2017 10:36 48.84429 1.7913208          alerte                                  
11       21/11/2017 10:36 48.81356 1.6759643         EndMouv                                  

Example :

If there is an icon in my cluster, with appairage = rouge, the color of the cluster should be red.

If there is no red icon, If there is an icon in my cluster, with temperature = orange, the color of the cluster should be orange.

... for each variable (temperature, appairage, maintenance). And if all icons in the culster have their variables ok, the cluster should be green.

My map looks like :

enter image description here

I found a way to change the range for coloring the clusters on the Internet. But I don't want to color per number of markers in the cluster.


Solution

  • It is possible to color the clustered icons based on the properties of the icons clustered together. The easiest method might be to use htmlwidgets and call a javascript function on map rendering.

    However, before getting to the htmlwidget, you need to set a clusterId for your clustered layer:

    addAwesomeMarkers(clusterId = "cluster" ...

    Now we can find this layer when in the htmlwidget:

    function(el, x) {
      map = this;  // the map object
      var cluster = map.layerManager.getLayer('cluster','cluster'); // the cluster layer
      
    

    Within the cluster layer, we want to create a function for the icon property iconCreateFunction:

    cluster.options.iconCreateFunction = function(d) {
        // generate icon
    }
    

    This function should:

    1. go through all the child markers represented by the clustered marker,
    2. identify the highest rank of those child markers
    3. return an appropriate icon

    1. Iterating through child markers

    For number one, and building on the above, we can iterate through each child marker with:

    cluster.options.iconCreateFunction = function(c) {
        var markers = c.getAllChildMarkers();
        markers.forEach(function(m) {
           // do something for each marker
        })
    }
    

    I'm using c to represent a cluster, m to represent each individual child marker

    2. Getting the Highest Ranked Marker

    The primary challenge in the list is identifying the highest rank of the child icons - as the data is not bound to the icons we are limited in options. Assuming that the color of the icons corresponds to the color code of the item in the dataframe, I will use the color of the icon to determine its priority/rank. After determining the highest ranking child, I will color the cluster based on that child's rank.

    I will color the cluster as follows (as I believe this is your intended result):

    • red if any child icons are red,
    • orange if none are red but there are some orange children, and
    • green if there is no orange or red children.

    To get the color, I need to access the proper property. The color (fill) of an (awesome) marker resides at:

    marker.options.icon.options.markerColor
    

    To compare colors, I'll use an object to represent each color's rank, this will allow for a simple comparison of color:

    var priority = {
      'green':0,
      'orange':1,
      'red':2
    }
    

    This allows:

    cluster.options.iconCreateFunction = function(c) {
      var markers = c.getAllChildMarkers();
      var priority = {
        'green': 0,
        'orange': 1,
        'red': 2
      };
      var highestRank = 0; // defaults to the lowest level to start
    
      markers.forEach(function(m) {
        var color = m.options.icon.options.markerColor;
        
        // check each marker to see if it is the highest value
        if(priority[color] > highestRank) {
          highestRank = priority[color];  
        }                      
      })
    }
    

    3. Returning an Icon

    Now that we have a value representing a color, we can return an icon. Leaflet clustered icons have limited styling options. They use L.divIcon(), which limits options somewhat. When combined with css styles for clustered labels, they create the familiar circle with green, yellow, and orange colors.

    These default styles have the following css classes:

    .marker-cluster-small // green
    .marker-cluster-medium  // yellow
    .marker-cluster-large // orange
    

    If we are happy with just using these classes, we can style the clustered polygons with minimal effort:

    var styles = [
        'marker-cluster-small', // green
        'marker-cluster-medium',  // yellow
        'marker-cluster-large' // orange
    ]
    
    var style = styles[highestRank];
    var count = markers.length;
    
    return L.divIcon({ html: '<div><span>'+count+'</span></div>', className: 'marker-cluster ' + style, iconSize: new L.Point(40, 40) });
    

    The whole widget therefore looks like:

    function(el,x) {
      map = this;  
      var cluster = map.layerManager.getLayer('cluster','cluster'); 
      cluster.options.iconCreateFunction = function(c) {
        var markers = c.getAllChildMarkers();
        var priority = {
          'green': 0,
          'orange': 1,
          'red': 2
        };
        var highestRank = 0; // defaults to the lowest level to start
    
        markers.forEach(function(m) {
          var color = m.options.icon.options.markerColor;
        
          // check each marker to see if it is the highest value
          if(priority[color] > highestRank) {
            highestRank = priority[color];  
          }                      
        })
    
        var styles = [
          'marker-cluster-small', // green
          'marker-cluster-medium',  // yellow
          'marker-cluster-large' // orange
        ]
    
        var style = styles[highestRank];
        var count = markers.length;
    
        return L.divIcon({ html: '<div><span>'+count+'</span></div>', className: 'marker-cluster ' + style, iconSize: new L.Point(40, 40) });
      }
    }
    

    Refining the Icons

    Changing Colors

    You probably want to have the high priority icons show up red. This can be done, but you need to add a css style to your map.

    One way to do this at the same time as changing the icon function above is to append a style to the page with javascript in your widget. You need to make two styles, one for the div holding the icon, and one for the icon, you can do both at once:

    var style = document.createElement('style');
      style.type = 'text/css';
      style.innerHTML = '.red, .red div { background-color: rgba(255,0,0,0.6); }'; // set both at the same time
      document.getElementsByTagName('head')[0].appendChild(style);
    

    (from https://stackoverflow.com/a/1720483/7106086)

    Don't forget to update what classes you are using in the styles array:

        var styles = [
          'marker-cluster-small', // green
          'marker-cluster-medium',  // yellow
          'red' // red
        ]
    

    Showing More Information in the Icon

    You aren't limited to a number in the icon, you could show 1-3-5 , representing one high priority, three medium etc. You just need to keep track of how many child icons of each priority are in each cluster:

    var children = [0,0,0];
    markers.forEach(function(m) {
      var color = m.options.icon.options.markerColor;
      children[priority[color]]++; // increment the appropriate value in the children array.
      ...
    

    Then show it with:

    return L.divIcon({ html: '<div><span>'+children.reverse()+'</span>...
    

    Givings something like:

    enter image description here


    Test Example

    This should be copy and pastable to show everything except the additional text in the icon (using the code in these documentation examples as a base):

    library(leaflet)
        
    # first 20 quakes
    df.20 <- quakes[1:50,]
    
    getColor <- function(quakes) {
      sapply(quakes$mag, function(mag) {
        if(mag <= 4) {
          "green"
        } else if(mag <= 5) {
          "orange"
        } else {
          "red"
        } })
    }
    
    icons <- awesomeIcons(
      icon = 'ios-close',
      iconColor = 'black',
      library = 'ion',
      markerColor = getColor(df.20)
    )
    
    leaflet(df.20) %>% addTiles() %>%
      addAwesomeMarkers(~long, ~lat, icon=icons, label=~as.character(mag), clusterOptions = markerClusterOptions(), group = "clustered", clusterId = "cluster") %>%
      htmlwidgets::onRender("function(el,x) {
      map = this;  
      
      var style = document.createElement('style');
      style.type = 'text/css';
      style.innerHTML = '.red, .red div { background-color: rgba(255,0,0,0.6); }'; // set both at the same time
      document.getElementsByTagName('head')[0].appendChild(style);
    
    
      var cluster = map.layerManager.getLayer('cluster','cluster'); 
      cluster.options.iconCreateFunction = function(c) {
        var markers = c.getAllChildMarkers();
        var priority = {
         'green': 0,
         'orange': 1,
         'red': 2
        };
        var highestRank = 0; // defaults to the lowest level to start
                            
        markers.forEach(function(m) {
        var color = m.options.icon.options.markerColor;
                            
        // check each marker to see if it is the highest value
        if(priority[color] > highestRank) {
           highestRank = priority[color];  
         }                      
      })
                            
      var styles = [
        'marker-cluster-small', // green
        'marker-cluster-large',  // orange
        'red' // red
      ]
                            
      var style = styles[highestRank];
      var count = markers.length;
                            
       return L.divIcon({ html: '<div><span>'+count+'</span></div>', className: 'marker-cluster ' + style, iconSize: new L.Point(40, 40) });
     }
    }")