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.
- What is the cleanest way to merge both callback methods in one?
- 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 likedf_to_show = df.copy()
and work with it instead? - 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?