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:
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.
An example dashboard that demonstrates how to build a dashboard with querystrings included can be found at github.com/oegedijk/dash_oop_demo and has been deployed to https://dash-oop-demo.herokuapp.com/
Basic summary instructions:
In order to add querystring support to your app all you need is to:
- Pass
querystrings=True
parameters toDashApp
- Change the
def layout(self)
method todef layout(self, params=None)
- Inside your
DashComponents
wrap the elements that you want to track inself.querystring(params)(...)
:- i.e. change to
dcc.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)])
note: it is important to assign a proper .name
to components with querystring elements, as otherwise the elements will get a different random uuid id
each time you reboot the dashboard, breaking old querystrings.
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, bootstrap=dbc.themes.FLATLY)
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
: a tab with only european countriesself.asia
: a tab with only Asian countriesself.cases_only
: a tab with only cases data (for the whole world)self.deaths_only
: a tab with only deaths data (for the whole world)
In order to keep track of an attribute of a layout element we simply wrap it inside a self.querystring()(element_func)(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 DashComposite
. 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)
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 component_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
Addendum: Tracking querystring params of current tab only
When you define dashboard with lots of tabs, lots of components and lots of elements, the size of the querystring can explode rapidly, resulting in clumsy long urls to copy-paste. One solution if to only keep track of the parameters in the current open tab.
The downside is that the rest of the dashboard will take default values, but the upside is significantly smaller querystrings.
In order to implement this, you can make sure of the DashComponentTabs
as a stand-in replacement for dcc.Tabs
.
You simply replace
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)),
]),
with
self.querystring(params)(DashComponentTabs)(id="tabs",
tabs=[self.europe, self.asia, self.cases_only, self.deaths_only],
params=params, component=self, single_tab_querystrings=True)
And automatically all parameters from tabs other than the current tab will be excluded from the url querystring