-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Slider control decisions/implementation #742
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
First quick cut for the purpose of linking with frames + animations: |
Giving my two cents: UI design
No. I think the play button should be a separate component. Rationale: some folks may want to add a play button without a slider and vice-versa.
Horizontal only for the first iteration.
Same as all other layout components with
I'm thinking sliders will have a features
Yes. Absolutely.
I'm not sure what this refers to. Like a play button?
API design
No. Sliders options should be input in
That's a hard question. Maybe we can get away with making sliders only update one-way (like Adding a
First cut API: layout.sliders = [{
active: 0, // 1,2,3 -> index of active step
steps: [{
method: 'animate', // or 'restyle', 'relayout'
args: [], // passed as `Plotly[method](gd, ...args)`
label: '', // string label to appear on step ticks
}, {
/* */
}],
visible: true, // or false
x: 0,
xanchor: 'left',
y: 0,
yanchor: 'bottom',
font: {},
borderwidth: 0,
bordercolor: '#eee',
color: '#BEC8D9',
ticks: '',
ticklen: '',
tickcolor: ''
}, {
/* */
}]; the |
Agreed. That all makes good sense. The only potential blocker IMO is the verbosity, though if there's no use-case for continuous sliders it's not as bad. What about this: Do exactly what you specified. If it's actually a problem, then we could add (Related question: is there a use-case for continuous sliders? Pixels are discrete so it's nothing you can't technically do with enumerated sliders, just potentially much DRYer) |
cc @etpinard @monfera @chriddyp (and any others. please cc others who might have valuable input!) Related to sliders, a RFC on a proposal for more sophisticated declarative controls: The challenge: Other frameworks just let you attach arbitrary event handlers that execute code. Plotly allows no user-defined code, so other methods of implementing controls are necessary. The current state: Plotly currently allows controls to invoke API methods with provided arguments. These arguments are fixed as declared. The proposal: I suggest two formats for plotly commands. One style invokes a command directly (the status quo). The other uses a variable registry to build more complicated component interactions. Method 1: status quoTake the updatemenus: [{
buttons: [
{label: 'red', method: 'restyle', args: [{ 'marker.color': 'red', 'marker.size': 20]},
{label: 'blue', method: 'restyle', args: [{ 'marker.color', 'blue', 'maker.size': 30}]},
]
}] When selected, the corresponding command will be executed with the provided arguments. Method 2: Variable registryThe problem with the first method is that the arguments are fixed in place. If your slider has forty positions, you would need to define the restyle command forty times. If your command requires two variable inputs, you simply can't define it declaratively. A proposed solution is to register named variables in a central object, say updatemenus: [{
buttons: [
{label: 'Red', value: 'red'},
{label: 'Blue', value: 'blue'},
],
variable: 'myMarkerSizeVariable'
}] (These could be used with gd._controlComponentValues = {myMarkerSizeVariable: 'red'}; Note that no change has occurred yet. To use this, you could create a button: updatemenus: [{
buttons: [
{
label: 'change it!',
method: 'restyle',
args: [{'marker.color': '$myMarkerSizeVariable'}]
},
],
type: 'buttons',
}] Alternatively, the updatemenu itself could contain the To answer your question, yes, this would require deep traversal of the args object, looking for and performing any substitutions. Unless the user passes 1MB of raw data through the component I don't love the Sample interesting use-caseThis would allow, say, passing a four-dimensional dataset to a trace. Two slider values would select a slice in two of the dimensions and place the slide in a named variable. The Long story short, this enables declarative data slicing without too much additional machinery or complexity. |
For completeness, how would be Perhaps as: updatemenus: [{
buttons: [
{label: 'Red', value: ['red', 30]},
{label: 'Blue', value: ['blue', 20]},
],
variable: 'myMarkerSizeVariable'
}, {
type: 'buttons',
buttons: [
{
label: 'change it!',
method: 'restyle',
args: [{
'marker.color': '$myMarkerSizeVariable[0]',
'marker.size': '$myMarkerSizeVariable[1]'
}]
},
]
}] |
Yeah, good to clarify. Thanks. I was thinking: updatemenus: [{
buttons: [
{label: 'Red', value: ['red', 30]},
{label: 'Blue', value: ['blue', 20]},
],
variable: ['myMarkerSizeVariable0', 'myMarkerSizeVariable1']
}, {
type: 'buttons',
buttons: [
{
label: 'change it!',
method: 'restyle',
args: [{
'marker.color': '$myMarkerSizeVariable0',
'marker.size': '$myMarkerSizeVariable1'
}]
},
]
}] Only issue then is intuitive naming. |
I also considered that the |
What if we separate out the variable settings from the actions? Particularly in cases like multidimensional slicing, you'll need several controls setting different variables that go into the same action, then that would also let us do multiple things with the same control: sliders: [
{
label: 'Year',
values: [[0, 1990], [1, 1995], [2, 2000], [3, 2005], [4, 2010], [5, 2015]],
// if a control specifies variables, it should NOT specify method/args
variables: ['yearindex', 'year'],
value: '$year'
},
{
label: 'Something else',
steps: { // alternative to enumerating all values
start: 0,
stop: 12,
step: 1
},
variables: ['index1']
}
],
actions: [
{
method: 'slice', // or is this just restyle/animate through a transform?
args: [
['$yearindex', '$index1'],
0 // trace index
],
variables: ['$yearindex', '$index1'] // or just search through args to find these?
},
{
method: 'slice',
args: [
['$yearindex'],
1 // another trace that only has 3 dimensions, but we slice it on year with the same slider
]
}
] The |
I like the idea of action definitions! My hesitation:
FWIW, I would tend to automate the variable location. If you compile and use a single regexp, I don't know that it's really that much additional overhead. Also, I like the imperative version where you actually specify the API command and argument whether than a new grammar for operations like slices. |
It's true, action definitions are introducing a whole extra layer that's unnecessary 99% of the time - only when two or more controls need to be involved in the same edit. Perhaps if we define the slicing grammar right they can be decoupled in this particular use case, then we can see if there are any other use cases that still will need to be coupled... though I guess as long as we're always editing part of a declarative spec it should always be possible to keep them decoupled. So then perhaps we can do away with the idea of variables, and just have the sliders: [
{
label: 'Year',
// each item is an array, so you address $0, $1
values: [[0, 1990], [1, 1995], [2, 2000], [3, 2005], [4, 2010], [5, 2015]],
method: 'restyle',
// update attribute zsliceindices[0] of traces 0 and 1 with the first element of
// the chosen array out of values.
// Insert whatever attribute we choose to define how to slice...
args: [{'zsliceindices[0]': '$0'}, [0, 1]],
// display the year value next to the slider
valueLabel: '$1'
},
{
label: 'Age',
// each item is an object, so you address $i, $age
values: [{i: 0, age: '0-5 years'}, {i: 1, age: '5-10 years'}, {i: 2, age: '10-20 years'}],
// update a different, decoupled attribute of only trace 0
method: 'restyle',
args: [{'zsliceindices[1]': '$i'}, [0]],
valueLabel: '$age'
}
] What if we wanted to change different attributes of each trace? Do we have a restyle / update syntax that supports this, or do we need to allow several method/args pairs somehow? |
Thanks for the feedback! That makes sense. My leaning in the other direction comes from the fact that it throws out a whole class of operations for what I think amounts to effectively zero technical differences (a global lookup is not difficulty-wise different from component-local only). To be perfectly clear and unless I'm wrong, the substitution in your example only exists for the sake of DRYing up the notation but doesn't actually enable anything you couldn't accomplish by enumerating all the arguments for each slider position. My initial thought at compromise was to couple actions with components (since decoupling DRYs up but doesn't enable new behavior) but decouple the variables (since this does enable new uses). |
I'm not sure about that... what happens when two components use the same variable in an action? Do you require them both to define the full action and only do it once? Do you hunt for any actions that use the variable that changed and execute them all? Do you do nothing in one component, and wait until the other component is tweaked to make the change? All of those seem potentially confusing to users. That problem at least would be solved with action definitions, but it's really not clear to me that this is a capability we need. Perhaps we should to come at this from the other direction: what are you imagining as the declarative spec for slicing, or whatever other operations we're imagining wanting to do with sliders & dropdowns?
Correct. I think it may be pretty important to allow (and encourage) this though - otherwise you can (inadvertently or with good but misguided intention) generate different changes at different selection values, so your selection doesn't guarantee state, state is history-dependent. |
Okay, decent points! Long story short, I was really just thinking of sharing variables, period. So you wouldn't need to hunt for actions or dependencies. When a component changes, an method is invoked. No duplication or need to factor out actions. There's a bit of replication in invoking commands, but I feel like the interaction of just two parameters affecting a property or two already pushes things into an advanced use case, so that I don't immediately see the need to optimize for DRYness of complicated commands plus interactions (specifically, two sliders modifying two parameters of a transform would only requiring writing the command twice, such that factoring out an action may not even shorten. Is more than two or maybe three interacting sliders realistic?) You are correct though that it would be history-dependent. I don't like that, but with animations as a whole, that's been a bit of a compromise to allow working it into the current API. I would have loved to properly diff things or fully recreate an empty plot state and make everything truly stateless, but it just wasn't really feasible. TBH, I tend to see these the same way: it's admittedly a bit of a declarative API to accomplish imperative concepts… |
Honestly if it's just pushing things a bit too far, we can leave this on ice for now and just do a single parameter substitution. That might simplify things, avoid the need to account for edge cases, and would at least enable the obvious use case of modifying a parameter in order to prevent the immediate reaction, "Oooh, sliders! But wait, how do you… you mean you can't…?"—which I sorta feel like would happen if sliders only permitted enumerated commands+args. |
@etpinard, do you have any particular affinity to
Long story short, I think integer indexing is fine internally, but I don't see the need to expose this via the API. |
No, I don't. An integer |
from @rreusser this morning:
|
Wow. Resolved. |
I've mocked up a simple input for testing, but this is a more complete list of considerations and decisions that go into deciding what the slider control should look like and how it should work
The text was updated successfully, but these errors were encountered: