Gantt chart (px.timeline) with subplot with opacity and filtering implementation

OK here we go, I had to do lot of cleaning :innocent:
How does it look? :

from dash import html, Output, Input, State, dcc, Dash, no_update, Patch
import dash_mantine_components as dmc
import plotly.express as px
import pandas as pd
import sqlite3

app = Dash(name=__name__)

con = sqlite3.connect('factory_db.db')
# extract only the columns we use
df = pd.read_sql(
    'sources', con=con,
    columns=["client_name", "shift_day", "endpoint_name", "state_begin", "state_end", 'reason',
             'duration_min', 'color', 'state', 'period_name', 'operator']
)

# modify reason 'The reason is not specified' to 'Not specified'
df['reason'].replace('The reason is not specified', 'Not specified', inplace=True)

CARD_STYLE = dict(withBorder=True, shadow="sm", radius="md", style={'height': '450px'})
REASON_COLORS = dict(zip(df['reason'].unique(), df['color'].unique()))
# create custom labels like 'client_name' to 'Client name'
LABELS = {label: label.replace('_', ' ').capitalize() for label in df.columns}

# Init figures
fig_pie = px.pie(df, values='duration_min', names='reason', color='reason',
                 color_discrete_map=REASON_COLORS, labels=LABELS)

fig_timeline = px.timeline(
    df,
    x_start='state_begin',
    x_end='state_end',
    y='endpoint_name',
    color='reason',
    color_discrete_map=REASON_COLORS,
    labels=LABELS,
    custom_data=['state', 'reason', 'duration_min', "shift_day", 'period_name', 'operator']
)

fig_timeline.update_traces(
    hovertemplate=(
        '<br>State: <b>%{customdata[0]}</b>'
        '<br>Reason: <b>%{customdata[1]}</b>'
        '<br>Beginning: <b>%{base|%H:%M:%S</b> (%d.%m)}'
        '<br>Duration: <b>%{customdata[2]:.2f}</b> min.'
        '<br>'
        '<br>Shift day: <b>%{customdata[3]|%d.%m.%Y}</b>'
        '<br>Shift: <b>%{customdata[4]}</b>'
        '<br>Operator: <b>%{customdata[5]}</b><extra></extra>'
    )
)

fig_timeline.update_xaxes(side='top', dtick=3.6e+6, tickangle=0, tickformat='%H\n%Y-%m-%d')
fig_timeline.update_yaxes(title_text=None)
fig_timeline.update_layout(showlegend=False)


app.layout = dmc.Paper([
    dmc.Grid([
        dmc.Col([
            dmc.Card([
                html.H2(f'Client: {df["client_name"][0]}'),
                html.H3(f'Shift day: {df["shift_day"][0]}'),
                html.H3(f'Endpoint name: {df["endpoint_name"][0]}'),
                html.H3(f'State begin: {df["state_begin"].min()[:-4]}'),
                html.H3(f'State end: {df["state_end"].max()[:-4]}'),
                html.P("Select reasons to highlight:"),
                dcc.Dropdown(
                    id='dropdown-input',
                    value=df['reason'].unique(), options=df['reason'].unique(),
                    multi=True, placeholder='Select reasons'
                ),
                dmc.Button('Filter', id='dropdown-button'),
            ], **CARD_STYLE)
        ], span=6),
        dmc.Col([
            dmc.Card(
                dcc.Graph(figure=fig_pie),
                **CARD_STYLE)
        ], span=6),
        dmc.Col([
            dmc.Card(
                dcc.Graph(id='timeline-output', figure=fig_timeline),
                **CARD_STYLE)
        ], span=12),
    ], gutter="xl")
])


@app.callback(
    Output('timeline-output', 'figure'),
    Input('dropdown-button', 'n_clicks'),
    State('dropdown-input', 'value'),
    State('timeline-output', 'figure'),
    prevent_initial_call=True,
)
def graph_filtering(_, reasons, fig):
    if not reasons:
        return no_update

    # get the index list of the traces (reason groups) to highlight
    filtered_traces = [i for i in range(len(fig['data'])) if fig['data'][i]['name'] in reasons]
    
    # set the opacity to 1 for selected reasons and 0.2 for the others
    # using the new feature Patch to update only the prop 'opacity' without updating the whole figure
    patched_fig = Patch()
    for i in range(len(fig['data'])):
        patched_fig['data'][i]['opacity'] = 1 if i in filtered_traces else 0.2

    return patched_fig


if __name__ == '__main__':
    app.run_server(debug=True)

2 Likes