Tokyo metro map
This example is obviously inspired by the example of London Tube Lines in the Altair and Vega-Lite documentations.
It can feel a bit frustrating when discovering those libraries not to be able to extend it easily to other cities. We pick Tokyo here, and spice it up with a bilingual map (mostly in Japanese, but English appears with the mouse.)
Warning
It may happen that the visualisation is rendered before the fonts are downloaded. In case such thing happens, a simple refresh (F5) should be enough to fix things.
A close up map is built in the corresponding section.
Data acquisition
We will take two different datasets from OpenStreetMap: background information (the 23 Tokyo wards) and the subway lines. For people familiar with Tokyo subway systems, this does not include the JR lines (incl. Yamanote line).
from cartes.osm import Overpass
tokyo_wards = Overpass.request(
area={"name:en": "Tokyo", "admin_level": 4},
# Level 7 include cities (市) and wards (区)
rel=dict(admin_level=7, name=dict(regex="区$")),
)
tokyo_subway = Overpass.request(
area={"name:en": "Tokyo", "admin_level": 4, "as_": "tokyo"},
nwr=[
dict(railway="subway", area="tokyo"), # subway lines
dict(station="subway", area="tokyo"), # subway stations
],
)
Data preprocessing
The map background needs to be simplified, we do not need details at very fine resolution, which would also be heavy to download.
tokyo_wards = tokyo_wards.simplify(1e3)
There are some glitches in the metadata associated to the line segments, so there are two solutions:
edit the faulty segments on OpenStreetMap;
preprocess the data to correct those mistakes.
lines = tokyo_subway.data.query("type_ == 'way' and name == name").assign(
name=lambda df: df.name.str.split(" ", n=1, expand=True)
)
lines.loc[lines["name:en"] == "Toei Mita Line", "name"] = "都営地下鉄三田線"
lines.loc[lines["name:en"] == "Toei Asakusa Line", "name"] = "都営地下鉄浅草線"
lines.loc[lines["name"] == "京王電鉄京王線", "name:en"] = "Keio Railway Keio Line"
We collect the official colours associated to each lines from segments where the tag is filled:
colours = (
lines[["name", "name:en", "colour"]]
.query("colour==colour")
.groupby("name")
.agg({"name:en": "max", "colour": "max"})
.reset_index()
)
name | name:en | colour | |
---|---|---|---|
0 | 東京メトロ丸ノ内線 | Tokyo Metro Marunouchi Line | #F62E36 |
1 | 東京メトロ副都心線 | Tokyo Metro Fukutoshin Line | #B74D17 |
2 | 東京メトロ千代田線 | Tokyo Metro Chiyoda Line | #00BB85 |
3 | 東京メトロ半蔵門線 | Tokyo Metro Hanzomon Line | #8F76D6 |
4 | 東京メトロ南北線 | Tokyo Metro Namboku Line | #00AC9B |
5 | 東京メトロ日比谷線 | Tokyo Metro Hibiya Line | #B5B5AC |
6 | 東京メトロ有楽町線 | Tokyo Metro Yurakucho Line | #C1A470 |
7 | 東京メトロ東西線 | Tokyo Metro Tōzai Line | #0CA7ED |
8 | 東京メトロ銀座線 | Tokyo Metro Ginza Line | #FF9500 |
9 | 都営地下鉄三田線 | Toei Mita Line | #0079C2 |
10 | 都営地下鉄大江戸線 | Toei Oedo Line | #B6007A |
11 | 都営地下鉄新宿線 | Toei Shinjuku Line | #6CBB5A |
12 | 都営地下鉄浅草線 | Toei Asakusa Line | #E85298 |
Then we merge the lines into single elements, also in order to reduce the size of resulting JSON. Line simplification does not really work well here if we want subway lines to still go through the stations.
from shapely.ops import linemerge
def merge_line(elt):
return pd.Series(
{
"geometry": linemerge(elt.geometry.tolist()),
"name:en": elt["name:en"].max(),
}
)
lines = (
lines[["name", "name:en", "geometry"]]
.groupby("name").apply(merge_line).reset_index()
)
Data visualisation
import altair as alt
# First the colors
line_scale = alt.Scale(
domain=colours["name:en"].tolist(),
range=colours["colour"].tolist()
)
wards = alt.Chart(tokyo_wards)
basemap = alt.layer(
# The background
wards.mark_geoshape(color="gainsboro", stroke="white", strokeWidth=1.5),
# The names of the wards: in Japanese, the in English under the mouse pointer
wards.mark_text(fontSize=16, font="Noto Sans JP", fontWeight=100).encode(
alt.Text("name:N"), alt.Tooltip("name:ja-Latn:N"),
alt.Latitude("latitude:Q"), alt.Longitude("longitude:Q"),
),
# The subway lines: in English in the legend, bilingual under the mouse pointer
alt.Chart(lines).mark_geoshape(filled=False, strokeWidth=2)
.encode(
alt.Color(
"name:en:N", scale=line_scale,
legend=alt.Legend(
title=None, orient="bottom-left", offset=0, columns=2,
labelFont="Ubuntu", labelFontSize=12,
),
),
alt.Tooltip(["name:N", "name:en:N"]),
),
# Subway stations positions
alt.Chart(tokyo_subway.query("station == station"))
.mark_circle(size=30, color="darkslategray")
.encode(
alt.Latitude("latitude:Q"), alt.Longitude("longitude:Q"),
alt.Tooltip(["name:N", "name:en:N"]),
),
).properties(width=600, height=600)
basemap
Zoom in to downtown Tokyo
On this map, we choose to:
specify a projection centered on the central wards in order to be able to zoom in;
display the station names in small characters;
add the Yamanote line (in pale green).
# Collect the Yamonote line
tokyo_yamanote = Overpass.request(
area={"name:en": "Tokyo", "admin_level": 4},
way=dict(railway=True, name=dict(regex="山手線$")),
)
# The geometry will be enough here
yamanote = linemerge(tokyo_yamanote.data.geometry.to_list())
alt.layer(
# Recall previous visualisation
default,
# Some stations appear several times (once per exit?)
alt.Chart(tokyo_stations.data.drop_duplicates("name"))
.mark_text(fontSize=12, font="Noto Sans JP", fontWeight=100)
.encode(
alt.Text("name:N"), alt.Tooltip(["name:N", "name:en:N"]),
alt.Latitude("latitude:Q"), alt.Longitude("longitude:Q"),
),
# Yamanote line
alt.Chart(yamanote).mark_geoshape(
strokeWidth=10, opacity=0.3, color="#B1CB39", filled=False
)
).project(
# based on the coordinates of the map center
"conicConformal", rotate=[-139.77, -35.68], scale=450000
).configure_legend(
# there is not much space for the legend, so hide what's behind
fillColor="gainsboro", padding=10
)