The end of CartoCSS

Tom MacWright
Fri, 13 May 2016 13:00:00 -0400

This begins with a short story, you can skip to the demo if you want.

In late 2010, I started on a map rendering language called CartoCSS. We were working on TileMill, our first-generation map editing application, and wanted a fast language hosted in JavaScript that worked with Mapnik, our original map renderer. Following in the tradition of Cascadenik, we made the language similar to CSS. Drawing from the lineage of less, we added constants and other features to the language.

CartoCSS is the language Mapbox created for TileMill and Mapbox Studio Classic - a CSS-like syntax that renders static maps, and did so quite well. We’re incredibly proud that it also became a successful open source project, being used by other applications and companies.

Vector maps have been one of the largest leaps in Mapbox’s history. We built a map renderer from the ground up. We created the industry standard for distributing vector map tiles, the Mapbox Vector Tile spec, which has also been adopted by other companies, including Esri. We keep improving every detail of the system, from size efficiency to text rendering and animations, and building an ecosystem around Mapbox GL JS, the Mapbox iOS SDK, and the Mapbox Android SDK.

As part of this transition, we created a new language: the Mapbox GL Style Specification. It is the realization of the lessons we learned creating CartoCSS, and it is the next step in cartographic styling. Like the Mapbox Vector Tile specification, it’s an open standard and we’re seeing other organizations starting to adopt it.

I’ve had a minor role in the development of the Mapbox GL Style Specification. People like Lucas have worked for months building each part of the specification from the ground up. Last week, he introduced the first bits of data-driven styling in the spec & Mapbox GL JS.

I want to share with you why this is so important, and why I couldn’t be happier to see a successor to CartoCSS that leaps beyond its limitations and is built on powerful simplicity.

The problems with CSS syntax

You can see it in the name: CartoCSS is CSS-like. We wanted to create a language that was friendly to designers, and we thought CSS syntax would help. We built CartoCSS on less.js, a CSS parser with extra, non-standard CSS syntax, and we decided to keep some of it, like constants.

This was a mistake.

First, we invented a new syntax, and one that’s quite difficult to parse. Even worse, once it’s parsed, we never wrote the stringify step: you can process CartoCSS into an abstract representation, but never back into CartoCSS.

Second, the CSS model doesn’t quite work. You can see it in the way that it’s difficult to arrange layers top-to-bottom with CartoCSS: CSS is a language designed to style a document with an existing drawing order - HTML. But instead, CartoCSS is creating Mapnik Symbolizers once it is compiled to Mapnik XML, and those symbolizer can draw in any order - and any number of times. Hence the complexity of attachments, a way to draw the same data multiple times.

The Mapbox GL Style Specification is JSON and JavaScript

We decided to build the Mapbox GL Style Specification on JSON.

Up-front: JSON can be more intimidating to newcomers than CSS-like syntax. There are more quotes, commas instead of semicolons, and the language doesn’t permit comments. But something much, much more important is at stake.

Mapbox GL styles are dynamic styles. Our color switcher example is just a few lines of code because Mapbox GL Styles actually become JavaScript objects that can be manipulated by methods on the Mapbox GL JS style object.

But you don’t even need Mapbox GL JS to manipulate styles: unlike CartoCSS, JSON can be parsed, manipulated, and produced by nearly every programming language in existence. Applications can generate Mapbox GL styles as easily as they generate other API responses: without the complexity of syntax and its sophisticated interpretation, styling maps is accessible to every line of code. Parsing, generating, and validating JSON is a solved problem.

Mapbox GL styles are designed to be created and manipulated by hand, with editors like Mapbox Studio, and by computers themselves.

The visualization problem

The CartoCSS model for visualizations was never quite there. The fundamental thing you use in CartoCSS to style things differently is the filter: to create a choropleth map, you might write:

#volume [number_of_cases <= 434] {
  marker-fill: #BD0026;
}
#volume [number_of_cases <= 183] {
  marker-fill: #F03B20;
}
#volume [number_of_cases <= 130] {
  marker-fill: #FD8D3C;
}
#volume [number_of_cases <= 90] {
  marker-fill: #FECC5C;
}
#volume [number_of_cases <= 50] {
  marker-fill: #FFFFB2;
}

Now, over time we created a shorthand for this case: you could, instead, write:

#volume {
  [number_of_cases <= 434] {
    marker-fill: #BD0026;
  }
  [number_of_cases <= 183] {
    marker-fill: #F03B20;
  }
  [number_of_cases <= 130] {
    marker-fill: #FD8D3C;
  }
  [number_of_cases <= 90] {
    marker-fill: #FECC5C;
  }
  [number_of_cases <= 50] {
    marker-fill: #FFFFB2;
  }
}

The second example is fewer characters, but it creates the same layers to be rendered. You can see the issue for both of these: not only is the syntax verbose, it’s also inefficient, creating as many layers as you have bins in your visualization.

If you dabble with visualization technology, this isn’t the way it’s done: if you were writing this example in d3, for instance, you might write:

var colorScale = d3.scale.linear()
    .domain([434, 183, 130, 90, 50])
    .range(['BD0026', 'F03B20', 'FECC5C', 'FFFFB2']);

d3.selectAll('#volume')
  .attr('marker-fill', colorScale);

Note the lack of if statements, and that I don’t create multiple layers for each different color bin: instead we simply assign colors to the appropriate elements. Not only is this pattern clearer, it’s also much more efficient.

Data-driven styles

Data-driven styles have been one of the biggest tasks in Mapbox GL’s history, but they stand to change everything. Right now we just support data-driven styles for Mapbox GL JS, and only for circle-radius and circle-color.

We chose to do this the hard way. We want the default way people create visualizations to also be the easiest way, and also be the fastest way. We will make it easy to fall into the pit of success.

So what this means is that data-driven styles give you a syntax to define a function, just like in d3, that creates visualizations based on data. It does this by loading just the dependent data - just the variables you’re referring to in your visualization - and loading that data into WebGL, where it can be rendered instantly by shaders.

With data-driven styles, visualizations are fast enough to render in real-time with Mapbox GL JS. And the flexibility of mapping data to representation is just like d3: both simple to understand and powerful enough to represent all kinds of data without putting colors and other styling information in your source data.

The goods

This is a flashy map demo. But it’s also extremely simple: I hope you glance at the code and see what I mean.

Fin

You cannot get a simple system by adding simplicity to a complex system. -Richard O'Keefe

The Mapbox GL Style Specification both simplifies and superpowers styling maps, and lets algorithms join in on the fun. It’s the realization of what we learned about CartoCSS and the future of dynamic vector maps.