Dropdown menu button doesn't update the plotly timeline plot

Question:

I am trying to build a dropdown menu for plotly timeline plot. Here is my code:

import pandas as pd
import plotly.express as px

def main():
    d = {
    'T_ID': ['T1', 'T2', 'T3', 'T1'],
    'TYPE': ['Type1', 'Type2', 'Type3', 'Type2'],
    'SYS_START_TIME': ['2021-06-20 06:05', '2021-06-23 15:13', '2021-06-27 13:01', '2021-06-29 14:02'],
    'SYS_END_TIME': ['2021-06-20 11:39', '2021-06-23 15:25', '2021-06-27 13:09', '2021-06-29 15:09'],
}
    df = pd.DataFrame(data=d)

    df['SYS_START_TIME'] = pd.to_datetime(df['SYS_START_TIME'])
    df['SYS_END_TIME'] = pd.to_datetime(df['SYS_END_TIME'])

    labels = df["T_ID"].unique()

    buttonsLabels = [dict(label="All",
                          method="update",
                          visible=True,
                          args=[
                              {'x_start': [df.SYS_START_TIME]},
                              {'x_end': [df.SYS_END_TIME]},
                              {'y': [df.T_ID]},
                          ]
                          )]

    for label in labels:
        buttonsLabels.append(dict(label=label,
                                  method="update",
                                  visible=True,
                                  args=[
                                      {'x_start': [df.loc[df.T_ID == label, "SYS_START_TIME"]]},
                                      {'x_end': [df.loc[df.T_ID == label, "SYS_END_TIME"]]},
                                      {'y': [df.loc[df.T_ID == label, "T_ID"]]},
                                  ]
                                  ))

    fig = px.timeline(
        df,
        x_start="SYS_START_TIME",
        x_end="SYS_END_TIME",
        y="T_ID",
        color="TYPE",
        hover_data=['SYS_START_TIME', 'SYS_END_TIME', 'TYPE']
    )

    fig.update_layout(xaxis=dict(
        title='Date',
        tickformat='%Y-%m-%d %H:%M',
        dtick="D",
        showgrid=True
    ), updatemenus=[dict(buttons=buttonsLabels, showactive=True)])

    fig.show()

if __name__ == '__main__':
    main()

When I run the code, it shows all the data but when I press the buttons in the menu it doesn’t update the plot, it still shows all the data. Can anyone help me?

Asked By: Deep Learner

||

Answers:

I think the issue is that the args x_start and x_end that you’re passing to your buttons aren’t going to be recognized by plotly. If you look at fig.data, you’ll see that under the hood, px.timeline creates go.Bar traces for each T_ID using the arguments "base" and "x" to draw the horizontal bars, so you would want to pass these same arguments when creating your buttons. However, I would suggest a simpler approach.

Since you are already plotting traces for each "Type" of your data, you can instead create buttons that toggle only one trace to be visible depending on the type you select from the dropdown. One trick we can use is that customdata[0] contains the information about the type – if the user selects "Type1" from the dropdown, then the following list comprehension:

["Type1" in trace['customdata'][0][0] for trace in fig.data] 

… will return [True, False, False] and we can pass this information along to the "visible" argument for the "Type1" button.

To do this for all Type buttons, we can try something like the following code (and you’ll notice that I modified the xaxis range when each button is clicked, but you can change the padding to whatever you think looks best):

import pandas as pd
import plotly.express as px

def main():
    d = {
        'T_ID': ['T1', 'T2', 'T3'],
        'TYPE': ['Type1', 'Type2', 'Type3'],
        'SYS_START_TIME': ['2021-06-20 06:05', '2021-06-23 15:13', '2021-06-27 13:01'],
        'SYS_END_TIME': ['2021-06-20 11:39', '2021-06-23 15:25', '2021-06-27 13:09'],
    }
    df = pd.DataFrame(data=d)

    df['SYS_START_TIME'] = pd.to_datetime(df['SYS_START_TIME'])
    df['SYS_END_TIME'] = pd.to_datetime(df['SYS_END_TIME'])

    labels = df["T_ID"].unique()

    fig = px.timeline(
        df,
        x_start="SYS_START_TIME",
        x_end="SYS_END_TIME",
        y="T_ID",
        color="TYPE",
        hover_data=['SYS_START_TIME', 'SYS_END_TIME', 'TYPE']
    )

    start_ts = pd.to_datetime(min(df['SYS_START_TIME']))
    end_ts = pd.to_datetime(max(df['SYS_END_TIME']))
    xaxis_range = [start_ts-(end_ts - start_ts)/16, end_ts+(end_ts - start_ts)/2]
    buttonsLabels = [dict(label="All",
                          method="update",
                          visible=True,
                          args=[
                            {'visible': [True]*len(fig.data)},
                            {'xaxis.range': xaxis_range}
                          ]
                          )]
    
    for type_name in df.TYPE.unique():
        start_ts = pd.to_datetime(min(df[df['TYPE'] == type_name]['SYS_START_TIME']))
        end_ts = pd.to_datetime(max(df[df['TYPE'] == type_name]['SYS_END_TIME']))
        padding = pd.Timedelta("1d")
        xaxis_range = [start_ts-padding, end_ts+padding]
        visible_traces = [type_name in trace['customdata'][0][0] for trace in fig.data]
        buttonsLabels.append(dict(label=type_name,
                              method="update",
                              visible=True,
                              args=[
                                {'visible': visible_traces},
                                {'xaxis.range': xaxis_range}
                              ]
                              ))

    fig.update_layout(xaxis=dict(
        title='Date',
        tickformat='%Y-%m-%d %H:%M',
        dtick="D",
        showgrid=True
    ), updatemenus=[dict(buttons=buttonsLabels, showactive=True)])

    return fig

if __name__ == '__main__':
    fig = main()
    fig.show()

enter image description here

Answered By: Derek O

Thanks Derek, I was able to fix it by adding one extra column as combination of T_ID and TYPE. It is not perfect though. Please let me know if were able to come up with a better solution. Here is your code with my little amendment :

import pandas as pd
import plotly.express as px


def main():
    d = {
        'T_ID': ['T1', 'T2', 'T3', 'T1'],
        'TYPE': ['Type1', 'Type2', 'Type3', 'Type2'],
        'SYS_START_TIME': ['2021-06-20 06:05', '2021-06-23 15:13', '2021-06-27 13:01', '2021-06-29 14:02'],
        'SYS_END_TIME': ['2021-06-20 11:39', '2021-06-23 15:25', '2021-06-27 13:09', '2021-06-29 15:09'],
    }
    df = pd.DataFrame(data=d)
    df['LEGEND'] = df['T_ID'] + '-' + df['TYPE']

    df['SYS_START_TIME'] = pd.to_datetime(df['SYS_START_TIME'])
    df['SYS_END_TIME'] = pd.to_datetime(df['SYS_END_TIME'])

    labels = df["T_ID"].unique()

    fig = px.timeline(
        df,
        x_start="SYS_START_TIME",
        x_end="SYS_END_TIME",
        y="T_ID",
        color="LEGEND",
        hover_data=['SYS_START_TIME', 'SYS_END_TIME', 'TYPE', 'T_ID']
    )

    start_ts = pd.to_datetime(min(df['SYS_START_TIME']))
    end_ts = pd.to_datetime(max(df['SYS_END_TIME']))
    xaxis_range = [start_ts - (end_ts - start_ts) / 16, end_ts + (end_ts - start_ts) / 2]
    buttonsLabels = [dict(label="All",
                          method="update",
                          visible=True,
                          args=[
                              {'visible': [True] * len(fig.data)},
                              {'xaxis.range': xaxis_range}
                          ]
                          )]

    for t_id in df.T_ID.unique():
        start_ts = pd.to_datetime(min(df[df['T_ID'] == t_id]['SYS_START_TIME']))
        end_ts = pd.to_datetime(max(df[df['T_ID'] == t_id]['SYS_END_TIME']))
        padding = pd.Timedelta("1d")
        xaxis_range = [start_ts - padding, end_ts + padding]
        visible_traces = [t_id in trace['legendgroup'].split('-', 1)[0] for trace in fig.data]
        buttonsLabels.append(dict(label=t_id,
                                  method="update",
                                  visible=True,
                                  args=[
                                      {'visible': visible_traces},
                                      {'xaxis.range': xaxis_range}
                                  ]
                                  ))

    fig.update_layout(xaxis=dict(
        title='Date',
        tickformat='%Y-%m-%d %H:%M',
        dtick="D",
        showgrid=True
    ), updatemenus=[dict(buttons=buttonsLabels, showactive=True)])

    return fig


if __name__ == '__main__':
    fig = main()
    fig.show()
Answered By: Deep Learner