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 DashComponent
s.
- 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. - 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
CovidTimeSeries
and aCovidPieChart
, and adds its own dropdowns that control both subcomponents - CovidDashboard: a dashboard consisting of four
CovidComponent
s 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_factory
gets passed down to the subcomponents - The subcomponents are included in the layout with
self.timeseries.layout()
andself.piechart.layout()
. - A
DashConnector
calledDropDownConnector
is defined that equalizes the dropdown values of the timeseries and piechart components to that of theCovidComposite
dropdowns. ThisDropdownConnector
is then instantiated in the init.- An alternative would have been to define the
DropdownConnector
callbacks 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=True
andhide_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.Row
s and dbc.Col
s:
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_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
andasia_countries
is included - The parameters for the
dash_app
itself:- 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
DashFigureFactory
s will get imported, instantiated with the right parameters and passed down to theDashComponent
. - The
DashComponent
s will get imported from the right module, passed an instantiatedDashFigureFactory
along 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()