A small example showing the power of dash_oop_components in building a modular covid tracking dashboard

This dashboard has been deployed to https://dash-oop-demo.herokuapp.com/

Source code at http://github.com/oegedijk/dash_oop_demo

Dashboard Design

The dashboard consists one DashFigureFactory and four DashComponents.

  1. CovidPlots: DashFigureFactory that stores a covid dataset and can plot both timeseries and piecharts for a select number of countries for either cases or deaths.
  2. CovidTimeSeries: a component with a deaths/cases and a country dropdown that displays the timeseries plot
  3. CovidPieChart: a component with a deaths/cases and a country dropdown that displays the pie chart plot
  4. CovidComponent: a component that combines a CovidTimeSeries and a CovidPieChart, and adds its own dropdowns that control both subcomponents
  5. CovidDashboard: a dashboard consisting of four CovidComponents in four different tabs:
    1. One showing only european countries
    2. One showing only Asian countries
    3. One showing only cases
    4. One showing only deaths

So the CovidComponent gets re-used four times, with different configurations. And each CovidComponent reuses CovidTimeSeries and CovidPieChart.

The CovidPlots figure factory gets passed down by the CovidDashboard to the CovidComponents, and by the CovidComponents to the CovidTimeSeries and CovidPieChart.

Running the dashboard

We run the dashboard by passing the CovidDashboard to a DashApp and calling .run().

We can store the configuration with dashboard.to_yaml("covid_dashboard.yaml").

And then build a new DashApp instance directly from that configuration with `DashApp.from_yaml("covid_dashboard.yaml")

Imports

CovidPlots

First we build a plot factory that holds the dataframe, and has two plotting functions:

  • plot_time_series: plots a time series for a given list of countries for a given metric ('cases' or 'deaths')
  • plot_pie_chart: plots a pie chart for a given list of countries for a given metric ('cases' or 'deaths')

We store a list of countries and metrics to self.countries and self.metric, so that the components can use those to populate the dropdowns.

class CovidPlots(DashFigureFactory):
    def __init__(self, datafile="covid.csv", exclude_countries=[]):
        super().__init__()
        self.df = pd.read_csv(datafile)
        if exclude_countries:
            self.df = self.df[~self.df.countriesAndTerritories.isin(exclude_countries)]
        self.countries = self.df.countriesAndTerritories.unique().tolist()
        self.metrics = ['cases', 'deaths']
        
    def plot_time_series(self, countries, metric):
        return px.line(
            data_frame=self.df[self.df.countriesAndTerritories.isin(countries)],
            x='dateRep',
            y=metric,
            color='countriesAndTerritories',
            labels={'countriesAndTerritories':'Countries', 'dateRep':'date'},
            )
    
    def plot_pie_chart(self, countries, metric):
        return px.pie(
            data_frame=self.df[self.df.countriesAndTerritories.isin(countries)],
            names='countriesAndTerritories',
            values=metric,
            hole=.3,
            labels={'countriesAndTerritories':'Countries'}
            ) 
    
plot_factory = CovidPlots(datafile="covid.csv")
print(plot_factory.to_yaml())
dash_figure_factory:
  class_name: CovidPlots
  module: __main__
  params:
    datafile: covid.csv
    exclude_countries: []

CovidTimeSeries

A CovidTimeSeries component consisting of:

- A dropdown to select a number of countries
- A dropdown to select cases or deaths
- a time series plot

All parameters are automatically assigned to attributes. So we can simply use self.plot_factory or self.countries with having to specify the assignments in the __init__.

Both dropdowns can be hidden by passing hide_country_dropdown=True or hide_metric_dropdown=True. This works by using the make_hideable() staticmethod from the DashComponent class: We wrap the relevant part of the layout in a self.make_hideable(..., hide=self.hide_country_dropdown) wrapper. If hide==True then make_hideable() will wrap a hidden div around the element.

If no include_countries or include_metrics are given, then by default all countries and metrics get included.

class CovidTimeSeries(DashComponent):
    def __init__(self, plot_factory, 
                 hide_country_dropdown=False, include_countries=None, countries=None, 
                 hide_metric_dropdown=False, include_metrics=None, metric='cases', name=None):
        super().__init__()
        
        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]
        
    def layout(self):
        return dbc.Container([
            dbc.Row([
                dbc.Col([
                    html.H3("Covid Time Series"),
                    self.make_hideable(
                        dcc.Dropdown(
                            id=self.id('timeseries-metric-dropdown'),
                            options=[{'label': metric, 'value': metric} for metric in self.include_metrics],
                            value=self.metric,
                        ), hide=self.hide_metric_dropdown),
                    self.make_hideable(
                        dcc.Dropdown(
                            id=self.id('timeseries-country-dropdown'),
                            options=[{'label': country, 'value': country} for country in self.include_countries],
                            value=self.countries,
                            multi=True,
                        ), hide=self.hide_country_dropdown),
                    dcc.Graph(id=self.id('timeseries-figure'))
                ]),
            ])
        ])
    
    def component_callbacks(self, app):
        @app.callback(
            self.Output('timeseries-figure', 'figure'),
            self.Input('timeseries-country-dropdown', 'value'),
            self.Input('timeseries-metric-dropdown', 'value')
        )
        def update_timeseries_plot(countries, metric):
            if countries and metric:
                return self.plot_factory.plot_time_series(countries, metric)
            raise PreventUpdate

CovidPieChart

A CovidPieChart component consisting of:

- A dropdown to select a number of countries
- A dropdown to select cases or deaths
- a pie chart plot

The first parameter is a CovidPlots DashPlotFactory that will be used to return the right plot in the callbacks.

All parameters are automatically assigned to attributes. So we can simply use self.plot_factory or self.countries with having to specify the assignments in the __init__.

Both dropdowns can be hidden by passing hide_country_dropdown=True or hide_metric_dropdown=True. This works by using the make_hideable() staticmethod from the DashComponent class: We wrap the relevant part of the layout in a self.make_hideable(..., hide=self.hide_country_dropdown) wrapper. If hide==True then make_hideable() will wrap a hidden div around the element.

If no include_countries or include_metrics are given, then by default all countries and metrics get included.

class CovidPieChart(DashComponent):
    def __init__(self, plot_factory, 
                 hide_country_dropdown=False, include_countries=None, countries=None, 
                 hide_metric_dropdown=False, include_metrics=None, metric='cases', name=None):
        super().__init__()
        
        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]
        
    def layout(self):
        return dbc.Container([
            dbc.Row([
                dbc.Col([
                    html.H3("Covid Pie Chart"),
                    self.make_hideable(
                        dcc.Dropdown(
                            id=self.id('piechart-metric-dropdown'),
                            options=[{'label': metric, 'value': metric} for metric in self.include_metrics],
                            value=self.metric,
                        ), hide=self.hide_metric_dropdown),
                    self.make_hideable(
                        dcc.Dropdown(
                            id=self.id('piechart-country-dropdown'),
                            options=[{'label': country, 'value': country} for country in self.include_countries],
                            value=self.countries,
                            multi=True
                        ), hide=self.hide_country_dropdown),
                    dcc.Graph(id='piechart-figure-'+self.name)
                ]),
            ])
        ])
    
    def component_callbacks(self, app):
        @app.callback(
            self.Output('piechart-figure', 'figure'),
            self.Input('piechart-country-dropdown', 'value'),
            self.Input('piechart-metric-dropdown', 'value')
        )
        def update_timeseries_plot(countries, metric):
            if countries and metric:
                return self.plot_factory.plot_pie_chart(countries, metric)
            raise PreventUpdate

CovidComposite

A CovidComposite combines a CovidTimeSeries and CovidPieChart component into a single composite component.

  • The plot_factory gets passed down to the subcomponents
  • The subcomponents are included in the layout with self.timeseries.layout() and self.piechart.layout().
  • A DashConnector called DropDownConnector is defined that equalizes the dropdown values of the timeseries and piechart components to that of the CovidComposite dropdowns. This DropdownConnector is then instantiated in the init.
    • An alternative would have been to define the DropdownConnector callbacks directly in the def component_callbacks(self, app) of CovidComposite
  • The dropdowns of the subcomponents are hidden by setting hide_country_dropdown=True and hide_metric_dropdown=True
class DropdownConnector(DashConnector):
    """Connects the country and metric dropdown menus of a
    CovidComposite with the dropdowns of a CovidTimeSeries 
    and CovidPieChart respectively"""
    def __init__(self, composite, timeseries, piechart):
        super().__init__()
        
    def component_callbacks(self, app):
        @app.callback(
            self.timeseries.Output('timeseries-country-dropdown', 'value'),
            self.piechart.Output('piechart-country-dropdown', 'value'),
            self.composite.Input('dashboard-country-dropdown', 'value'),
        )
        def update_timeseries_plot(countries):
            return countries, countries
        
        @app.callback(
            self.timeseries.Output('timeseries-metric-dropdown', 'value'),
            self.piechart.Output('piechart-metric-dropdown', 'value'),
            self.composite.Input('dashboard-metric-dropdown', 'value'),
        )
        def update_timeseries_plot(metric):
            return metric, metric
        
class CovidComposite(DashComponent):
    """A composite DashComponent of a CovidTimeSeries and CovidPieChart, with a dropdown
    added that controls both subcomponents."""
    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)
        
        self.connector = DropdownConnector(self, self.timeseries, self.piechart)
        
    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=self.id('dashboard-metric-dropdown'),
                            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=self.id('dashboard-country-dropdown'),
                            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)
        

CovidDashboard

The final CovidDashboard consists of four CovidComposites subcomponents, each in its own tab, and each with a slightly different configuration.

  • First tab only shows European counties, by passing include_countries=[...]
  • Second tab only shows Asian countries
  • Third includes all countries, but only shows cases, and hides the metric dropdown. The default initial countries are ['China', 'Italy', 'Brazil']
  • Third includes all countries, but only shows deaths, and hides the metric dropdown. The default initial countries are ['China', 'Italy', 'Brazil']
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__()
        
        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", 
                                         include_metrics=['cases'], metric='cases',
                                         hide_metric_dropdown=True,
                                         countries=['China', 'Italy', 'Brazil'], name="case")
        self.deaths_only = CovidComposite(self.plot_factory, "Deaths Only", 
                                          include_metrics=['deaths'], metric='deaths',
                                          hide_metric_dropdown=True,
                                          countries=['China', 'Italy', 'Brazil'], name='death')
        
    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)
    
dashboard = CovidDashboard(plot_factory)
print(dashboard.to_yaml())
dash_component:
  class_name: CovidDashboard
  module: __main__
  params:
    plot_factory:
      dash_figure_factory:
        class_name: CovidPlots
        module: __main__
        params:
          datafile: covid.csv
          exclude_countries: []
    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
    name: Dth4uYnM6R

dashboard.compute_querystring_params()
dashboard.get_querystring_params()
[('tabs', 'value'),
 ('dashboard-metric-dropdown-eur', 'value'),
 ('dashboard-country-dropdown-eur', 'value'),
 ('dashboard-metric-dropdown-asia', 'value'),
 ('dashboard-country-dropdown-asia', 'value'),
 ('dashboard-metric-dropdown-case', 'value'),
 ('dashboard-country-dropdown-case', 'value'),
 ('dashboard-metric-dropdown-death', 'value'),
 ('dashboard-country-dropdown-death', 'value')]

Start app

Pass the dashboard to the DashApp, and add the bootstrap stylesheet that is needed to correctly display all the dbc.Rows and dbc.Cols:

app = DashApp(dashboard, port=9050, querystrings=True, bootstrap=True)

And finally we run the app:

if run_app: # remove to run
    app.run()

Store App config and reload

We can check out the configuration that was generated for our dashboard. This includes:

  • What the topline dashboard_component is, from where to import it and with which parameters to start it
  • The plot_factory parameter gets automatically replaced with the configuration for the plot_factory:
    • includes information on how to import and all parameters
  • The default list of parameters for europe_countries and asia_countries is included
  • The parameters for the dash_app itself:
    • port=8050
    • mode='dash'
    • external_stylesheets=dbc.themes.BOOTSTRAP
print(app.to_yaml())
dash_app:
  class_name: DashApp
  module: dash_oop_components.core
  params:
    dashboard_component:
      dash_component:
        class_name: CovidDashboard
        module: __main__
        params:
          plot_factory:
            dash_figure_factory:
              class_name: CovidPlots
              module: __main__
              params:
                datafile: covid.csv
                exclude_countries: []
          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
          name: GqoAJVV5mh
    port: 9050
    mode: dash
    querystrings: true
    bootstrap: true
    kwargs:
      external_stylesheets:
      - https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css
      suppress_callback_exceptions: true

Now we can store this configuration to disk:

app.to_yaml("covid_dashboard.yaml")

And reload another DashApp from this configuration. This means that:

  • The releve3nt DashFigureFactorys will get imported, instantiated with the right parameters and passed down to the DashComponent.
  • The DashComponents will get imported from the right module, passed an instantiated DashFigureFactory along with other parameters.
  • Either a dash.Dash() or a jupyter_dash.JupyterDash() app gets started, depending on mode.
app2 = DashApp.from_yaml("covid_dashboard.yaml")
Warning: the use of _register_callbacks() will be deprecated! Use component_callbacks() from now on.
Warning: the use of _register_callbacks() will be deprecated! Use component_callbacks() from now on.
Warning: the use of _register_callbacks() will be deprecated! Use component_callbacks() from now on.
Warning: the use of _register_callbacks() will be deprecated! Use component_callbacks() from now on.
Warning: the use of _register_callbacks() will be deprecated! Use component_callbacks() from now on.

We can check that the configuration of this new app2 is indeed the same as app:

print(app2.to_yaml())
dash_app:
  class_name: DashApp
  module: dash_oop_components.core
  params:
    dashboard_component:
      dash_component:
        class_name: CovidDashboard
        module: __main__
        params:
          plot_factory:
            dash_figure_factory:
              class_name: CovidPlots
              module: __main__
              params:
                datafile: covid.csv
                exclude_countries: []
          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
          name: FpEgUgzXDj
    port: 9050
    mode: dash
    querystrings: true
    bootstrap: true
    kwargs:
      external_stylesheets:
      - https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css
      - https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css
      suppress_callback_exceptions: true

And if we run it it still works!

if run_app:
    app2.run()