This version of the component plug-in design pattern will not be added to a future version of Dash. However, this was the precursor for the All-in-One Components that were added to Dash 2.0 🎉
See the All-in-One Components section in the Dash documentation
- ----------------------------------------------------------------------------------
- This documentation describes code in a previous version of dash-labs (v0.4.0)
- and is included here for legacy purposes only.
-
- You can install v0.4.0 with:
- pip install dash-labs==0.4.0
- ----------------------------------------------------------------------------------
Here is a proposed architecture that can be used to extract component creation and callback behavior into a separate class. The ComponentPlugin
class encapsulates the creation of inputs and output components, and the generation of output property value functionality.
Here are the most important methods in the ComponentPlugin
interface
class ComponentPlugin:
def __init__(self, config, ...):
pass
@property
def args(self):
raise NotImplementedError
@property
def output(self):
raise NotImplementedError
@property
def get_output_values(self, args_value):
raise NotImplementedError
To make use of a ComponentPlugin
subclass as a part of a callback, plugin users would use the following pattern:
...
plugin = FancyPlugin(**plugin_config)
@app.callback(
inputs={
input1=...,
input2=...,
plugin_values=plugin.args,
},
outputs=[output1, plugin.output],
template=tpl,
)
def hello_plugin(input1, input2, plugin_values):
# Do stuff with input1 and inputs2 to build result1 and, optionally,
# opts
return result, plugin.get_output_values(plugin_values, **opts)
...
The ability to pass components to @app.callback
allows plugins to define their own input and output components, as well as define dependencies to make it possible to both input and output properties of the same component. Following this pattern, the plugins do not need to define their own callbacks, making it much easier to compose plugins and connect them with custom functionality.
The tuple/dict grouping feature of @app.callback
allow plugins to store any number of input and output components and make them look like a single value to the user. e.g. plugin.args
and plugin_values
above can be dictionaries with any number of keys, but the user can treat them as a single scalar value, so that they can always follow the same usage pattern.
Here is an example of a fairly sophisticated plugin for displaying a DataTable
. This plugin supports table paging, sorting, and filtering, and can be configured to operate in either clientside or serverside configurations. While the clientside and serverside configuration logic is very different, involving different callback properties, the user can switch between these modes using a single constructor argument.
The clientside functionality is taken from https://dash.plotly.com/datatable/interactivity, and the serverside functionality is taken from https://dash.plotly.com/datatable/callbacks.
Here is an example of an app that uses this plugin to create a DataTable
that supports serverside paging, sorting, and filtering.
Note that the DataFrame that is input to the DataTable is first filtered using a Dropdown on the sex
column of the dataset. This is an example of how plugins can support integration with the external logic of a callback.
demos/component_plugin_demos/datatable_component_plugin.py
import plotly.express as px
import dash_labs as dl
import dash_bootstrap_components as dbc
import dash
df = px.data.tips()
app = dash.Dash(__name__, plugins=[dl.plugins.FlexibleCallbacks()])
tpl = dl.templates.DbcCard(app, title="Table Component Plugin")
# serverside = False
serverside = True
table_plugin = dl.component_plugins.DataTablePlugin(
df=df, page_size=10, sort_mode="single", filterable=True,
serverside=serverside, template=tpl
)
@app.callback(
args=[
tpl.new_dropdown(["Male", "Female"], label="Patron Gender", clearable=True),
table_plugin.args
],
output=table_plugin.output,
template=tpl,
)
def callback(gender, plugin_input):
if gender:
filtered_df = df.query(f"sex == {repr(gender)}")
else:
filtered_df = df
return table_plugin.get_output_values(plugin_input, filtered_df)
app.layout = dbc.Container(fluid=True, children=tpl.children)
if __name__ == "__main__":
app.run_server(debug=True)
And here is an example of using the same plugin to display the contents of the table (post filtering) in a plotly express figure:
demos/component_plugin_demos/datatable_component_plugin_and_graph.py
import plotly.express as px
import dash_labs as dl
import dash_bootstrap_components as dbc
import dash
df = px.data.tips()
app = dash.Dash(__name__, plugins=[dl.plugins.FlexibleCallbacks()])
tpl = dl.templates.DbcCard(app, title="Table Component Plugin", columns=8)
# serverside = False
serverside = True
table_plugin = dl.component_plugins.DataTablePlugin(
df=df,
page_size=10,
sort_mode="single",
filterable=True,
serverside=serverside,
template=tpl,
)
@app.callback(
args=[
tpl.new_dropdown(["Male", "Female"], label="Patron Gender", clearable=True),
table_plugin.args,
],
output=table_plugin.output,
template=tpl,
)
def callback(gender, plugin_input):
if gender:
filtered_df = df.query(f"sex == {repr(gender)}")
else:
filtered_df = df
return table_plugin.get_output_values(plugin_input, filtered_df)
app.layout = dbc.Container(fluid=True, children=tpl.children)
if __name__ == "__main__":
app.run_server(debug=True)
The DataTablePlugin.get_processed_dataframe
method applies all serverside filtering and sorting to the input DataFrame, allowing the callback to use this data in a plotly express figure. The example passes the preprocessed DataFrame, dff
, to get_output_values
. As a performance optimization, because we know that this DataFrame has already been preprocessed there is no need for get_output_values
to perform this preprocessing a second time. Setting preprocessed=True
tells get_output_values
to skip the preprocessing step and display the input DataFrame as-is.
The Component Plugin interface provides a convenience install_callback
method that will automatically install a callback to enable the plugin's default behavior. In the case of the DataTablePlugin
, this shortcut can be used if the contents of the input DataFrame
never need to change.
Here is an example of this approach
demos/component_plugin_demos/datatable_component_plugin2.py
import plotly.express as px
import dash_labs as dl
import dash_bootstrap_components as dbc
import dash
df = px.data.tips()
app = dash.Dash(__name__, plugins=[dl.plugins.FlexibleCallbacks()])
tpl = dl.templates.DbcCard(app, title="DataTablePlugin")
table_plugin = dl.component_plugins.DataTablePlugin(
df=df,
page_size=10,
sort_mode="single",
filterable=True,
serverside=False,
template=tpl,
)
table_plugin.install_callback(app)
app.layout = dbc.Container(fluid=True, children=tpl.children)
if __name__ == "__main__":
app.run_server(debug=True)
A component plugins configured with install_callback
can also be used without a template. In this case, the Dash components created by the component plugin should be accessed using the .args_components
and output_components
properties. Both of these properties return a list of Dash components, and in the example below they are concatenated together as the children
property of a top-level Div
component.
demos/component_plugin_demos/datatable_component_no_template.py
import plotly.express as px
import dash_labs as dl
import dash_html_components as html
import dash
df = px.data.tips()
app = dash.Dash(__name__, plugins=[dl.plugins.FlexibleCallbacks()])
table_plugin = dl.component_plugins.DataTablePlugin(
df=df,
page_size=10,
sort_mode="single",
filterable=True,
serverside=False,
)
table_plugin.install_callback(app)
app.layout = html.Div(children=
table_plugin.args_components + table_plugin.output_components
)
if __name__ == "__main__":
app.run_server(debug=True)
Here is a ComponentPlugin implementation of a shape drawing app similar to that described in https://dash.plotly.com/annotations. This Plugin displays a greyscale image in a plotly figure that is configured to draw rectangle shapes on drag. The current rectangle can also be edited by clicking it to activate shape editing mode.
The plugin provides helper methods to extract the current bounds (if any) of the active rectangle (get_rect_bounds
), and to extract the selected slice of the original image (get_image_slice
). The get_output_values
method supports a title
argument that can be used to add a custom title to the resulting figure.
Here is an example that simply sets the title to the coordinates of the current rectangle bounds
demos/component_plugin_demos/image_roi2.py
import dash
import dash_labs as dl
import dash_bootstrap_components as dbc
from skimage import data
img = data.camera()
app = dash.Dash(__name__, plugins=[dl.plugins.FlexibleCallbacks()])
tpl = dl.templates.DbcCard(app, title="Image Intensity Explorer", columns=4)
img_plugin = dl.component_plugins.GreyscaleImageROI(img, template=tpl, title="Bounds:")
@app.callback(args=[img_plugin.args], output=img_plugin.output, template=tpl)
def callback(inputs_value):
bounds = img_plugin.get_rect_bounds(inputs_value)
title = "Bounds: {}".format(bounds)
return img_plugin.get_output_values(inputs_value, title=title)
app.layout = dbc.Container(fluid=True, children=tpl.children)
if __name__ == "__main__":
app.run_server(debug=True)
And here is an example that uses get_image_slice
to extract the pixels within the rectangle and display their intensities in a histogram.
demos/component_plugin_demos/image_roi_histogram.py
import dash
import dash_labs as dl
import dash_bootstrap_components as dbc
from skimage import data
import plotly.express as px
img = data.camera()
app = dash.Dash(__name__, plugins=[dl.plugins.FlexibleCallbacks()])
tpl = dl.templates.DbcSidebar(
app,
title="Image Intensity Explorer",
sidebar_columns=6,
figure_template=True,
)
img_plugin = dl.component_plugins.GreyscaleImageROI(img, template=tpl, title="Bounds:")
@app.callback(
args=[img_plugin.args], output=[img_plugin.output, tpl.new_graph()], template=tpl
)
def callback(inputs_value):
bounds = img_plugin.get_rect_bounds(inputs_value)
img_slice = img_plugin.get_image_slice(inputs_value)
hist_figure = {}
if img_slice is not None:
raveled_imge_slice = img_slice.ravel()
if len(raveled_imge_slice) > 0:
hist_figure = (
px.histogram(raveled_imge_slice)
.update_layout(title_text="Intensity", showlegend=False)
.update_xaxes(range=[0, 255])
)
title = "Bounds: {}".format(bounds)
return [img_plugin.get_output_values(inputs_value, title=title), hist_figure]
app.layout = dbc.Container(fluid=True, children=tpl.children)
if __name__ == "__main__":
app.run_server(debug=True)
Here is a component plugin that can be used to display a dynamic label for a component using its current value and a format string.
demos/component_plugin_demos/dynamic_label_plugin.py
import dash
import dash_labs as dl
import dash_bootstrap_components as dbc
import numpy as np
import plotly.express as px
app = dash.Dash(__name__, plugins=[dl.plugins.FlexibleCallbacks()])
tpl = dl.templates.DbcSidebar(app, title="Dynamic Label Plugin", figure_template=True)
phase_plugin = dl.component_plugins.DynamicLabelPlugin(
tpl.new_slider(1, 10, value=4, label="Phase: {:.1f}", tooltip=False), template=tpl
)
@app.callback(
args=dict(
fun=tpl.new_dropdown(["sin", "cos", "exp"], label="Function"),
phase_inputs=phase_plugin.args,
),
output=[tpl.new_graph(), phase_plugin.output],
template=tpl,
)
def callback(fun, phase_inputs):
phase = phase_plugin.get_value(phase_inputs)
xs = np.linspace(-10, 10, 100)
fig = px.line(x=xs, y=getattr(np, fun)(xs + phase), title="Function Value")
return [fig, phase_plugin.get_output_values(phase_inputs)]
app.layout = dbc.Container(fluid=True, children=tpl.children)
if __name__ == "__main__":
app.run_server(debug=True)
Here is a more sophisticated example that combines
DbcSidebarTabs
template withDARKLY
bootstrap theme- Label in sidebar displays the name of the current active tab
DynamicLabelPlugin
is used to display the current slider valueDataTablePlugin
is used to display a serversideDataTable
in a tab
demos/component_plugin_demos/bootstrap_themes_tabs_table_dynamic_input.py
import dash
import dash_labs as dl
import plotly.express as px
import plotly.graph_objects as go
import dash_bootstrap_components as dbc
app = dash.Dash(__name__, plugins=[dl.plugins.FlexibleCallbacks()])
df = px.data.gapminder()
df = df[[c for c in df.columns if not c.startswith("iso_")]]
years = sorted(df.year.drop_duplicates())
continents = list(df.continent.drop_duplicates())
tabs = dict(scatter="Scatter", hist="Histogram", table="Table")
tpl = dl.templates.DbcSidebarTabs(
app,
tabs,
title=f"Dash Labs App",
theme=dbc.themes.DARKLY,
figure_template=True,
)
table_plugin = dl.component_plugins.DataTablePlugin(
df.iloc[:0],
sort_mode="single",
location="table",
page_size=15,
serverside=True,
filterable=True,
)
year_label_plugin = dl.component_plugins.DynamicLabelPlugin(
tpl.new_slider(
years[0],
years[-1],
step=5,
value=years[-1],
label="Year: {}",
tooltip=False,
)
)
@app.callback(
args=dict(
continent=tpl.new_checklist(continents, value=continents, label="Continents"),
year_args=year_label_plugin.args,
logs=tpl.new_checklist(
["log(x)"], value="log(x)", label="Axis Scale", location="scatter"
),
table_inputs=table_plugin.args,
tab=tpl.tab_input(),
),
output=[
tpl.new_graph(location="scatter"),
tpl.new_graph(location="hist"),
table_plugin.output,
year_label_plugin.output,
dl.Output(
dbc.Label(children="Current Tab: ", className="h5"),
"children",
location="sidebar",
),
],
template=tpl,
)
def callback(year_args, continent, logs, table_inputs, tab):
# Get year value from plugin
year = year_label_plugin.get_value(year_args)
logs = logs or []
# Let parameterize infer output component
year_df = df[df.year == year]
if continent:
year_df = year_df[year_df.continent.isin(continent)]
if not len(year_df):
return [go.Figure(), go.Figure()]
title = f"Life Expectancy ({year})"
scatter_fig = (
px.scatter(
year_df,
x="gdpPercap",
y="lifeExp",
size="pop",
color="continent",
hover_name="country",
log_x="log(x)" in logs,
size_max=60,
)
.update_layout(title_text=title, margin=dict(l=0, r=0, b=0))
.update_traces(marker_opacity=0.8)
)
hist_fig = px.histogram(
year_df, x="lifeExp", color="continent", barnorm=""
).update_layout(
title_text=title,
)
return (
scatter_fig,
hist_fig,
table_plugin.get_output_values(table_inputs, df=year_df),
year_label_plugin.get_output_values(year_args),
"Current Tab: " + tabs[tab],
)
app.layout = dbc.Container(fluid=True, children=tpl.children)
if __name__ == "__main__":
app.run_server(debug=True)