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)
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 usetpl.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).