Learn how to use Dash Bio for next-gen sequencing & quality control. 🧬 Register for the Oct 27 webinar.

📣 Dash Labs 0.3.0: Template System API Updates

Hi All,

We just published a new version of Dash Lab (0.3.0) to PyPI that contains a few API updates to the template system. Special thanks to @adamschroeder, @AnnMarieW, and @Emil for their feedback over in 📣 Dash Labs 0.2.0. Here is a summary of the discussion and changes.

The key observation was that using the words “input” and “output” to denote where components should be placed in a template was confusing, because of the overlap with the existing use of Input, State, and Output in callback dependency objects.

To clarify things we decided to make the following changes:

  • The _input and _output suffixes on the template component constructor methods have been removed, and a new_ prefix has been added. So tpl.button_input(...) is now tpl.new_button(...).
  • The term “role” has been replaced by “location”. And templates now define a set of descriptive location values where components can be positioned. So, when using the DbcSidebar template, the use of tpl.new_slider(..., role="input") has been replaced with tpl.new_slider(..., location="sidebar"). For the DbcRow template, this would now be tpl.new_slider(..., location="left"). Templates document all of their supported locations in their constructor docstring.

For more information, see the updated versions of chapters 3 and 4 in the documentation at dash-labs/docs at main · plotly/dash-labs · GitHub. Or, see the PR at Rename "role" to "location" throughout Dash Labs and rename component builder methods by jonmmease · Pull Request #25 · plotly/dash-labs · GitHub.

Thanks!
-Jon

4 Likes

These updates look good!

One further enhancement I would suggest is to change

@app.callback

decorator to

@tpl.callback

so that the templates collect the callback information and to add the template to the app at the end. Then you don’t need to create the app until the last step and components are easier to compose as submodules don’t depend on an “app” object being available.

Hi Jon, thanks a lot for all this work, super excited to see those changes happening.

I just tried out the component plugin system which looks fantastic to “componentise” blocks of code.

For practice, I’m creating a simple SynchronisedSliderPlugin that has a Slider and an Input that stay in sync. Now, I can get the thing to work using .install_callbacks(app) which gives this:

synchronised_slider_1

So far so good.

Now I’m trying to modify the layout of this component so in the plugin I write something like this:

template.add_component(
    dbc.Row([
        dbc.Col(_slider, sm=8, md=9),
        dbc.Col(_input, sm=4, md=3),
    ]),
    "top"
)

Without using install_callbacks on my component I can see that the layout was updated according to the layout:

image

However as soon as I call install_callbacks to actually start synchronising things I get a dash.exceptions.DuplicateIdError, looks like using components in a callback doesn’t check whether it already exists in the template and just adds it a second time?

And a few more questions:

  • Do component plugins have to be used with templates?
  • Could we consider something that would transpyle the function to js to do some simple callbacks like this one on the clientside?
  • Will flexible callbacks be available in clientside_callbacks for Dash 2.0?

Thanks a lot!

Hi @RenaudLN, thanks for giving the component plugin pattern a spin! I just added an example to the documentation of using a component plugin without a template: dash-labs/05-ComponentPlugingPattern.md at main · plotly/dash-labs · GitHub

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)

Does that help? If not, feel free to share you’re full code sample.

These are good questions about clientside callbacks. I think it could make sense for a component plugin to optionally define callback logic as a clientside callback. I’ll need to think about it a little more. So far, I haven’t dug into what it will take to support the enhanced callback features in clientside callback definitions. JavaScript doesn’t have named keyword arguments the like Python, so that part probably wouldn’t translate. But the tuple/“dict” grouping would.

Thanks!

Yeah thanks it helps heaps!

synchronised_slider_2

I ended up creating a new layout_component property to group the elements in a given layout, instantiated the plugin without a template, then added the object to the template with tpl.add_component and it works like a charm.

4 Likes

Hey @hhoeflin - I thought you might be interested in this post. The proposal for the global dash proxy object looks it would solve this: 📣 Modular & Reusable Python Components (With Attached Callbacks)

Thanks for working on introducing simpler ways of building nicely looking apps @jmmease ! I think this would be a really great addition to dash and make it more appealing for novices to quickly get up and running.

One things I found rather confusing is adding a multi selection dropdown to an app that uses a bootstrap template, such as DbcSideBar. I initially tried tpl.new_dropdown(..., multi=True), but it threw an error saying that tpl.new_dropdown has no multi parameter. I tried opts=(dict(multi=True)), but this throws and error saying that dbc.Select does not have a multi parameter. This is rather confusing as I would expect tpl.new_dropdown to use dcc.Dropdown, and this is also what is mentioned in the documentation.

Fortunately, I came across this previous forum post that explains that the template functions return different components depending on which template is used. I eventually wrote the dropdown from scratch, not understanding how I could use a bootstrap based template with a multi select dropdown in any other way. This initially gave me problems with the id parameter until I realized I needed to use {'uid': '...'} for the id rather than just a string as when not using templates.

My understanding of the templates was that they were primarily introduced to make it quicker to build and switch the appearance of apps. To me, it is unexpected that changing from one template to another would change the underlying components used and require me to rewrite code to keep the same functionality. I think it is important that a simple app that starts out using a template can be easily extended by adding more advanced functionality rather than having to rewrite much of the existing code. Would it be possible to change templates (or introduce an option) to use the dcc components for the widgets such as dropdowns, sliders, etc even with bootstrap based apps? Or am I doing something wrong here and this is in fact already possible?

Hi @joelostblom

This same question was asked on the Dash Labs Github recently, and I think it’s worth repeating here. I agree that it’s confusing when components change depending on the template. I think it would be better to have the same component used throughout.

However, it is possible to use other components with the templates, they just need to be added without using the helper function (ie such as tpl.new_dropdown) Here is a an example of how to add a dcc.Dropdown to a dbc template:


import dash
import dash_labs as dl
import dash_bootstrap_components as dbc
import dash_core_components as dcc
import plotly.express as px
import plotly.graph_objects as go

# Make app and template
app = dash.Dash(__name__, plugins=[dl.plugins.FlexibleCallbacks()])
tpl = dl.templates.DbcCard(app, "Gapminder", figure_template=True)

# Load and preprocess dataset
df = px.data.gapminder()
years = sorted(df.year.drop_duplicates())
continents = list(df.continent.drop_duplicates())

multi_dropdown = dcc.Dropdown(
    options=[{"label": c, "value": c} for c in continents], value=continents, multi=True
)


@app.callback(
    args=dict(
        year=tpl.new_slider(years[0], years[-1], step=5, value=years[-1], label="Year"),
        continent=dl.Input(multi_dropdown, label="Continents"),
        logs=tpl.new_checklist(
            ["log(x)"],
            value="log(x)",
            label="Axis Scale",
        ),
    ),
    output=tpl.new_graph(),
    template=tpl,
)
def callback(year, continent, logs):
    # 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()

    title = f"Life Expectancy ({year})"
    return (
        px.scatter(
            year_df,
            x="gdpPercap",
            y="lifeExp",
            size="pop",
            color="continent",
            hover_name="country",
            log_x="log(x)" in logs,
            size_max=60,
            title=title,
        )
        .update_layout(margin=dict(l=0, r=0, b=0))
        .update_traces(marker_opacity=0.8)
    )


app.layout = dbc.Container(fluid=True, children=tpl.children)

if __name__ == "__main__":
    app.run_server(debug=True)

Thanks for the reply @AnnMarieW ! Your solution is what I was referring to when I said that I created the dropdown “manually” (but I realize that was not very clear). Good to know that this is the recommended workaround for the time being (and nice to see an id is not required, I must have been mixing it up with another error initially.

Fingers crossed that components are made consistent between templates before the release of Dash 2.0 :crossed_fingers: