Repository navigation
Dynamic Layouts (Wildcard Props) #475
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=idto all core components? It doesn't look like most of them do this yet, though one or two do.idis 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_componentsallows users to separately specifyidandkeyfor all elements. That's probably fine, but the dynamic layout docs should highlight importancekey. For example if you make a largerhtml.Divof 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 inInputorStatetoo.- Single integer:
Output('graph-{n:d}', 'figure')->kwargs['n'] = int - Single string:
Output('info-{item:s}-summary', 'children')->kwargs['item'] = str
(should we make:sthe default, so you only need to specify it if you want an integer match?)
- Single integer:
- Multi-item matches can be used only in
InputandState, 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)
(requiresnto 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]
Activity
alexcjohnson commentedon Nov 30, 2018
@nicolaskruchten reminded me we can already alter the layout by including components in the
childrenproperty - thanks, that certainly simplifies things! updated the above to reflect this, but the notes about these callbacks still apply.chriddyp commentedon Dec 14, 2018
For
n, if the layout was a list of inputs that couldn't be deleted, then it'd be like[0, 1, 2, 3, 4]. But, if the inputs could each be deleted, then it might end up being like[1, 4, 6]. Am I understanding this correctly?chriddyp commentedon Dec 14, 2018
I'm going to try to write the "todo mvc" app with this framework (example app: http://todomvc.com/examples/react/#/, specifications: https://github.com/tastejs/todomvc/blob/master/app-spec.md#functionality)
chriddyp commentedon Dec 15, 2018
OK, here's a stab at writing the todo mvc. Things to look out for in the code:
children) asStateis new for me. I've only done this whenchildrenis a string, not when it's a list of components. I'm not sure how we handle this currently (i.e. do we convert the component json back into a proper DashComponent?)chriddyp commentedon Dec 15, 2018
Next up, I'd like try to create the late elasticsearch falcon UI. This'll be more of a challenge of hidden/dynamic inputs

chriddyp commentedon Dec 18, 2018
I started writing the elasticsearch UI in the screnshot above and I found that if I prefixed all of my controls with the same string, then I could write a single callback that would handle an arbitrary set of inputs (many inputs hide/show depending on which elasticsearch columns are available or which elasticsearch aggregation options are selected). Really sweet!
One thing that I'm finding a little tedious is the string concatenation and constants management (reusing the same prefix across callbacks and components). Also, I find the number casting syntax a little tough to read.
I'm curious if we could expand out our regex groups to be stricter key-value pairs (key representing the "group"). An input could belong to multiple groups with multiple key-value pairs.
This is inspired by html's
formattribute (https://stackoverflow.com/a/6644180/4142536).In the examples below, we have:
OutputPattern/InputPattern/StatePattern: Used to "match" repeated sets of inputs and outputs by a certain key ingroup. In the wildcard pattern above, this was e.g.city-{s}InputGroup/StateGroup- A way to get a set of inputs/states that match a certainfilter. In the wildcard pattern above, this was e.g.city-{*:s}Multiple 1-1 relationships.
Single N-1 relationship
Multiple N-M relationships
Outputs targeting different groups
A single 1-Many update
Some additional notes:
The values in the
groupkeyword (key-value pairs that classify an input) can be strings or numbers. This simplifies the wildcard casting a bit.They group keys can be descriptive, making the callback signatures pretty easy to read
In
OutputPattern/InputPattern,groupkeyword and value is repeated. Feels too repetitive.We're applying filters over the components but not the properties. Something still feels off here: with control groups, we're basically getting lucky that every control has the same property name (
value).I just realized that this general problem could also solve:
The positional call signature doesn't feel write to me. In
flask, theflask.requestimport is this variable that flask writes on every request and it's packed with all of the meta info about the request (e.g.flask.request.headersorflask.request.params). Maybe we could do the same thing in Dash as a backwards compatible way to provide all of this info. Withgroup, we could provide nice key-value pairs in this object.Pulling from an example below, we have:
but with a global object, this could be:
We could end up stuffing other data in there in the future, like which input has changed or when the last input changed. We'd be able to add stuff without changing backwards compatibility too. There was an original discussion about this call signature here (never resolved or really investigated): Determining Which Input Has Been Fired #291
chriddyp commentedon Dec 18, 2018
Here's a rough draft at the elasticsearch UI. Seems possible! I was particularly curious about the "range aggregation" type: for that type, the UI has an arbitrary number of min/max inputs. So, not only is that UI component dynamic, it's also variable. Seems like all of the controls can inerhit the same prefix/control group and be used in a single callback to create the query. Really awesome!
I think that the amount of code to create this UI would be comparable to my original react-redux implementation and as easy to grok.
chriddyp commentedon Dec 18, 2018
Also, it seems like this problem might solve an issue with "partially defined" inputs/outputs. In the current version of Dash, there is this limitation that either all of the inputs/outputs need to be present on the page or none of them. So, if you wanted to share inputs across multiple tabs (via a store), it wouldn't be possible as the
Storewould need to be omnipresent, while it's inputs and outputs would hide and show as the tabs render.Here is a quick sketch on how this might be done with dynamic callbacks:
alexcjohnson commentedon Dec 19, 2018
Looking at duplicate IDs: #299 #320 - validating the initial layout is great but I suspect we'll want a dynamic uniqueness test, particularly when we have wildcard components. For example @chriddyp's TODO app would break in a very confusing way: it sets the id of an added item based on the length of the list, but if you delete from the middle of the list, the last item may have an id greater than the length 💥 (the easy solution for this, I think, would just be to use
n_submitas the number in the new item ID)alexcjohnson commentedon Dec 22, 2018
Interesting idea - then we can construct an
id/ Reactkeyout of these values for a specific item, for uniqueness & performance, but that need not be visible to the user. The patternComponent(group={...})seems funny to me though, asgroupthere really functions as theid- it's the grouping variables AND their values which together become its unique identifier. So can we just keep the nameid? ie ifidis a string the component is a regular non-grouped item, but if it's a dict the component is addressable as part of a group?The complexity of
OutputPattern,InputGroupetc is a bit confusing to me - could we not just useOutputandInputplus the same dict structure with special entries for "one arbitrary value", "all values", and "all (numerically) smaller values"? Since this would be a value that already has a key, it wouldn't need any more information than that. Value type would only matter for "all smaller", we could just throw an error in that case if we encountered a non-numeric value.Revision: move matching IDs info from args to a global var
I really like the idea of a global variable containing the match information! For a lot of simpler applications you don't need this information, so this would limit the function signature to just the explicit inputs and state. And it avoids sticky questions about whether you make these positional args (in what order, if there are multiple wildcards?), or kwargs that you need to know the naming convention for...
I think the most straightforward and complete way to do this would be to exactly match the signature of
app.callbackfor the non-wildcard cases, and expand to the actual items found when you have wildcards. So for example:Prop mismatch
Now, what about the problem of different
component_propfor different items in the set? I'm tempted to say "just make a new set" for each distinct prop. For example changeto
I suppose we could invent a way to distinguish between them when creating the
Inputthough, something like:But that seems ugly and hard to use, for not that much benefit. Or perhaps we augment the component itself with an extra mapping of props? Like:
Then this particular component could use
'value'as a synonym for'children'in callback signatures. Or we could augment both theInputand thePcomponents to use a totally new prop in callbacks.Anyway either of these features could be layered on later, they wouldn't be required for the rest of dynamic callbacks to work.
mungojam commentedon Dec 22, 2018
Following on from the discussion on the forum I've had a go at implementing the todoMVC example, adapting the @chriddyp version above
This first attempt is the minimum possible changes I could do to the original while making use of my proposed new way to link to callbacks. I have still required wildcard inputs/state in this version and have had to introduce a few tricks/new features. They feel logical to me as functional programming concepts if that is the direction you want to take things:
Current()being a special case ofState()that can be accessed by any function in the chain.Next I would like to try refactoring it to remove some setting of ids and explore how it would work if Event() and maybe Input() were both declared at the point of declaration. Although maybe we always want id to be set if it is going to be used to derive
keyin future.mungojam commentedon Dec 23, 2018
I've settled on my preferred balance where
Event()andOutput()links are moved to the components, whileInput()andState()are left as callback decorators with the option for patterns to select multiple inputs.You can see what this looks like.
I have also added some other new features that make this work nicely for dynamic layouts such as a
Source()function on callbacks that are Event triggered which tends to be how event handlers work elsewhere.The best example is the removal buttons against each item:
[-]Dynamic Layouts[/-][+]Dynamic Layouts (Wildcard Props)[/+]deemson commentedon Mar 20, 2019
Guys, what's the status of this issue? Is there anything dev-related you need help with?
alexcjohnson commentedon Mar 20, 2019
It just hasn't bubbled up to the top of the TODO list yet. Feel free to take a stab at it, though I suspect it will be a fairly involved effort. Otherwise, we hope to get to it within the next couple of months.
5 remaining items
alexcjohnson commentedon Sep 19, 2019
@christianwengert we’re always looking for organizations to fund & prioritize our open source work. could you get in touch with us here? https://plot.ly/products/consulting-and-oem/
ProbonoBonobo commentedon Nov 21, 2019
I would encourage it, but let me ask a slightly different question: is there a compelling reason for prohibiting an application from setting arbitrary properties? By "arbitrary properties" I mean any valid html attribute that isn't explicitly defined by a component.
I ask because, in the absence of a common spec, raising a
ValueErrorwhen an application tries to annotate a node with custom key/value pairs seems awfully perverse. I would understand if that were something that maybe, as web developers, we just learn to live without because transpiling a Python application to React is hard and there's a few tradeoffs. For example, if you try to monkey-patch component instances with a bunch of random props at instantiation time, Dash spits out an error and refuses to build the app until you remove the offending property names.Why though? If doing so inevitably caused horrible runtime errors, it would make sense that Dash should not only warn but prevent you from shooting yourself in the foot. But when I suppressed those compile-time checks in my local fork (which requires also tweaking the definition of
dash.development.base_component.Component.to_plotly_json(), but it's a trivial fix) the app works beautifully. It works exactly as expected. The foreign props are even visible to the dev_tools_ui debugger's callback graph, which was pleasantly surprising.Originally I had only intended to modify
Component.__init__()to addaria-anddata-to a component's list of valid prefixes (tangential rant: the base class defers to its subclasses to define_valid_wildcard_prefixes, which is something that's already been codified in the W3C spec. Why? Lots of any third-party components don't define any._valid_wildcard_prefixesat all, which makes extending them impossible -- and imho they shouldn't have to). But once I saw how to accomplish that, I realized that it would be just easy to abolish the property check completely to support any property. And boom, suddenly I became 2-3x more productive in Dash. It's helpful for so many use cases. Yesterday, for example, I was annoyed that mydbc.Inputcomponent applied spellchecking to a name field and didn't expose an option in its signature that to disable that. But sincespellcheckis a valid HTML attribute that is parsed by the browser, setting that attribute to "false" on the Python object directly disables the red squigglies where they're unwanted. This would be impossible under the current regime of whitelisted attributes.nicolaskruchten commentedon Jan 31, 2020
From chatting with @alexcjohnson today... even with wildcard props implemented as in #1103, to make a nice Todo app we need array operations callbacks described in #968
schaefer01 commentedon Feb 24, 2020
HI, I am a user waiting on this. I'd like to dynamically change the label in a button to create a single button that changes function based on state. At a minimum two states, like a toggle but with text inside.
change build (plotly#475)
change build (#475)