📱

Get Our Mobile App

Take your business learning on the go!

Download on the App StoreGet it on Google Play

I made maps that show time instead of space

Václav Volhejn10:44

Transcription

Regular maps are good at showing space, but they're not good at showing time. So I decided it's time to fix that. It's time to build maps that show time instead of space.

Here's a map you can find on the street in London. It shows you where you are, and to give you a sense of scale, it also has a circle that says "15-minute walk." But what's always bothered me about this is that this circle is a lie. That's because a circle is a set of points with equal distance from the center, not equal travel time. For example, let's draw a circle around this point. All of these points are 500m away from the center, but they're certainly not the same walking distance away. Because, and this is true, you can just walk in a straight line to get from point A to point B. If we were to draw an accurate 5-minute walk outline, it would look something like this. Unfortunately, Transport for London has yet to answer my complaint emails, so I guess we're going to have to make a map of our own.

Obviously, I'm not the first person to realize this, and the line we drew there already has a name. It's called an isochrone: a line that's a fixed travel time away from a starting point. If you draw a bunch of isochrones with different times, which you'll get is an isochrone map. Here's the earliest known example from 1881. It shows how many days it takes to get from London to any place on Earth. And here's a simple modern-day isochrone map with isochrones for a 15, 30, and 60-minute walk. Since walking speed is more or less constant, you can see how the isochrones smooth out to circles as we increase the radius.

But if we instead visualize how far you can get by car, things get funkier. You can see the smallest isochrone is really irregular, and even the big ones aren't smoothed out completely. That's simply because the speed of a car varies much more than the speed of a pedestrian. So that's one way to make maps show time, but there's two things that I still don't like about isochrone maps. One, they have a fixed starting point, so if you care about travel times between other places, you're out of luck. And two, more importantly, they're kind of boring. They're still built on top of the same spatial first maps, where time is a second-class citizen. It's time we break free from the constraints of the Mercator projection and build something radically new: a map that truly shows time instead of space.

Here's what we're going to do. Instead of bending the circles into weirdly shaped isochrones, we're going to bend the map itself to show time. One way to think about it is we're going to bend the isochrones back into circles. If there's a pair of points that's close together in space but takes long to travel between, such as two points on opposite sides of the Thames, then we'll pull them apart. And vice versa, points that look far apart but are easy to travel between will get pulled together. For cars, an example would be two points on a highway. They might be far away, but since it's a high-speed road, it's just a short drive.

Let's start by overlaying a grid of points over the map. For each pair of points, we're going to add a spring with a certain target length. If the points are further away than the target length, they'll get pulled together, and if they're too close, they'll be pushed apart. Like with physical springs, the strength of the pull or push is proportional to the distance. So then we'll have a whole bunch of springs, and different springs will try to move the points in different directions. So we'll just simulate the behavior of the whole spring system until it settles into a stable solution.

Now, to create a spring between two points, we'll need to compute two things. One, how far away the points are. Because Earth is round, allegedly, it's a bit more complicated than just using the Pythagorean theorem. We'll have to compute what's called the great-circle distance. Let's walk through the math in detail. Just kidding, it's 2024, so let's just write the function signature and have Copilot do the rest for us. I actually didn't even know that you could use Greek letters to name variables in TypeScript, and in this case, it's almost not a terrible idea. Thanks, Copilot.

And the second thing we need to know is how long it takes to travel between the two points. Now, that's the hard part. We're going to be looking at traveling by car because, as mentioned, it leads to more interesting distortion than walking. But to compute travel time by car, you need to take into account the street layout, traffic, one-way streets, and so on and so on. It's so hard, in fact, that I decided to use the Google Maps API to do the work for me. It's just the service that my program can ask, "How long does it take to travel between A and B if I travel by car?" and it gives back an answer.

Now that we have the distance and the travel time, we can divide them to get a speed that tells us how fast we would be traveling if we were going in a straight line between the two points. This speed doesn't really matter to us in absolute terms, but it does give us a measure of how fast it takes to travel between these two points compared to the rest of the pairs. For example, let's look at the route between the American Museum of Natural History and the Empire State Building. It takes 20 minutes to drive between them. But now look at how long it takes to drive from the museum to the Liberty Skyline Park. That's around 30 minutes, despite the two actually being closer to each other if we were traveling in a straight line. The speed between the museum and the Empire State Building would be about 10 km/h, whereas traveling to the park would be 5 km/h. Assuming the average speed across all pairs is 8 km/h, this means the Empire State Building is supposed to be a bit closer to the museum, whereas the park should be a bit further. We'll add springs with lengths that reflect this fact.

So now that we know how to make a single spring, we'll just add them between all pairs of points, simulate the spring system, and get a spacetime map. Well, that's what I thought initially, anyway. It turns out there's a couple more things we need to do to get to that point. First, we need to build the front end, which is the thing that's going to actually display the maps and stretch them according to the grid. I used the library called PixiJS, which is all right, but it's documented pretty sparsely, and I also ran into a bug that nobody on the internet seems to have had before. I did manage to work around that somehow, and now we have a map that we can bend however we like. Then we just implement the spring dynamics. And okay, that might be a bit too much. Let's make the springs a bit weaker, and there we go.

One big issue here is that I have no idea what the stretched map should look like, so it's kind of difficult for me to tell whether we're doing things correctly or not. I mean, things are moving around, so I guess that's good, right? Well, at some point, I realized that I had swapped the X and Y axes on my grid, so the stretching we're doing now is just completely wrong. I decided then it might be time to invest into some debugging tooling. I made a layer that shows where the points are relative to where they started, one that shows the grid, and one that shows the order in which the points are represented internally. I used that last one to discover that the axes were swapped. I also want to see what the springs are doing, and this seemed like a reasonable way to do it. If the arrow is red, it means the point is being pushed away from here, and if it's blue, it means the point is being pulled towards here.

After I fixed the swapped axes, I found another issue. With a regular grid, some of the points might end up in unreachable places, like Lake Zurich, which understandably confuses the API that's trying to compute the travel times. To fix that, I used a different API endpoint to snap the points to locations that are actually reachable. As far as I can tell, everything now works correctly, but we're still not quite getting the effect that I was hoping for. The points are moving around, but it's not really interpretable enough for me to be able to say, "Oh, these points get stretched because you need to do a detour here," and "These get compressed because there is a really fast shortcut between them." And I think that's because the resolution of the grid isn't high enough. So let's make the grid finer to see the movements in more detail. Let's make a 19x19 grid. It will have about 60,000 springs. Now, computationally, that's fine, but using the Google Maps API to compute all of those travel times would cost about $300 per map. Now, that's a little steep for me, so we'll have to compromise.

Instead of computing the travel times between all pairs, we'll only compute them for points that are less than a kilometer apart or something like that. I set this distance threshold so that one grid only costs about $10. The API gives you $200 free credits per month, and that ended up covering the costs. So we have fewer springs than before, and technically, that's fine. Things still work the same, but I did find that the map started to be a little janky. I thought the long-distance springs wouldn't really matter much, but they kind of stabilized the whole thing. But luckily, we can fill in those missing travel times using a little bit of algorithmic trickery. We use the fact that you can approximate the travel time between two points A and B using a midpoint C. Then you just iterate over all midpoints and pairs of points, and you get a full distance matrix of travel times. This is called the Floyd-Warshall algorithm, and it's a fancy name for three nested for loops. Copilot knows this as well, so I don't have to bother coding it myself.

And now that I don't have to pay a fortune to create each map, let's make a whole bunch of them. These maps are great, but they're not perfect, and in fact, they can never be. Look at this visualization of the springs. Most of them are still unhappy. They're trying to push the points one way or another. That means that the constraints we've set aren't perfectly satisfied, and so we're not visualizing the travel times perfectly. Unfortunately, there's not much we can do about that. There's not too much space to maneuver in two dimensions. It would help if we allowed the points to move around in three or even more dimensions, but then it would no longer be a map in the traditional sense.

One way to improve accuracy in 2D would be to loosen the requirement of having a map that's accurate globally. Remember the isochrone maps from the beginning? They only show the travel time from a certain point. We can do something similar, but more fun. Let's make it so that when you hover over a point, the map is going to try to become more accurate around that point by making the springs that are close by stronger and the distant ones weaker. I love how fluid this looks when you move your cursor around. You get the feeling that the map is three-dimensional and it's made of jelly, and you're making it move around with a hair dryer or something like that.

At this point, the research part is done, and all that remains is putting in the work and turning it into a usable website. I think it's going to be like a day of work to do. That okay, that took a lot longer than I thought it would, but it's finally done. I linked the site in the description. Check it out. The code is also open source if you want to have a look. I'll leave you with a few more seconds of jelly map footage. See you in the next video.