Welcome to Dash¶
UI elements for both websites and jupyter notebooks¶
Dash is a comparatively easy and stable way to build standalone or jupyter based widgets/apps.
It is based on flask
and react.js
this combined with the html elements of dash enables one to write interactive and scalable webpages without knowing JavaScript or HTML.
Since it is flask based dash servers can be deployed in the same way as a standalone web service.
Originally Dash is designed for web applications but with the help of the jupyter-dash
library it works very well in notebooks.
All components listed here can also be found under https://dash.plotly.com/dash-core-components for a more in depth documentation.
from dash import Dash, dcc, html
import jupyter_dash
The UI is defined as a hierarchy of HTML components inside the app.layout.
Many dash objects have a children
attribute that we can put more dash objects into.
The most commonly used structuring tool is html.Div
.
basic_layout = html.Div(
children=[
html.Div(["some nested text ", "some parallel text"]),
html.Br(), # this is just a line break
html.Div([html.Div("some nested text "), html.Div("some parallel text")]),
html.Br(),
html.Div(
[
html.Div(
"some nested text ",
style={"display": "inline-block"},
),
html.Div(
"some parallel text",
style={"display": "inline-block"},
),
],
),
]
)
# standalone dash server
app1 = Dash("app1")
app1.layout = basic_layout
# app1.run_server(debug=True, port=5050, use_reloader=False)
This would run a dash server and provide an ip address to open the app in a new browser tab. However this does not really work in jupyter notebooks as the cell never actually finishes.
A better solution inside Notebooks is the jupyter-dash
library. This enables us to run the entire notebook while either calling dash in a standalone mode or inline
.
app2 = jupyter_dash.JupyterDash("app2")
app2.layout = basic_layout
app2.run_server(debug=True, port=8071, mode="inline")
/opt/hostedtoolcache/Python/3.11.10/x64/lib/python3.11/site-packages/dash/dash.py:579: UserWarning: JupyterDash is deprecated, use Dash instead. See https://dash.plotly.com/dash-in-jupyter for more details.
Styling and components¶
With the style
argument most dash components can be changed according to the css standard.
Most dash components are found under dcc
, though some are in html
.
With just these we can generate a UI that can't really do anything yet.
For data visualization Dash works very well with the plotly
library.
Note: Dash also supports css style sheets. See: https://dash.plotly.com/external-resources
external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"]
my_style = {"width": "30%", "margin-top": "20px", "margin-bottom": "20px"}
app3 = jupyter_dash.JupyterDash("app3", external_stylesheets=external_stylesheets)
app3.layout = html.Div(
[
"Choosing and displaying a function:",
dcc.Dropdown(
options=["x^2", "2x", "e^x"],
value="x^2",
style=my_style,
),
html.Div(
dcc.RangeSlider(min=0, max=20, step=1, value=[5, 15]),
style={"width": "50%"},
),
html.Button(
"Click_me",
style=my_style,
),
dcc.Graph(),
]
)
app3.run_server(debug=True, port=8072, mode="inline")
Callbacks¶
With the use of callbacks we can now add functionality to all our elements.
In this example I want to be able to choose a function type, set the x limits for the calculation and show the graph upon clicking the button.
The Dash callbacks allow us to access and monitor each object variable.
For this to work we first need to assign IDs to every object we want to interact with.
Many of the Dividers for example don't need a specific ID.
Note that even though the n_click
value of the button is not used it must still be the first function argument since its the value we want to observe.
app4 = jupyter_dash.JupyterDash("app4")
app4.layout = html.Div(
[
"Choosing and displaying a function:",
dcc.Dropdown(
options=["x^2", "2x", "e^x"],
value="x^2",
style=my_style,
id="dropdown",
),
html.Div(
dcc.RangeSlider(
min=0,
max=20,
step=1,
value=[5, 15],
id="slider",
),
style={"width": "50%"},
),
html.Button(
"Click_me",
style=my_style,
id="button",
),
dcc.Graph(id="graph"),
]
)
Callbacks can have as many inputs and outputs as needed.
Any component provided as Input
will trigger the callback, while State
can be used to obtain certain variables without triggering the function.
Basically every property of the selected object can be interacted with.
Eg.: One can give an ID to a html.Div
and attach to or rewrite its children attribute, thus potentially rewriting the entire app within one callback.
Lastly Output
is used to define which object the return value will be assigned to.
The order of function and return arguments is dependent on the order in the decorator.
Ouput, Ìnput
and State
must always be used in exactly this order.
from dash import Input, Output, State
@app4.callback(
Output("graph", "figure"),
Input("button", "n_clicks"),
State("dropdown", "value"),
State("slider", "value"),
)
def update_graph(n_clicks, dropdown_value, slider_value):
def _plot_function(x, function_name):
if function_name == "x^2":
return x**2
elif function_name == "2x":
return 2 * x
elif function_name == "e^x":
return np.exp(x)
else:
raise ValueError(f"Unknown function_name: {function_name}")
x_range = np.linspace(slider_value[0], slider_value[1], 100)
y = _plot_function(x_range, dropdown_value)
figure = px.line(x=x_range, y=y, title=dropdown_value)
return figure
app4.run_server(debug=True, port=8073, mode="inline")
Dynamically add more widgets¶
So far we have only considered static IDs and that is fine for many work cases. However sometimes it might be necessary to add widgets inside of callbacks. An example for this could be the creation of a new tab with its own button and text on the inside.
For these callbacks dash provides three patterns MATCH
ALL
and AllSMALLER
.
Here I will only go over MATCH
, for more information see https://dash.plotly.com/pattern-matching-callbacks
from dash.dependencies import MATCH
app5 = jupyter_dash.JupyterDash("app5", external_stylesheets=external_stylesheets)
app5.layout = html.Div(
[
html.Button("Add Tab", id="button_add_tab"),
dcc.Tabs(id="tabs", children=[]),
]
)
@app5.callback(
Output("tabs", "children"),
Input("button_add_tab", "n_clicks"),
State("tabs", "children"),
prevent_initial_call=True,
)
def add_tab(n_clicks, tabs_children):
new_tab = dcc.Tab(
label=f"Tab {n_clicks}",
children=[
html.Div(
[
html.Button(
f"Button {n_clicks}",
id={"type": "button_tab", "index": n_clicks},
),
html.Div(
f"Button {n_clicks} clicked 0 times. ",
id={"type": "div_tab", "index": n_clicks},
),
]
)
],
)
tabs_children.append(new_tab)
return tabs_children
@app5.callback(
Output({"type": "div_tab", "index": MATCH}, "children"),
Input({"type": "button_tab", "index": MATCH}, "n_clicks"),
State({"type": "button_tab", "index": MATCH}, "id"),
prevent_initial_call=True,
)
def tabs_button_click(n_clicks, button_id):
return f"Button {button_id['index']} clicked {n_clicks} times. "
app5.run_server(debug=True, port=8074, mode="inline")
Using dash inside a class¶
Unfortunately the decorator style of dashs callbacks we have used so far is very much incompatible with encapsulating the dash app inside a class.
Normally the app itself is to be used in the whole module.
@self.app.callback()
or smililar things don't work.
However we can simply refer any function as a callback as seen here:
A piece of warning though:¶
The dash website advices against using a callback to access out of scope data or variables. As far as I can tell this is only relevant when deploying the dash server in a way that multiple user access the same instance and it should not be a problem for local or cloud hosted python environments.
class App6:
def __init__(self):
external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"]
self.app6 = jupyter_dash.JupyterDash(
"app6", external_stylesheets=external_stylesheets
)
my_style = {
"width": "50%",
"margin-top": "20px",
"margin-bottom": "20px",
}
self.app6.layout = html.Div(
[
"Choosing and displaying a function:",
dcc.Dropdown(
options={"x^2": "quadratic", "2x": "linear", "e^x": "exponential"},
value="x^2",
style=my_style,
id="dropdown",
),
html.Div(
dcc.RangeSlider(
min=0,
max=20,
step=1,
value=[5, 15],
id="slider",
),
style={"width": "50%"},
),
html.Button(
"Click_me",
style=my_style,
id="button",
),
dcc.Graph(id="graph"),
]
)
self.app6.callback(
Output("graph", "figure"),
Input("button", "n_clicks"),
State("dropdown", "value"),
State("slider", "value"),
)(self.update_graph)
def update_graph(self, n_clicks, dropdown_value, slider_value):
def _plot_function(x, function_name):
if function_name == "x^2":
return x**2
elif function_name == "2x":
return 2 * x
elif function_name == "e^x":
return np.exp(x)
elif function_name == None:
return None
else:
raise ValueError(
f"Unknown function_name: {function_name}, type: {type(function_name)}"
)
x_range = np.linspace(slider_value[0], slider_value[1], 100)
y = _plot_function(x_range, dropdown_value)
if y is not None:
figure = px.line(x=x_range, y=y, title=dropdown_value)
else:
figure = px.line()
return figure
def run(self, port=8081):
self.app6.run_server(debug=True, port=port, mode="inline")
app_6 = App6()
app_6.run(port=8811)
Additional Dash Components¶
Extended dash functionality¶
Dash extensions DashBlueprint¶
Blueprints can be used to create and plan dash layouts and callbacks. Because these blueprints do not call the DashApp directly they can be created in differend scopes, files or libraries and later imported when needed. This can help keep the actual code much cleaner.
from dash_extensions.enrich import DashBlueprint, DashProxy # , html, Output, Input
bp = DashBlueprint()
bp.layout = html.Div(
[
"Choosing and displaying a function:",
dcc.Dropdown(
options=["x^2", "2x", "e^x"],
value="x^2",
style=my_style,
id="dropdown",
),
html.Div(
dcc.RangeSlider(
min=0,
max=20,
step=1,
value=[5, 15],
id="slider",
),
style={"width": "50%"},
),
html.Button(
"Click_me",
style=my_style,
id="button",
),
dcc.Graph(id="graph"),
]
)
@bp.callback(
Output("graph", "figure"),
Input("button", "n_clicks"),
State("dropdown", "value"),
State("slider", "value"),
)
def update_graph2(n_clicks, dropdown_value, slider_value):
def _plot_function(x, function_name):
if function_name == "x^2":
return x**2
elif function_name == "2x":
return 2 * x
elif function_name == "e^x":
return np.exp(x)
else:
raise ValueError(f"Unknown function_name: {function_name}")
x_range = np.linspace(slider_value[0], slider_value[1], 100)
y = _plot_function(x_range, dropdown_value)
figure = px.line(x=x_range, y=y, title=dropdown_value)
return figure
app7 = DashProxy(blueprint=bp)
# app7.run_server()
The problem here is that DashProxy and JupyterDash are not compatible.
If you run DashProxy.run_server()
in a notebook the cell will never finish.
Personal grievances¶
I have two problems with dash that I have not found a good solution for.
First is that dash code can get very convoluted and messy.
Extracting part of the layout into individual functions can help a lot, but it still mostly looks messy.Second is Dash's tendency to swallow error messages, especially inside a notebook.
This can be somewhat circumvented by running dash in a browser as that at least provides you with some of the messages. But mostly its just annoying.
Also printing and logging doesn't always work either
And to finish off small more comprehensive examples: