Create filters with multiple callbacks

I have recently started to work with plotly in order to create interactive dashboards. I am still learning about it, so I would like to know the best practices in order to create filters which affects several figures in my app.

This is what my app.py looks like:

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
import plotly.express as px
import pandas as pd
from datetime import datetime
from pandas import Timestamp

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

colors = {
    'background': '#111111',
    'text': '#7FDBFF'
}

data = {'Id Incidencia': {0: 'INC000006722157',
  1: 'INC000006722000',
  2: 'INC000006721939',
  3: 'INC000006708347',
  4: 'INC000006723090',
  5: 'INC000006736601',
  6: 'INC000006736721',
  7: 'INC000006724926',
  8: 'INC000006725331',
  9: 'INC000006725229',
  10: 'INC000006722542',
  11: 'INC000006722729',
  12: 'INC000006723246',
  13: 'INC000006722574',
  14: 'INC000006741563',
  15: 'INC000006722571',
  16: 'INC000006741632',
  17: 'INC000006741568',
  18: 'INC000006741636',
  19: 'INC000006741640'},
 'Fecha Apertura': {0: Timestamp('2020-12-07 12:28:30'),
  1: Timestamp('2020-12-07 09:52:06'),
  2: Timestamp('2020-12-07 10:13:06'),
  3: Timestamp('2020-12-02 09:02:45'),
  4: Timestamp('2020-12-07 20:37:53'),
  5: Timestamp('2020-12-12 00:35:16'),
  6: Timestamp('2020-12-12 00:46:48'),
  7: Timestamp('2020-12-08 15:21:15'),
  8: Timestamp('2020-12-08 20:04:14'),
  9: Timestamp('2020-12-08 18:33:54'),
  10: Timestamp('2020-12-07 15:52:59'),
  11: Timestamp('2020-12-07 18:33:22'),
  12: Timestamp('2020-12-07 23:56:08'),
  13: Timestamp('2020-12-07 17:11:05'),
  14: Timestamp('2020-12-14 13:31:05'),
  15: Timestamp('2020-12-07 17:06:55'),
  16: Timestamp('2020-12-14 13:44:35'),
  17: Timestamp('2020-12-14 13:33:40'),
  18: Timestamp('2020-12-14 13:46:38'),
  19: Timestamp('2020-12-14 13:51:34')}}
df = pd.DataFrame(data)
df["Fecha Apertura"] = pd.to_datetime(df["Fecha Apertura"])
df = df.set_index('Fecha Apertura')

periods = [('D', '%Y-%m-%d'), ('M', '%Y-%m'), ('Y', '%Y')]
grouped_data = df.groupby(df.index.to_period(periods[0][0]))["Id Incidencia"].count()
fig2 = px.line(
    x=grouped_data.index.strftime(periods[0][1]),
    y=grouped_data.values,
    title='Distribución temporal de las incidencias',
    labels={
        'x': 'Fecha',
        'y': 'Nº incidencias'
    }
)

app.layout = html.Div(children=[
    dcc.Dropdown(
        id='groupby-period',
        options=[
            {'label': 'Día', 'value': 0},
            {'label': 'Mes', 'value': 1},
            {'label': 'Año', 'value': 2}
        ],
        value=0,
        clearable=False,
        searchable=False
    ),

    dcc.DatePickerRange(
        id='date-picker-range',
        start_date_placeholder_text="Fecha Inicio",
        end_date_placeholder_text="Fecha Fin",
        calendar_orientation='horizontal',
    ),

    dcc.Graph(
        id='line-chart-apertura',
        figure=fig2
    )
])

@app.callback(
    Output('line-chart-apertura', 'figure'),
    Input('groupby-period', 'value')
)
def update_graphs_by_period(period):
    periods = [('D', '%Y-%m-%d'), ('M', '%Y-%m'), ('Y', '%Y')]
    grouped_data = df.groupby(df.index.to_period(periods[period][0]))["Id Incidencia"].count()

    fig = px.line(
        x=grouped_data.index.strftime(periods[period][1]),
        y=grouped_data.values,
        title='Distribución temporal de las incidencias',
        labels={
            'x': 'Fecha',
            'y': 'Nº incidencias'
        }
    )

    fig.update_layout(transition_duration=500)

    return fig

@app.callback(
    Output('line-chart-apertura', 'figure'),
    [
        Input('date-picker-range', 'start_date'),
        Input('date-picker-range', 'end_date')
    ],
    prevent_initial_call=True
)
def filter_by_date_range(start_date, end_date):
    if start_date is None or end_date is None:
        return dash.no_update

    start_date_object = datetime.fromisoformat(start_date)
    end_date_object = datetime.fromisoformat(end_date)

    mask = (df.index >= start_date_object) & (df.index <= end_date_object)
    df_range = df.loc[mask]

    periods = [('D', '%Y-%m-%d'), ('M', '%Y-%m'), ('Y', '%Y')]
    grouped_data = df_range.groupby(df_range.index.to_period(periods[0][0]))["Id Incidencia"].count()
    fig = px.line(
        x=grouped_data.index.strftime(periods[0][1]),
        y=grouped_data.values,
        title='Distribución temporal de las incidencias',
        labels={
            'x': 'Fecha',
            'y': 'Nº incidencias'
        }
    )

    fig.update_layout(transition_duration=500)

    return fig

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

I will explain it a little:

  • I have uploaded a simple df example with only the columns I am using on my example: a date column and an unique identifier one. When deployed, data will be retrieved from a cloud environment, so it is important to keep the original data.
  • There are two filters which control the same line-chart figure (the idea is to apply them later to more different figures):
    • Dropdown: which defines the granularity of the line chart. It simply groups the timestamps by day, month or year depending on the user dropdown selection.
    • Date picker range: where the user can select a date range to be shown on the figure. It must be compatible with the other filter: if we select a date range, it must be shown according to the date granularity selected on the dropdown.

Until now, I have achieved to have both filters working independently, but I am struggling to have both of them working at the same time. I know my actual approach is not possible since a figure can only be affected by a single callback, so I need to combine them into a single one. However, I do not know what the cleanest and best practice method would be.

  1. What is the cleanest way to merge both callback methods in one?
  2. Is my approach of filtering by date range with df_range = df.loc[mask] the way I should do it, or would it be better to have a global df object like df_to_show = df.copy() and work with it instead?
  3. Regarding to #2: how could I achieve that if a callback function modifies the df_to_show global variable, all the figures using it notice it so that they redraw themselves?