Skip to content

Dynamic Layouts (Wildcard Props) #475

Closed
@alexcjohnson

Description

@alexcjohnson

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 specify id and key for all elements. That's probably fine, but the dynamic layout docs should highlight importance key. For example if you make a larger html.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 in Output, that same identifier can be used in Input or State 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?)
  • Multi-item matches can be used only in Input and State, and the corresponding args (and kwargs) 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)
      (requires n to be used in the Output)
    • All strings: Input('options-{*v:s}', 'value') -> kwargs['v'] = list(str)

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]

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions