Search code examples
pythonaltairvega-lite

Layered Altair Line Plot with Multiple Colors per Line


I am trying to map two lines on a single chart in Altair that change color midway through the plot.

Here is what the plot would look like ideally: enter image description here

So far I have successfully plot two lines on a single graph. Each line takes a custom color, but the colors do not change at the midway point.

Here's the code: (1) dummy version of my data:

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

data_list = [(['1', '2']*20)*2,
             [0]*20+[1]*20,
             np.repeat(pd.date_range(datetime.today(), periods=20).tolist(), 2),
             np.random.normal(size=40)
            ]
data_df = pd.DataFrame(data_list).transpose()
data_df.columns = ['group', 'post', 'datetime', 'value']

(2) Altair code

PROPERTIES = {"height": 200, "width": 800}
line_chart_1 = (
        alt.Chart(data_df)
        .transform_calculate(group="datum.group == '1' ? 'Group A:1' : 'Group B:1' ")
        .transform_filter("datum.post == 0")
        .mark_line(opacity=0.8, strokeWidth=3, strokeCap='round')
        .encode(
            alt.X("datetime:T", title=None),
            alt.Y("value:Q", title=None),
            alt.Color(
                "group:N",
                scale=alt.Scale(
                    domain=("Group A:1", "Group B:1"), range=("#F44E4E", "#0442BF")
                ),
                title=None,
                legend=alt.Legend(orient="bottom-left"),
            ),
        )
    )
        
line_chart_2 = (
    alt.Chart(data_df)
    .transform_calculate(group="datum.group == '1' ? 'Group A:2' : 'Group B:2' ")
    .transform_filter("datum.post == 1")

    .mark_line(opacity=0.8, strokeWidth=3, strokeCap='round')
    .encode(
        alt.X("datetime:T", title=None),
        alt.Y("value:Q", title=None),
        alt.Color(
            "group:N",
            scale=alt.Scale(
                domain=("Group A:2", "Group B:2"), range=("#F20505", "#63CAF2")
            ),
            title=None,
            legend=alt.Legend(orient="bottom-left"),
        ),
    )
)
area_chart = (
    alt.Chart(data_df)
    .mark_rect(opacity=0.5)
    .encode(
        alt.X("start:T"),
        alt.X2("stop:T"),
        y=alt.value(0),
        y2=alt.value(PROPERTIES["height"]),
        fill=alt.Color(
            "post:N",
            scale=alt.Scale(domain=(0, 1), range=("lightgray", "lightgray")),
            legend=None,
        ),
    )
)
alt.layer(area_chart, line_chart_2, line_chart_1).properties(**PROPERTIES)

enter image description here

I also saw this post and tried the following:

line_chart = alt.layer(
    alt.Chart().mark_line(),
    alt.Chart().mark_line().encode(color='post:N'),
    data=data_df
).transform_filter(
    'datum.group==="1"',
).encode(
    x='datetime:T',
    y=alt.Y('value:Q', impute={'value': None}),
)

line_chart_2 = alt.layer(
    alt.Chart().mark_line(),
    alt.Chart().mark_line().encode(color='post:N'),
    data=data_df
).transform_filter(
    'datum.group==="2"',
).encode(
    x='datetime:T',
    y=alt.Y('value:Q', impute={'value': None}),
)

alt.layer(line_chart, line_chart_2)

enter image description here

Which gets me really close, but I cannot figure out how to change the colors of the line segments or add a legend.

Anyone know how to get one of these plots over the finish line? Thanks in advance for your help!


Solution

  • You need to explicitly state that you want independent legends using the resolve_scale function. I tried running your code but the x values of your mark_rect are missing. Using only the line charts, it will go something like this

    alt.layer(line_chart_1, line_chart_2).properties(**PROPERTIES).resolve_scale(color='independent')
    

    enter image description here