All classes in the library are derived from DashComponentBase
which is a an abstract base class (ABC) that provides:
- automatic storing of parameters to attributes
- automatic storing of parameters to a
._stored_params
property - exporting component to a config dict with
.to_config()
- exporting component to a yaml file with
.to_yaml()
- building component from a config dict with classmethod
.from_config()
- building component from a yaml file with classmethod
.from_yaml()
Define a class T
as a childclass of DashComponentBase
and initialize an instance t
:
class T(DashComponentBase):
def __init__(self, a=1, b=2, **kwargs):
super().__init__(child_depth=2)
t = T(a=2, b=3)
Get the configuration of the instance t
:
print(t.to_config())
Get the configuration in yaml
format:
print(t.to_yaml())
Tests showing parameters have been assigned to attributes and config stores all relevenat data to rebuild the instance from the config:
- parameters have been assigned to attributes
- class_name and module have been recorded
- parameters have been recorded
assert t.a == 2
assert t.b == 3
assert t.to_config()['dash_component']['class_name'] == "T"
assert t.to_config()['dash_component']['module'] == "__main__"
assert t.to_config()['dash_component']['params']['a'] == t.a
assert t.to_config()['dash_component']['params']['b'] == t.b
Store to yaml:
t.to_yaml("T.yaml")
Load from yaml and check that loaded instance t2
is same as t
:
t2 = T.from_yaml("T.yaml")
assert t2.to_config()['dash_component']['class_name'] == "T"
assert t2.to_config()['dash_component']['module'] == "__main__"
assert t2.to_config()['dash_component']['params']['a'] == t.a
assert t2.to_config()['dash_component']['params']['b'] == t.b
Override parameters upon loading:
t2 = T.from_yaml("T.yaml", b=4)
assert t2.b == 4
If **kwargs
in the class definition than unknown parameters get loads to self.kwargs
:
t2 = T.from_yaml("T.yaml", c=5)
assert hasattr(t2, "kwargs")
assert "c" in t2.kwargs
assert t2.kwargs["c"] == 5
Store to file ("pickle", although also works with dill and joblib) and reload from file:
t2.dump("t") # stores to "t.pkl"
t3 = DashComponentBase.from_file("t.pkl")
print(t3.to_yaml())
assert t3.kwargs["c"] == 5
If suffix is ".pkl" can simply leave it out. And you can load from any DashComponentBase
child class:
t4 = T.from_file("t")
print(t4.to_yaml())
assert t4.kwargs["c"] == 5
A DashFigureFactory
loads data and provides plots, tables, lists, dicts, etc that can be used for building your dashboard.
This provides a clean seperation where all your data preparation and visualisation logic goes into one place (the DashFigureFactory
) and all the dashboard layout and interaction logic goes into another (the DashComponents
). This means that you only have to change the data representation in one place, and the whole dashboard will adapt.
When used as a parameter to a DashComponent
(e.g. parameter figure_factory
)the parameter figure_factory
gets replaced by the figure factory configuration dict. This means that a DashComponent
automatically loads its underlying DashFigureFactory
parameters upon load!
In some cases you may perform a lot of costly calculations during the initialization of the DashFigureFactory
(e.g. calculate SHAP values). In this case you may want to pickle the DashFigureFactory
and reload it from file. If you add a parameter filepath
to the parameters and .dump()
the DashFigureFactory
, then you can automatically reload it from pickle by adding .from_yaml("component.yaml", try_pickles=True)
.
Example use of DashFigureFactory
: ListFactory
A DashFigureFactory that simply stores a list and has a return_list()
method:
class ListFactory(DashFigureFactory):
def __init__(self, list_input, filepath=None):
super().__init__()
def list_length(self):
return len(self.list_input)
def return_list(self, first_n_items=None):
if first_n_items is None: first_n_items = self.list_length()
if first_n_items <= len(self.list_input):
return self.list_input[:first_n_items]
return None
list_factory = ListFactory(["this", "is", "a", "dumb", "example"], filepath="list_factory.pkl")
print(list_factory.return_list())
print(list_factory.to_yaml())
assert list_factory.list_input == ['this', 'is', 'a', 'dumb', 'example']
assert list_factory.return_list() == ['this', 'is', 'a', 'dumb', 'example']
assert list_factory.return_list(2) == ['this', 'is']
Storing and loading from yaml:
list_factory.to_yaml("list_factory.yaml")
list_factory2 = DashFigureFactory.from_yaml("list_factory.yaml")
assert list_factory2.list_input == list_factory.list_input
assert list_factory2.return_list() == list_factory.return_list()
assert list_factory2.return_list(2) == list_factory.return_list(2)
- If you use
force_pickles=True
but filepath does not exist, then it will throw a FileNotFoundError. - If you use
try_pickles=True
but filepath does not exist, then it will simply show a warning.
list_factory = ListFactory(["this", "is", "a", "dumb", "example"], filepath="list_factory2.pkl")
list_factory.to_yaml("list_factory2.yaml")
try:
ListFactory.from_yaml("list_factory2.yaml", force_pickles=True)
except Exception as e:
assert isinstance(e, FileNotFoundError)
list_factory = ListFactory.from_yaml("list_factory2.yaml", try_pickles=True)
So you have to make sure the file exists by dumping it:
list_factory = ListFactory(["this", "is", "a", "dumb", "example"], filepath="list_factory.pkl")
list_factory.to_yaml("list_factory.yaml")
list_factory.dump()
list_factory2 = ListFactory.from_yaml("list_factory.yaml", force_pickles=True)
assert isinstance(list_factory2, ListFactory)
A DashComponent
combines a dash
layout with dash
callbacks.
It provides:
- a
.layout()
method that returns the layout for the component - a
.register_callbacks(app)
method that allow you to register the callbacks of the component to a specific dash app - automatic conversion of any
DashFigureFactory
parameters to it's config (this means that figure factory will be automatically loaded upon load) - automatic registration of all
DashComponent
subcomponents in its attributes (which means that callbacks of all subcomponents will also automatically be registered) - a
make_hideable
staticmethod that makes it easy to hide parts of a layout depending on configuration bools - a
querystring()
method that allows you to store the state of your dashboard in the url querystring, and then load the state of the dashboard back from that url.
It inherits from DashComponentBase
so:
- All parameters are automatically stored to attributes and to
._stored_params
- Can be exported and loaded from config and yaml
- Can be dumped to pickle/dill/joblib and loaded
.from_file()
IMPORTANT:
- add
+self.name
to all theid
's in your layout to make sure layoutid
's are unique. - define your callbacks in
_register_callbacks(self, app)
(note the underscore!) - If you're using
self.querystring(params)(...)
, then set theself.name
of the component to something definitive and readable, as otherwise each run it gets assigned a new random uuid string everytime you reboot your app, breaking previously generated querystring urls.
Tracking every single parameter in a big multi tab dashboard can result in extremely long querystring urls.
However often you would only want to share the analysis on a single tab anyway, so we can shorten the url by only keeping track of the parameters of the tab that you are on at the moment. To make this possible we define a drop-in replacement for dcc.Tabs
called DashComponentTabs
.
When you pass an id
and a list of DashComponents
, this generates automatically
a
dcc.Tabs(id=id, children = [dcc.Tab(tab.layout(), label=tab.title) for tab in tabs])
When you pass a component
parameter and do not set single_tab_querystrings=False
, then DashComponentTabs
stores the querystring parameter for each tab in component._tab_params
. This will get used by DashApp
to exclude all parameters that are not on the current tab from being stored in the querystring url.
Example usage:
self.querystring(params)(DashComponentTabs)(
component=self, id="tabs", tabs=[self.list1, self.list2], params=params)
Example use of DashComponent
:
import dash_html_components as html
import dash_core_components as dcc
- You can pass a
name
to aDashComponent
to be used as a unique suffix for all the layout id's. If you don't pass a name, a random uuid identifierself.name
gets generated instead. However if you are going to track querystrings, it's best to pass a specific, short and readable name. - You can store the state of your dashboard to the url querystring by wrapping the
elements that you would like to track in a
self.querystrings(params)(...)
wrapper.- The state of the querystring will be passed down to the layout function as
params
. - You pass these params to
self.querystring
, and indicate which elements of the layour function you would like to track and reload from state. - If no attributes are listed, by default "value" gets tracked
- The state of the querystring will be passed down to the layout function as
- callbacks should be defined in
_register_callbacks(app)
(note the underscore!)- the
register_callbacks(app)
will then first register all callbacks of subcomponents and then call this_register_callbacks(app)
method.
- the
class T(DashComponent):
def __init__(self, name=None):
super().__init__()
def layout(self, params=None):
return html.Div([
self.querystring(params)(dcc.Input)(id=self.id("input1")),
self.querystring(params, "min", "max")(dcc.Input)(id=self.id("input2")),
self.querystring(params, "min", "max", "value")(dcc.Input)(id=self.id("input3"))
])
t= T(name="0")
t.get_querystring_params()
t= T(name="0")
t.get_querystring_params()
input1_attributes = [qs[1] for qs in t.get_querystring_params() if qs[0]=="input1-0"]
input2_attributes = [qs[1] for qs in t.get_querystring_params() if qs[0]=="input2-0"]
input3_attributes = [qs[1] for qs in t.get_querystring_params() if qs[0]=="input3-0"]
assert input1_attributes == ['value'], "if no attributes explicitly listen, default to track 'value'"
assert not 'value' in input2_attributes, "value should not be in attributes if other attributes are explicitly listed"
assert input3_attributes[0] == 'value', "value should always be the first attributes in the list!"
DashComponent
example: ListComponent
class ListComponent(DashComponent):
def __init__(self, list_factory, first_n=2, name=None):
super().__init__()
def layout(self, params=None):
return html.Div([
self.querystring(params)(dcc.Input)(
id=self.id("input-first-n"),
type="number",
value=self.first_n,
min=0,
max=self.list_factory.list_length()),
html.Div(id=self.id("output-div"),
children=" ".join(self.list_factory.return_list(self.first_n))),
])
def component_callbacks(self, app):
@app.callback(
self.Output("output-div", "children"),
self.Input("input-first-n", "value")
)
def update_div(first_n):
if first_n is not None:
return " ".join(self.list_factory.return_list(first_n))
raise PreventUpdate
list_component = ListComponent(list_factory, name="1")
list_component.get_querystring_params()
assert list_component.list_factory.return_list() == ['this', 'is', 'a', 'dumb', 'example']
assert isinstance(list_component.layout(), html.Div)
assert list_component.get_querystring_params()[0][1] == 'value'
print(list_component.to_yaml())
Store and reload the component (with the figure factory automatically getting reloaded as well!):
list_component.to_yaml("list_component.yaml")
list_component2 = ListComponent.from_yaml("list_component.yaml")
assert isinstance(list_component2, ListComponent)
list_component.dump()
list_component2 = ListComponent.from_yaml("list_component.yaml", try_pickles=True)
assert isinstance(list_component2, ListComponent)
assert list_component2.list_factory.return_list() == list_component.list_factory.return_list()
list_factory.to_config()['dash_figure_factory']['params']['filepath']
See that force_pickle=True
fails when the DashFigureFactory
's filepath is non-existent, but
that try_pickles=True
simply reload the DashFigureFactory
from config:
list_factory._stored_params['filepath'] = "non_existing_file.pkl"
list_component = ListComponent(list_factory)
list_component.to_yaml("list_component.yaml")
try:
list_component.from_yaml("list_component.yaml", force_pickles=True)
except Exception as e:
assert isinstance(e, FileNotFoundError)
list_factory._stored_params['filepath'] = "file_factory.pkl"
list_component.to_yaml("list_component.yaml")
Composite DashComponent
Example: ListComposite
A ListComposite
if a combination of two ListComponents
, both with different initial settings for first_n
.
- the subcomponents get defined in the init with
self.list1 = ...
andself.list2 = ...
- the subcomponents get included in the layout by including
self.list1.layout()
andself.list2.layout()
- callbacks can be written that include elements from the ListComposite and the subcomponents
- you need to add the
.name
from the subcomponents to properly identify the element id's - in this case the reset button resets the two inputs of the subcomponents:
@app.callback( Output("input-first-n-"+self.list1.name, "value"), Output("input-first-n-"+self.list2.name, "value"), Input("reset-button-"+self.name, "n_clicks") )
- you need to add the
- additional callbacks should be defined under
_register_callbacks(self, app)
! (note the underscore!) - callbacks of the subcomponents also get automatically registered, and included when
DashApp
callsregister_components(app)
(note the lack of underscore here!)
class ListComposite(DashComponent):
def __init__(self, list_factory, first_n1=2, first_n2=3, name=None):
super().__init__()
self.list1 = ListComponent(list_factory, first_n=first_n1, name="1")
self.list2 = ListComponent(list_factory, first_n=first_n2, name="2")
def layout(self, params=None):
return html.Div([
html.Button("Reset", id=self.id("reset-button")),
self.querystring(params)(DashComponentTabs)(
component=self, id="tabs", tabs=[self.list1, self.list2], params=params)
])
def component_callbacks(self, app):
@app.callback(
self.list1.Output("input-first-n", "value"),
self.list2.Output("input-first-n", "value"),
self.Input("reset-button", "n_clicks")
)
def reset_inpus(n_clicks):
if n_clicks:
return self.first_n1, self.first_n2
raise PreventUpdate
list_composite = ListComposite(list_factory, name="main")
list_composite.get_querystring_params()
list_composite.layout()
list_composite._tab_params
print(list_composite._querystring_params)
print(list_composite.list1._querystring_params)
print(list_composite.list2._querystring_params)
list_composite.compute_querystring_params()
querystring_params = list_composite.get_querystring_params()
assert len(querystring_params) == 3, \
"should be three querystring params, one from each subcomponent plus 'tabs'!"
querystring_params
unreachable_querystring_params = list_composite.get_unreachable_querystring_params()
assert len(unreachable_querystring_params) == 0, \
"should be no unreachable params in this component!"
unreachable_querystring_params
print(list_composite.to_yaml())
subcomponents are automatically registerd to a _components
list when calling register_components()
.
(this gets called initially in register_callbacks()
)
list_composite.register_components()
list_composite._components
assert len(list_composite._components) == 2
When we build a dash app from this list_composite, we can check that all three callbacks (one for the composite, and one each for each subcomponent) have indeed been registered:
app = dash.Dash()
app.layout = list_composite.layout()
list_composite.register_callbacks(app)
assert len(app.callback_map) == 3, \
("Should be three callbacks (one for the composite, and one each for each "
"subcomponent) have indeed been registered)")
Using DashConnectors
between DashComponents
:
You can define DashConnectors
that connect the inputs and outputs of multiple DashComponents
.
You can do this because you have access to the .name
property of each subcomponent.
DashConnectors
never register callbacks from any of its subcomponents.
Using DashConnectors can help clean up your code, especially if you can re-use them.
class ResetConnector(DashConnector):
def __init__(self, list_composite, list_component1, list_component2):
super().__init__()
def component_callbacks(self, app):
@app.callback(
[self.list_component1.Output("input-first-n", "value"),
self.list_component2.Output("input-first-n", "value")],
self.list_composite.Input("reset-button", "n_clicks")
)
def reset_inpus(n_clicks):
if n_clicks:
return self.list_composite.first_n1, self.list_composite.first_n2
raise PreventUpdate
class ListComposite(DashComponent):
def __init__(self, list_factory, first_n1=2, first_n2=3, name=None):
super().__init__()
self.list1 = ListComponent(self.list_factory, first_n=first_n1, name="1")
self.list2 = ListComponent(self.list_factory, first_n=first_n2, name="2")
self.connector = ResetConnector(self, self.list1, self.list2)
def layout(self, params=None):
return html.Div([
html.Button("Reset", id="reset-button-"+self.name),
self.querystring(params)(DashComponentTabs)(
component=self, id="tabs", tabs=[self.list1, self.list2], params=params)
])
list_composite = ListComposite(list_factory)
print(list_composite.to_yaml())
In order to run your DashboardComponent
dashboard you can pass it to a DashApp
and run it:
DashApp(dashboard_component).run()
Args:
dashboard_component
(DashComponent): component to be runport
(int): port to run the servermode
({'dash', 'external', 'inline', 'jupyterlab'}): type of dash server to startquerystrings
(bool): save state to url querystring and load from querystringkwargs
: all kwargs will be passed down to dash.Dash. See below the docstring of dash.Dash
Example use of DashApp
You can build and run dash app by simply passing a DashComposite
to DashApp
and then running it:
db = DashApp(list_composite, mode='external', port=9000, querystrings=True, bootstrap=dbc.themes.FLATLY)
- You can set the port with
port=8051
- You can run the dashboard inline in a notebook by pasing
mode='inline'
- or
mode='external'
ormode='jupyterlab'
- default is
mode='dash'
- or
- Track parameters in the url querystring with querystrings=True
- Any additional parameters will be passed on the to
dash.Dash()
constructor
if run_app:
db.run()
You can also store and reload an entire dashboard:
print(db.to_yaml())
db.to_yaml("dashboard.yaml")
db2 = DashApp.from_yaml("dashboard.yaml", force_pickles=True)
if run_app:
db2.run()
from nbdev.export import *
notebook2script()