Our recent Capital Bikeshare post received a lot of Twitter love, particularly for the 3D view of elevation profile. Several people even asked how it was done, so I’ve put together a little guide to making your own.

Since Mapbox.js was not built with 3D in mind, we’ll hack some of the existing map elements with CSS and JavaScript to bring 3D to Mapbox.js. Onward!

##What we’ll cover##

##Draw a flat map##

We’ll start in two dimensions. First, initiate a Mapbox.js map in the usual fashion (I will add it to a div with id="map", but you can use whatever).

var map = L.mapbox.map('map', 'peterqliu.9d05be4d',{
  zoomControl: false})
  .setView([48.8572046,2.348], 14);

Next, add markers with L.divIcon, wrapping your custom HTML in a div classed marker. We’ll eventually stand these upright in 3D space, but for now, design them as if they were lying flat on the map. To illustrate, here I’ve defined my custom markers as a linked PNG, and added them to the map:

  var myIcon = L.divIcon({
    className:'',
    html:'<div class="marker"><img src="marker.png"/></div>',
    iconSize: [35, 46]
  });

  L.marker(<latlng>,{icon:myIcon}).addTo(map);

Next, in our CSS we’ll add some styling to set up our 3D space:

/ *make vertical objects visible */
#map {
  overflow:visible;
}

/* removes a flickering background color */
.leaflet-container {
  background:none;
}

/* set up our 3D space */
#map,
#map * {
  -webkit-transform-style:preserve-3d;
  transform-style:preserve-3d;
}

/* Optional: this will help you see what's going on as we build the tilted view, but can be removed */
.marker {
  -webkit-transform:translateZ(1px);
  transform:translateZ(1px);
}

#Say Hello to CSS Transformations#

3D might sound like something that involves hardcore number-crunching and SOHCAHTOA in code. But thanks to a rule called transform (with its vendor prefixes), modern browsers can perform basic operations right in CSS. It also happens to be fast for 2 reasons:

1) Layout isolation: Transforms change the visual appearance of an element, but not the space it occupies on the page. This means the browser can focus on moving, turning, stretching, and skewing an element, without worrying about whether it pushes down the other content on the page.

2) Hardware acceleration: all the above calculations are offloaded to your computer’s GPU, freeing up your main thread to do other number-crunching.

You can read more about the performance perks of transform here.

##Rotation, made simple##

There are four CSS transform properties: translate, scale, rotate, and skew. As it turns out, Mapbox.js already uses translate to move markers as you zoom and pan around the map.

But we’ll focus on rotation here. If we look at an image head-on, there are three axes around which it can turn: a horizontal X-axis so that it spins like an awning window, a vertical Y-axis so that it spins like a revolving door, and a Z-axis pointing straight at you, so that it spins like a windmill. By default, the center of the image is also the center of rotation.

##Let’s turn some things!##

Play around with the demo at the top of this page. Feel free to tick and untick the boxes in any sequence, to get a sense for what each transformation does.

1) Tilt the map back

Our first step is to tilt the entire map object, to get that oblique view: we’ll want to tip it backward, along the X axis:

#map {
  transform:rotateX(-60deg);
}
/* Pick a number between 0 and -90.*/

For this and all subsequent CSS rules, remember to add -webkit- and other applicable vendor prefixes to render correctly on all browsers.

2) Add some perspective

That’s a start, but our map looks awfully squished– in real-life objects, objects nearby are large, and shrink from view as they approach the vanishing point. Fortunately, we can do precisely this by applying a perspective rule to the parent element of our transformed object (in my case, that’s the <body> tag):

body {
  perspective:1200;
}

This defines how far we are from the transformed object: smaller values make objects appear closer, by producing more extreme foreshortening. I’ve found that values between 1000-1800 work best, but season to your taste.

3) Flip the markers up

Now we can stand our markers up on the map. First, let’s find them in the DOM:

<div class="leaflet-marker-pane"> // map markers go in here
  <div class="leaflet-marker-icon">
    <div class='marker'>
      // Your custom HTML here //
    </div>
  </div>
  <div class="leaflet-marker-icon">...</div>
  <div class="leaflet-marker-icon">...</div>
</div>

Tucked inside each .leaflet-marker-icon is the custom marker HTML we wrote earlier, wrapped in a div that we classed as .marker. Let’s apply the rotation here– tilted like the map, but forward and a full 90 degrees:

.marker {
  transform:rotateX(90deg);
}

4) Set the origin of rotation

So far, so good. But before we move on, let’s make sure we applied our rotation correctly.

Earlier, we learned that by default, all rotations are applied at the center of each element. This worked well for tilting our map back, but check out what happened when rotating our markers around their respective centers (Fig 2-1):

The marker is only half above the map surface, poking out like a shark fin. Instead of rotating the marker around its center, we need to put the pivot point at its bottom tip (Fig 2-2), so that the entire marker stands above the map.

We can also do this in CSS, via transform-origin. The rule takes vertical and horizontal positioning as its two parameters:

.marker {
  transform-origin: bottom center;
}

Now our markers are fully above the map.

###4) Rotate the map ###

A challenge with 3D spaces is obstruction: dense groups of tall markers can often block each other, and it becomes useful to pivot the entire map around to see what’s behind. There are a number of affordances you can build to accomplish this: explicit left/right buttons to rotate in each direction, or a mouse dragging event like the one I made for the Capital Bikeshare demo.

Regardless of your input UI, you’ll want to translate that user action to an angle measure that turns the map around. On top of the rotateX we applied in step #1 to tilt the map, we’ll add a rotateZ to spin it around like a lazy susan:

// after rotation is triggered
document.getElementsByClassName('marker')
	.setAttribute("style", "transform:rotateX(-60deg) rotateZ( // PIVOT ANGLE HERE (positive is clockwise) // )");

(Earlier rotations like tilting the map and propping up markers have been static transformations that we applied declaratively with CSS. However, this one will change with user interaction, which means we’ll have to do it in JavaScript).

###5) Pivot markers to face the user ### We’re almost done! You might have noticed that though we’ve rotated our markers in 3D space, they don’t become 3D objects. This flatness becomes really apparent when the map rotates 90 in either direction, as we look on them edgewise and get only slivers of each marker. How do we solve this?

Here’s a thought experiment: turn your head left and right, while keeping your eyes on this sentence. What keeps your gaze from sliding off the screen as your head turns away?

Your eyeballs do a compensatory rotation (rotateY, in fact) to negate your head turning. That’s exactly what we’ll do for our markers, to keep them pointing toward us:

// after map rotation is triggered
document.getElementsByClassName('marker')
	.setAttribute("style", "transform:rotateX(-90deg) rotateY( // PIVOT ANGLE HERE // )");

And that’s it! You have the basics for a three-dimensional map with Mapbox.js

##The Extra Mile##

If you really want to spruce up your map and refine some nitpicky details, here are some further considerations:

Reduce the horizon: Leaflet (upon which Mapbox.js is built) smartly loads only tiles currently visible in the frame. But tipping the map backwards exposes the edge of this frame, which you might not want. To force-load tiles outside the frame, try:

var getPxBounds = map.getPixelBounds;
map.getPixelBounds = function () {
    var bounds = getPxBounds.call(this);
    // ... extend the bounds
    bounds.min.x=bounds.min.x-1000;
    bounds.min.y=bounds.min.y-1000;
    bounds.max.x=bounds.max.x+1000;
    bounds.max.y=bounds.max.y+1000;
    return bounds;
};

Disable scroll-zooming: Zooming still works in 3D, but I’ve found that map controls work better than the mousewheel here. If you are using mouse dragging for Z-rotation, I also suggest map.dragging.disable() to avoid inadvertent panning.

Popups: The simplest 3D popup implementation I’ve found involves a hidden <div>, set inside the marker element itself that appears on hover. If you find a way to do this with default Mapbox.js popups, let me know!

Discretion: 3D objects are certainly cooler than flat, but that doesn’t mean it’s the best for every (or even most) use cases– traditional chloropeths and heat maps can often display the same data more simply, and equally capably. Be judicious with the extra dimension, and use only when it truly illuminates.