pythonaltair

Second independent axis in Altair


I would like to add a second (independent) x-axis to my figure, demonstrating a month for a given week.

Here is my snippet:

import pandas as pd
import numpy as np
from datetime import datetime, timedelta

weeks = list(range(0, 54))
start_date = datetime(1979, 1, 1)
week_dates = [start_date + timedelta(weeks=w) for w in weeks]

years = list(range(1979, 2024))
data = {
    "week": weeks,
    "date": week_dates,
    "month": [date.strftime("%b") for date in week_dates],
}
# Precipitation
data.update({str(year): np.random.uniform(0, 100, size=len(weeks)) for year in years})
df = pd.DataFrame(data)
df["Mean_Precipitation"] = df[[str(year) for year in years]].mean(axis=1)

rain_calendar = alt.Chart(df, title=f"Weekly precipitation volumes").mark_bar().encode(
    alt.X('week:O', title='Week in calendar year', axis=alt.Axis(labelAlign="right", labelAngle=-45)),
    alt.Y(f'1979:Q', title='Rain'),
    alt.Color(f"1979:Q", scale=alt.Scale(scheme='redyellowgreen')),
    alt.Order(f"1979:Q", sort="ascending")).properties(width=850, height=350)

line = alt.Chart(df).mark_line(interpolate='basis', strokeWidth=2, color='#FB0000').encode(
    alt.X('week:O'),
    alt.Y('Mean_Precipitation:Q'))

month_labels = alt.Chart(df).mark_text(
    align='center', baseline='top', dy=30
).encode(
    alt.X('week:O', title=None), text='month:N')

combined_chart = alt.layer(rain_calendar, line, month_labels).resolve_scale(x='shared').configure_axisX(
    labelAlign="right", labelAngle=-45, titlePadding=10, titleFontSize=14)\
    .configure_axisY(titlePadding=10, titleFontSize=14).configure_bar(binSpacing=0)

So far, this is the best outcome that I've got:

enter image description here

It is a topic related to discussed one there: Second x axis or x labels


Solution

  • This can be done by using the timeUnit parameter of the x encoding along with the axis format and, optionally, labelExpr parameters.

    Also, Altair works best with long form data and can easily handle the aggregation for you, so I have updated the data generation with this in mind. If your real data is already in short form, you can use pandas melt function. More details are in the documentation

    Chart with week number and month

    import pandas as pd
    import numpy as np
    from datetime import datetime, timedelta
    import altair as alt
    
    start_date = datetime(1979, 1, 1)
    end_date = datetime(2024, 12, 31)
    df = pd.DataFrame(pd.date_range(start_date, end=end_date, freq="W"), columns=['date'])
    df['Precipitation'] = np.random.uniform(0, 100, size=len(df))
    
    base = alt.Chart(df, title="Weekly precipitation volumes").encode(
        x=alt.X(
            "date:O",
            timeUnit="week",
            title="Week in calendar year",
            axis=alt.Axis(
                format='W %-W/%b', 
                labelExpr="split(datum.label, '/')"
            ),
        ),
    )
    
    rain_calendar = (
        base.mark_bar()
        .encode(
            y=alt.Y("Precipitation:Q", title="Rain"),
            color=alt.Color("Precipitation:Q", scale=alt.Scale(scheme="redyellowgreen"), title="1979"),
        )
        .transform_filter(alt.expr.year(alt.datum['date']) == 1979)
        .properties(width=850, height=350)
    )
    
    line = base.mark_line(interpolate="basis", strokeWidth=2, color="#FB0000").encode(
        y="mean(Precipitation):Q"
    )
    
    combined_chart = (
        alt.layer(rain_calendar, line)
        .configure_axisX(
            titlePadding=10, titleFontSize=14
        )
        .configure_axisY(titlePadding=10, titleFontSize=14)
        .configure_bar(binSpacing=0)
    )
    
    combined_chart