Description
It would be really powerful if we could have the layout of an app, not just individual component properties, respond to state changes. Some use cases that come to mind from previous (non-dash) apps I've made:
- Creating an arbitrary number of graphs, based on a multi-select dropdown
- Chained filters to a database query, where all previous filters determine the options available to the next filter
- You might want a new component (or set of components) for each item in some query results. Perhaps each items gets its own graph, or each gets a full report page.
There would be one new piece of user API: wildcards in property callbacks. But first some notes about callbacks affecting the layout (via children
)
Thoughts @plotly/dash ?
Layout callbacks
(edit: we can already do this by having a callback output the children
of some component - So these are just regular app.callback
, I've updated the pseudocode to reflect this but there are some notes I wanted to add)
app.layout = html.Div([
dcc.Dropdown(id='dd', multi=True, ...),
html.Div(id='graphs')
])
@app.callback(Output('graphs', 'children'), [Input('dd', 'value')])
def graphs(selected):
return html.Div([graph(val) for val in selected])
def graph(val):
return dcc.Graph(id='graph-{}'.format(val), figure={...})
That example uses val
to construct the id. Importantly, Graph
elements set the key
prop to match id
independent of their original location, which means an existing Graph
will be reused independent of its position among siblings, which both improves performance and would preserve any GUI interactions on the graph for the original item even if a new item is added before it.
Two related thoughts:
- For dynamic layout support, should we extend
key=id
to all core components? It doesn't look like most of them do this yet, though one or two do.id
is tied to the meaning of the component via callbacks so I can't see any downside to keeping this correspondence everywhere, anyone disagree? dash_html_components
allows users to separately specifyid
andkey
for all elements. That's probably fine, but the dynamic layout docs should highlight importancekey
. For example if you make a largerhtml.Div
of a graph and some controls surrounding it, you'd like that whole item to be reused.
There's at least one very common use case that on its surface requires a circular dependency, which would be nice to avoid: "remove this item", like an ❎ button to remove a pane. For the case of creating panes from a dropdown or other list, we could just say "don't do that" (since this would be two separate controls for the same thing, and you can always deselect the item in the list where you selected it). But in other cases you just have an "add new item" button (so you'd use n_clicks
for that button, and State
for the existing list, to add new items). I suppose we could have the ❎ button just permanently hide the item, and you need to check the item visibility in the rest of your callbacks? Any better solution to this case? I suppose it's possible that we could just plumb it up to have the ❎ delete its own parent... maybe that won't get flagged as a circular dep, though in principle it is one.
Wildcards in regular (property) callbacks
When we have these repeating items, a single callback will need to manage a whole class of output, and some callbacks will want to use an entire class or a subset of that class as an input. I propose a wildcard syntax based on str.format
that can handle integers and strings, and returns the matched values in kwargs
.
- Single items can be used in
Output
, this creates one set of dependencies for each matching item. If an identifier has already been used inOutput
, that same identifier can be used inInput
orState
too.- Single integer:
Output('graph-{n:d}', 'figure')
->kwargs['n'] = int
- Single string:
Output('info-{item:s}-summary', 'children')
->kwargs['item'] = str
(should we make:s
the default, so you only need to specify it if you want an integer match?)
- Single integer:
- Multi-item matches can be used only in
Input
andState
, and the correspondingargs
(andkwargs
) will be lists. I suppose if you have two multi-item matches in a single id you would get a nested list...- All integers:
Input('text-input-{*m:d}', 'value')
->kwargs['m'] = list(int)
- All previous integers:
Input('dropdown-{*m<n:d}', 'value)
->kwargs['m'] = list(int)
(requiresn
to be used in theOutput
) - All strings:
Input('options-{*v:s}', 'value')
->kwargs['v'] = list(str)
- All integers:
Some examples:
Interdependencies within one dynamically-created pane:
@app.callback(
Output('graph-{name:s}', 'figure'),
[Input('log-scale-{name:s}', 'value')]
)
def make_graph(is_log_scale, name):
return {'data': get_data(name), 'layout': {'yaxis': {'type': 'log' if is_log_scale else 'linear'}}
Use a list of filters to construct a query:
@app.callback(
Output('results', 'children'),
[Input('filter-field-{*n:d}', 'value'), Input('filter-values-{*n:d}', 'value')]
)
def get_count(fields, values, n):
# we don't use n but our function needs to accept it - it's a list of integers
# or could just take **kwargs
filters = ['{} in ("{}")'.format(field, '","'.join(vals)) for field, vals in zip(fields, values)]
res = run_query('select count(*) from users where ' + ' and '.join(filters))[0][0]
return 'Found {} matching users'.format(res)
Use all previous filters to find values available to this filter:
@app.callback(
Output('filter-values-{n:d}', 'options'),
[Input('filter-field-{n:d}', 'value'),
Input('filter-field-{*m<n:d}', 'value'),
Input('filter-values-{*m<n:d}', 'value')]
)
def get_values(field, prev_fields, prev_vals, n, m):
filters = ['{} in ("{}")'.format(f, '","'.join(v)) for f, v in zip(prev_fields, prev_vals)]
res = run_query('select distinct(' + field + ') from users where ' + ' and '.join(filters))
return [{
'label': capitalize(v[0]),
'value': v[0]
} for v in res]