diff --git a/src/plot_api/helpers.js b/src/plot_api/helpers.js index 26c03943d6a..9e6baae57e7 100644 --- a/src/plot_api/helpers.js +++ b/src/plot_api/helpers.js @@ -215,7 +215,6 @@ function cleanAxRef(container, attr) { // Make a few changes to the data right away // before it gets used for anything exports.cleanData = function(data, existingData) { - // Enforce unique IDs var suids = [], // seen uids --- so we can weed out incoming repeats uids = data.concat(Array.isArray(existingData) ? existingData : []) @@ -348,18 +347,35 @@ exports.cleanData = function(data, existingData) { if(!Lib.isPlainObject(transform)) continue; - if(transform.type === 'filter') { - if(transform.filtersrc) { - transform.target = transform.filtersrc; - delete transform.filtersrc; - } + switch(transform.type) { + case 'filter': + if(transform.filtersrc) { + transform.target = transform.filtersrc; + delete transform.filtersrc; + } - if(transform.calendar) { - if(!transform.valuecalendar) { - transform.valuecalendar = transform.calendar; + if(transform.calendar) { + if(!transform.valuecalendar) { + transform.valuecalendar = transform.calendar; + } + delete transform.calendar; + } + break; + + case 'groupby': + if(transform.style && !Array.isArray(transform.style)) { + var prevStyles = transform.style; + var styleKeys = Object.keys(prevStyles); + + transform.style = []; + for(var j = 0; j < styleKeys.length; j++) { + transform.style.push({ + target: styleKeys[j], + value: prevStyles[styleKeys[j]] + }); + } } - delete transform.calendar; - } + break; } } } diff --git a/src/transforms/groupby.js b/src/transforms/groupby.js index 0cd744529ca..dba79dfc878 100644 --- a/src/transforms/groupby.js +++ b/src/transforms/groupby.js @@ -35,14 +35,25 @@ exports.attributes = { ].join(' ') }, style: { - valType: 'any', - dflt: {}, - description: [ - 'Sets each group style.', - 'For example, with `groups` set to *[\'a\', \'b\', \'a\', \'b\']*', - 'and `style` set to *{ a: { marker: { color: \'red\' } }}', - 'marker points in group *\'a\'* will be drawn in red.' - ].join(' ') + _isLinkedToArray: 'style', + target: { + valType: 'string', + role: 'info', + description: [ + 'The group value which receives these styles.' + ].join(' ') + }, + value: { + valType: 'any', + role: 'info', + dflt: {}, + description: [ + 'Sets each group style.', + 'For example, with `groups` set to *[\'a\', \'b\', \'a\', \'b\']*', + 'and `style` set to *[{target: \'a\', value: { marker: { color: \'red\' } }}]', + 'marker points in group *\'a\'* will be drawn in red.' + ].join(' ') + }, } }; @@ -71,11 +82,22 @@ exports.supplyDefaults = function(transformIn) { if(!enabled) return transformOut; coerce('groups'); - coerce('style'); + + var styleIn = transformIn.style; + var styleOut = transformOut.style = []; + + if(styleIn) { + for(var i = 0; i < styleIn.length; i++) { + styleOut[i] = {}; + Lib.coerce(styleIn[i], styleOut[i], exports.attributes.style, 'target'); + Lib.coerce(styleIn[i], styleOut[i], exports.attributes.style, 'value'); + } + } return transformOut; }; + /** * Apply transform !!! * @@ -115,6 +137,7 @@ function pasteArray(newTrace, trace, j, a) { } function transformOne(trace, state) { + var i; var opts = state.transform; var groups = trace.transforms[state.transformIndex].groups; @@ -128,9 +151,13 @@ function transformOne(trace, state) { var arrayAttrs = PlotSchema.findArrayAttributes(trace); - var style = opts.style || {}; + var style = opts.style || []; + var styleLookup = {}; + for(i = 0; i < style.length; i++) { + styleLookup[style[i].target] = style[i].value; + } - for(var i = 0; i < groupNames.length; i++) { + for(i = 0; i < groupNames.length; i++) { var groupName = groupNames[i]; var newTrace = newData[i] = Lib.extendDeepNoArrays({}, trace); @@ -145,9 +172,9 @@ function transformOne(trace, state) { newTrace.name = groupName; - // there's no need to coerce style[groupName] here + // there's no need to coerce styleLookup[groupName] here // as another round of supplyDefaults is done on the transformed traces - newTrace = Lib.extendDeepNoArrays(newTrace, style[groupName] || {}); + newTrace = Lib.extendDeepNoArrays(newTrace, styleLookup[groupName] || {}); } return newData; diff --git a/test/jasmine/tests/transform_groupby_test.js b/test/jasmine/tests/transform_groupby_test.js index bb2ea0f607e..6d1ed150d33 100644 --- a/test/jasmine/tests/transform_groupby_test.js +++ b/test/jasmine/tests/transform_groupby_test.js @@ -6,7 +6,6 @@ var destroyGraphDiv = require('../assets/destroy_graph_div'); var assertDims = require('../assets/assert_dims'); var assertStyle = require('../assets/assert_style'); - describe('groupby', function() { describe('one-to-many transforms:', function() { @@ -19,7 +18,10 @@ describe('groupby', function() { transforms: [{ type: 'groupby', groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'], - style: { a: {marker: {color: 'red'}}, b: {marker: {color: 'blue'}} } + style: [ + {target: 'a', value: {marker: {color: 'red'}}}, + {target: 'b', value: {marker: {color: 'blue'}}} + ] }] }]; @@ -30,7 +32,10 @@ describe('groupby', function() { transforms: [{ type: 'groupby', groups: ['b', 'a', 'b', 'b', 'b', 'a', 'a'], - style: { a: {marker: {color: 'green'}}, b: {marker: {color: 'black'}} } + style: [ + {target: 'a', value: {marker: {color: 'green'}}}, + {target: 'b', value: {marker: {color: 'black'}}} + ] }] }]; @@ -58,6 +63,58 @@ describe('groupby', function() { }); }); + it('Accepts deprecated object notation for styles', function(done) { + var oldStyleMockData = [{ + mode: 'markers', + x: [1, -1, -2, 0, 1, 2, 3], + y: [1, 2, 3, 1, 2, 3, 1], + transforms: [{ + type: 'groupby', + groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'], + style: { + a: {marker: {color: 'red'}}, + b: {marker: {color: 'blue'}} + } + }] + }]; + var data = Lib.extendDeep([], oldStyleMockData); + data[0].marker = { size: 20 }; + + var gd = createGraphDiv(); + var dims = [4, 3]; + + Plotly.plot(gd, data).then(function() { + assertStyle(dims, + ['rgb(255, 0, 0)', 'rgb(0, 0, 255)'], + [1, 1] + ); + + return Plotly.restyle(gd, 'marker.opacity', 0.4); + }).then(function() { + assertStyle(dims, + ['rgb(255, 0, 0)', 'rgb(0, 0, 255)'], + [0.4, 0.4] + ); + + expect(gd._fullData[0].marker.opacity).toEqual(0.4); + expect(gd._fullData[1].marker.opacity).toEqual(0.4); + + return Plotly.restyle(gd, 'marker.opacity', 1); + }).then(function() { + assertStyle(dims, + ['rgb(255, 0, 0)', 'rgb(0, 0, 255)'], + [1, 1] + ); + + expect(gd._fullData[0].marker.opacity).toEqual(1); + expect(gd._fullData[1].marker.opacity).toEqual(1); + }).then(done); + + // The final test for restyle updates using deprecated syntax + // is ommitted since old style syntax is *only* sanitized on + // initial plot, *not* on restyle. + }); + it('Plotly.restyle should work', function(done) { var data = Lib.extendDeep([], mockData0); data[0].marker = { size: 20 }; @@ -92,7 +149,10 @@ describe('groupby', function() { expect(gd._fullData[1].marker.opacity).toEqual(1); return Plotly.restyle(gd, { - 'transforms[0].style': { a: {marker: {color: 'green'}}, b: {marker: {color: 'red'}} }, + 'transforms[0].style': [[ + {target: 'a', value: {marker: {color: 'green'}}}, + {target: 'b', value: {marker: {color: 'red'}}} + ]], 'marker.opacity': 0.4 }); }).then(function() { @@ -192,7 +252,10 @@ describe('groupby', function() { transforms: [{ type: 'groupby', groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'], - style: { a: {marker: {color: 'red'}}, b: {marker: {color: 'blue'}} } + style: [ + {target: 'a', value: {marker: {color: 'red'}}}, + {target: 'b', value: {marker: {color: 'blue'}}} + ] }] }]; @@ -387,7 +450,10 @@ describe('groupby', function() { transforms: [{ type: 'groupby', groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'], - style: { a: {marker: {color: 'red'}}, b: {marker: {color: 'blue'}} } + style: [ + {target: 'a', value: {marker: {color: 'red'}}}, + {target: 'b', value: {marker: {color: 'blue'}}} + ] }] }]; @@ -401,8 +467,9 @@ describe('groupby', function() { transforms: [{ type: 'groupby', groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'], - style: { - a: { + style: [{ + target: 'a', + value: { marker: { color: 'orange', size: 20, @@ -410,8 +477,10 @@ describe('groupby', function() { color: 'red' } } - }, - b: { + } + }, { + target: 'b', + value: { mode: 'markers+lines', // heterogeonos attributes are OK: group 'a' doesn't need to define this marker: { color: 'cyan', @@ -426,7 +495,7 @@ describe('groupby', function() { color: 'purple' } } - } + }] }] }]; @@ -447,11 +516,14 @@ describe('groupby', function() { transforms: [{ type: 'groupby', groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'], - style: { - a: {marker: {size: 30}}, + style: [{ + target: 'a', + value: {marker: {size: 30}} + }, { // override general color: - b: {marker: {size: 15, line: {color: 'yellow'}}, line: {color: 'purple'}} - } + target: 'b', + value: {marker: {size: 15, line: {color: 'yellow'}}, line: {color: 'purple'}} + }] }] }]; @@ -464,7 +536,7 @@ describe('groupby', function() { transforms: [{ type: 'groupby', groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'], - style: {/* can be empty, or of partial group id coverage */} + style: [/* can be empty, or of partial group id coverage */] }] }]; @@ -548,7 +620,10 @@ describe('groupby', function() { transforms: [{ type: 'groupby', // groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'], - style: { a: {marker: {color: 'red'}}, b: {marker: {color: 'blue'}} } + style: [ + {target: 'a', value: {marker: {color: 'red'}}}, + {target: 'b', value: {marker: {color: 'blue'}}} + ] }] }]; @@ -561,7 +636,10 @@ describe('groupby', function() { transforms: [{ type: 'groupby', groups: [], - style: { a: {marker: {color: 'red'}}, b: {marker: {color: 'blue'}} } + style: [ + {target: 'a', value: {marker: {color: 'red'}}}, + {target: 'b', value: {marker: {color: 'blue'}}} + ] }] }]; @@ -574,7 +652,10 @@ describe('groupby', function() { transforms: [{ type: 'groupby', groups: null, - style: { a: {marker: {color: 'red'}}, b: {marker: {color: 'blue'}} } + style: [ + {target: 'a', value: {marker: {color: 'red'}}}, + {target: 'b', value: {marker: {color: 'blue'}}} + ] }] }]; diff --git a/test/jasmine/tests/transform_multi_test.js b/test/jasmine/tests/transform_multi_test.js index 6930effbd8e..f08dcbfb558 100644 --- a/test/jasmine/tests/transform_multi_test.js +++ b/test/jasmine/tests/transform_multi_test.js @@ -225,7 +225,13 @@ describe('multiple transforms:', function() { transforms: [{ type: 'groupby', groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'], - style: { a: {marker: {color: 'red'}}, b: {marker: {color: 'blue'}} } + style: [{ + target: 'a', + value: {marker: {color: 'red'}}, + }, { + target: 'b', + value: {marker: {color: 'blue'}} + }] }, { type: 'filter', operation: '>' @@ -239,7 +245,13 @@ describe('multiple transforms:', function() { transforms: [{ type: 'groupby', groups: ['b', 'a', 'b', 'b', 'b', 'a', 'a'], - style: { a: {marker: {color: 'green'}}, b: {marker: {color: 'black'}} } + style: [{ + target: 'a', + value: {marker: {color: 'green'}} + }, { + target: 'b', + value: {marker: {color: 'black'}} + }] }, { type: 'filter', operation: '<', @@ -329,7 +341,13 @@ describe('multiple transforms:', function() { expect(gd._fullData[1].marker.opacity).toEqual(1); return Plotly.restyle(gd, { - 'transforms[0].style': { a: {marker: {color: 'green'}}, b: {marker: {color: 'red'}} }, + 'transforms[0].style': [[{ + target: 'a', + value: {marker: {color: 'green'}} + }, { + target: 'b', + value: {marker: {color: 'red'}} + }]], 'marker.opacity': 0.4 }); }).then(function() { @@ -437,7 +455,13 @@ describe('multiple traces with transforms:', function() { transforms: [{ type: 'groupby', groups: ['a', 'a', 'b', 'a', 'b', 'b', 'a'], - style: { a: {marker: {color: 'red'}}, b: {marker: {color: 'blue'}} } + style: [{ + target: 'a', + value: {marker: {color: 'red'}}, + }, { + target: 'b', + value: {marker: {color: 'blue'}} + }] }, { type: 'filter', operation: '>' @@ -508,7 +532,13 @@ describe('multiple traces with transforms:', function() { }); return Plotly.restyle(gd, { - 'transforms[0].style': { a: {marker: {color: 'green'}}, b: {marker: {color: 'red'}} }, + 'transforms[0].style': [[{ + target: 'a', + value: {marker: {color: 'green'}}, + }, { + target: 'b', + value: {marker: {color: 'red'}} + }]], 'marker.opacity': [0.4, 0.6] }); }).then(function() {