Scaled data values can ensure that a user’s eyes are drawn to important data points rather than geographical anomalies associated with many choropleth maps, and do so without the added abstraction of scaled dot maps.

Using open data from the United States Department of Agriculture’s Food Access Research Atlas to map the percent of people in each county in the United States with low access to healthy food as defined by the USDA. Ideally, the counties with the highest percentages of people without access to food stand out from the rest regardless of other parameters like county size, population, etc.

dots, choropleth

A choropleth map (left) and scaled dots (right) depicting USDA food access data

scaled text

Scaled data values showing the same data

SQL: Cartography disguised as query language

TileMill’s PostGIS interface allows us to customize our data source, optimizing our data for rendering this type of textual heat map.

The following query assumes we have two tables in a PostGIS database: one with the tabular USDA data and another with county polygons from the U.S. Census Bureau.

create table access_poly as (
  select
    a.gid,
    a.geoid,
    a.the_geom,
    a.name,
    a.state as state_fips,
    b.*,
    coalesce(cast(round(b.PCT_LACCESS_POP10,1) as text)||'%', 'n/a') as pct_access
  from counties a left join access b
  on cast(a.geoid as numeric) = cast(b.fips as numeric)
  order by PCT_LACCESS_POP10 desc
);

In addition to joining the tabular data to the geometry, the query performs the following cartographic functions:

  • Order the records by PCT_LACCESS_POP10 so that larger values are at the top of table. This means Mapnik will draw them first, in favor of smaller values.
  • Improve legibility by creating a new column pct_access that rounds percentages to one decimal place and concatenates a percentage sign.
  • Display ‘n/a’ where a county has a null value for PCT_LACCESS_POP10 rather than leave an empty space.
  • Perform a left join against the county geometry so that we do not have blank areas on the map where there should be counties.

CartoCSS for every zoom level

With CartoCSS, you can quickly make sure that these scales look good at every zoom level and maintain their relative proportions - crucial for effectively communicating your data data to users. Using CSS-style variables and some simple math, we can quickly scale six data classes that determine the size of each number across zoom levels.

// ------ variables ---------------------------------
@c1: 1.5 + 0.5;
@c2: 2.5 - 0.5;
@c3: 3.0 - 0.5;
@c4: 3.5 - 0.5;
@c5: 4.0 - 0.5;
@c6: 4.5 - 0.5;

#accesspoly {
  // fade in county boundaries at higher z levels
  line-color:#fff;
  [zoom<7] { line-opacity:0; }
  [zoom=7] { line-opacity:.15; }
  [zoom=8] { line-opacity:.25; }
  [zoom=9] { line-opacity:.5; }
  [zoom=10]{ line-opacity:1; }
  line-width:0.5;
  line-dasharray:2,10;

  // invisible polgons
  polygon-opacity:0;

  // ------ zoom-specific label styles ------------------
  ::labels{
    text-name: '[pct_access]';
    text-face-name:'Futura Condensed Medium';
    text-fill:#fff;
    text-size:20;
    [zoom=3] {
      [pct_laccess_pop10<=10]{ text-size:(@c1 * 3); }
      [pct_laccess_pop10>10] { text-size:(@c2 * 3); }
      [pct_laccess_pop10>20] { text-size:(@c3 * 3); }
      [pct_laccess_pop10>30] { text-size:(@c4 * 3); }
      [pct_laccess_pop10>40] { text-size:(@c5 * 3); }
      [pct_laccess_pop10>50] { text-size:(@c6 * 3); }
    }
    [zoom=4] {
      [pct_laccess_pop10<=10]{ text-size:(@c1 * 4); }
      [pct_laccess_pop10>10] { text-size:(@c2 * 4); }
      [pct_laccess_pop10>20] { text-size:(@c3 * 4); }
      [pct_laccess_pop10>30] { text-size:(@c4 * 4); }
      [pct_laccess_pop10>40] { text-size:(@c5 * 4); }
      [pct_laccess_pop10>50] { text-size:(@c6 * 4); }
    }

    [zoom=11]{
      [pct_laccess_pop10<=10]{ text-size:(@c1 * 10); }
      [pct_laccess_pop10>10] { text-size:(@c2 * 10); }
      [pct_laccess_pop10>20] { text-size:(@c3 * 10); }
      [pct_laccess_pop10>30] { text-size:(@c4 * 10); }
      [pct_laccess_pop10>40] { text-size:(@c5 * 10); }
      [pct_laccess_pop10>50] { text-size:(@c6 * 10); }
    }
  }
}

Note that TileMill needs the numeric column pct_laccess_pop10 to classify the values, but we are feeding the string column pct_access to the text-name symbolizer to display the more legible rounded values with percent signs.

You can check out the full CartoCSS stylesheet and SQL queries on Github.

With our textual heat map layer rendered and uploaded to MapBox hosting, all that’s left is to add a custom MapBox layer as a base map and publish. It’s now easier than ever to sign up and start publishing your data-driven maps with MapBox.