Lesson 2
In the previous lesson, we drew the airport locations by manually transforming latitudes and longitudes into screen coordinates. In this lesson, we will start looking at Lux transformation nodes. Tranformation nodes make programs simpler and more expressive by giving Lux more information about the structure of the scene.
For example, it is rarely the case that we will want to plot latitudes and longitudes like we did in lesson 1 by simply mapping them directly to window coordinates. There are several features we want in our visualization related to transformations:
- We want to use a real map projection, like the Mercator or the Albers. These define the way latitudes and longitudes become screen coordinates, and can be more complicated than an affine transformation.
- We want to control aspect ratio distortion: what if the window is 900x300 pixels?
- We want to be able to zoom in to specific regions of the map, or zoom out to have a global view. Similarly, we want to drag the map around to change the viewing window.
- We want to select regions of the map by clicking with the mouse. Mouse events are fundamentally in screen coordinates, but we want to have programmatic access to the region coordinates in the coordinate system that the data is defined, so we can do things like brushing and linking.
We start by picking a map projection. In this tutorial we will use the venerable (if understandably frowned upon) Mercator projection. This means that at some point in the code, latitudes and longitudes must be transformed into Mercator coordinates. The straightforward way to do it would be to encode the new values directly on the position attribute of the dots object. You'd call a function like the following:
// from Lux's source code Shade.Scale.Geo.latlong_to_mercator = Shade(function(lat, lon) { lat = lat.div(2).add(Math.PI/4).tan().log(); return Shade.vec(lon, lat); });
Then, instead of writing
var dots = Lux.Marks.dots({ position: Shade.vec(lats, lons).radians(), // ...
you'd write:
// This is not how the current example works! var dots = Lux.Marks.dots({ position: Shade.Scale.Geo.latlong_to_mercator(lats.radians(), lons.radians()), // ...
With a simple piece of code like we have right now, this would almost be good enough, but there's a variety of reasons you don't want to do it. First, imagine you needed to add a new element to your visualization that also uses latitude and longitude as coordinates. Then you'd have to manually make sure that the same transformation is applied to this element. And what happens if you change the map projection? Instead, the current example works by adding a new subscene to Lux's main scene; one that specifies that we are in fact working with latitudes and longitudes, and that we want to see the result of a Mercator projection:
var dots = Lux.Marks.dots({ position: Shade.vec(lats, lons).radians(), // ... }); var lat_lon_scene = Lux.Scene.Transform.Geo.latlong_to_mercator(); lat_lon_scene.add(dots);
Notice that the dots object is unchanged. The way this works is simple and general. Remember that Lux uses actors with appearances to specify how things are drawn. When actors are added to a scene, their appearances are transformed depending on the scene they are in. In addition, if scene A contains scene B, then the apperances of actors in scene B are transformed by scene B, then by scene A (and so on for arbitrarily nested transformations).
The Mercator projection implemented in Lux leaves longitudes unchanged, which means that the traditional Mercator square (covering latitudes roughly in the range [-85, 85]) is mapped to a square with range [-π, π] in both coordinates. This puts the United States area outside the WebGL normalized view extent of [-1, 1], so if we were to leave the visualization like that, you would not see much.
In order to see the US airports, we need an additional transformation to take the Mercator coordinates into the [-1, 1] range. We will do this by creating a new subscene with an explicit transformation function:
var zoom_scene = Lux.scene({transform: function(appearance) { appearance.position = appearance.position.div(3); return appearance; }});
We then add the mercator projection scene to the zooming scene, and the zooming scene to the main scene:
Lux.Scene.add(zoom_scene); zoom_scene.add(lat_lon_scene);
Notice that the each scene now contains objects that take one coordinate system and transform to another, from latitudes and longitudes to projected Mercator coordinates to screen coordinates. This separation of concerns will bring concrete advantages very soon. In the next lesson, we will start adding interactive capabilities to the visualization.
Back to the index.