New in dash_oop_components: store and load state to/from url querystring!

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:

QuerystringDemoGif

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

  1. Pass querystrings=True parameters to DashApp
  2. Change the def layout(self) method to def layout(self, params=None)
  3. Inside your DashComponents’ layout 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)])
    

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

Step 4:

Deploy your dashboard!

Managed to get the gif to work:

store-gif

Introduced a new DashComponentTabs component that allows you to store only the parameters of the current tab in the querystring, resulting in potentially much shorter (and more shareable) urls:

https://dash-oop-demo.herokuapp.com/?tabs=eur&dashboard-metric-dropdown-eur=cases&dashboard-country-dropdown-eur=%5B%27Italy%27%2C+%27Spain%27%2C+%27Portugal%27%2C+%27Norway%27%5D

vs

https://dash-oop-demo.herokuapp.com/?tabs=cases&dashboard-metric-dropdown-eur=deaths&dashboard-country-dropdown-eur=%5B%27Belgium%27%2C+%27Netherlands%27%2C+%27Spain%27%2C+%27France%27%2C+%27United_Kingdom%27%2C+%27Portugal%27%5D&dashboard-metric-dropdown-asia=cases&dashboard-country-dropdown-asia=%5B%27China%27%2C+%27South_Korea%27%5D&dashboard-metric-dropdown-cases=cases&dashboard-country-dropdown-cases=%5B%27Netherlands%27%2C+%27Belgium%27%5D&dashboard-metric-dropdown-deaths=deaths&dashboard-country-dropdown-deaths=%5B%27Netherlands%27%2C+%27Belgium%27%5D

DashComponentsTabs derives from dcc.Tabs, but takes a list of DashComponents instead of dcc.Tab elements to initialize it. It then also stores in ._tab_params which querystring params belong to which tab so that DashApp can exclude all querystring parameters that do not belong to the current tab:

def layout(self, params=None):
        return dbc.Container([
            dbc.Row([
                html.H1("Covid Dashboard"),
            ]),
            dbc.Row([
                dbc.Col([
                    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)
                ])
            ])
        ], fluid=True)