šŸ“£ Dash Labs 0.2.0

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

Thanks again for the feedback! Version 0.3.0 has some of the API updates suggested in this thread. Letā€™s move further discussion over to šŸ“£ Dash Labs 0.3.0: Template System API Updates.

1 Like