Skip to content

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

Closed
rreusser opened this issue Jul 13, 2016 · 18 comments
Closed

Slider control decisions/implementation #742

rreusser opened this issue Jul 13, 2016 · 18 comments
Labels
feature something new

Comments

@rreusser
Copy link
Contributor

rreusser commented Jul 13, 2016

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

@rreusser
Copy link
Contributor Author

rreusser commented Jul 13, 2016

First quick cut for the purpose of linking with frames + animations:

@etpinard
Copy link
Contributor

etpinard commented Sep 1, 2016

Giving my two cents:

UI design

  • play button?

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/vertical?

Horizontal only for the first iteration.

  • positioning?

Same as all other layout components with x / y in plot-area coordinates and xanchor / yanchor. I'd vote for making the default position just below the plot area.

  • label (slider name + value label)

I'm thinking sliders will have a steps fields analogous to updatemenus buttons where each step object will have optionally a label

features

  • multiple sliders

Yes. Absolutely.

  • autoplay?

I'm not sure what this refers to. Like a play button?

  • loading (create plot; load frames; then decide whether to autoplay?)

API design

  • naming slider, slider2, etc...

No. Sliders options should be input in layout.sliders which is an array container similar to layout.updatemenus.

  • referring to values (slider.value, slider2.value, string substitution in arguments? deep substitution?)

That's a hard question. Maybe we can get away with making sliders only update one-way (like layout.updatemenus) via the JSON serialization API and fire events (e.g. plotly_slider) so that JS users could add in their own hooks to perform two-way updates.

Adding a value or valuesrc field which would correspond to the data/layout field that the slider updates sounds pretty hard to generalise. Maybe be could restrict two-way updates to frames?

  • configuration parameters
  • similarities with other input controls? room for shared code in implementation?
  • more considerations?

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 steps part will input will be a bit verbose for most use cases, but the above API feels like the easiest to declare general updates. In addition, this API is very analogous to layout.updatemenus, which should make the learning 📈 easy.

@rreusser
Copy link
Contributor Author

rreusser commented Sep 1, 2016

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 steptemplate or a similar concept in which you define it in a DRY form and it computes the actual steps as listed above. Versioning-wise, it'd just be an enhancement so shouldn't be problematic to consider later, if at all.

(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)

@rreusser
Copy link
Contributor Author

rreusser commented Sep 23, 2016

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 quo

Take the updatemenus component. The current style will be not affected so that you can define commands like this:

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 registry

The 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 gd._controlComponentValues. You would instead define your updatemenu as:

updatemenus: [{   
  buttons: [
    {label: 'Red', value: 'red'},
    {label: 'Blue', value: 'blue'},
  ],
  variable: 'myMarkerSizeVariable'
}]

(These could be used with arrayOk to permit multiple labels and values.) Then when the user interacts with the component, the result is that the variable registry receives the values and is then:

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 method and args and could look up its own value This isn't so relevant for updatemenus, but for sliders it would allow passing a continuous variable to a method or at least avoiding specifying a full method and args for every slider position. (For updatemenus, I'd like to enable hoisting of the method/args from the individual button level to the component level so that you can really DRY up the component definitions)

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 args, this is probably acceptable in the vast majority of cases (we could add a flag to disable substitution if your use-case is particularly pathological).

I don't love the $variableName syntax since it's perhaps not unique enough, though it is rather intuitive. Glad to hear second opinions on that one or anything else here.

Sample interesting use-case

This 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 method and args of the slider would look up named values from both sliders and pass them to a transform, say, slicing the dataset down to two dimensions, which is just an x-y trace.

Long story short, this enables declarative data slicing without too much additional machinery or complexity.

@etpinard
Copy link
Contributor

etpinard commented Sep 23, 2016

For completeness, how would be arrayOk case be set?

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]'
      }]
    },
  ]
}]

@rreusser
Copy link
Contributor Author

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. value/variable vs. values/variables.

@rreusser
Copy link
Contributor Author

I also considered that the $variableName syntax might conflict with latex equations or… dollar amounts? But since substitution only occurs if the variable exists and since latex is whitespace-insensitive (just use $ variableName in your latex if for some weird reason you require it), I don't know that this would lead to unexpected or un-trivially-solvable interactions.

@alexcjohnson
Copy link
Collaborator

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 {start, stop, step} syntax is appealing for reducing overhead, though I'm not sure how to deal with the fact that it's going to want to map between some scaled value and an index, at least when it's used with slicing.

@rreusser
Copy link
Contributor Author

rreusser commented Sep 23, 2016

I like the idea of action definitions! My hesitation:

  • at what point have we defined a grammar that's a little too complicated for people to comprehend easily and intuitively (debatably, this is more intuitive, which is why I like it)
  • does it conflict with the existing approach or otherwise undermine it in a way that will lead to unnecessary confusion?

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.

@alexcjohnson
Copy link
Collaborator

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 values entries be either arrays or objects:

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?

@rreusser
Copy link
Contributor Author

rreusser commented Sep 23, 2016

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

@alexcjohnson
Copy link
Collaborator

a global lookup is not difficulty-wise different from component-local only

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?

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

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.

@rreusser
Copy link
Contributor Author

rreusser commented Sep 23, 2016

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…

@rreusser
Copy link
Contributor Author

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.

@rreusser
Copy link
Contributor Author

rreusser commented Sep 27, 2016

@etpinard, do you have any particular affinity to active: <integer> in order to define the selected position? Here are the cases I can come up with:

  • labeled positions: if the slider positions are defined by a string label, then that seems perfectly adequate to specify the position without the need for an additional integer index
  • continuous values: It seems perfectly reasonable that a slider could represent a continuous value, which at the very least would need to be broken down in terms of min/max/count or min/step/count. This could be integer-indexed but (despite past mistakes) I think it would make more sense to allow a continuous-valued input and quantize it to the scale immediately on initialization.

Long story short, I think integer indexing is fine internally, but I don't see the need to expose this via the API.

@etpinard
Copy link
Contributor

do you have any particular affinity to active:

No, I don't. An integer active attribute was the easiest option for updatemenus when I first wrote them, but we can extend it to allow for string values.

@rreusser rreusser mentioned this issue Sep 28, 2016
10 tasks
@etpinard
Copy link
Contributor

etpinard commented Sep 29, 2016

from @rreusser this morning:

The more I think about it, I think any attempt to manage events and hook one thing up to another is really going to suffer unless we basically just bite the bullet and implement some concept of variables and actions. The current event-based hookup is sooo imperative...

It does what it needs, but it requires the user to deal with some unrelated internal concept of events in order to get it done.

Feels like animating should set the frame (edit: and actually do the animating) and then everything else (edit: e.g. control component updates) should follow from the frame variable changing

Like updateevent doesn't generalize, but more importantly it's not even something I would want to generalize

I think two-way binding can be solved, and at any rate declarative APIs tend to be easier to implement than hooking together a bunch of imperative parts… declaratively…

@rreusser
Copy link
Contributor Author

rreusser commented Nov 9, 2016

Wow. Resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature something new
Projects
None yet
Development

No branches or pull requests

3 participants