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.
- CovidPlots:
DashFigureFactorythat stores a covid dataset and can plot both timeseries and piecharts for a select number of countries for either cases or deaths. - CovidTimeSeries: a component with a deaths/cases and a country dropdown that displays the timeseries plot
- CovidPieChart: a component with a deaths/cases and a country dropdown that displays the pie chart plot
- CovidComponent: a component that combines a
CovidTimeSeriesand aCovidPieChart, and adds its own dropdowns that control both subcomponents - CovidDashboard: a dashboard consisting of four
CovidComponents in four different tabs:- One showing only european countries
- One showing only Asian countries
- One showing only cases
- 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")
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())
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
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
A CovidComposite combines a CovidTimeSeries and CovidPieChart component into a single composite component.
- The
plot_factorygets passed down to the subcomponents - The subcomponents are included in the layout with
self.timeseries.layout()andself.piechart.layout(). - A
DashConnectorcalledDropDownConnectoris defined that equalizes the dropdown values of the timeseries and piechart components to that of theCovidCompositedropdowns. ThisDropdownConnectoris then instantiated in the init.- An alternative would have been to define the
DropdownConnectorcallbacks directly in thedef component_callbacks(self, app)ofCovidComposite
- An alternative would have been to define the
- The dropdowns of the subcomponents are hidden by setting
hide_country_dropdown=Trueandhide_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)
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())
dashboard.compute_querystring_params()
dashboard.get_querystring_params()
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()
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_factoryparameter 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_countriesandasia_countriesis included - The parameters for the
dash_appitself:- port=8050
- mode='dash'
- external_stylesheets=dbc.themes.BOOTSTRAP
print(app.to_yaml())
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 theDashComponent. - The
DashComponents will get imported from the right module, passed an instantiatedDashFigureFactoryalong with other parameters. - Either a
dash.Dash()or ajupyter_dash.JupyterDash()app gets started, depending onmode.
app2 = DashApp.from_yaml("covid_dashboard.yaml")
We can check that the configuration of this new app2 is indeed the same as app:
print(app2.to_yaml())
And if we run it it still works!
if run_app:
app2.run()