Search code examples
pythonaltairvega-lite

Changing overlap order of a line chart in altair


I generate a line chart in Altair. I'd like to control which lines are "on top" of the stack of lines. In my example here, I wish for the red line to be on top (newest date) and then descend down to the yellow (oldest date) to be on the bottom.

enter image description here

I tried to control this with the sort parameter of of alt.Color but regardless of sort='ascending' or sort='descending' the order of the line overlap will not change.

How can I control this? Was hoping I can do this without sorting my source dataframe itself.

data = [{'review_date': dt.date(year=2022, month=2, day=24),  'a':19, 'b':17, 'c':12, 'd':8},
{'review_date': dt.date(year=2022, month=2, day=23),  'a':20, 'b':16, 'c':14, 'd':8},
{'review_date': dt.date(year=2022, month=2, day=22),  'a':22, 'b':16, 'c':14, 'd':10},
{'review_date': dt.date(year=2022, month=2, day=21),  'a':14, 'b':13, 'c':12, 'd':5},]

df = pd.DataFrame(data).melt(id_vars=['review_date'], value_name='price', var_name='contract')
df.review_date = pd.to_datetime(df.review_date)

domain = df.review_date.unique()
range_ = ['red', 'blue', 'gray', 'yellow'] 

alt.Chart(df, title='foo').mark_line().encode(
x=alt.X('contract:N'),
y=alt.Y('price:Q',scale=alt.Scale(zero=False)),
color=alt.Color('review_date:O', sort="ascending", scale=alt.Scale(domain=domain, range=range_)   )
).interactive()

Solution

  • By default, graphical marks are plotted in the order they occur in the dataframe (as you noted), which means that the elements last in the dataframe will be plotted last and end up on top in the chart (called the highest "layer" or the highest "z-order"):

    import pandas as pd
    import altair as alt
    
    
    df = pd.DataFrame({
        'a': [1, 2, 1, 2],
        'b': [1.1, 2.1, 1.0, 2.2],
        'c': ['point1', 'point1', 'point2', 'point2']
    })
    
    alt.Chart(df).mark_circle(size=1000).encode(
        x='a',
        y='b',
        color='c'
    )
    

    enter image description here

    When you set the sort parameter of the color encoding, you are not changing the z-order for the dots, you are only changing the order in which they are assigned a color. In the plot below, "point2" is still on top, but it is now blue instead of orange:

    alt.Chart(df).mark_circle(size=1000).encode(
        x='a',
        y='b',
        color=alt.Color('c', sort='descending')
    )
    

    enter image description here

    If we wanted to change the z-ordering so that "point1" is on top, we would have to specify this with the order encoding:

    alt.Chart(df).mark_circle(size=1000).encode(
        x='a',
        y='b',
        color='c',
        order=alt.Order('c', sort='descending')
    )
    

    enter image description here

    However, as you can read in the Vega-Lite documentation the order encoding has a special behavior for stacked and path marks, including line mark, where it controls the order in which the points are connected in a line rather than their z-ordering/layering.

    Therefore, I believe the only way you can achieve the desired behavior is by sorting that column. You can do this during chart construction:

    alt.Chart(df).mark_line(size=10).encode(
        x='a',
        y='b',
        color='c'
    )
    

    enter image description here

    alt.Chart(df.sort_values('c', ascending=False)).mark_line(size=10).encode(
        x='a',
        y='b',
        color='c'
    )
    

    enter image description here