-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
Slider component #986
Conversation
@rreusser can you share a few screenshots here for the design team? |
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. Here it is at reduced size. The label size is calculated and it drops labels so that they don't overlap. You can also achieve other looks by messing with the constants. They're not currently editable by end-users, but they certainly could be: Ticks too. Just throwing out possibilities here… |
@etpinard Added mock. The big shortcoming is that it doesn't hook up to events since mocks don't work with animations. |
|
||
function makeInputProxy(gd, sliderGroup, sliderOpts) { | ||
sliderOpts.inputProxy = gd._fullLayout._paperdiv.selectAll('input.' + constants.inputProxyClass) | ||
.data([0]); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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: { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)); | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done ✅
There was a problem hiding this 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.' |
There was a problem hiding this comment.
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: { |
There was a problem hiding this comment.
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', |
There was a problem hiding this comment.
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', |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks!
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅
}); | ||
} | ||
|
||
function drawTicks(sliderGroup, sliderOpts) { |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 + ')'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for the comment
would mind taking a look at #986 (comment) ? |
@etpinard thoughts on decircularizing requires vs. upping the circularity tolerance threshold? |
Every component that calls a That So, go ahead and bump |
Thanks @etpinard. Will gladly bump threshold! |
Love these sliders. 💃 from me on the design side. |
add streaming maxpoints max and dflt
@etpinard I've removed
The baseline is now within the range of what can be styled: |
There was a problem hiding this 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: { |
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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'], |
There was a problem hiding this comment.
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: { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. Good idea.
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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": [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
great mock! Thanks 🎉
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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
-> othercurrentvalue
nested attributes
}); | ||
}); | ||
|
||
describe('update sliders interactions', function() { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅
There was a problem hiding this 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.
expect(mockCopy.layout.sliders[0].active).toEqual(0); | ||
|
||
done(); | ||
}, 100); |
There was a problem hiding this comment.
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 🎉
There was a problem hiding this comment.
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.
I have no strong opinion about this. I'll you decide what's best. |
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. |
@rreusser I'll attach the gif here for reference: looks like some race condition exist makes the That happen once and I can't replicate it though. So, let's merge this in 💃 whenever you're happy with it. |
Because it's particularly evasive (race condition?), @etpinard noted a layout issue that's captured here: 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. |
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:
Notable features: