GEOG 585
Open Web Mapping

Symbolizing layers based on attribute values

Print

Choropleth maps, proportional symbols maps, and on-the-fly feature filtering all provide alternative functionality to the typical "dots on a map" mashup. How can you achieve these things with Leaflet? In this section of the lesson, we'll talk about how to create Leaflet symbols based on attributes in your data. You can then apply these symbols to vectors to get various types of maps, such as choropleth maps based on class breaks.

First, it's important to be clear that symbolizing layers on the client using Leaflet code is not the only way to achieve choropleth maps, proportional symbols, etc., on the web. Earlier in this course, you became familiar with dynamically drawn WMS maps defined by SLD styling, and tiled maps defined by CartoCSS. In these approaches, you do the cartography on the server rather than the client. Server-side maps are most appropriate for advanced cartographic effects. They may also result in better performance when hundreds or thousands of features are in play.

Why, then, are we discussing making our specialized maps with Leaflet styles when so many other cartographic tools lie within reach? The reason is that defining symbols on the client opens the door to flexibility and interactivity.

  • The flexibility comes because the data is separated from the styling; thus, you don't have to set up multiple web services in order to get multiple types of styling in your apps. Each app can define different styling rules in its client side code while utilizing the same source data.
  • The interactivity comes from the ability to do re-classification and symbolization on the fly by invoking client-side code functions. For example, you could allow the user to switch from an equal interval classification to a quantile classification, or adjust the maximum size of a proportional symbol, all without making a return trip to the server or setting up additional web services.

Reading attributes in Leaflet

In order to create a styling rule based on some attribute value, it's first necessary to read the attribute. How do you do this in Leaflet?

Let's consider a dataset that I downloaded from the Buenos Aires open data portal showing subway ("subte") lines. Each subway line is composed of many polyline segments. Each polyline segment has an attribute stating which subway line it belongs to (e.g., "LINEA B"). I've created a GeoJSON file out of this and I want to display it in a web map using unique colors for each subway line, like this:

 Subway map with unique colors for each line
Figure 8.1

First, let's take the easy case where the color value is directly coded into the attribute table. Notice the COLOR field below which contains color hex values for each line segment:

 Subway attribute table
Figure 8.2

If you are fortunate enough to have this setup and the colors work for you, then you can apply them directly to the color property when you define the layer style. In Leaflet you can use the syntax feature.properties.<PROPERTY> to get the value of any feature attribute:

// Set up styles for subway lines
function subteStyle(feature) {
  return {
    "color": feature.properties.COLOR,
    "weight": 5
  };
}          
// Create layer and add it to the map
var subteLayer = L.geoJSON(subteData, {
  style: subteStyle
});

The above code creates a vector layer from the variable subteData which comes from a GeoJSON file. To style the layer, the sybteStyle function reads the hex value from the COLOR field and inserts it in the color property of the layer. Notice how the syntax feature.properties.COLOR is utilized to read the color value.

Symbols for unique nominal attributes

Although the above technique is convenient, it's not always practical. What if the colors provided in the file aren't appropriate for your map? Fortunately, in our scenario, you could work around this by providing color values in your JavaScript (i.e., your client-side) code based on the name of the subway line.

Let's look at how you'd apply some style rules on the client side using Leaflet. Examine the following variation on the code we viewed previously. This code produces the exact same map:

// Set up styles for subway lines
function subteStyle(feature) {
  var colorToUse;
  var line = feature.properties.LINEASUB;
            
  if (line === "LINEA A") colorToUse = "#46C7FA";
  else if (line === "LINEA B") colorToUse = "#E81D1A";
  else if (line === "LINEA C") colorToUse = "#4161BA";
  else if (line === "LINEA D") colorToUse = "#599C65";
  else if (line === "LINEA E") colorToUse = "#65018A";
  else if (line === "LINEA H") colorToUse = "#FAF739";
  else colorToUse = "#000000";
            
  return {
    "color": colorToUse,
    "weight": 5
  };
}
          
// Create layer and add it to the map
var subteLayer = L.geoJSON(subteData, {
  style: subteStyle
});

The above example employs a function subteStyle to read the attribute LINEASUB from each feature, thereby figuring out the name of the subway line (LINEA A, LINEA B, etc.). If/then logic is applied to find the appropriate color to use based on the subway line name. Finally, this color is applied to a style returned by the function.

Proportional symbols based on numeric attributes

If you have a field with some numeric attribute, such as the length of a subway line or the number of riders per day, you may want to size the symbols proportionally so that big values are represented with a bigger symbol. Let's consider an example where we have a GeoJSON dataset showing statistics for some of South America's largest metro rail systems. You'll get more familiar with this dataset in the lesson walkthrough, but here's the basic attribute table and map:

 Table of metro systems
Figure 8.3
 Map of metro systems with basic symbols
Figure 8.4

In the above image, the metro systems are all drawn using the same symbol. But let's suppose that you wanted to proportionally size the symbols so that the more stations in a metro system, the larger the map symbol. Notice the STATIONS attribute contains this information. The desired map would look something like the following:

 Metro systems with proportional symbols
Figure 8.5

Accomplishing the proportional symbols in Leaflet requires you to define some mathematical function that will size each symbol based on each feature's attribute value for STATIONS.  The syntax below is somewhat advanced, but pay attention to the line which reads the value for STATIONS, divides it by 80, and multiplies it by 30 to derive the width and height in pixels for any given metro system symbol. These numbers signify that a metro system with 80 stations will have a symbol 30 pixels wide, a metro system with fewer stations will be less than 30 pixels, and a metro system with more stations will be greater than 30 pixels (the numbers 80 and 30, of course, are entirely arbitrary and could be adjusted to fit your own data):

// function to size each icon proportionally based on number of stations
function iconByStations(feature){
  var calculatedSize = (feature.properties.STATIONS / 80) * 30;
            
  // create metro icons
  return L.icon({
    iconUrl: 'metro.svg',
    iconSize: [calculatedSize, calculatedSize]
  });
}
        
// create the GeoJSON layer and call the styling function with each marker
var metroLayer = L.geoJSON(metroData,  {
  pointToLayer: function (feature, latlng) {
    return L.marker(latlng, {icon: iconByStations(feature)});
  }
});

In the above code, iconSize is a two-item JavaScript array containing the width and the height in pixels that should be applied to the icon. Also, notice the use of the pointToLayer property, which is necessary when you want to replace the default Leaflet markers with your own graphics.

Symbols for data classes based on numeric attributes

In some situations, it may be more appropriate to break up your numerical data into various classifications that are symbolized by graduated colors or some similar approach. This is especially true for line and polygon datasets that are sometimes harder to conceptualize using proportional symbols. The boundaries for your classes (in Esri software you may have heard these referred to as "class breaks") could be determined based on equal interval, quantile, natural breaks, or some other arbitrary scheme.

For example, in the image below, metro systems with over 100 stations are symbolized using dark red. Systems with 50 to 100 stations are symbolized with red. Systems with fewer than 50 stations are symbolized with pink:

 Metro systems with colorized classifications
Figure 8.6

To symbolize data classes in Leaflet, we'll read one of the feature attributes, then use if/then logic to check it against our class breaks. The code below defines the three classes used in our metro rail example. Each class references a different SVG symbol with its unique hue of red:

// create metro icons
var metroLowIcon = L.icon({
  iconUrl: 'metro_low.svg',
  iconSize: [25,25]
});

var metroMediumIcon = L.icon({
  iconUrl: 'metro_medium.svg',
  iconSize: [25,25]
});

var metroHighIcon = L.icon({
  iconUrl: 'metro_high.svg',
  iconSize: [25,25]
});
          
// function to use different icons based on number of stations
function iconByStations(feature){
  var icon;
  if (feature.properties.STATIONS >= 100) icon = metroHighIcon;
  else if (feature.properties.STATIONS >= 50) icon = metroMediumIcon;
  else icon = metroLowIcon;

  return icon;
}
        
// create the GeoJSON layer and call the styling function with each marker
var metroLayer = L.geoJSON(metroData,  {
  pointToLayer: function (feature, latlng) {
    return L.marker(latlng, {icon: iconByStations(feature)});
  }
});

Although the above may look like a lot of code, notice that half of it is just setting up the icons. A function that classified lines or polygons might be much simpler because a single style could be defined with a varying stroke or fill color based on the attribute of interest.

If you intend to use a classification system such as Jenks natural breaks, equal interval, quantile, etc., you must calculate the break values yourself (or find a library that does it) before defining the rules. You can either do this by hand or add more JavaScript code to calculate the values on the fly.

Ways to limit feature visibility based on attributes

In some situations, you may want to display only a subset of the features in a dataset, based on some attribute value or combination of values. (If you're familiar with Esri ArcMap, this is called a "definition query".) Suppose you wanted to show only the metro systems whose COUNTRY attribute was coded as "Brazil":

 Metro systems filtered by COUNTRY to show Brazil only
Figure 8.7

You can do this directly in the filter property when you create the layer in Leaflet. Here you write a function that evaluates a feature and returns a value of true or false. Leaflet will only draw features for which a value of true is returned:

// create metro icons
var metroIcon = L.icon({
  iconUrl: 'metro.svg',
  iconSize: [25,25]
});

        
// create the GeoJSON layer and call the styling function with each marker
var metroLayer = L.geoJSON(metroData,  {
  pointToLayer: function (feature, latlng) {
    return L.marker(latlng, {
      icon: metroIcon    
    });
  },
  filter: function(feature, layer) {
    if (feature.properties.COUNTRY === "Brazil") return true;
    else return false;
  }
});

The filter function in the above example tests for features where the COUNTRY property equals "Brazil". Note that the above example simply reads an attribute, it does not do a spatial query to find the bounds of Brazil. Your website will run much faster if you can preprocess the data to put a country attribute on each metro system, rather than trying to figure that out on the fly using spatial processing in a web environment.

Now, let's look at a scenario with a numeric attribute. Suppose we wanted to show only the metro systems with over 75 stations:

 Metro systems filtered to those with over 75 stations
Figure 8.8

This could be accomplished with the following filter:

// create metro icons
var metroIcon = L.icon({
  iconUrl: 'metro.svg',
  iconSize: [25,25]
});

        
// create the GeoJSON layer and call the styling function with each marker
var metroLayer = L.geoJSON(metroData,  {
  pointToLayer: function (feature, latlng) {
    return L.marker(latlng, {
      icon: metroIcon    
    });
  },
  filter: function(feature, layer) {
    if (feature.properties.STATIONS > 75) return true;
    else return false;
  }
});

In the code above, note the query for a STATIONS value that is greater than 75.

Conclusion

Leaflet offers a variety of options for symbolizing data on the client side based on attribute values. You should not feel limited to using the default symbology or a single symbol type for the entire layer.

Creating Leaflet styles can require some trial and error, along with some skillful debugging. You'll have more success if you start with a working example and tweak it piece by piece, frequently stopping to test your alterations. This section of the lesson is deliberately code-heavy so that you will have many examples available to get you started.