Welcome to Dash Crush, your walk-through tutorial to advanced interactive Data Viz dashboards using Dash Plotly.
In the previous chapter, we learned how to use dash_bootstrap_components' themes, styles, and custom CSS to make pretty layouts. In this part, we will grasp one basic but powerful concept of Data Visualization that will help you make your dashboards user-friendly.
TL;DR
In this chapter:
- Progressive disclosure
- Using icons
- Hiding content from users
- Introduction to pattern-matching callbacks
The Zen of Data Visualization
Dash is a fantastic tool for building dashboards. Nevertheless, even the best tool does not do everything for its users. Data Visualization theory provides good practices that, for no money, make your app more user-friendly. I use extensively, especially two, which I would like to share:
- Single-screen experience: fitting the content of an app on a single screen saves users from distracting scrolling.
- Progressive disclosure: less is best, as it does not overwhelm the user. If an option is not relevant in the current context - hide it.
We have already used single-screen experience in the previous parts by fitting our layout to the visible area, but no more. To illustrate progressive disclosure, we will modify the code from the last part (you can find it here: https://github.com/ptrhbt/dash-crush/tree/1-layout) to show and hide information depending on the user's action.
The code
We will use dash-bootstrap-components' Collapse to implement an app that modifies the visibility of elements according to the user's action. We will also familiarize ourselves with Foundation icons and make our first introduction to pattern-matching callbacks. Let's go!
Install packages
Create and activate your virtual environment, and install the following packages:
pip install dash pandas dash-bootstrap-components
Use Foundation icons
Our entry point is dashboard.py
. We pass the DBC theme and link to Foundation Icons' CSS as external stylesheets to the Dash constructor:
# dashboard.py
import dash
import dash_bootstrap_components as dbc
from src.layout import layout
EXTERNAL_STYLESHEETS = [
dbc.themes.MINTY,
"https://cdnjs.cloudflare.com/ajax/libs/foundicons/3.0.0/foundation-icons.min.css"
]
APP = dash.Dash(__name__, external_stylesheets=EXTERNAL_STYLESHEETS)
APP.layout = layout.create_layout
# Run the Dash app
if __name__ == "__main__":
APP.run_server(debug=True)
The layout
Our layout files will be located in the src/layout
directory. The navbar, plot pane, grid, and style are the same as in the previous chapter. The changes will be done mostly in the sidebar and work_pane and one small change in the top-level layout.
The sidebar
Our sidebar will consist of an expandable menu, built using dbc.Collapse, and a "Plot" button.
The complete code looks like:
# src/layout/sidebar.py
from dash import html
import dash_bootstrap_components as dbc
from src.layout.grid import spacer
def sidebar_menu():
"""Sidebar menu"""
return html.Div(
[
_menu_collapse(),
spacer(10),
_buttons(),
]
)
def _menu_collapse():
"""Collapsible menu"""
return html.Div(
[
html.Div(
dbc.Button(
[html.I(className="fi-play"), "Menu"],
id={"role": "sidebar_collapse_toggle", "index": "menu"},
color="primary",
outline=True,
className="btn-sidebar-1st selected",
style={"width": "100%"},
)
),
dbc.Collapse(
dbc.ButtonGroup(
[
dbc.Button(
"Material",
id={"role": "menu_button", "index": "material"},
color="primary",
outline=True,
className="btn-sidebar-2nd",
),
dbc.Button(
"Color",
id={"role": "menu_button", "index": "color"},
color="primary",
outline=True,
className="btn-sidebar-2nd",
),
],
vertical=True,
style={"width": "100%"},
),
id={"role": "sidebar_collapse", "index": "menu"},
is_open=True,
),
]
)
def _buttons():
return html.Div(
[
dbc.Button("Plot", id="plot_button", color="primary"),
],
className="d-grid gap-2",
)
Let's go through the key elements of it:
- the menu consists of the trigger button "Menu" and the collapse section that contains two more buttons, "Material" and "Color".
- additionally, the "Menu" button contains an icon. We use Foundation icons. A complete list of available icons can be found here: https://zurb.com/playground/foundation-icon-fonts-3. We need the
html.I
component to use the foundation icon in our code. Just pass the icon name prefixed withfi-
as className, e.g.:html.I(className="fi-play")
- inner buttons are styled as a single continuous menu using
dbc.ButtonGroup
.
Buttons have the compoundid
being dict like:id={"role": "menu_button", "index": "color"},
. They are needed to use pattern-match callbacks.role
defines what each component does. And in our case, this is common for all buttons. However,index
differentiates elements within groups but is common for each component handling one functionality. - the visibility of collapse will be modified using a callback.
- to style the appearance of the "Menu" button:
- span them to the full width of the sidebar
- differentiate the top-level button and nested buttons' looks
- mark which button has been selected
To do so, we will use custom CSS passed withclassName
. We will add CSS code later.
The work pane
Our work pane will consist of two collapses to separate different groups of parameters. One will be used for material selection, the other for visual parameters (color palette selection for the plot).
Here is the complete code:
# src/layout/work_pane.py
from dash import html, dcc
import dash_bootstrap_components as dbc
from src.layout.grid import spacer
from src.layout.style import COLORS
MATERIALS = ["GaAs", "InAs"]
PALETTES = ["Plotly", "Bold", "Vivid"]
def work_pane():
"""Working area"""
return html.Div(
[
# REMARK: collapse order should be the same as button order in sidebar
_material_collapse(),
_color_collapse(),
],
style={"backgroundColor": COLORS["light-gray"]},
className="scrollable-pane",
id="work_pane",
)
def _material_collapse():
"""Collapsible container for material"""
return dbc.Collapse(
html.Div(
[
spacer(10),
html.Label("Material"),
dcc.Dropdown(
id="material_dropdown",
options=[{"label": name, "value": name} for name in MATERIALS],
value="GaAs",
),
]
),
style={"backgroundColor": COLORS["light-gray"]},
id={"role": "workpane_collapse", "index": "material"},
is_open=True,
)
def _color_collapse():
"""Collapsible container for color"""
return dbc.Collapse(
html.Div(
[
spacer(10),
html.Label("Color palette"),
dcc.Dropdown(
id="color_dropdown",
options=[{"label": name, "value": name} for name in PALETTES],
value="Plotly",
),
]
),
style={"backgroundColor": COLORS["light-gray"]},
id={"role": "workpane_collapse", "index": "color"},
)
The important thing to notice is that dbc.Collapse
s also have the compound id
. Both collapses have the same role workpane_collapse
but index
should match the index
field of a specific button in the sidebar, which will trigger the collapse.
The top-level layout
One small aesthetic change in the layout is to remove padding around the content of the sidebar column. Add the following class:
className="padding-less",
to dbc.Col
containing the sidebar menu. We will define the padding-less
class in CSS later.
Custom CSS
We will differentiate the look of the top and nested sidebar buttons using custom CSS located in assets/sidebar-menu.css
/* assets/sidebar-menu.css */
.sidebar-menu {
min-height: 100vh; /* full-visible-height */
}
.btn-sidebar-1st {
border-bottom: 1px solid #dfe8f3;
border-top: 1px solid #dfe8f3;
border-left: none;
border-right: none;
}
.btn-sidebar-1st:focus {
box-shadow: none;
}
.btn-sidebar-1st.selected i {
transform: rotate(90deg);
}
.btn-sidebar-2nd {
background-color: #fafafa;
border-bottom: 1px solid #dfe8f3;
border-top: 1px solid #dfe8f3;
border-left: none;
border-right: none;
}
.btn-sidebar-2nd:focus {
box-shadow: none;
}
.btn-sidebar-2nd.selected {
border-left: 4px solid;
}
Interesting points:
.btn-sidebar-1st
and.btn-sidebar-2nd
describe the appearance when the button is in the "idle" state.- the suffix
:focus
in the style describes what happens to the button when it is focused with the mouse (no shadow) - the suffix
.selected
describes what happens when a button is clicked:.btn-sidebar-1st.selected i
will have the icon rotated by 90 degrees (i
means it is the style of an icon).btn-sidebar-2nd.selected
will have a left border that is bolder
We need to set the style of the selected button in callbacks to modify its style.
To style the layout of text and icons on the button, we use the following CSS (assets/button.css
):
/* assets/button.css */
button i {
margin-left: 0.5rem;
float: left;
}
.btn i {
margin-left: 0.5rem;
float: left;
}
.btn-icon i {
margin-left: 0rem;
float: none;
}
Will we define one more style in assets/padding-less.css
to remove the padding from the sidebar column:
/* assets/padding-less.css */
.padding-less {
padding: 0 0 0 0;
}
Callbacks
The layout's definition is done. However, we still need to put the interactivity inside our app. Therefore, we will add callbacks to bring our app to life through engaging interaction. And since we need more than one callback, we will structure them similarly to the layout in the src/callbacks
folder.
The progressive disclosure
Firstly, we will add a callback responsible for:
- collapsing the menu in the sidebar and changing the style of selected buttons
- collapsing work areas in the work pane
The sidebar
Firstly, we add a callback that handles clicking on the "Menu" button, toggles underlying options collapse, and changes the appearance of the "Menu" button. It will be located in src/callbacks/sidebar.py
:
# src/callbacks/sidebar.py
from dash import dcc, Input, Output, State, MATCH
def create_callbacks(app):
@app.callback(
Output({"role": "sidebar_collapse", "index": MATCH}, "is_open"),
Output({"role": "sidebar_collapse_toggle", "index": MATCH}, "className"),
Input({"role": "sidebar_collapse_toggle", "index": MATCH}, "n_clicks"),
State({"role": "sidebar_collapse", "index": MATCH}, "is_open"),
prevent_initial_call=True,
)
def _toggle_menu(_, is_open):
collapse_is_open = not is_open
class_name = "btn-sidebar-1st"
if collapse_is_open:
class_name = "btn-sidebar-1st selected"
return collapse_is_open, class_name
We use Plotly's mechanism called "pattern matching callbacks". In our case, it's handled by the keyword MATCH
. By using it we order Plotly to execute a callback for the elements that have matching index fields.
Let's analyze the logic: the trigger is clicking on the top-level button. We evaluate the state of the corresponding collapse. If it was open - we close it, if it was closed - we open it. Finally, we modify the style of the top-level button. The visual effect will be a rotation of the icon by 90 degrees.
(Note: in our case, we have only one top-level button, so a pattern-matching callback is an over-engineering, as we could use a regular callback)
The work pane
Now, we add another callback to src/callbacks/work_pane.py
that will:
- trigger the work pane's collapse
- modify the style of nested sidebar buttons.
# src/callbacks/work_pane.py
import json
import dash
from dash import Input, Output, State, ALL
from src.callbacks.utils import get_caller_id
def create_callbacks(app):
"""Create workpane callbacks"""
@app.callback(
Output({"role": "workpane_collapse", "index": ALL}, "is_open"),
Output({"role": "processing_button", "index": ALL}, "className"),
Input({"role": "menu_button", "index": ALL}, "n_clicks"),
State({"role": "menu_button", "index": ALL}, "id"),
prevent_initial_call=True,
)
def _toggle_work_pane(_, ids):
caller_id = get_caller_id(dash.callback_context)
caller = json.loads(caller_id)
is_open = [entry["index"] == caller["index"] for entry in ids]
class_names = [
"btn-sidebar-2nd selected" if entry["index"] == caller["index"] else "btn-sidebar-2nd"
for entry in ids
]
return is_open, class_names
Again, we use pattern-matching callbacks, but this time differently. We use the keyword ALL
, which means that when the Input property of one component changes, data from all components is passed to the callback function. We need to do it this way because we want the action of opening the work pane collapse to be exclusive, i.e., when one collapse opens, the others should be closed.
Furthermore, we achieve that purpose by using two mechanisms:
- passing
id
s of all components to the callback function as a state. - determining what the actual trigger of the callback was.
The first part is straightforward. Second, we need to inspect the so-called callback_context
of Dash. It preserves information about the source of the callback. As it is quite an often action, I have implemented the handy helper get_caller_id
, stored in src/callbacks/utils.py
:
# src/callbacks/utils.py
from dash.exceptions import PreventUpdate
def get_caller_id(context):
"""Get caller id from dash callback context"""
if not context.triggered:
raise PreventUpdate
caller_id = context.triggered[0]["prop_id"].split(".")[0]
return caller_id
As you can see, there is no magic there, but we need to traverse the context.
Finally, we iterate over all the id
s twice to determine if each collapse is open and assign className to trigger buttons in the sidebar.
Plotting
The last callback to implement will be plotting. We will modify callbacks from the previous chapter to evaluate the color palette. The code will be located in src/callbacks/plot_pane.py
.
# src/callbacks/plot_pane.py
from itertools import cycle
from dash import Input, Output, State
from dash.exceptions import PreventUpdate
import pandas as pd
import plotly.graph_objs as go
import plotly.express as px
MATERIAL_DATA = {"GaAs": pd.read_csv("GaAs.dat"), "InAs": pd.read_csv("InAs.dat")}
PALETTES = {
"Plotly": px.colors.qualitative.Plotly,
"Bold": px.colors.qualitative.Bold,
"Vivid": px.colors.qualitative.Vivid,
}
TRACES = ["E_so", "E_lh", "E_hh", "E_c"]
def create_figure(to_draw):
"""Style graph"""
layout = go.Layout(
autosize=True,
title="Band structure",
xaxis={"title": "k (wave vector) [1/nm]"},
yaxis={"title": "E (Energy) [eV]"},
)
return go.Figure(data=to_draw, layout=layout)
def create_callbacks(app):
"""Create callbacks"""
@app.callback(
Output("band_structure_graph", "figure"),
Input("plot_button", "n_clicks"),
State("material_dropdown", "value"),
State("color_dropdown", "value"),
prevent_initial_call=True,
)
def plot(n_clicks, material, color_palette): # pylint: disable=unused-argument
if not n_clicks or not material or not color_palette:
raise PreventUpdate
data = MATERIAL_DATA[material]
palette = cycle(PALETTES[color_palette])
to_draw = []
for trace in TRACES:
to_draw.append(
go.Scatter(
x=data["k"], y=data[trace], mode="lines", name=trace, marker_color=next(palette)
)
)
return create_figure(to_draw)
Let's go through the code. We pass color_palette
and material
as states to plotting callbacks and use them to select the data to be plotted and the palette to be used. We use three of the color palettes defined in Plotly Express. itertools' cycle
is used to convert a list into a cyclical iterator (to prevent exceeding the range). Finally, we assign marker_color
as the next color from the palette for each trace.
Using callbacks
The final step is creating callbacks in dashboard.py
to be able to use them. To do so, we import all the callbacks:
import src.callbacks.sidebar
import src.callbacks.work_pane
import src.callbacks.plot_pane
Wrap them into function:
def create_callbacks(app):
"""Create all callabacks"""
src.callbacks.sidebar.create_callbacks(app)
src.callbacks.work_pane.create_callbacks(app)
src.callbacks.plot_pane.create_callbacks(app)
And execute the function on the instance of APP:
create_callbacks(APP)
We are ready to go!
Starting the app:
Start the app with python dashboard.py
. You should see an indication of a running app in your console. The dashboard is available under http://localhost:8050.
Summary:
Quick recap:
- Hide from the user all the controls that are not relevant.
- Add
"https://cdnjs.cloudflare.com/ajax/libs/foundicons/3.0.0/foundation-icons.min.css"
to EXTERNAL_STYLESHEETS to use Foundation icons and refer to them viahtml.I
component with thefi-
prefix. - What you can't style with pure Dash, you can most probably style with CSS.
- Use compound IDs and access your components with pattern-matching special keywords
MATCH
andALL
.
The complete code from this chapter can be found on GitHub:
https://github.com/ptrhbt/dash-crush/tree/2-progressive-disclosure
There is also the file requirements.txt
which lists all packages used. You may install it to your virtual env with pip install -r requirements.txt
.
References:
For expanding your knowledge of topics from this chapter, you may want to visit:
- the Dash documentation for pattern-matching callbacks: https://dash.plotly.com/pattern-matching-callbacks
- Plotly colors: https://plotly.com/python/discrete-color/
We will explore more of Dash magic in the upcoming parts of Dash Crush.