From f7414099931123c5ad317b720e8fa225e727ac4e Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Fri, 28 Oct 2016 17:47:36 -0400 Subject: [PATCH 01/12] Rough cut at slider-populating transform --- lib/index.js | 3 +- lib/magic.js | 11 ++ src/components/sliders/defaults.js | 2 +- src/transforms/magic.js | 158 +++++++++++++++++++++ test/jasmine/tests/transform_magic_test.js | 14 ++ 5 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 lib/magic.js create mode 100644 src/transforms/magic.js create mode 100644 test/jasmine/tests/transform_magic_test.js diff --git a/lib/index.js b/lib/index.js index c578165aadd..76f94834c44 100644 --- a/lib/index.js +++ b/lib/index.js @@ -50,7 +50,8 @@ Plotly.register([ // Plotly.register([ require('./filter'), - require('./groupby') + require('./groupby'), + require('./magic') ]); module.exports = Plotly; diff --git a/lib/magic.js b/lib/magic.js new file mode 100644 index 00000000000..7e5ead15007 --- /dev/null +++ b/lib/magic.js @@ -0,0 +1,11 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +module.exports = require('../src/transforms/magic'); diff --git a/src/components/sliders/defaults.js b/src/components/sliders/defaults.js index b4b3bdce900..aeb7b8d6b93 100644 --- a/src/components/sliders/defaults.js +++ b/src/components/sliders/defaults.js @@ -45,7 +45,7 @@ function sliderDefaults(sliderIn, sliderOut, layoutOut) { var steps = stepsDefaults(sliderIn, sliderOut); - var visible = coerce('visible', steps.length > 0); + var visible = coerce('visible', true);// steps.length > 0); if(!visible) return; coerce('active'); diff --git a/src/transforms/magic.js b/src/transforms/magic.js new file mode 100644 index 00000000000..b8d620030f4 --- /dev/null +++ b/src/transforms/magic.js @@ -0,0 +1,158 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var Lib = require('../lib'); + +var sliderDefault = + +exports.moduleType = 'transform'; + +exports.name = 'magic'; + +exports.attributes = { + sliderindex: { + valType: 'integer', + role: 'info', + dflt: 0 + }, + framegroup: { + valType: 'string', + role: 'info', + description: 'A group name for the generated set of frames' + }, + enabled: { + valType: 'boolean', + role: 'info', + dflt: true, + }, + animationopts: { + valType: 'any', + role: 'info' + } +}; + +exports.supplyDefaults = function(transformIn) { + var transformOut = {}; + + function coerce(attr, dflt) { + return Lib.coerce(transformIn, transformOut, exports.attributes, attr, dflt); + } + + var enabled = coerce('enabled'); + + if(enabled) { + coerce('framegroup'); + coerce('sliderindex'); + coerce('animationopts'); + } + + return transformOut; +}; + +exports.calcTransform = function(gd, trace, opts) { + var framegroup = opts.framegroup; + var i, filterIndex; + + if(!gd.layout.sliders) { + gd.layout.sliders = []; + } + var slider = gd.layout.sliders[opts.sliderindex]; + if(!slider) { + slider = gd.layout.sliders[opts.sliderindex] = {}; + } + + var transforms = gd.data[trace.index].transforms; + + // If there are no transforms, there's nothing to be done: + if(!transforms) return; + + // Find the first filter transform: + for(filterIndex = 0; filterIndex < transforms.length; filterIndex++) { + if(transforms[filterIndex].type === 'filter') { + break; + } + } + + // Looks like no transform was found: + if(filterIndex >= transforms.length) { + return; + } + + var filter = transforms[filterIndex]; + + // Currently only handle target data as arrays: + if(!Array.isArray(filter.target)) { + return; + } + + var frames = gd._transitionData._frames; + var existingFrameIndices = []; + for(i = 0; i < frames.length; i++) { + if(frames[i].group === framegroup) { + existingFrameIndices.push(i); + } + } + + + var groupHash = {}; + var target = filter.target; + for(i = 0; i < target.length; i++) { + groupHash[target[i]] = true; + } + var groups = Object.keys(groupHash); + + var steps = []; + for(i = 0; i < groups.length; i++) { + steps[i] = { + label: groups[i], + value: groups[i], + method: 'animate', + args: [[groups[i]], opts.animationopts] + } + } + + slider.steps = steps; + + for(i = 0; i < groups.length; i++) { + var frame; + if(i < existingFrameIndices.length) { + frame = frames[existingFrameIndices[i]]; + } else { + frame = { + group: framegroup, + }; + frames.push(frame); + } + + // Overwrite the frame. The goal isn't to preserve frames as they were. + // The goal is to avoid affecting *other* frames from outside the group + frame.name = groups[i]; + + if(!frame.data) { + frame.data = []; + } + + if(!frame.data[trace.index]) { + frame.data[trace.index] = {}; + } + + frame.data[trace.index]['transforms[' + filterIndex + '].value'] = [groups[i]]; + } + + var hash = gd._transitionData._frameHash = {}; + for(i = 0; i < gd._transitionData._frames.length; i++) { + frame = gd._transitionData._frames[i]; + if(frame && frame.name) { + hash[frame.name] = frame; + } + } + + return trace; +}; \ No newline at end of file diff --git a/test/jasmine/tests/transform_magic_test.js b/test/jasmine/tests/transform_magic_test.js new file mode 100644 index 00000000000..4d797d9f063 --- /dev/null +++ b/test/jasmine/tests/transform_magic_test.js @@ -0,0 +1,14 @@ +var Plotly = require('@lib/index'); +var Filter = require('@lib/filter'); + +var Plots = require('@src/plots/plots'); +var Lib = require('@src/lib'); + +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var assertDims = require('../assets/assert_dims'); +var assertStyle = require('../assets/assert_style'); + +describe('filter transforms defaults:', function() { + +}); \ No newline at end of file From 4bb7e52b67e3e60c30408728eb0cad2b6957b0f1 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Mon, 31 Oct 2016 10:35:20 -0400 Subject: [PATCH 02/12] Make transform more robust to changes in definitions --- src/transforms/magic.js | 105 +++++++++++++++++++++++++++++----------- 1 file changed, 76 insertions(+), 29 deletions(-) diff --git a/src/transforms/magic.js b/src/transforms/magic.js index b8d620030f4..2c20a36948d 100644 --- a/src/transforms/magic.js +++ b/src/transforms/magic.js @@ -10,8 +10,6 @@ var Lib = require('../lib'); -var sliderDefault = - exports.moduleType = 'transform'; exports.name = 'magic'; @@ -57,6 +55,7 @@ exports.supplyDefaults = function(transformIn) { }; exports.calcTransform = function(gd, trace, opts) { + var frame; var framegroup = opts.framegroup; var i, filterIndex; @@ -92,15 +91,6 @@ exports.calcTransform = function(gd, trace, opts) { return; } - var frames = gd._transitionData._frames; - var existingFrameIndices = []; - for(i = 0; i < frames.length; i++) { - if(frames[i].group === framegroup) { - existingFrameIndices.push(i); - } - } - - var groupHash = {}; var target = filter.target; for(i = 0; i < target.length; i++) { @@ -108,33 +98,90 @@ exports.calcTransform = function(gd, trace, opts) { } var groups = Object.keys(groupHash); - var steps = []; + var step, src; + var steps = slider.steps = slider.steps || []; + var indexLookup = {}; + + for(i = 0; i < steps.length; i++) { + step = steps[i]; + + // Track the indices of the traces that generated a given slider step. + // If all other traces are removed as sources of this slider step, then + // the step should be removed + src = step._srcTraces; + if(src.length === 1 && src[0] === trace.index) { + step._flagForDelete = true; + } + + // Create a lookup table to go from value -> index + indexLookup[steps[i].value] = i; + } + + // Iterate through all unique target values for this slider step: for(i = 0; i < groups.length; i++) { - steps[i] = { - label: groups[i], - value: groups[i], - method: 'animate', - args: [[groups[i]], opts.animationopts] + var label = groups[i]; + + // The index of this step comes from what already exists via the lookup table: + var index = indexLookup[label]; + + // Or if not found, then append it: + if(index === undefined) index = steps.length; + + step = steps[index]; + if(step) { + // Update the existing step: + if(step._srcTraces.indexOf(trace.index) === -1) { + step._srcTraces.push(trace.index); + } + } else { + step = steps[index] = { + _srcTraces: [trace.index], + label: groups[i], + value: groups[i], + method: 'animate', + }; } + + if(!step.args) step.args = [[groups[i]]]; + step.args[1] = Lib.extendDeep(step.args[1] || {}, opts.animationopts || {}); + + // Unset this entirely since this step is needed: + delete step._flagForDelete; } - slider.steps = steps; + // Iterate through the steps and delete any that were: + // 1. only used by this trace, and + // 2. were not encountered above + for(i = steps.length - 1; i >= 0; i--) { + if(steps[i]._flagForDelete) { + steps = steps.splice(i, 1); + } + } + + // Create a lookup table so we can match frames by the group and label + // and update frames accordingly: + var group; + var frames = gd._transitionData._frames; + var frameLookup = {}; + for(i = 0; i < frames.length; i++) { + if(frames[i].group === framegroup) { + frameLookup[frames[i].name] = i; + } + } + // Now create the frames: for(i = 0; i < groups.length; i++) { - var frame; - if(i < existingFrameIndices.length) { - frame = frames[existingFrameIndices[i]]; - } else { + group = groups[i]; + frame = frames[frameLookup[group]]; + + if(!frame) { frame = { - group: framegroup, + name: groups[i], + group: framegroup }; frames.push(frame); } - // Overwrite the frame. The goal isn't to preserve frames as they were. - // The goal is to avoid affecting *other* frames from outside the group - frame.name = groups[i]; - if(!frame.data) { frame.data = []; } @@ -147,8 +194,8 @@ exports.calcTransform = function(gd, trace, opts) { } var hash = gd._transitionData._frameHash = {}; - for(i = 0; i < gd._transitionData._frames.length; i++) { - frame = gd._transitionData._frames[i]; + for(i = 0; i < frames.length; i++) { + frame = frames[i]; if(frame && frame.name) { hash[frame.name] = frame; } From 7ba657c9695d73467c0bda38caa9fa3d7ddd41d8 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Mon, 31 Oct 2016 10:37:41 -0400 Subject: [PATCH 03/12] Minor cleanup for slider transform --- src/components/sliders/defaults.js | 2 +- src/transforms/magic.js | 8 ++++---- test/jasmine/tests/transform_magic_test.js | 7 ++++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/components/sliders/defaults.js b/src/components/sliders/defaults.js index aeb7b8d6b93..b4b3bdce900 100644 --- a/src/components/sliders/defaults.js +++ b/src/components/sliders/defaults.js @@ -45,7 +45,7 @@ function sliderDefaults(sliderIn, sliderOut, layoutOut) { var steps = stepsDefaults(sliderIn, sliderOut); - var visible = coerce('visible', true);// steps.length > 0); + var visible = coerce('visible', steps.length > 0); if(!visible) return; coerce('active'); diff --git a/src/transforms/magic.js b/src/transforms/magic.js index 2c20a36948d..a65569887bc 100644 --- a/src/transforms/magic.js +++ b/src/transforms/magic.js @@ -104,7 +104,7 @@ exports.calcTransform = function(gd, trace, opts) { for(i = 0; i < steps.length; i++) { step = steps[i]; - + // Track the indices of the traces that generated a given slider step. // If all other traces are removed as sources of this slider step, then // the step should be removed @@ -123,10 +123,10 @@ exports.calcTransform = function(gd, trace, opts) { // The index of this step comes from what already exists via the lookup table: var index = indexLookup[label]; - + // Or if not found, then append it: if(index === undefined) index = steps.length; - + step = steps[index]; if(step) { // Update the existing step: @@ -202,4 +202,4 @@ exports.calcTransform = function(gd, trace, opts) { } return trace; -}; \ No newline at end of file +}; diff --git a/test/jasmine/tests/transform_magic_test.js b/test/jasmine/tests/transform_magic_test.js index 4d797d9f063..049dee1180c 100644 --- a/test/jasmine/tests/transform_magic_test.js +++ b/test/jasmine/tests/transform_magic_test.js @@ -1,4 +1,4 @@ -var Plotly = require('@lib/index'); +/* var Plotly = require('@lib/index'); var Filter = require('@lib/filter'); var Plots = require('@src/plots/plots'); @@ -10,5 +10,6 @@ var assertDims = require('../assets/assert_dims'); var assertStyle = require('../assets/assert_style'); describe('filter transforms defaults:', function() { - -}); \ No newline at end of file + +}); +*/ From 43103b349cd891200bd939ca2b5b6d953aaf7d89 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Mon, 31 Oct 2016 11:42:31 -0400 Subject: [PATCH 04/12] Major rework of slider-generating transform --- src/transforms/magic.js | 107 ++++++++++++++++++++++------------------ 1 file changed, 60 insertions(+), 47 deletions(-) diff --git a/src/transforms/magic.js b/src/transforms/magic.js index a65569887bc..d9ef1342f93 100644 --- a/src/transforms/magic.js +++ b/src/transforms/magic.js @@ -57,7 +57,7 @@ exports.supplyDefaults = function(transformIn) { exports.calcTransform = function(gd, trace, opts) { var frame; var framegroup = opts.framegroup; - var i, filterIndex; + var i, j, filterIndex; if(!gd.layout.sliders) { gd.layout.sliders = []; @@ -67,6 +67,8 @@ exports.calcTransform = function(gd, trace, opts) { slider = gd.layout.sliders[opts.sliderindex] = {}; } + if(!slider._allGroups) slider._allGroups = {}; + var transforms = gd.data[trace.index].transforms; // If there are no transforms, there's nothing to be done: @@ -95,31 +97,28 @@ exports.calcTransform = function(gd, trace, opts) { var target = filter.target; for(i = 0; i < target.length; i++) { groupHash[target[i]] = true; + slider._allGroups[target[i]] = true; } - var groups = Object.keys(groupHash); + var allGroups = Object.keys(slider._allGroups); - var step, src; + var step; var steps = slider.steps = slider.steps || []; var indexLookup = {}; for(i = 0; i < steps.length; i++) { step = steps[i]; - // Track the indices of the traces that generated a given slider step. - // If all other traces are removed as sources of this slider step, then - // the step should be removed - src = step._srcTraces; - if(src.length === 1 && src[0] === trace.index) { - step._flagForDelete = true; - } - // Create a lookup table to go from value -> index indexLookup[steps[i].value] = i; } + // Duplicate the container array of steps so that we can reorder them + // according to the merged steps: + var existingSteps = steps.slice(0); + // Iterate through all unique target values for this slider step: - for(i = 0; i < groups.length; i++) { - var label = groups[i]; + for(i = 0; i < allGroups.length; i++) { + var label = allGroups[i]; // The index of this step comes from what already exists via the lookup table: var index = indexLookup[label]; @@ -127,35 +126,14 @@ exports.calcTransform = function(gd, trace, opts) { // Or if not found, then append it: if(index === undefined) index = steps.length; - step = steps[index]; - if(step) { - // Update the existing step: - if(step._srcTraces.indexOf(trace.index) === -1) { - step._srcTraces.push(trace.index); - } - } else { - step = steps[index] = { - _srcTraces: [trace.index], - label: groups[i], - value: groups[i], - method: 'animate', - }; - } + step = steps[i] = existingSteps[index] || { + label: label, + value: label, + args: [[label]], + method: 'animate', + }; - if(!step.args) step.args = [[groups[i]]]; step.args[1] = Lib.extendDeep(step.args[1] || {}, opts.animationopts || {}); - - // Unset this entirely since this step is needed: - delete step._flagForDelete; - } - - // Iterate through the steps and delete any that were: - // 1. only used by this trace, and - // 2. were not encountered above - for(i = steps.length - 1; i >= 0; i--) { - if(steps[i]._flagForDelete) { - steps = steps.splice(i, 1); - } } // Create a lookup table so we can match frames by the group and label @@ -163,36 +141,71 @@ exports.calcTransform = function(gd, trace, opts) { var group; var frames = gd._transitionData._frames; var frameLookup = {}; + var existingFrameIndices = []; for(i = 0; i < frames.length; i++) { if(frames[i].group === framegroup) { - frameLookup[frames[i].name] = i; + frameLookup[frames[i].name] = frames[i]; + existingFrameIndices.push(i); } } + // Need to know *all* traces affected by this so that we set filters + // even if they're not affected by this particular group + var allTraceFilterLookup = {}; + var frameIndices = {}; + var frameIndex; + // Now create the frames: - for(i = 0; i < groups.length; i++) { - group = groups[i]; - frame = frames[frameLookup[group]]; + for(i = 0; i < allGroups.length; i++) { + group = allGroups[i]; + frame = frameLookup[group]; + + frameIndex = existingFrameIndices[i]; + if(frameIndex === undefined) { + frameIndex = frames.length; + } + frameIndices[group] = frameIndex; if(!frame) { frame = { - name: groups[i], + name: allGroups[i], group: framegroup }; - frames.push(frame); } + // Overwrite the frame at this position with the frame corresponding + // to this frame of allGroups: + frames[frameIndex] = frame; + if(!frame.data) { frame.data = []; } + if(!frame._filterIndexByTrace) frame._filterIndexByTrace = {}; + frame._filterIndexByTrace[trace.index] = filterIndex; + allTraceFilterLookup = Lib.extendFlat(allTraceFilterLookup, frame._filterIndexByTrace); + if(!frame.data[trace.index]) { frame.data[trace.index] = {}; } + } - frame.data[trace.index]['transforms[' + filterIndex + '].value'] = [groups[i]]; + // Construct the property updates: + for(i = 0; i < allGroups.length; i++) { + group = allGroups[i]; + frameIndex = frameIndices[group]; + frame = frames[frameIndex]; + frame.data = []; + frame.traces = Object.keys(allTraceFilterLookup); + for(j = 0; j < frame.traces.length; j++) { + frame.traces[j] = parseInt(frame.traces[j]); + var traceIndex = frame.traces[j]; + frame.data[j] = {}; + frame.data[j]['transforms[' + allTraceFilterLookup[traceIndex] + '].value'] = [group]; + } } + // Reconstruct the frame hash, just to be sure it's all good: var hash = gd._transitionData._frameHash = {}; for(i = 0; i < frames.length; i++) { frame = frames[i]; From 60ed9e8d9618b4ffd1e1ddc6e9bd5c4162020e45 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Mon, 31 Oct 2016 12:49:08 -0400 Subject: [PATCH 05/12] Split filter-generating transform into two phases --- src/transforms/magic.js | 48 ++++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/src/transforms/magic.js b/src/transforms/magic.js index d9ef1342f93..2e825329977 100644 --- a/src/transforms/magic.js +++ b/src/transforms/magic.js @@ -54,22 +54,25 @@ exports.supplyDefaults = function(transformIn) { return transformOut; }; -exports.calcTransform = function(gd, trace, opts) { - var frame; - var framegroup = opts.framegroup; - var i, j, filterIndex; +exports.transform = function(dataOut, extras) { + var i, filterIndex; - if(!gd.layout.sliders) { - gd.layout.sliders = []; + var transform = extras.transform; + var trace = extras.fullTrace; + var layout = extras.layout; + + if(!layout.sliders) { + layout.sliders = []; } - var slider = gd.layout.sliders[opts.sliderindex]; + + var slider = layout.sliders[transform.sliderindex]; + if(!slider) { - slider = gd.layout.sliders[opts.sliderindex] = {}; + slider = layout.sliders[transform.sliderindex] = {}; } - if(!slider._allGroups) slider._allGroups = {}; - - var transforms = gd.data[trace.index].transforms; + if(!slider._allGroupHash) slider._allGroupHash = {}; + var transforms = trace.transforms; // If there are no transforms, there's nothing to be done: if(!transforms) return; @@ -86,6 +89,7 @@ exports.calcTransform = function(gd, trace, opts) { return; } + slider._filterIndex = filterIndex; var filter = transforms[filterIndex]; // Currently only handle target data as arrays: @@ -97,9 +101,9 @@ exports.calcTransform = function(gd, trace, opts) { var target = filter.target; for(i = 0; i < target.length; i++) { groupHash[target[i]] = true; - slider._allGroups[target[i]] = true; + slider._allGroupHash[target[i]] = true; } - var allGroups = Object.keys(slider._allGroups); + var allGroups = Object.keys(slider._allGroupHash); var step; var steps = slider.steps = slider.steps || []; @@ -133,9 +137,23 @@ exports.calcTransform = function(gd, trace, opts) { method: 'animate', }; - step.args[1] = Lib.extendDeep(step.args[1] || {}, opts.animationopts || {}); + step.args[1] = Lib.extendDeep(step.args[1] || {}, transform.animationopts || {}); } + return dataOut; +}; + +exports.calcTransform = function(gd, trace, opts) { + var frame; + var framegroup = opts.framegroup; + var i, j; + var slider = gd.layout.sliders[opts.sliderindex]; + var allGroups = Object.keys(slider._allGroupHash); + var transforms = gd.data[trace.index].transforms; + + // If there are no transforms, there's nothing to be done: + if(!transforms) return; + // Create a lookup table so we can match frames by the group and label // and update frames accordingly: var group; @@ -182,7 +200,7 @@ exports.calcTransform = function(gd, trace, opts) { } if(!frame._filterIndexByTrace) frame._filterIndexByTrace = {}; - frame._filterIndexByTrace[trace.index] = filterIndex; + frame._filterIndexByTrace[trace.index] = slider._filterIndex; allTraceFilterLookup = Lib.extendFlat(allTraceFilterLookup, frame._filterIndexByTrace); if(!frame.data[trace.index]) { From 544c73cc35cb410165b8bff97fc74bb885990182 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Mon, 31 Oct 2016 13:14:22 -0400 Subject: [PATCH 06/12] Rename to populate-slider --- lib/index.js | 2 +- lib/{magic.js => populate-slider.js} | 2 +- src/transforms/{magic.js => populate-slider.js} | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename lib/{magic.js => populate-slider.js} (76%) rename src/transforms/{magic.js => populate-slider.js} (98%) diff --git a/lib/index.js b/lib/index.js index 76f94834c44..33809659600 100644 --- a/lib/index.js +++ b/lib/index.js @@ -51,7 +51,7 @@ Plotly.register([ Plotly.register([ require('./filter'), require('./groupby'), - require('./magic') + require('./populate-slider') ]); module.exports = Plotly; diff --git a/lib/magic.js b/lib/populate-slider.js similarity index 76% rename from lib/magic.js rename to lib/populate-slider.js index 7e5ead15007..60d321fb14a 100644 --- a/lib/magic.js +++ b/lib/populate-slider.js @@ -8,4 +8,4 @@ 'use strict'; -module.exports = require('../src/transforms/magic'); +module.exports = require('../src/transforms/populate-slider'); diff --git a/src/transforms/magic.js b/src/transforms/populate-slider.js similarity index 98% rename from src/transforms/magic.js rename to src/transforms/populate-slider.js index 2e825329977..d125f412f42 100644 --- a/src/transforms/magic.js +++ b/src/transforms/populate-slider.js @@ -12,7 +12,7 @@ var Lib = require('../lib'); exports.moduleType = 'transform'; -exports.name = 'magic'; +exports.name = 'populate-slider'; exports.attributes = { sliderindex: { @@ -46,8 +46,8 @@ exports.supplyDefaults = function(transformIn) { var enabled = coerce('enabled'); if(enabled) { - coerce('framegroup'); coerce('sliderindex'); + coerce('framegroup', 'slider-' + transformOut.sliderindex + '-group'); coerce('animationopts'); } From 74be1b022c037b7fed18099642a4e8b56f6a3070 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 2 Nov 2016 09:19:41 -0400 Subject: [PATCH 07/12] Add test file --- ...st.js => transform_populate_slider_test.js} | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) rename test/jasmine/tests/{transform_magic_test.js => transform_populate_slider_test.js} (55%) diff --git a/test/jasmine/tests/transform_magic_test.js b/test/jasmine/tests/transform_populate_slider_test.js similarity index 55% rename from test/jasmine/tests/transform_magic_test.js rename to test/jasmine/tests/transform_populate_slider_test.js index 049dee1180c..b3ff74a6528 100644 --- a/test/jasmine/tests/transform_magic_test.js +++ b/test/jasmine/tests/transform_populate_slider_test.js @@ -1,4 +1,4 @@ -/* var Plotly = require('@lib/index'); +var Plotly = require('@lib/index'); var Filter = require('@lib/filter'); var Plots = require('@src/plots/plots'); @@ -10,6 +10,20 @@ var assertDims = require('../assets/assert_dims'); var assertStyle = require('../assets/assert_style'); describe('filter transforms defaults:', function() { + var gd; + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + it('passes', function(done) { + Plotly.plot(gd, [{ + x: [1, 2, 3] + }]).then(done); + }); }); -*/ From 64644707368a725270165c16c40f8c6468276df6 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Mon, 7 Nov 2016 17:43:25 -0500 Subject: [PATCH 08/12] Sorta rewrite the transform --- src/plots/plots.js | 7 + src/transforms/populate-slider.js | 165 +++++-- .../tests/transform_populate_slider_test.js | 452 +++++++++++++++++- 3 files changed, 572 insertions(+), 52 deletions(-) diff --git a/src/plots/plots.js b/src/plots/plots.js index 1254552dcec..42b600cece3 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1090,6 +1090,8 @@ plots.purge = function(gd) { // remove modebar if(fullLayout._modeBar) fullLayout._modeBar.destroy(); + gd._transitionData._interruptCallbacks.length = 0; + if(gd._transitionData && gd._transitionData._animationRaf) { window.cancelAnimationFrame(gd._transitionData._animationRaf); } @@ -1784,6 +1786,11 @@ plots.transition = function(gd, data, layout, traces, frameOpts, transitionOpts) } function completeTransition(callback) { + // This a simple workaround for tests which purge the graph before animations + // have completed. That's not a very common case, so this is the simplest + // fix. + if(!gd._transitionData) return; + flushCallbacks(gd._transitionData._interruptCallbacks); return Promise.resolve().then(function() { diff --git a/src/transforms/populate-slider.js b/src/transforms/populate-slider.js index d125f412f42..7377bbecd88 100644 --- a/src/transforms/populate-slider.js +++ b/src/transforms/populate-slider.js @@ -9,16 +9,30 @@ 'use strict'; var Lib = require('../lib'); +var supplySliderDefaults = require('../components/sliders/defaults'); exports.moduleType = 'transform'; exports.name = 'populate-slider'; exports.attributes = { + filterindex: { + valType: 'integer', + role: 'info', + dflt: 0, + description: [ + 'Array index of the filter transform. If not provided, it will use the', + 'first available filter transform for this trace' + ].join(' ') + }, sliderindex: { valType: 'integer', role: 'info', - dflt: 0 + dflt: 0, + description: [ + 'Array index of the slider component. If not provided, it will create', + 'a new slider in the plot layout' + ].join(' ') }, framegroup: { valType: 'string', @@ -29,6 +43,7 @@ exports.attributes = { valType: 'boolean', role: 'info', dflt: true, + description: 'Whether the transform is ignored or not.' }, animationopts: { valType: 'any', @@ -47,6 +62,7 @@ exports.supplyDefaults = function(transformIn) { if(enabled) { coerce('sliderindex'); + coerce('filterindex'); coerce('framegroup', 'slider-' + transformOut.sliderindex + '-group'); coerce('animationopts'); } @@ -60,6 +76,7 @@ exports.transform = function(dataOut, extras) { var transform = extras.transform; var trace = extras.fullTrace; var layout = extras.layout; + var transforms = trace.transforms; if(!layout.sliders) { layout.sliders = []; @@ -71,54 +88,120 @@ exports.transform = function(dataOut, extras) { slider = layout.sliders[transform.sliderindex] = {}; } - if(!slider._allGroupHash) slider._allGroupHash = {}; - var transforms = trace.transforms; + var infos = slider._autoStepInfo; + if(!infos) infos = slider._autoStepInfo = {}; - // If there are no transforms, there's nothing to be done: - if(!transforms) return; + var info = infos[dataOut[0].index]; + if(!info) info = infos[dataOut[0].index] = {}; - // Find the first filter transform: - for(filterIndex = 0; filterIndex < transforms.length; filterIndex++) { - if(transforms[filterIndex].type === 'filter') { - break; + if(!transform.filterindex) { + // Find the first filter transform: + for(filterIndex = 0; filterIndex < transforms.length; filterIndex++) { + if(transforms[filterIndex].type === 'filter') { + break; + } } + } else { + filterIndex = transform.filterindex; } // Looks like no transform was found: - if(filterIndex >= transforms.length) { - return; + if(filterIndex >= transforms.length || !transforms[filterIndex] || transforms[filterIndex].type !== 'filter') { + return dataOut; } - slider._filterIndex = filterIndex; + info._transforms = transforms; + info._transform = transform; + info._filterIndex = filterIndex; + info._filter = transforms[filterIndex]; + info._trace = dataOut[0]; + info._transformIndex = transforms.indexOf(transform); + + return dataOut; +}; + +exports.calcTransform = function(gd, trace, opts) { + var i, j; + + var layout = gd.layout; + var slider = layout.sliders[opts.sliderindex]; + var transforms = trace.transforms; + + var info = slider._autoStepInfo[trace.index]; + + if(!info) return trace; + + var filterIndex = info._filterIndex; + + // If there was no filter from above, then bail: + if(!filterIndex) return trace; + var filter = transforms[filterIndex]; - // Currently only handle target data as arrays: - if(!Array.isArray(filter.target)) { - return; + // Compute the groups pulled in by this trace: + info._groups = {}; + var target = filter.target; + var groupHash = {}; + if(trace.visible) { + for(i = 0; i < target.length; i++) { + groupHash[target[i]] = true; + } } + info._groups = Object.keys(groupHash); + + // Check through all traces to make sure the groups + // still apply: + var tTransform; + var traceIndices = Object.keys(slider._autoStepInfo); + var allGroups = {}; + for(i = 0; i < traceIndices.length; i++) { + var tInfo = slider._autoStepInfo[i]; + + if(!tInfo) continue; + + // This is a little crazy, but we need to update references to the trace, + // otherwise they tend to be outdated: + var t = tInfo._trace; + + // The referene to the trace seems outdate so that we need to check the visibility + // of the *newly default-supplied trace: + var curTrace = gd._fullData[t.index]; + if(!curTrace || !curTrace.visible || !curTrace.transforms) continue; + + // If any of these conditions (and perhaps more) apply, then the + // trace's groups should no longer rapply + if(t.visible && t.transforms && t.transforms[tInfo._transformIndex]) { + var tTransform = t.transforms[tInfo._transformIndex]; + var tFilter = t.transforms[tInfo._filterIndex]; + } - var groupHash = {}; - var target = filter.target; - for(i = 0; i < target.length; i++) { - groupHash[target[i]] = true; - slider._allGroupHash[target[i]] = true; + // There's no exit event, so we just have to look through these and remove + // a trace's groups if it appears to be no longer present or active: + if(!tTransform || !tFilter || !Array.isArray(tFilter.target)) { + delete slider._autoStepInfo[i]; + } else { + for(j = 0; j < tFilter.target.length; j++) { + allGroups[tFilter.target[j]] = true; + } + } } - var allGroups = Object.keys(slider._allGroupHash); + + var allGroups = Object.keys(allGroups); + console.log('allGroups:', allGroups); var step; - var steps = slider.steps = slider.steps || []; - var indexLookup = {}; + var steps = slider.steps; + if(!steps) steps = slider.steps = []; + var indexLookup = {}; for(i = 0; i < steps.length; i++) { - step = steps[i]; - - // Create a lookup table to go from value -> index indexLookup[steps[i].value] = i; } // Duplicate the container array of steps so that we can reorder them // according to the merged steps: var existingSteps = steps.slice(0); + steps.length = 0; // Iterate through all unique target values for this slider step: for(i = 0; i < allGroups.length; i++) { @@ -137,22 +220,11 @@ exports.transform = function(dataOut, extras) { method: 'animate', }; - step.args[1] = Lib.extendDeep(step.args[1] || {}, transform.animationopts || {}); + step.args[1] = Lib.extendDeep(step.args[1] || {}, info._transform.animationopts || {}); } - return dataOut; -}; - -exports.calcTransform = function(gd, trace, opts) { var frame; var framegroup = opts.framegroup; - var i, j; - var slider = gd.layout.sliders[opts.sliderindex]; - var allGroups = Object.keys(slider._allGroupHash); - var transforms = gd.data[trace.index].transforms; - - // If there are no transforms, there's nothing to be done: - if(!transforms) return; // Create a lookup table so we can match frames by the group and label // and update frames accordingly: @@ -199,10 +271,6 @@ exports.calcTransform = function(gd, trace, opts) { frame.data = []; } - if(!frame._filterIndexByTrace) frame._filterIndexByTrace = {}; - frame._filterIndexByTrace[trace.index] = slider._filterIndex; - allTraceFilterLookup = Lib.extendFlat(allTraceFilterLookup, frame._filterIndexByTrace); - if(!frame.data[trace.index]) { frame.data[trace.index] = {}; } @@ -214,15 +282,18 @@ exports.calcTransform = function(gd, trace, opts) { frameIndex = frameIndices[group]; frame = frames[frameIndex]; frame.data = []; - frame.traces = Object.keys(allTraceFilterLookup); - for(j = 0; j < frame.traces.length; j++) { - frame.traces[j] = parseInt(frame.traces[j]); - var traceIndex = frame.traces[j]; + frame.traces = []; + for(j = 0; j < traceIndices.length; j++) { + frame.traces[j] = parseInt(traceIndices[j]); + var tInfo = slider._autoStepInfo[j]; + if(!tInfo) continue; frame.data[j] = {}; - frame.data[j]['transforms[' + allTraceFilterLookup[traceIndex] + '].value'] = [group]; + frame.data[j]['transforms[' + tInfo._filterIndex + '].value'] = [group]; } } + supplySliderDefaults(gd.layout, gd._fullLayout); + // Reconstruct the frame hash, just to be sure it's all good: var hash = gd._transitionData._frameHash = {}; for(i = 0; i < frames.length; i++) { diff --git a/test/jasmine/tests/transform_populate_slider_test.js b/test/jasmine/tests/transform_populate_slider_test.js index b3ff74a6528..872e1f3ea00 100644 --- a/test/jasmine/tests/transform_populate_slider_test.js +++ b/test/jasmine/tests/transform_populate_slider_test.js @@ -1,5 +1,6 @@ var Plotly = require('@lib/index'); var Filter = require('@lib/filter'); +var constants = require('@src/components/sliders/constants'); var Plots = require('@src/plots/plots'); var Lib = require('@src/lib'); @@ -8,8 +9,9 @@ var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var assertDims = require('../assets/assert_dims'); var assertStyle = require('../assets/assert_style'); +var fail = require('../assets/fail_test'); -describe('filter transforms defaults:', function() { +describe('populate-slider transform', function() { var gd; beforeEach(function() { @@ -21,9 +23,449 @@ describe('filter transforms defaults:', function() { destroyGraphDiv(); }); - it('passes', function(done) { - Plotly.plot(gd, [{ - x: [1, 2, 3] - }]).then(done); + /* describe('invalid usage without a filter transform', function () { + var slider, frames; + + beforeEach(function (done) { + Plotly.plot(gd, [{ + x: [1, 2, 3, 4], + y: [5, 6, 7, 8], + ids: ['a', 'b', 'a', 'b'], + transforms: [{type: 'populate-slider'}] + }]).then(done); + }); + + it('ignores the transform', function() { + expect(true).toBe(true); + }); + }); + + describe('a single trace grouped into two categories', function () { + var slider, frames; + var animationopts; + + beforeEach(function (done) { + animationopts = {frame: {duration: 0}, transition: {duration: 0}}; + Plotly.plot(gd, [{ + x: [1, 2, 3, 4], + y: [5, 6, 7, 8], + ids: ['a', 'b', 'a', 'b'], + transforms: [{ + type: 'populate-slider', + animationopts: animationopts + }, { + type: 'filter', + target: ['g1', 'g1', 'g2', 'g2'] + }] + }]).then(function () { + slider = gd.layout.sliders[0]; + frames = gd._transitionData._frames; + }).then(done); + }); + + it('adds a slider to layout', function() { + expect(gd.layout.sliders.length).toEqual(1); + }); + + it('adds two steps to the slider', function() { + expect(slider.steps.length).toEqual(2); + expect(slider.steps[0].label).toEqual('g1'); + expect(slider.steps[1].label).toEqual('g2'); + }); + + it('sets the API commands to change the frame', function() { + expect(slider.steps[0].method).toEqual('animate'); + expect(slider.steps[1].method).toEqual('animate'); + + expect(slider.steps[0].args).toEqual([['g1'], animationopts]); + expect(slider.steps[1].args).toEqual([['g2'], animationopts]); + }); + + it('creates two frames', function () { + expect(frames.length).toEqual(2); + + // First frame: + expect(frames[0].name).toEqual('g1'); + expect(frames[0].group).toEqual('slider-0-group'); + expect(frames[0].data).toEqual([{'transforms[1].value': ['g1']}]); + expect(frames[0].traces).toEqual([0]); + + // Second frame: + expect(frames[1].name).toEqual('g2'); + expect(frames[1].group).toEqual('slider-0-group'); + expect(frames[1].data).toEqual([{'transforms[1].value': ['g2']}]); + expect(frames[1].traces).toEqual([0]); + + // Has updated the frame hash: + expect(Object.keys(gd._transitionData._frameHash)).toEqual(['g1', 'g2']); + }); + + it('filters the data', function (done) { + clickFirstSlider(1).then(function () { + // Click the second step and confirm udpated: + expect(gd._fullLayout._currentFrame).toEqual('g2'); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 3, y: 7})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 4, y: 8})); + + return clickFirstSlider(0); + }).then(function () { + // Click the first step and confirm udpated: + expect(gd._fullLayout._currentFrame).toEqual('g1'); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 1, y: 5})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 2, y: 6})); + }).catch(fail).then(done); + }); + + it('updates the slider steps when data changes', function (done) { + expect(slider.steps.length).toEqual(2); + + Plotly.restyle(gd, {'transforms[1].target': [['g1', 'g1', 'g1', 'g1']]}).then(function () { + expect(slider.steps.length).toEqual(1); + + return Plotly.restyle(gd, 'transforms[1].target', [['g1', 'g2', 'g3', 'g4']], [0]); + }).then(function () { + expect(slider.steps.length).toEqual(4); + }).catch(fail).then(done); + }); + }); + + describe('two traces', function () { + var slider, frames; + var animationopts; + + beforeEach(function (done) { + animationopts = {frame: {duration: 0}, transition: {duration: 0}}; + Plotly.plot(gd, [{ + x: [1, 2, 3, 4], + y: [5, 6, 7, 8], + ids: ['a', 'b', 'a', 'b'], + transforms: [{ + type: 'populate-slider', + animationopts: animationopts + }, { + type: 'filter', + target: ['g1', 'g1', 'g2', 'g2'] + }] + }, { + x: [9, 10, 11, 12], + y: [13, 14, 15, 16], + ids: ['a', 'b', 'a', 'b'], + transforms: [{ + type: 'populate-slider', + animationopts: animationopts + }, { + type: 'filter', + target: ['g2', 'g3', 'g2', 'g3'] + }] + }]).then(function () { + slider = gd.layout.sliders[0]; + frames = gd._transitionData._frames; + }).then(done); + }); + + it('merges groups', function () { + expect(slider.steps.length).toEqual(3); + expect(slider.steps[0].args).toEqual([['g1'], animationopts]); + expect(slider.steps[1].args).toEqual([['g2'], animationopts]); + expect(slider.steps[2].args).toEqual([['g3'], animationopts]); + }); + + it('filters the first trace', function (done) { + clickFirstSlider(1).then(function () { + // Click the second step and confirm udpated: + expect(gd._fullLayout._currentFrame).toEqual('g3'); + expect(gd.calcdata[0].length).toEqual(1); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: false, y: false})); + + return clickFirstSlider(0); + }).then(function () { + // Click the first step and confirm udpated: + expect(gd._fullLayout._currentFrame).toEqual('g1'); + expect(gd.calcdata[0].length).toEqual(2); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 1, y: 5})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 2, y: 6})); + + return clickFirstSlider(0.5); + }).then(function () { + // Click the second step and confirm udpated: + expect(gd._fullLayout._currentFrame).toEqual('g2'); + expect(gd.calcdata[0].length).toEqual(2); + expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 3, y: 7})); + expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 4, y: 8})); + }).catch(fail).then(done); + }); + + it('filters the second trace', function (done) { + clickFirstSlider(1).then(function () { + // Click the second step and confirm udpated: + expect(gd._fullLayout._currentFrame).toEqual('g3'); + expect(gd.calcdata[1].length).toEqual(2); + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 10, y: 14})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 12, y: 16})); + + return clickFirstSlider(0); + }).then(function () { + // Click the first step and confirm udpated: + expect(gd._fullLayout._currentFrame).toEqual('g1'); + expect(gd.calcdata[1].length).toEqual(1); + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: false, y: false})); + + return clickFirstSlider(0.5); + }).then(function () { + // Click the second step and confirm udpated: + expect(gd._fullLayout._currentFrame).toEqual('g2'); + expect(gd.calcdata[1].length).toEqual(2); + expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 9, y: 13})); + expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 11, y: 15})); + }).catch(fail).then(done); + }); + }); + + function clickFirstSlider(fraction, eventName) { + var firstGroup = gd._fullLayout._infolayer.select('.' + constants.railTouchRectClass); + var firstGrip = gd._fullLayout._infolayer.select('.' + constants.gripRectClass); + var railNode = firstGroup.node(); + var touchRect = railNode.getBoundingClientRect(); + + // Dispatch a click on the right side of the bar: + railNode.dispatchEvent(new MouseEvent('mousedown', { + clientY: touchRect.top + 5, + clientX: touchRect.left + touchRect.width * fraction, + })); + + // TODO: fix this race condition with a one-time event listener (requires separate PR): + return new Promise(function(resolve, reject) { + gd.once(eventName || 'plotly_animated', resolve); + + // Set a timeout of 1000ms before it fails: + setTimeout(reject, 1000); + }); + }*/ + + + describe('with a realistic two-trace setup', function() { + var slider, frames; + var animationopts; + + beforeEach(function(done) { + animationopts = {frame: {duration: 0}, transition: {duration: 0}}; + + Plotly.plot(gd, { + 'data': [{ + 'name': 'Asia', + 'mode': 'markers', + 'x': [ + 30.332, 53.832, 39.348, 41.366, 50.54896, + 31.997, 56.923, 41.216, 43.415, 44.50136, + 34.02, 59.923, 43.453, 45.415, + ], + 'y': [ + 820.8530296, 11635.79945, 661.6374577, 434.0383364, 575.9870009, + 853.10071, 12753.27514, 686.3415538, 496.9136476, 487.6740183, + 836.1971382, 14804.6727, 721.1860862, 523.4323142, + ], + 'ids': [ + 'Afghanistan', 'Bahrain', 'Bangladesh', 'Cambodia', 'China', + 'Afghanistan', 'Bahrain', 'Bangladesh', 'Cambodia', 'China', + 'Afghanistan', 'Bahrain', 'Bangladesh', 'Cambodia', + ], + 'text': [ + 'Afghanistan', 'Bahrain', 'Bangladesh', 'Cambodia', 'China', + 'Afghanistan', 'Bahrain', 'Bangladesh', 'Cambodia', 'China', + 'Afghanistan', 'Bahrain', 'Bangladesh', 'Cambodia', + ], + 'marker': { + 'sizemode': 'area', + 'sizeref': 200000, + 'size': [ + 9240934, 138655, 51365468, 5322536, 637408000, + 10267083, 171863, 56839289, 6083619, 665770000, + 11537966, 202182, 62821884, 6960067, + ] + }, + 'transforms': [{ + 'type': 'populate-slider', + 'sliderindex': 0, + 'framegroup': 'frames-by-year', + 'animationopts': { + 'mode': 'immediate', + 'frame': {'redraw': false}, + 'transition': {'duration': 400} + } + }, { + 'type': 'filter', + 'target': [ + '1957', '1957', '1957', '1957', '1957', + '1962', '1962', '1962', '1962', '1962', + '1967', '1967', '1967', '1967', + ], + 'operation': '{}', + 'value': ['1952'] + }] + }, + { + 'name': 'Europe', + 'mode': 'markers', + 'x': [ + 55.23, 66.8, 68, 53.82, 59.6, + 59.28, 67.48, 69.24, 58.45, 66.61, + 66.22, 70.14, 70.94, 70.42, + 67.69, 70.63, 71.44, 67.45, 70.9 + ], + 'y': [ + 1601.056136, 6137.076492, 8343.105127, 973.5331948, 2444.286648, + 1942.284244, 8842.59803, 9714.960623, 1353.989176, 3008.670727, + 2760.196931, 12834.6024, 13149.04119, 5577.0028, + 3313.422188, 16661.6256, 16672.14356, 2860.16975, 6597.494398 + ], + 'ids': [ + 'Albania', 'Austria', 'Belgium', 'Bosnia and Herzegovina', 'Bulgaria', + 'Albania', 'Austria', 'Belgium', 'Bosnia and Herzegovina', 'Bulgaria', + 'Albania', 'Austria', 'Belgium', 'Bulgaria', + 'Albania', 'Austria', 'Belgium', 'Bosnia and Herzegovina', 'Bulgaria' + ], + 'text': [ + 'Albania', 'Austria', 'Belgium', 'Bosnia and Herzegovina', 'Bulgaria', + 'Albania', 'Austria', 'Belgium', 'Bosnia and Herzegovina', 'Bulgaria', + 'Albania', 'Austria', 'Belgium', 'Bulgaria', + 'Albania', 'Austria', 'Belgium', 'Bosnia and Herzegovina', 'Bulgaria' + ], + 'marker': { + 'sizemode': 'area', + 'sizeref': 200000, + 'size': [ + 1282697, 6927772, 8730405, 2791000, 7274900, + 1476505, 6965860, 8989111, 3076000, 7651254, + 1984060, 7376998, 9556500, 8310226, + 2263554, 7544201, 9709100, 3819000, 8576200 + ] + }, + 'transforms': [{ + 'type': 'populate-slider', + 'sliderindex': 0, + 'framegroup': 'frames-by-year', + 'animationopts': { + 'mode': 'immediate', + 'frame': {'redraw': false}, + 'transition': {'duration': 0} + } + }, { + 'type': 'filter', + 'target': [ + '1952', '1952', '1952', '1952', '1952', + '1957', '1957', '1957', '1957', '1957', + '1967', '1967', '1967', '1967', + '1972', '1972', '1972', '1972', '1972' + ], + 'operation': '{}', + 'value': ['1952'] + }] + }], + 'layout': { + 'width': window.innerWidth, + 'height': window.innerHeight, + 'title': 'Life Expectancy vs. GDP Per Capita', + 'xaxis': { + 'autorange': false, + 'range': [20, 80] + }, + 'yaxis': { + 'type': 'log', + 'autorange': false, + 'range': [2, 5] + }, + 'updatemenus': [{ + 'type': 'buttons', + 'transition': {'duration': 0}, + 'showactive': false, + 'yanchor': 'top', + 'xanchor': 'right', + 'y': 0, + 'x': -0.02, + 'pad': {'t': 50}, + 'buttons': [{ + 'label': 'Play', + 'method': 'animate', + 'args': ['frames-by-year', { + 'mode': 'immediate', + 'frame': {'duration': 0, 'redraw': false}, + 'transition': {'duration': 0} + }] + }] + }], + 'sliders': [{ + 'yanchor': 'top', + 'y': 0, + 'pad': {'t': 20} + }] + } + }).then(done); + }); + + it('creates slider options', function() { + expect(gd._fullLayout.sliders[0].steps.length).toBe(5); + }); + + it('removes slider options when the first trace is hidden', function(done) { + Plotly.restyle(gd, {visible: [false]}, [0]).then(function() { + expect(gd._fullLayout.sliders[0].steps.length).toBe(4); + }).catch(fail).then(done); + }); + + it('removes slider options when the second trace is hidden', function(done) { + Plotly.restyle(gd, {visible: [true, false]}, [0, 1]).then(function() { + expect(gd._fullLayout.sliders[0].steps.length).toBe(3); + }).catch(fail).then(done); + }); + + it('adds slider options when traces are re-shown', function(done) { + Plotly.restyle(gd, {visible: [true, false]}, [0, 1]).then(function() { + expect(gd._fullLayout.sliders[0].steps.length).toBe(3); + return Plotly.restyle(gd, {visible: [true, true]}, [0, 1]); + }).then(function() { + expect(gd._fullLayout.sliders[0].steps.length).toBe(5); + }).catch(fail).then(done); + }); + + it('removes slider options when the target data changes', function(done) { + // Set both target data sets to all one value: + Plotly.restyle(gd, {'transforms[1].target': [[ + '1957', '1957', '1957', '1957', '1957', '1957', '1957', '1957', '1957', '1957', + '1957', '1957', '1957', '1957', + ], [ + '1957', '1957', '1957', '1957', '1957', '1957', '1957', '1957', '1957', '1957', + '1957', '1957', '1957', '1957', '1957', '1957', '1957', '1957', '1957' + ]]}, [0, 1]).then(function() { + // Confirm the presence of only one option: + expect(gd._fullLayout.sliders[0].steps.length).toBe(1); + }).catch(fail).then(done); + }); + + it('adds slider options when the target data changes', function(done) { + console.log('\n\n'); + Plotly.restyle(gd, {'transforms[1].target': [[ + '1951', '1952', '1953', '1954', '1955', '1956', '1957', '1957', '1957', '1957', + '1957', '1957', '1957', '1957', '1957', '1957', '1957', '1957', '1957' + ]]}, [1]).then(function() { + console.log(gd._fullLayout.sliders[0].steps); + expect(gd._fullLayout.sliders[0].steps.length).toBe(9); + }).catch(fail).then(done); + }); + + it('removes slider options when the transform is disabled', function(done) { + console.log('done with initial\n\n'); + Plotly.restyle(gd, {'transforms[0].enabled': [false]}, [1]).then(function() { + expect(gd._fullLayout.sliders[0].steps.length).toBe(3); + }).catch(fail).then(done); + }); + + // TODO: We can't currently handle this case. The slider will not disappear + // if all traces are hidden. :( + /* it('removes slider options when all traces are hidden', function (done) { + Plotly.restyle(gd, {visible: [false, false]}, [0, 1]).then(function () { + expect(gd._fullLayout.sliders[0].steps.length).toBe(0); + }).catch(fail).then(done); + });*/ + }); }); From 98fcc2353c23ca29a283a7a69f4df27339d14a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 7 Nov 2016 18:37:34 -0500 Subject: [PATCH 09/12] [PoC] add supplyLayoutDefaults handler for transform modules --- src/plots/plots.js | 17 +++++++++++++++++ src/transforms/filter.js | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/src/plots/plots.js b/src/plots/plots.js index 42b600cece3..335f92c5f1a 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -364,6 +364,9 @@ plots.sendDataToCloud = function(gd) { // gd._fullLayout._basePlotModules // is a list of all the plot modules required to draw the plot. // +// gd._fullLayout._transformModules +// is a list of all the transform modules invoked. +// plots.supplyDefaults = function(gd) { var oldFullLayout = gd._fullLayout || {}, newFullLayout = gd._fullLayout = {}, @@ -646,6 +649,8 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { basePlotModules = fullLayout._basePlotModules = [], cnt = 0; + fullLayout._transformModules = []; + function pushModule(fullTrace) { dataOut.push(fullTrace); @@ -863,6 +868,8 @@ function supplyTransformDefaults(traceIn, traceOut, layout) { transformOut = _module.supplyDefaults(transformIn, traceOut, layout, traceIn); transformOut.type = type; transformOut._module = _module; + + Lib.pushUnique(layout._transformModules, _module); } else { transformOut = Lib.extendFlat({}, transformIn); @@ -1063,6 +1070,16 @@ plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData) { } } + // transform module layout defaults + var transformModules = layoutOut._transformModules; + for(i = 0; i < transformModules.length; i++) { + _module = transformModules[i]; + + if(_module.supplyLayoutDefaults) { + _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData); + } + } + // should FX be a component? Plotly.Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); diff --git a/src/transforms/filter.js b/src/transforms/filter.js index 8c9a23b7071..b827592ac13 100644 --- a/src/transforms/filter.js +++ b/src/transforms/filter.js @@ -120,6 +120,10 @@ exports.supplyDefaults = function(transformIn) { return transformOut; }; +exports.supplyLayoutDefaults = function() { + console.log('supplyLayoutDefaults !!!') +}; + exports.calcTransform = function(gd, trace, opts) { if(!opts.enabled) return; From c324b4bacb076a7f0e59c282a43a1d6cea8b6967 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 8 Nov 2016 11:47:32 -0500 Subject: [PATCH 10/12] Rewrite the slider-populating transform --- src/plot_api/plot_api.js | 2 + src/plots/plots.js | 22 +- src/transforms/filter.js | 4 - src/transforms/populate-slider.js | 279 ++++++++---------- .../tests/transform_populate_slider_test.js | 116 +++----- 5 files changed, 180 insertions(+), 243 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index cbb4ad51e10..ae8161e9927 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -117,6 +117,8 @@ Plotly.plot = function(gd, data, layout, config) { if(!gd.layout || graphWasEmpty) gd.layout = helpers.cleanLayout(layout); + if(!gd._transitionData) Plots.createTransitionData(gd); + // if the user is trying to drag the axes, allow new data and layout // to come in but don't allow a replot. if(gd._dragging && !gd._transitioning) { diff --git a/src/plots/plots.js b/src/plots/plots.js index 335f92c5f1a..6192e2cb758 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -438,7 +438,7 @@ plots.supplyDefaults = function(gd) { } // finally, fill in the pieces of layout that may need to look at data - plots.supplyLayoutModuleDefaults(newLayout, newFullLayout, newFullData); + plots.supplyLayoutModuleDefaults(newLayout, newFullLayout, newFullData, gd._transitionData); // TODO remove in v2.0.0 // add has-plot-type refs to fullLayout for backward compatibility @@ -477,12 +477,6 @@ plots.supplyDefaults = function(gd) { (gd.calcdata[i][0] || {}).trace = trace; } } - - // Create all the storage space for frames, but only if doesn't already - // exist: - if(!gd._transitionData) { - plots.createTransitionData(gd); - } }; // Create storage for all of the data related to frames and transitions: @@ -1039,12 +1033,12 @@ function calculateReservedMargins(margins) { return resultingMargin; } -plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData) { +plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData, transitionData) { var i, _module; // can't be be part of basePlotModules loop // in order to handle the orphan axes case - Plotly.Axes.supplyLayoutDefaults(layoutIn, layoutOut, fullData); + Plotly.Axes.supplyLayoutDefaults(layoutIn, layoutOut, fullData, transitionData); // base plot module layout defaults var basePlotModules = layoutOut._basePlotModules; @@ -1056,7 +1050,7 @@ plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData) { // e.g. gl2d does not have a layout-defaults step if(_module.supplyLayoutDefaults) { - _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData); + _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData, transitionData); } } @@ -1066,7 +1060,7 @@ plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData) { _module = modules[i]; if(_module.supplyLayoutDefaults) { - _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData); + _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData, transitionData); } } @@ -1076,19 +1070,19 @@ plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData) { _module = transformModules[i]; if(_module.supplyLayoutDefaults) { - _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData); + _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData, transitionData); } } // should FX be a component? - Plotly.Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); + Plotly.Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData, transitionData); var components = Object.keys(Registry.componentsRegistry); for(i = 0; i < components.length; i++) { _module = Registry.componentsRegistry[components[i]]; if(_module.supplyLayoutDefaults) { - _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData); + _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData, transitionData); } } }; diff --git a/src/transforms/filter.js b/src/transforms/filter.js index b827592ac13..8c9a23b7071 100644 --- a/src/transforms/filter.js +++ b/src/transforms/filter.js @@ -120,10 +120,6 @@ exports.supplyDefaults = function(transformIn) { return transformOut; }; -exports.supplyLayoutDefaults = function() { - console.log('supplyLayoutDefaults !!!') -}; - exports.calcTransform = function(gd, trace, opts) { if(!opts.enabled) return; diff --git a/src/transforms/populate-slider.js b/src/transforms/populate-slider.js index 7377bbecd88..2046455d1f6 100644 --- a/src/transforms/populate-slider.js +++ b/src/transforms/populate-slider.js @@ -70,166 +70,110 @@ exports.supplyDefaults = function(transformIn) { return transformOut; }; -exports.transform = function(dataOut, extras) { - var i, filterIndex; - - var transform = extras.transform; - var trace = extras.fullTrace; - var layout = extras.layout; - var transforms = trace.transforms; - - if(!layout.sliders) { - layout.sliders = []; - } - - var slider = layout.sliders[transform.sliderindex]; - - if(!slider) { - slider = layout.sliders[transform.sliderindex] = {}; - } - - var infos = slider._autoStepInfo; - if(!infos) infos = slider._autoStepInfo = {}; - - var info = infos[dataOut[0].index]; - if(!info) info = infos[dataOut[0].index] = {}; - - if(!transform.filterindex) { - // Find the first filter transform: - for(filterIndex = 0; filterIndex < transforms.length; filterIndex++) { - if(transforms[filterIndex].type === 'filter') { - break; - } - } - } else { - filterIndex = transform.filterindex; - } - - // Looks like no transform was found: - if(filterIndex >= transforms.length || !transforms[filterIndex] || transforms[filterIndex].type !== 'filter') { - return dataOut; - } - - info._transforms = transforms; - info._transform = transform; - info._filterIndex = filterIndex; - info._filter = transforms[filterIndex]; - info._trace = dataOut[0]; - info._transformIndex = transforms.indexOf(transform); - +exports.transform = function(dataOut) { return dataOut; }; -exports.calcTransform = function(gd, trace, opts) { - var i, j; - - var layout = gd.layout; - var slider = layout.sliders[opts.sliderindex]; - var transforms = trace.transforms; - - var info = slider._autoStepInfo[trace.index]; - - if(!info) return trace; - - var filterIndex = info._filterIndex; +function computeGroups(fullData) { + var i, j, nTrans, trace, transforms, addTransform, filterTransform, target, sliderindex, filterindex; + var allGroupHash = {}; + var addTransforms = {}; + // iterate through *all* traces looking for anything with an populate-slider transform + for(i = 0; i < fullData.length; i++) { + // Bail out if no transforms: + trace = fullData[i]; + if(!trace || !trace.visible) continue; + transforms = fullData[i].transforms; + if(!transforms) continue; + nTrans = transforms.length; + + // Find the add-slider transform for this trace: + for(j = 0; j < nTrans; j++) { + addTransform = transforms[j]; + if(addTransform.type === exports.name) break; + } - // If there was no filter from above, then bail: - if(!filterIndex) return trace; + // Bail out if either no add transform or not enabled: + if(j === nTrans || !addTransform.enabled) continue; - var filter = transforms[filterIndex]; + sliderindex = addTransform.sliderindex; + filterindex = addTransform.filterindex; - // Compute the groups pulled in by this trace: - info._groups = {}; - var target = filter.target; - var groupHash = {}; - if(trace.visible) { - for(i = 0; i < target.length; i++) { - groupHash[target[i]] = true; + // Find the filter transform for this slider: + if(!filterindex) { + for(filterindex = 0; filterindex < nTrans; filterindex++) { + filterTransform = transforms[filterindex]; + if(filterTransform.type === 'filter') break; + } + if(filterindex === nTrans) continue; + addTransform.filterindex = filterindex; } - } - info._groups = Object.keys(groupHash); - // Check through all traces to make sure the groups - // still apply: - var tTransform; - var traceIndices = Object.keys(slider._autoStepInfo); - var allGroups = {}; - for(i = 0; i < traceIndices.length; i++) { - var tInfo = slider._autoStepInfo[i]; + // Bail out if this transform is disabled or not handled: + if(!filterTransform.enabled) continue; + if(!Array.isArray((target = filterTransform.target))) continue; + addTransform._filterTransform = filterTransform; - if(!tInfo) continue; + // Store the add transform for later use: + if(!addTransforms[sliderindex]) addTransforms[sliderindex] = {}; + addTransforms[sliderindex][trace.index] = addTransform; - // This is a little crazy, but we need to update references to the trace, - // otherwise they tend to be outdated: - var t = tInfo._trace; - - // The referene to the trace seems outdate so that we need to check the visibility - // of the *newly default-supplied trace: - var curTrace = gd._fullData[t.index]; - if(!curTrace || !curTrace.visible || !curTrace.transforms) continue; - - // If any of these conditions (and perhaps more) apply, then the - // trace's groups should no longer rapply - if(t.visible && t.transforms && t.transforms[tInfo._transformIndex]) { - var tTransform = t.transforms[tInfo._transformIndex]; - var tFilter = t.transforms[tInfo._filterIndex]; - } - - // There's no exit event, so we just have to look through these and remove - // a trace's groups if it appears to be no longer present or active: - if(!tTransform || !tFilter || !Array.isArray(tFilter.target)) { - delete slider._autoStepInfo[i]; - } else { - for(j = 0; j < tFilter.target.length; j++) { - allGroups[tFilter.target[j]] = true; - } + if(!allGroupHash[sliderindex]) allGroupHash[sliderindex] = {}; + for(j = 0; j < target.length; j++) { + allGroupHash[sliderindex][target[j]] = true; } } - var allGroups = Object.keys(allGroups); - console.log('allGroups:', allGroups); + return { + bySlider: allGroupHash, + transformsByTrace: addTransforms + }; +} - var step; - var steps = slider.steps; - if(!steps) steps = slider.steps = []; - - var indexLookup = {}; - for(i = 0; i < steps.length; i++) { - indexLookup[steps[i].value] = i; +function createSteps(idx, slider, groups, transforms) { + var i, transform; + var traceIndices = Object.keys(transforms); + var animationopts = {}; + for(i = 0; i < traceIndices.length; i++) { + transform = transforms[traceIndices[i]]; + animationopts = Lib.extendDeep(animationopts, transform.animationopts); + } + if(Array.isArray(slider.steps)) { + slider.steps.length = 0; + } else { + slider.steps = []; } - - // Duplicate the container array of steps so that we can reorder them - // according to the merged steps: - var existingSteps = steps.slice(0); - steps.length = 0; // Iterate through all unique target values for this slider step: - for(i = 0; i < allGroups.length; i++) { - var label = allGroups[i]; + for(i = 0; i < groups.length; i++) { + var label = groups[i]; + var frameName = 'slider-' + idx + '-' + label; - // The index of this step comes from what already exists via the lookup table: - var index = indexLookup[label]; - - // Or if not found, then append it: - if(index === undefined) index = steps.length; - - step = steps[i] = existingSteps[index] || { + slider.steps[i] = { label: label, value: label, - args: [[label]], + args: [[frameName], animationopts], method: 'animate', }; + } +} - step.args[1] = Lib.extendDeep(step.args[1] || {}, info._transform.animationopts || {}); +function computeFrameGroup(sliderindex, transforms) { + var i, framegroup; + for(i = 0; i < transforms.length; i++) { + framegroup = transforms[i].framegroup; + if(framegroup) return framegroup; } - var frame; - var framegroup = opts.framegroup; + return 'populate-slider-group-' + sliderindex; +} - // Create a lookup table so we can match frames by the group and label - // and update frames accordingly: - var group; - var frames = gd._transitionData._frames; +function createFrames(sliderindex, framegroup, groups, transforms, transitionData) { + var i, j, group, frame, frameIndex, transform; + + var traceIndices = Object.keys(transforms); + var frameIndices = {}; + var frames = transitionData._frames; var frameLookup = {}; var existingFrameIndices = []; for(i = 0; i < frames.length; i++) { @@ -239,15 +183,9 @@ exports.calcTransform = function(gd, trace, opts) { } } - // Need to know *all* traces affected by this so that we set filters - // even if they're not affected by this particular group - var allTraceFilterLookup = {}; - var frameIndices = {}; - var frameIndex; - // Now create the frames: - for(i = 0; i < allGroups.length; i++) { - group = allGroups[i]; + for(i = 0; i < groups.length; i++) { + group = groups[i]; frame = frameLookup[group]; frameIndex = existingFrameIndices[i]; @@ -258,50 +196,79 @@ exports.calcTransform = function(gd, trace, opts) { if(!frame) { frame = { - name: allGroups[i], + name: 'slider-' + sliderindex + '-' + groups[i], group: framegroup }; } // Overwrite the frame at this position with the frame corresponding - // to this frame of allGroups: + // to this frame of groups: frames[frameIndex] = frame; if(!frame.data) { frame.data = []; } - if(!frame.data[trace.index]) { - frame.data[trace.index] = {}; + for(j = 0; j < traceIndices.length; j++) { + if(frame.data[traceIndices[j]]) continue; + frame.data[traceIndices[j]] = {}; } } // Construct the property updates: - for(i = 0; i < allGroups.length; i++) { - group = allGroups[i]; + for(i = 0; i < groups.length; i++) { + group = groups[i]; frameIndex = frameIndices[group]; frame = frames[frameIndex]; frame.data = []; frame.traces = []; for(j = 0; j < traceIndices.length; j++) { + transform = transforms[traceIndices[j]]; frame.traces[j] = parseInt(traceIndices[j]); - var tInfo = slider._autoStepInfo[j]; - if(!tInfo) continue; frame.data[j] = {}; - frame.data[j]['transforms[' + tInfo._filterIndex + '].value'] = [group]; + frame.data[j]['transforms[' + transform.filterindex + '].value'] = [group]; } } - supplySliderDefaults(gd.layout, gd._fullLayout); + recomputeFrameHash(transitionData); +} - // Reconstruct the frame hash, just to be sure it's all good: - var hash = gd._transitionData._frameHash = {}; - for(i = 0; i < frames.length; i++) { +function recomputeFrameHash(transitionData) { + var frame; + var frames = transitionData._frames; + var hash = transitionData._frameHash = {}; + for(var i = 0; i < frames.length; i++) { frame = frames[i]; if(frame && frame.name) { hash[frame.name] = frame; } } +} + +exports.supplyLayoutDefaults = function(layoutIn, layoutOut, fullData, transitionData) { + var sliders, i, sliderindex, slider, transforms, framegroup; + + var groups = computeGroups(fullData); + var sliderIndices = Object.keys(groups.bySlider); + + // Bail out if there are no options: + if(!sliderIndices.length) return layoutOut; + + if(!layoutOut.sliders) { + sliders = layoutOut.sliders = []; + } + + for(i = 0; i < sliderIndices.length; i++) { + sliderindex = sliderIndices[i]; + slider = sliders[sliderindex] = sliders[sliderindex] || {}; + transforms = groups.transformsByTrace[sliderindex]; + groups = Object.keys(groups.bySlider[sliderindex]); + + framegroup = computeFrameGroup(sliderindex, transforms); + + createSteps(sliderindex, slider, groups, transforms); + createFrames(sliderindex, framegroup, groups, transforms, transitionData); + } - return trace; + supplySliderDefaults(layoutOut, layoutIn); }; diff --git a/test/jasmine/tests/transform_populate_slider_test.js b/test/jasmine/tests/transform_populate_slider_test.js index 872e1f3ea00..6a11f3a9d87 100644 --- a/test/jasmine/tests/transform_populate_slider_test.js +++ b/test/jasmine/tests/transform_populate_slider_test.js @@ -1,14 +1,8 @@ var Plotly = require('@lib/index'); -var Filter = require('@lib/filter'); var constants = require('@src/components/sliders/constants'); -var Plots = require('@src/plots/plots'); -var Lib = require('@src/lib'); - var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); -var assertDims = require('../assets/assert_dims'); -var assertStyle = require('../assets/assert_style'); var fail = require('../assets/fail_test'); describe('populate-slider transform', function() { @@ -23,10 +17,8 @@ describe('populate-slider transform', function() { destroyGraphDiv(); }); - /* describe('invalid usage without a filter transform', function () { - var slider, frames; - - beforeEach(function (done) { + describe('invalid usage without a filter transform', function() { + beforeEach(function(done) { Plotly.plot(gd, [{ x: [1, 2, 3, 4], y: [5, 6, 7, 8], @@ -40,11 +32,11 @@ describe('populate-slider transform', function() { }); }); - describe('a single trace grouped into two categories', function () { + describe('a single trace grouped into two categories', function() { var slider, frames; var animationopts; - beforeEach(function (done) { + beforeEach(function(done) { animationopts = {frame: {duration: 0}, transition: {duration: 0}}; Plotly.plot(gd, [{ x: [1, 2, 3, 4], @@ -57,7 +49,7 @@ describe('populate-slider transform', function() { type: 'filter', target: ['g1', 'g1', 'g2', 'g2'] }] - }]).then(function () { + }]).then(function() { slider = gd.layout.sliders[0]; frames = gd._transitionData._frames; }).then(done); @@ -77,63 +69,62 @@ describe('populate-slider transform', function() { expect(slider.steps[0].method).toEqual('animate'); expect(slider.steps[1].method).toEqual('animate'); - expect(slider.steps[0].args).toEqual([['g1'], animationopts]); - expect(slider.steps[1].args).toEqual([['g2'], animationopts]); + expect(slider.steps[0].args).toEqual([['slider-0-g1'], animationopts]); + expect(slider.steps[1].args).toEqual([['slider-0-g2'], animationopts]); }); - it('creates two frames', function () { + it('creates two frames', function() { expect(frames.length).toEqual(2); // First frame: - expect(frames[0].name).toEqual('g1'); - expect(frames[0].group).toEqual('slider-0-group'); + expect(frames[0].name).toEqual('slider-0-g1'); + expect(frames[0].group).toEqual('populate-slider-group-0'); expect(frames[0].data).toEqual([{'transforms[1].value': ['g1']}]); expect(frames[0].traces).toEqual([0]); // Second frame: - expect(frames[1].name).toEqual('g2'); - expect(frames[1].group).toEqual('slider-0-group'); + expect(frames[1].name).toEqual('slider-0-g2'); + expect(frames[1].group).toEqual('populate-slider-group-0'); expect(frames[1].data).toEqual([{'transforms[1].value': ['g2']}]); expect(frames[1].traces).toEqual([0]); // Has updated the frame hash: - expect(Object.keys(gd._transitionData._frameHash)).toEqual(['g1', 'g2']); + expect(Object.keys(gd._transitionData._frameHash)).toEqual(['slider-0-g1', 'slider-0-g2']); }); - it('filters the data', function (done) { - clickFirstSlider(1).then(function () { + it('filters the data', function(done) { + clickFirstSlider(1).then(function() { // Click the second step and confirm udpated: - expect(gd._fullLayout._currentFrame).toEqual('g2'); + expect(gd._fullLayout._currentFrame).toEqual('slider-0-g2'); expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 3, y: 7})); expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 4, y: 8})); return clickFirstSlider(0); - }).then(function () { + }).then(function() { // Click the first step and confirm udpated: - expect(gd._fullLayout._currentFrame).toEqual('g1'); + expect(gd._fullLayout._currentFrame).toEqual('slider-0-g1'); expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 1, y: 5})); expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 2, y: 6})); }).catch(fail).then(done); }); - it('updates the slider steps when data changes', function (done) { + it('updates the slider steps when data changes', function(done) { expect(slider.steps.length).toEqual(2); - Plotly.restyle(gd, {'transforms[1].target': [['g1', 'g1', 'g1', 'g1']]}).then(function () { - expect(slider.steps.length).toEqual(1); + Plotly.restyle(gd, {'transforms[1].target': [['g1', 'g1', 'g1', 'g1']]}).then(function() { + expect(gd._fullLayout.sliders[0].steps.length).toEqual(1); return Plotly.restyle(gd, 'transforms[1].target', [['g1', 'g2', 'g3', 'g4']], [0]); - }).then(function () { - expect(slider.steps.length).toEqual(4); + }).then(function() { + expect(gd._fullLayout.sliders[0].steps.length).toEqual(4); }).catch(fail).then(done); }); }); - describe('two traces', function () { - var slider, frames; + describe('two traces', function() { var animationopts; - beforeEach(function (done) { + beforeEach(function(done) { animationopts = {frame: {duration: 0}, transition: {duration: 0}}; Plotly.plot(gd, [{ x: [1, 2, 3, 4], @@ -157,63 +148,61 @@ describe('populate-slider transform', function() { type: 'filter', target: ['g2', 'g3', 'g2', 'g3'] }] - }]).then(function () { - slider = gd.layout.sliders[0]; - frames = gd._transitionData._frames; - }).then(done); + }]).then(done); }); - it('merges groups', function () { + it('merges groups', function() { + var slider = gd._fullLayout.sliders[0]; expect(slider.steps.length).toEqual(3); - expect(slider.steps[0].args).toEqual([['g1'], animationopts]); - expect(slider.steps[1].args).toEqual([['g2'], animationopts]); - expect(slider.steps[2].args).toEqual([['g3'], animationopts]); + expect(slider.steps[0].args).toEqual([['slider-0-g1'], animationopts]); + expect(slider.steps[1].args).toEqual([['slider-0-g2'], animationopts]); + expect(slider.steps[2].args).toEqual([['slider-0-g3'], animationopts]); }); - it('filters the first trace', function (done) { - clickFirstSlider(1).then(function () { + it('filters the first trace', function(done) { + clickFirstSlider(1).then(function() { // Click the second step and confirm udpated: - expect(gd._fullLayout._currentFrame).toEqual('g3'); + expect(gd._fullLayout._currentFrame).toEqual('slider-0-g3'); expect(gd.calcdata[0].length).toEqual(1); expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: false, y: false})); return clickFirstSlider(0); - }).then(function () { + }).then(function() { // Click the first step and confirm udpated: - expect(gd._fullLayout._currentFrame).toEqual('g1'); + expect(gd._fullLayout._currentFrame).toEqual('slider-0-g1'); expect(gd.calcdata[0].length).toEqual(2); expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 1, y: 5})); expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 2, y: 6})); return clickFirstSlider(0.5); - }).then(function () { + }).then(function() { // Click the second step and confirm udpated: - expect(gd._fullLayout._currentFrame).toEqual('g2'); + expect(gd._fullLayout._currentFrame).toEqual('slider-0-g2'); expect(gd.calcdata[0].length).toEqual(2); expect(gd.calcdata[0][0]).toEqual(jasmine.objectContaining({x: 3, y: 7})); expect(gd.calcdata[0][1]).toEqual(jasmine.objectContaining({x: 4, y: 8})); }).catch(fail).then(done); }); - it('filters the second trace', function (done) { - clickFirstSlider(1).then(function () { + it('filters the second trace', function(done) { + clickFirstSlider(1).then(function() { // Click the second step and confirm udpated: - expect(gd._fullLayout._currentFrame).toEqual('g3'); + expect(gd._fullLayout._currentFrame).toEqual('slider-0-g3'); expect(gd.calcdata[1].length).toEqual(2); expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 10, y: 14})); expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 12, y: 16})); return clickFirstSlider(0); - }).then(function () { + }).then(function() { // Click the first step and confirm udpated: - expect(gd._fullLayout._currentFrame).toEqual('g1'); + expect(gd._fullLayout._currentFrame).toEqual('slider-0-g1'); expect(gd.calcdata[1].length).toEqual(1); expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: false, y: false})); return clickFirstSlider(0.5); - }).then(function () { + }).then(function() { // Click the second step and confirm udpated: - expect(gd._fullLayout._currentFrame).toEqual('g2'); + expect(gd._fullLayout._currentFrame).toEqual('slider-0-g2'); expect(gd.calcdata[1].length).toEqual(2); expect(gd.calcdata[1][0]).toEqual(jasmine.objectContaining({x: 9, y: 13})); expect(gd.calcdata[1][1]).toEqual(jasmine.objectContaining({x: 11, y: 15})); @@ -223,7 +212,6 @@ describe('populate-slider transform', function() { function clickFirstSlider(fraction, eventName) { var firstGroup = gd._fullLayout._infolayer.select('.' + constants.railTouchRectClass); - var firstGrip = gd._fullLayout._infolayer.select('.' + constants.gripRectClass); var railNode = firstGroup.node(); var touchRect = railNode.getBoundingClientRect(); @@ -240,16 +228,11 @@ describe('populate-slider transform', function() { // Set a timeout of 1000ms before it fails: setTimeout(reject, 1000); }); - }*/ + } describe('with a realistic two-trace setup', function() { - var slider, frames; - var animationopts; - beforeEach(function(done) { - animationopts = {frame: {duration: 0}, transition: {duration: 0}}; - Plotly.plot(gd, { 'data': [{ 'name': 'Asia', @@ -442,26 +425,21 @@ describe('populate-slider transform', function() { }); it('adds slider options when the target data changes', function(done) { - console.log('\n\n'); Plotly.restyle(gd, {'transforms[1].target': [[ '1951', '1952', '1953', '1954', '1955', '1956', '1957', '1957', '1957', '1957', '1957', '1957', '1957', '1957', '1957', '1957', '1957', '1957', '1957' ]]}, [1]).then(function() { - console.log(gd._fullLayout.sliders[0].steps); expect(gd._fullLayout.sliders[0].steps.length).toBe(9); }).catch(fail).then(done); }); it('removes slider options when the transform is disabled', function(done) { - console.log('done with initial\n\n'); Plotly.restyle(gd, {'transforms[0].enabled': [false]}, [1]).then(function() { expect(gd._fullLayout.sliders[0].steps.length).toBe(3); }).catch(fail).then(done); }); - // TODO: We can't currently handle this case. The slider will not disappear - // if all traces are hidden. :( - /* it('removes slider options when all traces are hidden', function (done) { + /* it('removes slider options when all traces are hidden', function (done) { Plotly.restyle(gd, {visible: [false, false]}, [0, 1]).then(function () { expect(gd._fullLayout.sliders[0].steps.length).toBe(0); }).catch(fail).then(done); From 9eaa9d7ab3987f1ac5c9cc76b661acf69a592acc Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 8 Nov 2016 16:08:18 -0500 Subject: [PATCH 11/12] Fix up issue with populate-slider transform --- src/transforms/populate-slider.js | 57 +++++++------------ .../tests/transform_populate_slider_test.js | 15 +++++ 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/transforms/populate-slider.js b/src/transforms/populate-slider.js index 2046455d1f6..be1db1f1fbc 100644 --- a/src/transforms/populate-slider.js +++ b/src/transforms/populate-slider.js @@ -63,7 +63,7 @@ exports.supplyDefaults = function(transformIn) { if(enabled) { coerce('sliderindex'); coerce('filterindex'); - coerce('framegroup', 'slider-' + transformOut.sliderindex + '-group'); + coerce('framegroup'); coerce('animationopts'); } @@ -160,8 +160,9 @@ function createSteps(idx, slider, groups, transforms) { function computeFrameGroup(sliderindex, transforms) { var i, framegroup; - for(i = 0; i < transforms.length; i++) { - framegroup = transforms[i].framegroup; + var keys = Object.keys(transforms); + for(i = 0; i < keys.length; i++) { + framegroup = transforms[keys[i]].framegroup; if(framegroup) return framegroup; } @@ -174,11 +175,9 @@ function createFrames(sliderindex, framegroup, groups, transforms, transitionDat var traceIndices = Object.keys(transforms); var frameIndices = {}; var frames = transitionData._frames; - var frameLookup = {}; var existingFrameIndices = []; for(i = 0; i < frames.length; i++) { - if(frames[i].group === framegroup) { - frameLookup[frames[i].name] = frames[i]; + if(frames[i] === null || frames[i].group === framegroup) { existingFrameIndices.push(i); } } @@ -186,7 +185,10 @@ function createFrames(sliderindex, framegroup, groups, transforms, transitionDat // Now create the frames: for(i = 0; i < groups.length; i++) { group = groups[i]; - frame = frameLookup[group]; + frame = frames[existingFrameIndices[i]] || { + name: 'slider-' + sliderindex + '-' + groups[i], + group: framegroup + }; frameIndex = existingFrameIndices[i]; if(frameIndex === undefined) { @@ -194,34 +196,12 @@ function createFrames(sliderindex, framegroup, groups, transforms, transitionDat } frameIndices[group] = frameIndex; - if(!frame) { - frame = { - name: 'slider-' + sliderindex + '-' + groups[i], - group: framegroup - }; - } - // Overwrite the frame at this position with the frame corresponding // to this frame of groups: frames[frameIndex] = frame; - if(!frame.data) { - frame.data = []; - } - - for(j = 0; j < traceIndices.length; j++) { - if(frame.data[traceIndices[j]]) continue; - frame.data[traceIndices[j]] = {}; - } - } - - // Construct the property updates: - for(i = 0; i < groups.length; i++) { - group = groups[i]; - frameIndex = frameIndices[group]; - frame = frames[frameIndex]; - frame.data = []; - frame.traces = []; + if(!frame.data) frame.data = []; + if(!frame.traces) frame.traces = []; for(j = 0; j < traceIndices.length; j++) { transform = transforms[traceIndices[j]]; frame.traces[j] = parseInt(traceIndices[j]); @@ -230,6 +210,11 @@ function createFrames(sliderindex, framegroup, groups, transforms, transitionDat } } + // null out the remaining frames that were created by this transform + for(i = groups.length; i < existingFrameIndices.length; i++) { + frames[existingFrameIndices[i]] = null; + } + recomputeFrameHash(transitionData); } @@ -252,11 +237,9 @@ exports.supplyLayoutDefaults = function(layoutIn, layoutOut, fullData, transitio var sliderIndices = Object.keys(groups.bySlider); // Bail out if there are no options: - if(!sliderIndices.length) return layoutOut; + if(!sliderIndices.length) return layoutIn; - if(!layoutOut.sliders) { - sliders = layoutOut.sliders = []; - } + sliders = layoutIn.sliders = layoutIn.sliders || []; for(i = 0; i < sliderIndices.length; i++) { sliderindex = sliderIndices[i]; @@ -270,5 +253,7 @@ exports.supplyLayoutDefaults = function(layoutIn, layoutOut, fullData, transitio createFrames(sliderindex, framegroup, groups, transforms, transitionData); } - supplySliderDefaults(layoutOut, layoutIn); + supplySliderDefaults(layoutIn, layoutOut); + + return layoutIn; }; diff --git a/test/jasmine/tests/transform_populate_slider_test.js b/test/jasmine/tests/transform_populate_slider_test.js index 6a11f3a9d87..0a73650dfb0 100644 --- a/test/jasmine/tests/transform_populate_slider_test.js +++ b/test/jasmine/tests/transform_populate_slider_test.js @@ -108,6 +108,21 @@ describe('populate-slider transform', function() { }).catch(fail).then(done); }); + it('cleans up frames', function(done) { + var frames = gd._transitionData._frames; + expect(frames).toEqual([ + {name: 'slider-0-g1', group: 'populate-slider-group-0', data: [{'transforms[1].value': ['g1']}], traces: [0]}, + {name: 'slider-0-g2', group: 'populate-slider-group-0', data: [{'transforms[1].value': ['g2']}], traces: [0]} + ]); + + Plotly.restyle(gd, {'transforms[1].target': [['g1', 'g1', 'g1', 'g1']]}).then(function() { + expect(frames).toEqual([ + {name: 'slider-0-g1', group: 'populate-slider-group-0', data: [{'transforms[1].value': ['g1']}], traces: [0]}, + null, + ]); + }).then(done); + }); + it('updates the slider steps when data changes', function(done) { expect(slider.steps.length).toEqual(2); From 47a8929ff219d7c888bad030e7df47a9af99bbce Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 8 Nov 2016 16:25:31 -0500 Subject: [PATCH 12/12] Fix redundancies --- src/plots/plots.js | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/plots/plots.js b/src/plots/plots.js index 72c6f2daf63..c0ea5774798 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1041,7 +1041,7 @@ plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData, trans // can't be be part of basePlotModules loop // in order to handle the orphan axes case - Plotly.Axes.supplyLayoutDefaults(layoutIn, layoutOut, fullData, transitionData); + Plotly.Axes.supplyLayoutDefaults(layoutIn, layoutOut, fullData); // base plot module layout defaults var basePlotModules = layoutOut._basePlotModules; @@ -1053,7 +1053,7 @@ plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData, trans // e.g. gl2d does not have a layout-defaults step if(_module.supplyLayoutDefaults) { - _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData, transitionData); + _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData); } } @@ -1063,17 +1063,7 @@ plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData, trans _module = modules[i]; if(_module.supplyLayoutDefaults) { - _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData, transitionData); - } - } - - // transform module layout defaults - var transformModules = layoutOut._transformModules; - for(i = 0; i < transformModules.length; i++) { - _module = transformModules[i]; - - if(_module.supplyLayoutDefaults) { - _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData, transitionData); + _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData); } } @@ -1088,14 +1078,14 @@ plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData, trans } // should FX be a component? - Plotly.Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData, transitionData); + Plotly.Fx.supplyLayoutDefaults(layoutIn, layoutOut, fullData); var components = Object.keys(Registry.componentsRegistry); for(i = 0; i < components.length; i++) { _module = Registry.componentsRegistry[components[i]]; if(_module.supplyLayoutDefaults) { - _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData, transitionData); + _module.supplyLayoutDefaults(layoutIn, layoutOut, fullData); } } };