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

📣 Dash Labs 0.2.0

Hi All!

Thanks for all of the great feedback on Dash Labs. Today I published a small update API update that, I hope, will make it easier to understand the template system. You can install the update with:

$ pip install dash-labs==0.2.0

Here’s the change. The tpl.layout(app) method has been replaced by a tpl.children property, and the app is now required as the first argument to all template constructors. Our hope is that this will make it more clear that the output of a template is simply a regular collection of Dash components that can be integrated into a larger app if desired. For example, the template result can be wrapped in a div.

html.Div(children=tpl.children)

One additional consequence of this change is that, for DBC-based templates, apps will need to provide the top-level dbc.Container component when building app.layout.

These changes are all demonstrated in a new documentation chapter that expands a bit on how templates can be used, and how to migrate an app away from using a template. You can find the documentation at dash-labs/06-TemplateIntegrationAndMigration.md at main · plotly/dash-labs · GitHub, but I will also include the contents below.

Thanks again!


Overview

This chapter demonstrates some more flexible uses of Dash Labs templates, and how to migrate an app away from using templates.

Getting started

We’re going to start with a very simple app that uses plotly express to display a plot of the Gapminder dataset, and provides a Dash slider to specify the year. This example uses the DbcCard template, which places all the outputs at the top of DBC Card, and the inputs at the bottom.

demos/06-integration-and-migration/getting_started.py

import dash
import dash_labs as dl
import dash_bootstrap_components as dbc
import plotly.express as px

# 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())

@app.callback(
    args=tpl.slider_input(
        years[0], years[-1], step=5, value=years[-1], label="Year"
    ),
    output=tpl.graph_output(),
    template=tpl,
)
def callback(year):
    # Let parameterize infer output component
    year_df = df[df.year == year]
    title = f"Life Expectancy ({year})"
    return px.scatter(
        year_df, x="gdpPercap", y="lifeExp", size="pop", color="continent",
        hover_name="country", size_max=60, title=title
    ).update_layout(
        margin=dict(l=0, r=0, b=0), height=400
    ).update_traces(marker_opacity=0.8)

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

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

Adding more controls

Now let’s add components to control which continents are shown, and whether the x-axis uses a log scale.

demos/06-integration-and-migration/adding_controls.py

import dash
import dash_labs as dl
import dash_bootstrap_components as dbc
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())


@app.callback(
    args=dict(
        year=tpl.slider_input(
            years[0], years[-1], step=5, value=years[-1], label="Year"
        ),
        continent=tpl.checklist_input(continents, value=continents, label="Continents"),
        logs=tpl.checklist_input(
            ["log(x)"], value="log(x)", label="Axis Scale",
        ),
    ),
    output=tpl.graph_output(),
    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)

Switching template

Adding additional component controls to the DbcCard template works, but it soon results in an awkward card that is very tall. Let’s switch to the DbcRow template. This template creats a DBC Row consisting of a Card to hold the inputs and a Card to hold the outputs. The only change required to the code above is in the definition of the template.

demos/06-integration-and-migration/switch_templates.py

...
tpl = dl.templates.DbcRow(app, title="Gapminder", input_cols=4, figure_template=True)
...

This shows how easy it can be to try out different templates if the template you start with doesn’t end up being a good fit.

Using multiple templates

As in the examples above, for simple data exploration apps a template can construct and entire app. But, another workflow is to create a larger app that contains one or more templates as subcomponents.

This example combines the DbcRow template from the previous example and adds a DbcCard template that is set up to explore the tips dataset. The layouts produced by these two templates are then placed in separate tabs of the final app.

demos/06-integration-and-migration/two_templates.py

import dash
import dash_labs as dl
import dash_bootstrap_components as dbc
import dash_html_components as html
import plotly.express as px
import plotly.graph_objects as go

app = dash.Dash(__name__, plugins=[dl.plugins.FlexibleCallbacks()])

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

# Make template for Gapminder row
gapminder_tpl = dl.templates.DbcRow(app, figure_template=True)

@app.callback(
    args=dict(
        year=gapminder_tpl.slider_input(
            years[0], years[-1], step=5, value=years[-1], label="Year"
        ),
        continent=gapminder_tpl.checklist_input(
            continents, value=continents, label="Continents"
        ),
        logs=gapminder_tpl.checklist_input(
            ["log(x)"],
            value="log(x)",
            label="Axis Scale",
        ),
    ),
    output=gapminder_tpl.graph_output(),
    template=gapminder_tpl,
)
def gapminder_callback(year, continent, logs):
    # Let parameterize infer output component
    year_df = gapminder_df[gapminder_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)
    )

# Load and preprocess tips dataset
tips_df = px.data.tips()

# Make template for tips row
tips_tpl = dl.templates.DbcCard(app, figure_template=True)

@app.callback(
    args=tips_tpl.checklist_input(["No", "Yes"], value=["No", "Yes"], label="Smoker"),
    output=tips_tpl.graph_output(),
    template=tips_tpl,
)
def tips_callback(smoker):
    plot_tips_df = tips_df[tips_df.smoker.isin(smoker)]
    if len(plot_tips_df) == 0:
        return go.Figure()

    return px.histogram(
        plot_tips_df,
        x="total_bill",
        y="tip",
        color="sex",
        marginal="rug",
        hover_data=tips_df.columns,
    )

# Create final tabbed layout
app.layout = dbc.Container(
    fluid=True,
    style={"padding": 20},
    children=[
        html.Div(
            children=[
                html.H2("Data Explorer"),
                html.Hr(),
                dbc.Tabs(
                    [
                        dbc.Tab(
                            dbc.Card(gapminder_tpl.children, body=True),
                            label="Gapminder",
                        ),
                        dbc.Tab(dbc.Card(tips_tpl.children, body=True), label="Tips"),
                    ]
                ),
            ]
        )
    ],
)

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

Migrate away from templates

Finally, here is an example of the steps needed to remove the use of templates in an app.

Before:
demos/06-integration-and-migration/switch_templates.py

import dash
import dash_labs as dl
import dash_bootstrap_components as dbc
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.DbcRow(app, title="Gapminder", input_cols=4, figure_template=True)

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


@app.callback(
    args=dict(
        year=tpl.slider_input(
            years[0], years[-1], step=5, value=years[-1], label="Year"
        ),
        continent=tpl.checklist_input(continents, value=continents, label="Continents"),
        logs=tpl.checklist_input(
            ["log(x)"], value="log(x)", label="Axis Scale",
        ),
    ),
    output=tpl.graph_output(),
    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)

After:
demos/06-integration-and-migration/remove_template.py

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

# [1] Make app (now include DBC stylesheet)
app = dash.Dash(
    __name__, plugins=[dl.plugins.FlexibleCallbacks()], external_stylesheets=[dbc.themes.FLATLY]
)

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

# [2] Make components
year_slider = dcc.Slider(
    min=years[0], max=years[-1], step=5, value=years[-1],
    tooltip={"placement": "bottom", "always_visible": True},
)

continent_checklist = dbc.Checklist(
    options=[{"value": opt, "label": opt} for opt in continents], value=continents
)

logs_checklist = dbc.Checklist(
    options=[{"value": "log(x)", "label": "log(x)"}], value="log(x)"
)

graph = dcc.Graph()

@app.callback(
    args=dict(  # [3]
        year=dl.Input(year_slider, "value"),
        continent=dl.Input(continent_checklist, "value"),
        logs=dl.Input(logs_checklist, "value"),
    ),
    output=dl.Output(graph, "figure"),
)
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)

# [4] Make custom layout
app.layout = dbc.Container(fluid=True, children=[
    html.H2("Gapminder"),
    html.Hr(),
    dbc.Row([
        dbc.Col(md=4, children=dbc.Card(body=True, children=[
            dbc.FormGroup([
                dbc.Label("Year", className="h5"),
                year_slider,
            ]),
            dbc.FormGroup([
                dbc.Label("Continent", className="h5"),
                continent_checklist,
            ]),
            dbc.FormGroup([
                dbc.Label("Axis Scale", className="h5"),
                logs_checklist
            ]),
        ])),
        dbc.Col(md=8, children=dbc.Card(body=True, children=graph)),
    ])
])

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

Explanation of changes (See bracketed numbers in the comments above):

  1. Without using a DBC-based template, the bootstrap stylesheet must be provided to the app manually.
  2. Instead of relying on the template component constructors to create components inline in the @app.callback definition, the individual components that serve as inputs to the callback are defined as local variables above the callback.
  3. The components defined in (2) are wrapped in Input/Output dependency objects in the callback definition. The callback body itself is unchanged.
  4. The components constructed in (2) are arranged in a custom DBC app layout using the regular DBC Row, Col, FormGroup, and Label components
4 Likes

hi @jmmease ,
I like the changes. It makes working with the template a lot clearer. And being able to combine the templates is also an advantage. I also like the fact that we can focus first on writing the components and interactivity between the components, and then focus on the layout.

That said, with this new template layout system, is it still possible to have multiple callbacks that depend on each other in the same template. For example, in the very first app that you showcase here with the gapminder data and the year slider… Let’s say I wanted to write another callback that takes the clickData of the scatter plot created in the tpl.graph_output() and use that data to create a line chart of the specific country clicked on. Is it possible to do that within the template layout system?

Hi @adamschroeder

You can use any of the components from the template in another callback. If the id was generated automatically, there is an example of how to access it here:

Hi @AnnMarieW Hi @jmmease,
I tried accessing the clickData of the first scatter plot output that Jon shows on this page, but I don’t think it’s available. I added: print(tpl.roles['output'][0]), at the end of this code provided by Jon:

@app.callback(
    args=tpl.slider_input(
        years[0], years[-1], step=5, value=years[-1], label="Year"
    ),
    output=tpl.graph_output(),
    template=tpl,
)
def callback(year):
    # Let parameterize infer output component
    year_df = df[df.year == year]
    title = f"Life Expectancy ({year})"
    return px.scatter(
        year_df, x="gdpPercap", y="lifeExp", size="pop", color="continent",
        hover_name="country", size_max=60, title=title
    ).update_layout(
        margin=dict(l=0, r=0, b=0), height=400
    ).update_traces(marker_opacity=0.8)

Which prints out:

ArgumentComponents(arg_component=Graph(id={‘uid’: ‘82e2e662-f728-b4fa-4248-5e3a0a5d2f34’}), arg_property=‘figure’, label_component=None, label_property=None, container_component=FormGroup(children=[Graph(id={‘uid’: ‘82e2e662-f728-b4fa-4248-5e3a0a5d2f34’})], id={‘uid’: ‘e6f4590b-9a16-4106-cf6a-659eb4862b21’, ‘name’: ‘container’}), container_property=‘children’)

There is a figure property but no clickData. Is there a way to access to clickData so I can created another chart from the first scatter plot created? Or would I have to leave the template layout system to access that?

Hi @adamschroeder

Update: The following examples work, but it’s not the recommended way. Please see @jmmease post#8 below for a better answer.



To answer your question:

You can get the id of the dcc.Graph() in the template like this:

graph_id = tpl.roles["output"][0].arg_component.id

Then you can use the id just like you would in any Dash callback.

In the first example below, the input is a regular Dash Input, and the Output is another template which is added to the app.layout. The second example shows adding the line chart to the original template,



Version 1: Using 2 templates:

import dash
from dash.dependencies import Input
import dash_labs as dl
import dash_bootstrap_components as dbc
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.DbcRow(app, title="Gapminder", input_cols=4, figure_template=True)

tpl_line = dl.templates.dbc.DbcCard(
    app, title="ClickData line chart", figure_template=True
)


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


@app.callback(
    args=dict(
        year=tpl.slider_input(
            years[0], years[-1], step=5, value=years[-1], label="Year"
        ),
        continent=tpl.checklist_input(continents, value=continents, label="Continents"),
        logs=tpl.checklist_input(["log(x)"], value="log(x)", label="Axis Scale",),
    ),
    output=tpl.graph_output(),
    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)
    )


graph_id = tpl.roles["output"][0].arg_component.id


@app.callback(
    Input(graph_id, "clickData"), output=tpl_line.graph_output(), template=tpl_line,
)
def display_click_data(clickData):
    country = ""
    fig = {}
    if clickData:
        country = clickData["points"][0]["hovertext"]
        fig = px.line(
            df[df.country == country],
            x="year",
            y="lifeExp",
            title=f"Country selected: {country}",
        )
    return fig


app.layout = dbc.Container(
    [
        tpl.children,
        dbc.Row(
            dbc.Col(tpl_line.children, width={"size": 8, "offset": 4},)
        ),
    ],
    fluid=True,
)


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

Version 2: Adding the line chart to the original template

(I like this version better)

import dash
from dash.dependencies import Input, Output
import dash_core_components as dcc
import dash_labs as dl
import dash_bootstrap_components as dbc
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.DbcRow(app, title="Gapminder", input_cols=4, figure_template=True)

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


@app.callback(
    args=dict(
        year=tpl.slider_input(
            years[0], years[-1], step=5, value=years[-1], label="Year"
        ),
        continent=tpl.checklist_input(continents, value=continents, label="Continents"),
        logs=tpl.checklist_input(["log(x)"], value="log(x)", label="Axis Scale",),
    ),
    output=tpl.graph_output(),
    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)
    )


graph_id = tpl.roles["output"][0].arg_component.id
line_chart = dcc.Graph(id="line_chart")
tpl.add_component(line_chart, role="output", after=0)


@app.callback(Output("line_chart", "figure"), Input(graph_id, "clickData"))
def display_click_data(clickData):
    country = ""
    fig = px.line(title=f"Country selected: {country}")
    if clickData:
        country = clickData["points"][0]["hovertext"]
        fig = px.line(
            df[df.country == country],
            x="year",
            y="lifeExp",
            title=f"Country selected: {country}",
        )
    return fig


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


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

Thank you @AnnMarieW and thank you @jmmease for building this. Ann, I just did what you did this morning as well :slight_smile:. I took your example and did tpl.add_component() and it turned out really nice. Thanks for your help.

Things are becoming clearer, but to be honest, for me, the template layout system is still more complicated than what we had before (Dash 1.0). With the old template system, I had to write more lines and provide more IDs but it was clear from the very beginning what my layout will look like and how to access each component through multiple callbacks.

With the new layout system, there are more ifs, buts, roles, rules, and accessing the IDs of certain outputs is not intuitive and hard to remember: tpl.roles["output"][0].arg_component.id
For example:

  1. with the new system, I can include template-tpl in a callback but not the same one in other callbacks. If I create another callback, I either have to create a new template or use tpl.add_component().

  2. Accessing the IDs of components created in a template layout is not easy, especially if the layout was tpl.div_output(). If I created 2 dcc.graphs() and 3 html.P()s in this div_output(), how would I access the IDs of those graphs or HTML, in case I wanted to use their data to generate other graphs?

  3. The callback Input and Output tend to align with the template input and output roles. But sometimes they won’t necessarily. If, for example, I want the first callback input of a dropdown to influence the callback output of a second dropdown’s options, both of these dropdowns would most likely be considered role=“input” inside the template, which is not seemingly contradicting. I initially thought that the callback output and input should align with the template roles.

Possible ways to move forward:
I really like the template layout system, where I don’t have to define rows and columns and most of it is pre-built. But including template=tpl only once and not being able to add to other callbacks the same tpl, and then adding components to tpl was all confusing. It would be a lot clearer to me if I could use template=tpl once at the very beginning or the end, and not think about formatting the template while I’m building the callback. Maybe a solution could be:
tpl.add_components(inputdata=[year, continent, logs], outputdata=[first_graph, second_graph])

I also really like the component constructors, as they make everything so much faster and easier to write, but the ID challenge should be solved. If accessing component IDs, that were generated in a callback with tpl, remains complicated (tpl.roles["output"][0].arg_component.id), I think we should not use automatically-created IDs and go back to defining our own IDs.

What do you think, @AnnMarieW. Would you make any changes or you prefer this new layout system over the original one?

1 Like

Hi @adamschroeder

I get that it can be confusing - the flexibility and concision it can make it harder to learn at first. I think this will improve with more tutorials, documentation and examples. Plus it’s still under development so maybe there’s a better way.

If you know that you want to use a component in more than one callback, you can assign your own id
either:

tpl.dropdown_input(["NYC", "MTL", "SF"], label="My Dropdown", id="my-dropdown")

or you can define a dropdown in the normal way:

dropdown =dcc.Dropdown(
        id='my-dropdown',
        options=[
            {'label': 'New York City', 'value': 'NYC'},
            {'label': 'Montreal', 'value': 'MTL'},
            {'label': 'San Francisco', 'value': 'SF'}
        ],
        value='NYC'
    ),

and use in a regular callback, or add it to a callback like this:

@app.callback(
    dl.Input(dropdown, label="Select a city:"),

I think the templates are a quick way to make some nice looking apps. When you want to customize things, if it’s a unique app, it may be faster to make it using Dash 1. If it’s a format you expect to use over and over again, making a custom template will save a lot of time and give apps a uniform style and “brand”.

2 Likes

Thanks for the feedback all. I’m still working on digesting it, but I wanted to provide an example to answer @adamschroeder’s first question

Let’s say I wanted to write another callback that takes the clickData of the scatter plot created in the tpl.graph_output() and use that data to create a line chart of the specific country clicked on. Is it possible to do that within the template layout system?

Here’s how I would go about that.

import dash
import dash_labs as dl
import dash_bootstrap_components as dbc
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())

@app.callback(
    args=tpl.slider_input(
        years[0], years[-1], step=5, value=years[-1], label="Year", id="slider",
    ),
    output=tpl.graph_output(id="gap-minder-graph"),
    template=tpl,
)
def callback(year):
    # Let parameterize infer output component
    year_df = df[df.year == year]
    title = f"Life Expectancy ({year})"
    return px.scatter(
        year_df, x="gdpPercap", y="lifeExp", size="pop", color="continent",
        hover_name="country", size_max=60, title=title, custom_data=["country"]
    ).update_layout(
        margin=dict(l=0, r=0, b=0), height=400
    ).update_traces(marker_opacity=0.8)


@app.callback(
    args=[
        dl.Input("gap-minder-graph", "clickData"),
        dl.Input("slider", "value")
    ],
    output=tpl.graph_output(),
    template=tpl,
)
def callback(click_data, year):
    if click_data:
        country = click_data['points'][0]['customdata'][0]
        country_df = df[df["country"] == country]
        return px.line(
            country_df, x="year", y="lifeExp", title=country
        ).add_vline(
            year, line_color="lightgray"
        ).update_layout(
            height=300
        ).update_yaxes(
            range=[30, 100]
        )
    else:
        return go.Figure(layout_height=300).update_yaxes(range=[30, 100])


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


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

gapminder_two_callbacks

The key idea is to use the template constructor in one of the callbacks, with a fixed id string, then use a dependency object with that id string in the other callback.

I think I’m going to generally not recommend going the template.roles direction. Really the only reason to do this is if you want to access the full container that the templates creates (the component that contains the label).

Hope that makes sense!
-Jon


Comment from @adamschroeder

with the new system, I can include template-tpl in a callback but not the same one in other callbacks. If I create another callback, I either have to create a new template or use tpl.add_component() .

The example above is also an example of passing the same template to multiple callbacks.


Comment from @adamschroeder

The callback Input and Output tend to align with the template input and output roles. But sometimes they won’t necessarily. If, for example, I want the first callback input of a dropdown to influence the callback output of a second dropdown’s options, both of these dropdowns would most likely be considered role=“input” inside the template, which is not seemingly contradicting. I initially thought that the callback output and input should align with the template roles.

If you don’t use a template constructor, and pass your own component to a dependency object, then the default role will be selected based on whether the component is enclosed in an Input or Output dependency.

e.g.

dl.Input(slider_component, "value")  # is the same as:
dl.Input(slider_component, "value", role="input")

dl.Output(graph_component, "figure")  # is the same as:
dl.Output(graph_component, "figure", role="output")

# Override default role to "output"
dl.Input(slider_component, "value", role="output")

Now, when you use a component constructor, the role defaults to the _{role} suffix on the constructor method.

e.g.

tpl.slider_input(...)  # is the same as
tpl.slider_input(..., role="input")

tpl.graph_output(...)  # is the same as
tpl.graph_output(..., role="output")

The main reason for including a default role in these methods is that they also include a default property, and it’s usually (though not always) pretty clear whether a property typically serves as an input or output. So the thinking is that having the role in the method name helps guide a user towards using the template constructors in the right place.

That said, these method names and their default parameters is a design decision that we can revisit if you have thoughts on alternatives.


Comment from @adamschroeder

If accessing component IDs, that were generated in a callback with tpl, remains complicated ( tpl.roles["output"][0].arg_component.id ), I think we should not use automatically-created IDs and go back to defining our own IDs.

Yeah, as I mentioned above, this is not a syntax that we want people to need to use to look up ids. The paradigm I had in mind is that you wouldn’t ever look up an auto-generated id. If you care about id’s lining up between callbacks, then you should specify them yourself as an argument to the template constructor. The auto-generated feature is just there for all of the cases where you don’t care (but where Dash still needs them internally order to wire up the callback).

1 Like

Hi @jmmease and thanks for the great examples!

What is the best way to change the layout of the output - for example, if you wanted the two graphs side-by-side?

If using tpl.roles to get the id is not recommended, should the last example in chapter 3 of the docs be updated?

# Get the dropdown components that were created by parameterize
x_component = tpl.roles["input"]["x"].arg_component
y_component = tpl.roles["input"]["y"].arg_component


# Define standalone function that computes what values to enable, reuse for both
# dropdowns with app.callback
def filter_options(v):
    """Disable option ability to plot x vs x"""
    return [
        {"label": label, "value": col, "disabled": col == v}
        for col, label in zip(feature_cols, feature_labels)
    ]

app.callback(Output(x_component.id, "options"), [Input(y_component.id, "value")])(
    filter_options
)

app.callback(Output(y_component.id, "options"), [Input(x_component.id, "value")])(
    filter_options
)






Maybe it could be like this:

import dash_labs as dl
import plotly.express as px
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
import dash


# Load data
df = px.data.iris()
feature_cols = [col for col in df.columns if "species" not in col]
feature_labels = [col.replace("_", " ").title() + " (cm)" for col in feature_cols]
feature_options = [
    {"label": label, "value": col} for col, label in zip(feature_cols, feature_labels)
]

# Build app and template
app = dash.Dash(__name__, plugins=[dl.plugins.FlexibleCallbacks()])
tpl = dl.templates.DbcSidebar(app, title="Iris Dataset")

# Use parameterize to create components
@app.callback(
    args=dict(
        x=dl.Input(dcc.Dropdown(options=feature_options, value="sepal_length", id="x")),
        y=dl.Input(dcc.Dropdown(options=feature_options, value="sepal_width", id="y")),
    ),
    template=tpl,
)
def iris(x, y):
    return dcc.Graph(figure=px.scatter(df, x=x, y=y, color="species"))


# Define standalone function that computes what values to enable, reuse for both
# dropdowns with app.callback
def filter_options(v):
    """Disable option ability to plot x vs x"""
    return [
        {"label": label, "value": col, "disabled": col == v}
        for col, label in zip(feature_cols, feature_labels)
    ]


app.callback(Output("x", "options"), [Input("y", "value")])(filter_options)

app.callback(Output("y", "options"), [Input("x", "value")])(filter_options)

x_container = tpl.roles["input"]["x"].container_component
y_container = tpl.roles["input"]["y"].container_component
output_component = tpl.roles["output"][0].container_component

app.layout = html.Div(
    [
        html.H1("Iris Feature Explorer"),
        html.H2("Select Features"),
        x_container,
        y_container,
        html.Hr(),
        html.H2("Feature Scatter Plot"),
        output_component,
    ]
)

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

should the last example in chapter 3 of the docs be updated?

It should definitely be less prominent. This is still what you would do in order to retrieve the full container (with label), but since I don’t think this is a workflow to start people off on, we should probably push this into an Advanced section.

What is the best way to change the layout of the output - for example, if you wanted the two graphs side-by-side?

One option would be to use the DbcRow template, and give one of the graphs the input role.

import dash
import dash_labs as dl
import dash_bootstrap_components as dbc
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.DbcRow(app, "Gapminder", figure_template=True, input_cols=6)

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

@app.callback(
    args=[
        dl.Input("gap-minder-graph", "clickData"),
        dl.Input("slider", "value")
    ],
    output=tpl.graph_output(role="input"),
    template=tpl,
)
def callback(click_data, year):
    if click_data:
        country = click_data['points'][0]['customdata'][0]
        country_df = df[df["country"] == country]
        return px.line(
            country_df, x="year", y="lifeExp", title=country
        ).add_vline(
            year, line_color="lightgray"
        ).update_layout(
            height=300
        ).update_yaxes(
            range=[30, 100]
        )
    else:
        return go.Figure(
            layout_height=300, layout_title="Click a Country Bubble"
        ).update_yaxes(range=[30, 100])


@app.callback(
    args=tpl.slider_input(
        years[0], years[-1], step=5, value=years[-1], label="Year", id="slider", role="input"
    ),
    output=tpl.graph_output(id="gap-minder-graph", role="output"),
    template=tpl,
)
def callback(year):
    # Let parameterize infer output component
    year_df = df[df.year == year]
    title = f"Life Expectancy ({year})"
    return px.scatter(
        year_df, x="gdpPercap", y="lifeExp", size="pop", color="continent",
        hover_name="country", size_max=60, title=title, custom_data=["country"]
    ).update_layout(
        margin=dict(l=0, r=0, b=0), height=400
    ).update_traces(marker_opacity=0.8)


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


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

gapmind_row

The “input” role components are added to the left card and that “output” role components are added to the right.

Also, note that I swapped the order of the callbacks so that the slider would end up on the bottom of the input card.

This template is only set up to produce two cards. Another template could be written that would place each output in a separate card. Or, like the tabs template, multiple cards could all be given their own role name so that it was possible to target multiple components to each card.

Does that all make sense?

-Jon

Yes, that makes sense - Thanks!

I think it would be great to add a chapter in the docs on how to make your own templates.

I really like all the options that role and kind provide. That said, I believe that the conflating of callback dependencies (Output, Input, State) and template roles (“output”, “input”), will make writing a dashboard harder to grasp for beginners, especially as they try to move things around inside the template.

I can see this code throwing many beginners off and leaving them confused:

@app.callback(
    args=[
        dl.Input("gap-minder-graph", "clickData"),
        dl.Input("slider", "value")
    ],
    output=tpl.graph_output(role="input"),
    template=tpl,
)

A beginner might ask:
If role=“input”, then I assume it’s like the dl.Input, but now which part of the callback decorator does the returned object of the callback function get assigned to?
Why does a component constructor (graph_output), which suggests an output role, get assigned an input role? What does it mean that they contradict eachother?

Another example from the docs:
fun=tpl.dropdown_input(["sin", "cos", "exp"], label="Function", kind=dl.State)

Dropdown_Input and dl.State seem to contradict each other. It is easy to get confused unless one understands early on the difference between role and kind.

I believe this confusion could be avoided by making a clearer distinction between callback dependencies and template roles. What helped me understand template roles was thinking of them as layout location. Component constructors with role=“input” will be grouped together in the same area of the layout. And the same goes for component constructors with role=“output”. What if we treated roles as location/section/card. And then assigned to them a string that suggests they are grouped together.

Here’s an example with the made-up property “card” and removing the component constructors’ suffixes. I also added a made-up “card_cols” property that would define the number of columns per card.

# Make app and template
app = dash.Dash(__name__, plugins=[dl.plugins.FlexibleCallbacks()])
tpl = dl.templates.DbcRow(app, "Gapminder", figure_template=True, card_cols={"one":4, "two":8})

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

@app.callback(
    args=tpl.slider(
        years[0], years[-1], step=5, value=years[-1], label="Year", id="slider", card="one"
    ),
    output=tpl.graph(id="gap-minder-graph", card="two"),
    template=tpl,
)
def callback(year):
    return px.scatter(...)

@app.callback(
    args=[
        dl.Input("gap-minder-graph", "clickData", card="one"),
        dl.Input("slider", "value", card="one")
    ],
    output=tpl.graph(card="one"),
    template=tpl,
)
def callback(click_data, year):
    if click_data:
        return px.line(...)


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


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

Perhaps we can have the same framework here, similar to roles, where dl.Input, Input, State, or args will all default to card one (or the same location on the template), while outputs will default to card two.

Thanks @adamschroeder, you do make a really good piont that using "input" and "output" as roles and method suffixes could be pretty confusing.

I was originally wanting to use standard names that most templates would share, in order to make it easier to switch between templates. But this effort of changing the “role” (or whatever we call it) when switching templates is pretty small, and so maybe it’s better for each template to define their own “roles” that are more explicit than “input”/“output”

What helped me understand template roles was thinking of them as layout location

Yeah, this is exactly my mental model. They are named “drop zones”, and it’s the templates responsibility to take the components in each drop zone and make something that looks good out of them.

One idea would be to rename “role” to “location”. Then each template would define it its doc string (and validation error message) what the possible locations are.

  • DbcCard could have location="top" and location="bottom".
  • DbcRow could have location="left" and location="right".
  • DbcSidebar could have location="sidebar" and location="main".
  • DbcTabs would have dynamic location names that match the names of the tabs that the user provides to the constructor, plus location="sidebar".

And templates could still decide on their own default mapping between Input/Output and their locations (keeping the default behavior the same as it is now).


The other area of confusion that I’ve struggled with is how to name the component constructor methods to make clear that they are different from the constructors of the corresponding components. E.g. how to make it clear the dbc.Button and tpl.button are different. That was one of the reasons for the role suffixes. Do you have any thoughts here? Something along the lines of:

  • tpl.make_button()
  • tpl.build_button()
  • tpl.button_dependency() / tpl.button_dep()
  • tpl.template_button()
  • tpl.button_prop()
  • etc.

-Jon

@jmmease I think renaming “role” to “location” with the possibility to assign top, bottom, right, left, would work very well.
As for the naming of the component constructors, let me take the day to think about it. One thing that will help me is to make sure I’m clear on the difference between tpl.button and dbc.button or dcc.button. Am I correct to assume that the main difference is that tpl.button will have the “location” property which the dbc.button won’t have, right? If I recall correctly, they also have the “kind” property. Is there any other difference that I’m missing?

Is there any other difference that I’m missing?

The main difference is what they return. The dbc.Button is a regular class constructor that returns an instance of the dbc.Button class.

tpl.button creates a dbc.Button, but it wraps it in a dependency object. So by default, it will return something like

dl.Input(dbc.Button(*opts), "value")

The convenience is that this is ready to use directly in the callback without having to provide your own dependency object. The kind argument determines whether the dependency object is an Input, State, or Output.

The easiest thing might be to look at the code for a few of the constructors, they are mostly pretty short @classmethod functions. Or you could just print out the return values to get a feel for what they are building.

Thanks!
-Jon

Thanks for the clarification, @jmmease
I really like the option tpl.make_dropdown() or tpl.create_dropdown(), because “create” implies that it is being created on the spot and that it is done only once. So when the user wants to reuse the component in other callbacks, it is implied that they have to reference the component with something else (dl.Input or dl.Output) instead of trying to reference it with the same tpl.create_dropdown().

This is just dreaming big, but what would simplify things even more is if we were able to make the jumping back and forth between tpl. and dl. less likely. Would it be possible to build the callback framework in such a way that we use tpl also to reference the created component in the previous callback? For example, in the hypothetical code below, we use tpl to create the components in the first callback; then, we use tpl in the second callback to reference the components created:

@app.callback(
    args=tpl.create_slider(
        years[0], years[-1], step=5, value=years[-1], label="Year", id="slider", location="left"
    ),
    output=tpl.create_graph(id="gap-minder-graph", location="right"),
    template=tpl,
)
def callback(year):
    return px.scatter(...)

@app.callback(
    args=[
        tpl.reuse_component("gap-minder-graph", "clickData"),
        tpl.reuse_component("slider", "value")
    ],
    output=tpl.create_graph(id="line-graph", location="right"),
    template=tpl,
)
def callback(click_data, year):
    if click_data:
        return px.line(...)

Given that tpl.reuse_component() is inside the args keyword, can the callback decorator generate default mapping and assume that anything inside args would equate to dl.Input? And in case we would like that reused component to be State in this callback, we just tpl.reuse_component("slider", "value", kind="dl.State").

Sorry for going off on a tangent here; I got a little excited about all this. Using dl.Input or dl.Output is not complicated, but I was trying to reduce the number of terms beginners would need to learn so creating an app is as seamless as possible.

I think make_dropdown is the current favorite internally as well, so that’s likely what I’ll go with in the next dash-labs release.

The reuse_component approach is a neat idea! It could have id be the first required argument. and kind with the same default value as the corresponding make_ variant. I’m not sure whether we’d want there to be the same default props, or require them to be specified explicitly.

One hesitation I would have with this approach is that it might delay the need for folks to eventually understand the Input/State/Output model. My intuition is that it’s nice to not have to think about this for the simple single callback case, but that when you starting working with multiple inter-related callbacks, it’s probably good to go ahead and introduce the full Input/State/Output model so that it’s more clear that you can use component properties from outside the template as callback inputs as well.

Does that make sense?
-Jon

In terms of the keywords, did you consider

  • new_dropdown (instead of make_dropdown)
  • use or with (instead of reuse_component)
  • new_dropdown (instead of make_dropdown)
  • use or with (instead of reuse_component)

@Emil I really like new_dropdown since it will hinder Dash users from thinking that they can use it again in another callback to reference the component created. The idea of tpl.use() is so simple and straightforward; nice idea :slight_smile:

@jmmease I completely understand your hesitation. And I see how introducing Input/State/Output at an early stage can help. I think it depends how ubiquitous we think the template system will be. If we reach a stage where the only reason people will revert to dl.Input/Output system is when they want to have more control over their layout, I think they will mostly stick to the easy and quick tpl system (especially beginners). And if they are comfortable using tpl and see little reason to switch to the longer version of dl.Input/Output, then, it might be better to not forcefully introduce that to them.

Taking myself as an example, when I realized that I could use Plotly Express almost all the time, I liked that I wasn’t encouraged to learn/use Graph Objects just in case I might need it in the future. I pretty much said to myself: “Graph Object is complicated and vast. I’ll learn it only if/when I need to”.
It’s not that the dl.Input/Output model is very complicated, but if I don’t need it for most of my apps, I would appreciate not having to learn it.

In addition, with the callback decorator including Inputs, State, Output keywords,

@app.callback(
   inputs=tpl.new_slider(
       years[0], years[-1], step=5, value=years[-1], label="Year", id="slider", location="left"
   ),
   output=tpl.new_graph(id="gap-minder-graph", location="right"),
   template=tpl,
)
def callback(year):
   return px.scatter(...)

@app.callback(
   output=tpl.new_graph(id="line-graph", location="right"),
   inputs=[
       tpl.use("gap-minder-graph", "clickData"),
       tpl.use("slider", "value")
   ],
   state=[xxx]
   template=tpl,
)
def callback(click_data, year, xxx):
   if click_data:
       return px.line(...)
)

we are getting a soft introduction to the Input/ State/ Output model. The learning bridge that we would have to cross eventually is the one that connects between “no template layout system used” == “must use dl.Output/Input in my callback and create components outside the callback”.

I guess it’s a matter of when we want Dash beginners to cross that bridge of knowledge. My personality type would push that bridge crossing until absolutely necessary. But, I’m sure others will appreciate crossing it early on, especially if building components outside the callback is likely to happen early in the game.

Thank you for creating Dash 2.0 @jmmease and for taking our opinion into account.

(One small clarification for reference: today to kick the tires on Dash Labs, you must use dl.Input but once this is baked into Dash proper, this will just be the same Input we already use in @callback, in case that’s not already clear :slight_smile: )

1 Like