MTA Icon Menu Altair#

My kid loves subways. Wanted to use Altair differently than most.

Decided to create a UI that:

  • Doesn’t look like Altair/Vega

  • Doesn’t use HTML/CSS

  • Data-viz as UI

Result#

MTA Icon

MTA icon set#

Used Claude.ai to add some data like color and RGB

import altair as alt
import polars as pl

data = [
  {"name": "1train", "src": "https://i.imgur.com/5w147gb.png", "line": "1", "color": "Red", "rgb": "#EE352E"},
  {"name": "2train", "src": "https://i.imgur.com/WbQXY6L.png", "line": "2", "color": "Red", "rgb": "#EE352E"},
  {"name": "3train", "src": "https://i.imgur.com/vgF9PMQ.png", "line": "3", "color": "Red", "rgb": "#EE352E"},
  {"name": "4train", "src": "https://i.imgur.com/fadogYd.png", "line": "4", "color": "Green", "rgb": "#00933C"},
  {"name": "5train", "src": "https://i.imgur.com/K05HApQ.png", "line": "5", "color": "Green", "rgb": "#00933C"},
  {"name": "6train", "src": "https://i.imgur.com/bRrwqkM.png", "line": "6", "color": "Green", "rgb": "#00933C"},
  {"name": "7train", "src": "https://i.imgur.com/S9CVRlJ.png", "line": "7", "color": "Purple", "rgb": "#B933AD"},
  {"name": "atrain", "src": "https://i.imgur.com/Uh6c4sD.png", "line": "A", "color": "Blue", "rgb": "#0039A6"},  
  {"name": "btrain", "src": "https://i.imgur.com/LA41bPi.png", "line": "B", "color": "Orange", "rgb": "#FF6319"},
  {"name": "ctrain", "src": "https://i.imgur.com/SMIYfnv.png", "line": "C", "color": "Blue", "rgb": "#0039A6"},
  {"name": "dtrain", "src": "https://i.imgur.com/b5sLbv0.png", "line": "D", "color": "Orange", "rgb": "#FF6319"},
  {"name": "etrain", "src": "https://i.imgur.com/OM1OEUM.png", "line": "E", "color": "Blue", "rgb": "#0039A6"},
  {"name": "ftrain", "src": "https://i.imgur.com/lvSg9sv.png", "line": "F", "color": "Orange", "rgb": "#FF6319"},
  {"name": "gtrain", "src": "https://i.imgur.com/ur7mAno.png", "line": "G", "color": "Light Green", "rgb": "#6CBE45"},
  {"name": "jtrain", "src": "https://i.imgur.com/3U4eNxl.png", "line": "JZ", "color": "Brown", "rgb": "#8B4513"},
  {"name": "ltrain", "src": "https://i.imgur.com/S6NeJtj.png", "line": "L", "color": "Light Gray", "rgb": "#A7A9AC"},
  {"name": "mtrain", "src": "https://i.imgur.com/0FZ6kum.png", "line": "M", "color": "Orange", "rgb": "#FF6319"},
  {"name": "ntrain", "src": "https://i.imgur.com/Y5vukLP.png", "line": "N", "color": "Yellow", "rgb": "#FCCC0A"},
  {"name": "qtrain", "src": "https://i.imgur.com/7BRayd1.png", "line": "Q", "color": "Yellow", "rgb": "#FCCC0A"},
  {"name": "rtrain", "src": "https://i.imgur.com/zUGeskm.png", "line": "R", "color": "Yellow", "rgb": "#FCCC0A"},
  {"name": "strain", "src": "https://i.imgur.com/1BJvHdC.png", "line": "S 42nd", "color": "Gray", "rgb": "#808080"},
  {"name": "srocktrain", "src": "https://i.imgur.com/1BJvHdC.png", "line": "S Rock", "color": "Gray", "rgb": "#808080"},    
  {"name": "srklntrain", "src": "https://i.imgur.com/1BJvHdC.png", "line": "S Fkln", "color": "Gray", "rgb": "#808080"},
  {"name": "wtrain", "src": "https://i.imgur.com/kUnS3Ko.png", "line": "W", "color": "Yellow", "rgb": "#FCCC0A"},
  {"name": "ztrain", "src": "https://i.imgur.com/momyYjI.png", "line": "JZ", "color": "Brown", "rgb": "#8B4513"}
]

subway_train_data = pl.DataFrame(data)
subway_train_data.head(5)
shape: (5, 5)
namesrclinecolorrgb
strstrstrstrstr
"1train""https://i.imgu…"1""Red""#EE352E"
"2train""https://i.imgu…"2""Red""#EE352E"
"3train""https://i.imgu…"3""Red""#EE352E"
"4train""https://i.imgu…"4""Green""#00933C"
"5train""https://i.imgu…"5""Green""#00933C"
(
    alt.Chart(subway_train_data).mark_image(
        width=20,
        height=20
    )
    .encode(
        x=alt.X('name:N').title(None),
        y=alt.Y('color:N').title(None),
        url='src:N',
    )
)
(
    alt.Chart(subway_train_data).mark_image(
        width=20,
        height=20
    )
    .encode(
        x=alt.X('name:N').axis(ticks=False, labels=False).title(None),
        y=alt.Y('color:N').axis(ticks=False, labels=False).title(None),
        url='src:N',
        row=alt.Row('color:N', spacing=0).title(None)
    )
    .resolve_scale(
        x='independent',
        y='independent'
    )
)
(
    alt.Chart(subway_train_data).mark_image(
        width=20,
        height=20
    )
    .encode(
        x=alt.X('name:N').axis(ticks=False, labels=False).title(None),
        y=alt.Y('color:N').axis(ticks=False, labels=False).title(None),
        url='src:N',
        row=alt.Row('color:N', spacing=0).title(None)
    )
    .resolve_scale(
        x='independent',
        y='independent'
    )
)
alt.Chart(subway_train_data).mark_image(
    width=20,
    height=20
).encode(
    x=alt.X('name:N').axis(labels=False, ticks=False, grid=False, domainWidth=0).title(None),  # domainWith=0 + some hacking
    y=alt.Y('color:N').axis(labels=False, ticks=False, grid=False, domainWidth=0).title(None),
    color=alt.Color('letter:N').legend(None), 
    url='src:N',
    row=alt.Row(
            'color:N', 
            title=None,
            header=alt.Header(labelFontSize=0),
            sort=["Red", "Green", "Purple", "Blue", "Orange"],
            spacing=2).title(None)
).resolve_scale(
    x='independent',
    y='independent'
)
subway_line_select = alt.selection_point(fields=['line'])

chart = alt.Chart(subway_train_data).mark_image(
    width=20,
    height=20
).encode(
    x=alt.X('name:N').axis(labels=False, ticks=False, grid=False, domainWidth=0).title(None),  # domainWith=0 + some hacking
    y=alt.Y('color:N').axis(labels=False, ticks=False, grid=False, domainWidth=0).title(None),
    color=alt.condition((subway_line_select), alt.Color('letter:N').legend(None), alt.value("lightgray")), 
    opacity=alt.condition((subway_line_select), alt.value(1), alt.value(0.3)), 
    url='src:N',
    row=alt.Row(
            'color:N', 
            title=None,
            header=alt.Header(labelFontSize=0),
            sort=["Red", "Green", "Purple", "Blue", "Orange"],
            spacing=2).title(None)
).resolve_scale(
    x='independent',
    y='independent'
).add_params(subway_line_select)

chart
# hacky way to get rid of gridlines
# domainWith=0 gets rid of axis line!!!!

chart.view = {}
chart.view['strokeWidth'] = 0
chart.save("subway_final.html")
chart.properties(
        title=alt.Title(
            "Select Subway Line(s)", 
            subtitle=["Shift-Click to select Multiple", ""], 
            orient="top"))

MTA Data#

import polars as pl
import altair as alt

alt.data_transformers.disable_max_rows()

# Read the data and format the date
df = pl.read_csv("data/MTA_Subway_Wait_Assessment__2015-2019.csv").with_columns(
    pl.col("month").str.to_date(format="%Y-%m")
).join(subway_train_data, on='line', how='left')
df.head()
shape: (5, 12)
monthlinedivisionday_typeperiodnum_timepoints_passing_wait _assessmentnum_sched_timepointswait assessmentnamesrccolorrgb
datestrstri64stri64i64f64strstrstrstr
2015-01-01"1""A DIVISION"1"offpeak"17864226360.789185"1train""https://i.imgu…"Red""#EE352E"
2015-01-01"1""A DIVISION"1"peak"10540150190.701778"1train""https://i.imgu…"Red""#EE352E"
2015-01-01"1""A DIVISION"2"offpeak"660174960.880603"1train""https://i.imgu…"Red""#EE352E"
2015-01-01"1""A DIVISION"2"peak"317736560.868982"1train""https://i.imgu…"Red""#EE352E"
2015-01-01"2""A DIVISION"1"offpeak"18357242560.756802"2train""https://i.imgu…"Red""#EE352E"
import polars as pl
import altair as alt

# Altair by default lets you plot 5000 rows. 
alt.data_transformers.disable_max_rows()

# Don't have a better way to do this
line_rgb_df = df.select('line', 'rgb').filter(pl.col("line")!="Systemwide").unique().to_pandas()
line_names = line_rgb_df['line'].to_list()
line_colors = line_rgb_df['rgb'].to_list()

longitudinal_mta = alt.Chart(df.to_pandas()).mark_line(opacity=0.5).encode(
    x=alt.X("yearmonth(month):T").title("Month"),
    y=alt.Y("wait assessment:Q").title("Wait Assessment (%)"),
    column=alt.Column("period:N", header=alt.Header(labelFontSize=15)).title(None),
    # color="line",
    color=alt.condition(subway_line_select, alt.Color(field="line", scale=alt.Scale(domain=line_names, range=line_colors)).legend(None), alt.value("lightgray")), 
    opacity=alt.condition(subway_line_select, alt.value(0.5), alt.value(0.1)), 
).transform_filter(
    (alt.datum.line != "Systemwide") & (alt.datum.day_type==1)
).properties(
    title=alt.Title(
        "MTA: 2015-2019 Percent of Train times meeting 'Wait assessment' metric", 
        subtitle=["Weekdays"],
        # subtitle="'Wait Assessment' refers to how 'regularly spaced' the trains are."
        ),
    width=200,
    height=150
).add_params(subway_line_select)

longitudinal_mta
wait_vs_timepoints = alt.Chart(df.to_pandas()).mark_circle(opacity=0.5).encode(
    x=alt.X("num_sched_timepoints:Q").title("Number of Timepoints"),
    y=alt.Y("wait assessment:Q").title("Wait Assessment (%)"),
    color=alt.condition(subway_line_select, alt.Color(field="line", scale=alt.Scale(domain=line_names, range=line_colors)).legend(None), alt.value("lightgray")), 
    opacity=alt.condition(subway_line_select, alt.value(0.5), alt.value(0.1)), 
    column=alt.Column("period:N", header=alt.Header(labelFontSize=15)).title(None),
    tooltip=["line:N", "wait assessment"]
).transform_filter(
    (alt.datum.line != "Systemwide") & (alt.datum.day_type==1)
).properties(
    title=alt.Title(
        "MTA: 2015-2019 'Wait assessment' vs. Total Timepoints, by Subway Line", 
        subtitle=["Weekdays"],
        # subtitle="'Wait Assessment' refers to how 'regularly spaced' the trains are."
        ),
    width=200,
    height=150
)

wait_vs_timepoints.add_params(subway_line_select)
final_fig = (
    chart | (longitudinal_mta & wait_vs_timepoints)
)
final_fig
final_fig.save("MTA_Wait_assessment.html")