Skip to content

Slider component #986

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

Merged
merged 43 commits into from
Oct 6, 2016
Merged

Slider component #986

merged 43 commits into from
Oct 6, 2016

Conversation

rreusser
Copy link
Contributor

@rreusser rreusser commented Sep 28, 2016

This PR implements a first cut at sliders #742
cc @etpinard

It has most of the necessary features (including hooking into the internal event dispatcher 🎉) and seems reasonably stable. It needs:

  • design feedback
  • should echo the selected value since the label may get dropped if there's not enough room
  • should expose more configuration options
  • perhaps account for continuous sliders out of the gate so we don't work ourselves into a backwards compatibility corner
  • fix circular deps… orrrr raise allowed circular deps 😏
  • make the constants less intelligent and more flexible. Just spell them out. Maybe grab tick constants from elsewhere.
  • tests 😬

Notable features:

  • measures text and sets major/minor tick stride to avoid label overlap
  • subscribes to events and updates accordingly
  • wooooo transitions

@etpinard etpinard added the feature something new label Sep 28, 2016
@etpinard
Copy link
Contributor

@rreusser can you share a few screenshots here for the design team?

@rreusser
Copy link
Contributor Author

rreusser commented Sep 28, 2016

Note for design-oriented eyes: As mentioned above, a big thing that this currently needs is a readout of the current value, as in "Year: 1980". The best place is probably above or below the slider (not next to it!) because variable-length user input is a mess when there's not enough room for it. Thoughts welcome.

Here's the slider at full size. It can be translated, but vertical orientation is not currently implemented. The play button is currently drawn next to it, but there is no general relation between the slider and buttons.

screen shot 2016-09-28 at 10 00 27

Here it is at reduced size. The label size is calculated and it drops labels so that they don't overlap.

screen shot 2016-09-28 at 10 00 45

You can also achieve other looks by messing with the constants. They're not currently editable by end-users, but they certainly could be:

screen shot 2016-09-28 at 10 06 18

Ticks too. Just throwing out possibilities here…

screen shot 2016-09-28 at 10 13 06

@rreusser
Copy link
Contributor Author

rreusser commented Sep 28, 2016

@etpinard Added mock. The big shortcoming is that it doesn't hook up to events since mocks don't work with animations.

screen shot 2016-09-28 at 10 47 05


function makeInputProxy(gd, sliderGroup, sliderOpts) {
sliderOpts.inputProxy = gd._fullLayout._paperdiv.selectAll('input.' + constants.inputProxyClass)
.data([0]);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not currently implemented. The idea is that it creates an <input type="range"> that captures focus (can it actually do that programatically?) so that accessibility things like keyboard arrows to change the slider position actually work. Just an idea. Might need to go.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the info

].join(' ')
},

transition: {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept this nested format so that it perfectly mirrors the way you'd define transition config for an animation. Otherwise it would need to be transitionduration: and transitioneasing:, which isn't bad. Open to thoughts here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 plotly.js is a big fan of nested attributes.

sliders.exit().remove();

// If no more sliders, clear the margisn:
if(sliders.exit().size()) clearPushMargins(gd);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When sliders disappear, it also needs to detach event subscriptions. That's very important! We should think about the proper way for components to do that in general.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.

sliderOpts.inputAreaLength = Math.round(sliderOpts.outerLength - sliderOpts.xpad * 2);
sliderOpts.railInset = Math.round(Math.max(0, constants.gripWidth - constants.railWidth) * 0.5);
sliderOpts.stepInset = Math.round(Math.max(sliderOpts.railInset, constants.gripWidth * 0.5));

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic in this area needs to be 30% less fancy and 40% more directly controlled by constants.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done ✅

Copy link
Contributor

@etpinard etpinard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rreusser this is looking great

The main issue I found so far was a naming inconsistency slider vs sliders. The slider directory should be named src/components/sliders/ consistent with layout.sliders attribute container.

dflt: 'restyle',
role: 'info',
description: [
'Sets the Plotly method to be called on click.'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/on click/on slide maybe?


steps: stepsAttrs,

updateevent: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

'value when an event of type `updateevent` is received. If',
'undefined, the data argument itself is used. If a string,',
'that property is used, and if a string with dots, e.g.',
'`item.0.label`, then `data[\'item\'][0][\'label\']` will',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. Plotly.restyle and Plotly.relayout use use [] to delimit array items in an attribute string e.g. item[0].label via Lib.nestedProperty .

I believe the item.0.label is a little more clear. But, maybe we should stick to item[0].label for consistency?

role: 'info',
dflt: 'fraction',
description: [
'Determines whether this color bar\'s length',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha I see, the descriptions were pasted over from other components.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hahaha yupppp.

return function(data) {
var value = data;
if(updatevalue) {
value = Lib.nestedProperty(data, updatevalue).get();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wait does nestedProperty work with items.0.label signature?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe I just didn't know about it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that's a good question… It may well not. I simply assumed (hoped?) it would. Thanks for noticing.

@@ -383,7 +386,7 @@ function styleOnMouseOut(item, menuOpts) {
}

// find item dimensions (this mutates menuOpts)
function findDimenstions(gd, menuOpts) {
function findDimensions(gd, menuOpts) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, I love finding typos and then finding the compensatory typos elsewhere…


exports.moduleType = 'component';

exports.name = 'slider';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

plural (to be consistent with layout.sliders

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

});
}

function drawTicks(sliderGroup, sliderOpts) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good enough for now i.e. no need to make the slider tick options on par with axis tick options (++ axes.js is most fun module to reuse).

Copy link
Contributor

@etpinard etpinard Sep 28, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EDIT: (++ axes.js is not the most fun module to reuse)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't directly use axes.js since the options didn't perfectly map 1-1, but followed the naming conventions.

item.on('mousedown', function() {
var grip = sliderGroup.select('.' + constants.gripRectClass);

d3.event.stopPropagation();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we might want to use a coverSlip in the range slider part II here to avoid selecting other elements on the graph on drag.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah but looks like d3.event.stopPropagation(); d3.event.preventDefault(); does the trick. Nevermind.

Copy link
Contributor Author

@rreusser rreusser Sep 28, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I wasn't 100% sure about my ad-hoc drag implementation. I feel like it ended up fairly concise, unsurprising, and reasonably robust, but I'm glad to refactor if there are helpers that manage some of this.


// Lib.setTranslate doesn't work here becasue of the transition duck-typing.
// It's also not necessary because there are no other transitions to preserve.
el.attr('transform', 'translate(' + (x - constants.gripWidth * 0.5) + ',' + 0 + ')');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the comment

@etpinard
Copy link
Contributor

@jackparmer @delekru

would mind taking a look at #986 (comment) ?

@rreusser
Copy link
Contributor Author

rreusser commented Sep 28, 2016

@etpinard thoughts on decircularizing requires vs. upping the circularity tolerance threshold?

@etpinard
Copy link
Contributor

thoughts on decircularizing requires vs. upping the cirularity threshold?

Every component that calls a Plotly method will have a circular dependency in the current state of the src/ folder. So, your PR should indeed add another circular dependency.

That test-syntax block is simply there to be aware that at some point var MAX_ALLOWED_CIRCULAR_DEPS = 0;.

So, go ahead and bump MAX_ALLOWED_CIRCULAR_DEPS !

@rreusser
Copy link
Contributor Author

Thanks @etpinard. Will gladly bump threshold!

fd4a1cc69ea211681649a4149050a415

@jackparmer
Copy link
Contributor

Love these sliders. 💃 from me on the design side.

add streaming maxpoints max and dflt
@rreusser rreusser mentioned this pull request Oct 4, 2016
@rreusser
Copy link
Contributor Author

rreusser commented Oct 6, 2016

@etpinard I've removed updateevent and updatevalue and cleaned up the slider options to more or less conform to axis options. The configurable options are now:

font
bgcolor
bordercolor
borderwidth
activebgcolor

ticklen
tickwidth
tickcolor
minorticklen

transition.duration
transition.easing

currentvalue.visible
currentvalue.xanchor
currentvalue.prefix
currentvalue.offset
currentvalue.font

The baseline is now within the range of what can be styled:

sliders

Copy link
Contributor

@etpinard etpinard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rreusser looking good!

In this review, there's:

  • 1 blocking comment about a slight defaults quirk.
  • 1 question about the slider coerce('bgcolor') call
  • 1 possible test enhancement.

role: 'style',
description: 'Sets the width (in px) of the border enclosing the slider.'
}
},
ticklen: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very nice

Lib.coerceFont(coerce, 'currentvalue.font', layoutOut.font);

coerce('bgcolor', layoutOut.paper_bgcolor);
coerce('bgcolor');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the default bgcolor isn't inherited from layoutOut.paper_bgcolor anymore. Is this on purpose?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No good reason except that it didn't actually seem like a desirable default since that would just make the slider transparent-looking instead of filled by default.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To elaborate slightly, this attribute wasn't used before. Until today it was a hard-coded constant.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it didn't actually seem like a desirable default

I agree 100%. I just wanted to make sure the above patch was not a typo 👍


xanchor: {
valType: 'enumerated',
values: ['left', 'center', 'right'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, nice job dropping the 'auto' value here.

].join(' ')
},

prefix: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably add a suffix attribute too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Good idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

coerce('currentvalue.visible');
coerce('currentvalue.xanchor');
coerce('currentvalue.prefix');
coerce('currentvalue.offset');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be:

var currentValue = coerce('currentvalue.visible');

if(currentValue) {
  coerce('currentvalue.xanchor');
  coerce('currentvalue.prefix');
  coerce('currentvalue.offset');
}

so that Plotly.validate can detect e.g.

layout.sliders = [{
  currentvalue: {
    visible: false,
    xanchor: 'left'
}];

// => `xanchor` has no effects

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

coerce('minorticklen');

Lib.coerceFont(coerce, 'font', layoutOut.font);
Lib.coerceFont(coerce, 'currentvalue.font', sliderOut.font);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similarly here, Lib.coerceFont(coerce, 'currentvalue.font', sliderOut.font); should only get coerced if currentvalue.visible is true.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -0,0 +1,172 @@
{
"data": [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great mock! Thanks 🎉

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Goal: hit as many properties as possible to avoid testing styles in jasmine.

@@ -23,4 +23,7 @@ module.exports = function failTest(error) {
} else {
expect(error).toBeUndefined();
}
if(error && error.stack) {
console.error(error.stack);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nicely done 🎉

var destroyGraphDiv = require('../assets/destroy_graph_div');
var fail = require('../assets/fail_test');

describe('sliders defaults', function() {
Copy link
Contributor

@etpinard etpinard Oct 6, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO:

  • add one defaults around currentvalue.visible -> other currentvalue nested attributes

});
});

describe('update sliders interactions', function() {
Copy link
Contributor

@etpinard etpinard Oct 6, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you thought about adding mouse interaction tests?

It should be doable to mock a drag interaction using new MouseEvent - unless there are complications that I can't think about.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@etpinard etpinard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rreusser looking good!

In this review, there's:

  • 1 blocking comment about a slight defaults quirk.
  • 1 question about the slider coerce('bgcolor') call
  • 1 possible test enhancement.

@rreusser
Copy link
Contributor Author

rreusser commented Oct 6, 2016

On a minor note, I precomputed the size of the text labels so that I could keep the prefix in the same place for right-anchored currentvalue output. The rationale is it makes it more clear which part is the value with the common use case of "label: value". I have mixed feelings on this.

ezgif-2629780489

@rreusser
Copy link
Contributor Author

rreusser commented Oct 6, 2016

@etpinard I've made the requested changes:

  • add suffix 70610ec
  • fix coerce when currentvalue !visible f25127c, 4b608ee
  • test mouse events (down, move, up) and associated appearance changes 6c9034d

expect(mockCopy.layout.sliders[0].active).toEqual(0);

done();
}, 100);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice. That's all I wanted. Thanks for taking the time to do this 🎉

Copy link
Contributor Author

@rreusser rreusser Oct 6, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No prob. Wasn't too bad; just hadn't previously tried mocking mouse events and positions.

@etpinard
Copy link
Contributor

etpinard commented Oct 6, 2016

I precomputed the size of the text labels so that I could keep the prefix in the same place for right-anchored currentvalue output. The rationale is it makes it more clear which part is the value with the common use case of "label: value".

I have no strong opinion about this. I'll you decide what's best.

@rreusser
Copy link
Contributor Author

rreusser commented Oct 6, 2016

I think it's fine as-is. I thought it would be preferable to completely right-justifying the text it since this cuts down on unneeded movement as you change the slider.

@etpinard
Copy link
Contributor

etpinard commented Oct 6, 2016

@rreusser I'll attach the gif here for reference:

gifrecord_2016-10-06_151534

looks like some race condition exist makes the currentvalue position change on relayout.

That happen once and I can't replicate it though.

So, let's merge this in 💃 whenever you're happy with it.

@rreusser
Copy link
Contributor Author

rreusser commented Oct 6, 2016

Because it's particularly evasive (race condition?), @etpinard noted a layout issue that's captured here:

gifrecord_2016-10-06_151534_360 gif png

I think this was an issue with bounding box computation returning null and the sizes not actually being numbers. The last commit on this PR should fix that, but just adding here for reference in case it pops up again.

@rreusser rreusser merged commit cb293c7 into master Oct 6, 2016
@rreusser rreusser deleted the slider-component branch October 6, 2016 19:59
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

Successfully merging this pull request may close these issues.

5 participants