Skip to content

Add fallbacks for manual manipulation of slider/frames #1233

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 2 commits into from
Dec 9, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 37 additions & 8 deletions src/components/sliders/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,18 @@ module.exports = function draw(gd) {
computeLabelSteps(sliderOpts);

Plots.manageCommandObserver(gd, sliderOpts, sliderOpts.steps, function(data) {
if(sliderOpts.active === data.index) return;
if(sliderOpts._dragging) return;

setActive(gd, gSlider, sliderOpts, data.index, false, true);
// NB: Same as below. This is *not* always the same as sliderOpts since
// if a new set of steps comes in, the reference in this callback would
// be invalid. We need to refetch it from the slider group, which is
// the join data that creates this slider. So if this slider still exists,
// the group should be valid, *to the best of my knowledge.* If not,
// we'd have to look it up by d3 data join index/key.
var opts = gSlider.data()[0];
Copy link
Contributor

Choose a reason for hiding this comment

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

Does the same behavior happen for updatemenus ?

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 think so. Need to investigate make a parallel PR for that. This is one is just of particular concern because of @bpostlethwaite's current project with the slider transform thing.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok. No need to address updatemenus in this PR.

Can you open an issue for that and for your This PR does not fix: point above?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See: #1234 #1235


if(opts.active === data.index) return;
if(opts._dragging) return;

setActive(gd, gSlider, opts, data.index, false, true);
});

drawSlider(gd, d3.select(this), sliderOpts);
Expand Down Expand Up @@ -225,6 +233,15 @@ function findDimensions(gd, sliderOpts) {
}

function drawSlider(gd, sliderGroup, sliderOpts) {
// This is related to the other long notes in this file regarding what happens
// when slider steps disappear. This particular fix handles what happens when
// the *current* slider step is removed. The drawing functions will error out
// when they fail to find it, so the fix for now is that it will just draw the
// slider in the first position but will not execute the command.
if(sliderOpts.active >= sliderOpts.steps.length) {
sliderOpts.active = 0;
}

// These are carefully ordered for proper z-ordering:
sliderGroup
.call(drawCurrentValue, sliderOpts)
Expand All @@ -251,9 +268,9 @@ function drawCurrentValue(sliderGroup, sliderOpts, valueOverride) {

switch(sliderOpts.currentvalue.xanchor) {
case 'right':
// This is anchored left and adjusted by the width of the longest label
// so that the prefix doesn't move. The goal of this is to emphasize
// what's actually changing and make the update less distracting.
// This is anchored left and adjusted by the width of the longest label
// so that the prefix doesn't move. The goal of this is to emphasize
// what's actually changing and make the update less distracting.
x0 = sliderOpts.inputAreaLength - constants.currentValueInset - sliderOpts.currentValueMaxWidth;
textAnchor = 'left';
break;
Expand Down Expand Up @@ -402,11 +419,21 @@ function setActive(gd, sliderGroup, sliderOpts, index, doCallback, doTransition)
}
}

function attachGripEvents(item, gd, sliderGroup, sliderOpts) {
function attachGripEvents(item, gd, sliderGroup) {
var node = sliderGroup.node();
var $gd = d3.select(gd);

// NB: This is *not* the same as sliderOpts itself! These callbacks
// are in a closure so this array won't actually be correct if the
// steps have changed since this was initialized. The sliderGroup,
// however, has not changed since that *is* the slider, so it must
// be present to receive mouse events.
function getSliderOpts() {
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 📚

return sliderGroup.data()[0];
}

item.on('mousedown', function() {
var sliderOpts = getSliderOpts();
gd.emit('plotly_sliderstart', {slider: sliderOpts});

var grip = sliderGroup.select('.' + constants.gripRectClass);
Expand All @@ -420,11 +447,13 @@ function attachGripEvents(item, gd, sliderGroup, sliderOpts) {
sliderOpts._dragging = true;

$gd.on('mousemove', function() {
var sliderOpts = getSliderOpts();
var normalizedPosition = positionToNormalizedValue(sliderOpts, d3.mouse(node)[0]);
handleInput(gd, sliderGroup, sliderOpts, normalizedPosition, false);
});

$gd.on('mouseup', function() {
var sliderOpts = getSliderOpts();
sliderOpts._dragging = false;
grip.call(Color.fill, sliderOpts.bgcolor);
$gd.on('mouseup', null);
Expand Down
17 changes: 17 additions & 0 deletions src/plots/plots.js
Original file line number Diff line number Diff line change
Expand Up @@ -1546,6 +1546,23 @@ plots.computeFrame = function(gd, frameName) {
return result;
};

/*
* Recompute the lookup table that maps frame name -> frame object. addFrames/
* deleteFrames already manages this data one at a time, so the only time this
* is necessary is if you poke around manually in `gd._transitionData_frames`
Copy link
Contributor

Choose a reason for hiding this comment

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

typo ⏰ gd._transitionData_frames -> gd._transitionData._frames

* and create and haven't updated the lookup table.
*/
plots.recomputeFrameHash = function(gd) {
var hash = gd._transitionData._frameHash = {};
var frames = gd._transitionData._frames;
for(var i = 0; i < frames.length; i++) {
var frame = frames[i];
if(frame && frame.name) {
hash[frame.name] = frame;
}
}
};

/**
* Extend an object, treating container arrays very differently by extracting
* their contents and merging them separately.
Expand Down
54 changes: 54 additions & 0 deletions test/jasmine/tests/sliders_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ describe('sliders initialization', function() {

Plotly.plot(gd, [{x: [1, 2, 3]}], {
sliders: [{
transition: {duration: 0},
steps: [
{method: 'restyle', args: [], label: 'first'},
{method: 'restyle', args: [], label: 'second'},
Expand All @@ -209,6 +210,59 @@ describe('sliders initialization', function() {
});
});

describe('ugly internal manipulation of steps', function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

great test case name!

'use strict';
var gd;

beforeEach(function(done) {
gd = createGraphDiv();

Plotly.plot(gd, [{x: [1, 2, 3]}], {
sliders: [{
transition: {duration: 0},
steps: [
{method: 'restyle', args: [], label: 'first'},
{method: 'restyle', args: [], label: 'second'},
]
}]
}).then(done);
});

afterEach(function() {
Plotly.purge(gd);
destroyGraphDiv();
});

it('adds and removes slider steps gracefully', function(done) {
expect(gd._fullLayout.sliders[0].active).toEqual(0);

// Set the active index higher than it can go:
Plotly.relayout(gd, {'sliders[0].active': 2}).then(function() {
// Confirm nothing changed
expect(gd._fullLayout.sliders[0].active).toEqual(0);

// Add an option manually without calling API functions:
gd.layout.sliders[0].steps.push({method: 'restyle', args: [], label: 'first'});

// Now that it's been added, restyle and try again:
return Plotly.relayout(gd, {'sliders[0].active': 2});
}).then(function() {
// Confirm it's been changed:
expect(gd._fullLayout.sliders[0].active).toEqual(2);

// Remove the option:
gd.layout.sliders[0].steps.pop();

// And redraw the plot:
return Plotly.redraw(gd);
}).then(function() {
// The selected option no longer exists, so confirm it's
// been fixed during the process of updating/drawing it:
expect(gd._fullLayout.sliders[0].active).toEqual(0);
}).catch(fail).then(done);
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 test case.

});
});

describe('sliders interactions', function() {
'use strict';

Expand Down