instructions on how to track the state of your dashboard in the url querystring

Make shareable dashboards by tracking state in url querystrings

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:

QuerystringDemo

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.

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:

  1. Pass querystrings=True parameters to DashApp
  2. Change the def layout(self) method to def layout(self, params=None)
  3. Inside your DashComponents wrap the elements that you want to track in self.querystring(params)(...):
    • i.e. change
      dcc.Input(id='input-'+self.name)
      
      to
      self.querystring(params)(dcc.Input)(id='input-'+self.name')
      
  4. 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.

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, 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 countries
  • self.asia: a tab with only Asian countries
  • self.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)

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 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