I realized that due to modular nature of the dash_oop_components library, it would be relatively straightforward to implement querystring support, so I did!
For a lot of analytical web apps it can be super useful to be able to share the state of a dashboard with others through a url. Imagine you have done a particular analysis on a particular tab, setting certain dropdowns and toggles and you wish to share these with a co-worker.
You could tell them to go to the dashboard with instructions to set the exact same dropdowns and toggles. But it would be much easier if you can simply send a url that rebuild the dashboard exactly as you saw it!
This can be done by storing the state of the dashboard in the querystring:
(Not sure including a gif wil work, so here’s the link to the gif: https://github.com/oegedijk/dash_oop_components/blob/master/dash_oop_demo.gif)
Here is a link to the starting dashboard dashboard: https://dash-oop-demo.herokuapp.com/
And here is a link that shows the same dashboard comparing Netherlands with Belgium in covid cases.
Tracking state with dash_oop_components
Thanks to the modular nature and tree structure of DashComponents
it is relatively straightforward to keep track of which elements should be tracked in the url querystring, and rebuild the page in accordance with the state of the querystring.
The example above can be found at github.com/oegedijk/dash_oop_demo
Basic summary instructions:
Assuming you’ve already built your app using DashComponents, in order to add querystring support to your app all you need to do is:
- Pass
querystrings=True
parameters toDashApp
- Change the
def layout(self)
method todef layout(self, params=None)
- Inside your
DashComponents
’ layout wrap the elements that you want to track inself.querystring(params)(...)
:- i.e. change
todcc.Input(id='input-'+self.name)
self.querystring(params)(dcc.Input)(id='input-'+self.name')
- i.e. change
- pass down
params
to all subcomponent layouts:def layout(self, params=None): return html.Div([self.subcomponent.layout(params)])
note1: it is important to assign a proper .name
to DashComponents
with querystring elements, as otherwise the elements will get a different random uuid id
each time you reboot the dashboard, breaking old querystrings.
note2: If you wish to track attributes other than value
, you can pass those explicitly: self.querystring(params, "min", "max")(dcc.Slider)(...)
Step 1: Turning on querystrings in DashApp
In order to turn on the tracking of querystrings you need to start DashApp
with the querystrings=True
parameter, e.g.:
dashboard = CovidDashboard(plot_factory)
app = DashApp(dashboard, querystrings=True, external_stylesheets=[dbc.themes.BOOTSTRAP])
app.run()
Step 2: Building DashComponent
with layout(params)
and self.querystring()
The example dashboard consists of four tabs that each contain the layout of a CovidComposite
subcomponent:
-
self.europe = CovidComposite(self.plot_factory, "Europe", include_countries=self.europe_countries, name="eur")
: a tab with only european countries -
self.asia = CovidComposite(self.plot_factory, "Asia", include_countries=self.asia_countries, name="asia")
: a tab with only Asian countries -
self.cases_only = CovidComposite(self.plot_factory, "Cases Only", metric='cases', hide_metric_dropdown=True, countries=['China', 'Italy', 'Brazil'], name="cases")
: a tab with only cases data (for the whole world) -
self.deaths_only = CovidComposite(self.plot_factory, "Deaths Only", metric='deaths', hide_metric_dropdown=True, countries=['China', 'Italy', 'Brazil'], name="deaths")
: a tab with only deaths data (for the whole world)
In order to keep track of which tab the user is on, we wrap the dcc.Tabs
element in a self.querystring(params)(..)
wrapper:
self.querystring(params)(dcc.Tabs)(id='tabs', ...)`
This will make sure that the value
attribute of the dcc.Tabs
element with id='tabs'
is tracked in the querystring, so that users will start on the same tab when you send them a link.
Other querystring parameters get tracked inside the subcomponent definition of CovidComposite
. In order to make sure that these subcomponents also receive the params
we need to pass those params down to the layout of our subcomponents as well:
dcc.Tab(..., children=self.europe.layout(params))
dcc.Tab(..., children=self.asia.layout(params))
dcc.Tab(..., children=self.cases_only.layout(params))
dcc.Tab(..., children=self.deaths_only.layout(params))
Note that we set the name
of the tabs to "eur"
, "asia"
, "cases"
and "deaths"
Full definition of CovidDashboard
:
class CovidDashboard(DashComponent):
def __init__(self, plot_factory,
europe_countries = ['Italy', 'Spain', 'Germany', 'France',
'United_Kingdom', 'Switzerland', 'Netherlands',
'Belgium', 'Austria', 'Portugal', 'Norway'],
asia_countries = ['China', 'Vietnam', 'Malaysia', 'Philippines',
'Taiwan', 'Myanmar', 'Thailand', 'South_Korea', 'Japan']):
super().__init__(title="Covid Dashboard")
self.europe = CovidComposite(self.plot_factory, "Europe",
include_countries=self.europe_countries, name="eur")
self.asia = CovidComposite(self.plot_factory, "Asia",
include_countries=self.asia_countries, name="asia")
self.cases_only = CovidComposite(self.plot_factory, "Cases Only",
metric='cases', hide_metric_dropdown=True,
countries=['China', 'Italy', 'Brazil'], name="cases")
self.deaths_only = CovidComposite(self.plot_factory, "Deaths Only",
metric='deaths', hide_metric_dropdown=True,
countries=['China', 'Italy', 'Brazil'], name="deaths")
def layout(self, params=None):
return dbc.Container([
dbc.Row([
html.H1("Covid Dashboard"),
]),
dbc.Row([
dbc.Col([
self.querystring(params)(dcc.Tabs)(id="tabs", value=self.europe.name,
children=[
dcc.Tab(label=self.europe.title,
id=self.europe.name,
value=self.europe.name,
children=self.europe.layout(params)),
dcc.Tab(label=self.asia.title,
id=self.asia.name,
value=self.asia.name,
children=self.asia.layout(params)),
dcc.Tab(label=self.cases_only.title,
id=self.cases_only.name,
value=self.cases_only.name,
children=self.cases_only.layout(params)),
dcc.Tab(label=self.deaths_only.title,
id=self.deaths_only.name,
value=self.deaths_only.name,
children=self.deaths_only.layout(params)),
]),
])
])
], fluid=True)
Step 3: tracking parameters in subcomponents:
A CovidComposite
DashComponent
consists of a CovidTimeSeries
, a CovidPieChart
and two dropdowns for metric and country selection. The value of the dropdowns get passed to the corresponding dropdowns of the subcomponents, which are hidden through the config params.
We would like to keep track of the state of these dropdowns so we wrap them inside a self.querystring()
:
For the metric dropdown:
self.querystring(params)(dcc.Dropdown)(id='dashboard-metric-dropdown-'+self.name, ...)
For the country dropdown:
self.querystring(params)(dcc.Dropdown)(id='dashboard-country-dropdown-'+self.name, ...)
And we also make sure that parameters can be passed down the layout with
def layout(self, params=None):
...
Full definition of CovidComposite
:
class CovidComposite(DashComponent):
def __init__(self, plot_factory, title="Covid Analysis",
hide_country_dropdown=False,
include_countries=None, countries=None,
hide_metric_dropdown=False,
include_metrics=None, metric='cases', name=None):
super().__init__(title=title)
if not self.include_countries:
self.include_countries = self.plot_factory.countries
if not self.countries:
self.countries = self.include_countries
if not self.include_metrics:
self.include_metrics = self.plot_factory.metrics
if not self.metric:
self.metric = self.include_metrics[0]
self.timeseries = CovidTimeSeries(
plot_factory,
hide_country_dropdown=True, countries=self.countries,
hide_metric_dropdown=True, metric=self.metric)
self.piechart = CovidPieChart(
plot_factory,
hide_country_dropdown=True, countries=self.countries,
hide_metric_dropdown=True, metric=self.metric)
def layout(self, params=None):
return dbc.Container([
dbc.Row([
dbc.Col([
html.H1(self.title),
self.make_hideable(
self.querystring(params)(dcc.Dropdown)(
id='dashboard-metric-dropdown-'+self.name,
options=[{'label': metric, 'value': metric} for metric in self.include_metrics],
value=self.metric,
), hide=self.hide_metric_dropdown),
self.make_hideable(
self.querystring(params)(dcc.Dropdown)(
id='dashboard-country-dropdown-'+self.name,
options=[{'label': metric, 'value': metric} for metric in self.include_countries],
value=self.countries,
multi=True,
), hide=self.hide_country_dropdown),
], md=6),
], justify="center"),
dbc.Row([
dbc.Col([
self.timeseries.layout(),
], md=6),
dbc.Col([
self.piechart.layout(),
], md=6)
])
], fluid=True)
def _register_callbacks(self, app):
@app.callback(
Output('timeseries-country-dropdown-'+self.timeseries.name, 'value'),
Output('piechart-country-dropdown-'+self.piechart.name, 'value'),
Input('dashboard-country-dropdown-'+self.name, 'value'),
)
def update_timeseries_plot(countries):
return countries, countries
@app.callback(
Output('timeseries-metric-dropdown-'+self.timeseries.name, 'value'),
Output('piechart-metric-dropdown-'+self.piechart.name, 'value'),
Input('dashboard-metric-dropdown-'+self.name, 'value'),
)
def update_timeseries_plot(metric):
return metric, metric