diff --git a/src/plot_api/helpers.js b/src/plot_api/helpers.js index f07b0508061..0da0f5051fe 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,38 @@ 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': + // Name has changed from `style` to `styles`, so use `style` but prefer `styles`: + transform.styles = transform.styles || transform.style; + + if(transform.styles && !Array.isArray(transform.styles)) { + var prevStyles = transform.styles; + var styleKeys = Object.keys(prevStyles); + + transform.styles = []; + for(var j = 0; j < styleKeys.length; j++) { + transform.styles.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..ef1b78426b1 100644 --- a/src/transforms/groupby.js +++ b/src/transforms/groupby.js @@ -34,15 +34,26 @@ exports.attributes = { 'with `x` [1, 3] and one trace with `x` [2, 4].' ].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(' ') + styles: { + _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 styles.', + 'For example, with `groups` set to *[\'a\', \'b\', \'a\', \'b\']*', + 'and `styles` 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.styles; + var styleOut = transformOut.styles = []; + + if(styleIn) { + for(var i = 0; i < styleIn.length; i++) { + styleOut[i] = {}; + Lib.coerce(styleIn[i], styleOut[i], exports.attributes.styles, 'target'); + Lib.coerce(styleIn[i], styleOut[i], exports.attributes.styles, '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 styles = opts.styles || []; + var styleLookup = {}; + for(i = 0; i < styles.length; i++) { + styleLookup[styles[i].target] = styles[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/plotschema_test.js b/test/jasmine/tests/plotschema_test.js index ccafd31b3bf..eba5226cf31 100644 --- a/test/jasmine/tests/plotschema_test.js +++ b/test/jasmine/tests/plotschema_test.js @@ -188,6 +188,13 @@ describe('plot schema', function() { }); }); + it('should work with registered transforms (2)', function() { + var valObjects = plotSchema.transforms.groupby.attributes; + var items = valObjects.styles.items || {}; + + expect(Object.keys(items)).toEqual(['style']); + }); + it('should work with registered components', function() { expect(plotSchema.traces.scatter.attributes.xcalendar.valType).toEqual('enumerated'); expect(plotSchema.traces.scatter3d.attributes.zcalendar.valType).toEqual('enumerated'); diff --git a/test/jasmine/tests/transform_groupby_test.js b/test/jasmine/tests/transform_groupby_test.js index bb2ea0f607e..049a2856b5f 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'}} } + styles: [ + {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'}} } + styles: [ + {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'], + styles: { + 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].styles': [[ + {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'}} } + styles: [ + {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'}} } + styles: [ + {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: { + styles: [{ + 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}}, + styles: [{ + 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 */} + styles: [/* 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'}} } + styles: [ + {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'}} } + styles: [ + {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'}} } + styles: [ + {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 06576734649..23e0b40a8b5 100644 --- a/test/jasmine/tests/transform_multi_test.js +++ b/test/jasmine/tests/transform_multi_test.js @@ -227,7 +227,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'}} } + styles: [{ + target: 'a', + value: {marker: {color: 'red'}}, + }, { + target: 'b', + value: {marker: {color: 'blue'}} + }] }, { type: 'filter', operation: '>' @@ -241,7 +247,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'}} } + styles: [{ + target: 'a', + value: {marker: {color: 'green'}} + }, { + target: 'b', + value: {marker: {color: 'black'}} + }] }, { type: 'filter', operation: '<', @@ -331,7 +343,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].styles': [[{ + target: 'a', + value: {marker: {color: 'green'}} + }, { + target: 'b', + value: {marker: {color: 'red'}} + }]], 'marker.opacity': 0.4 }); }).then(function() { @@ -439,7 +457,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'}} } + styles: [{ + target: 'a', + value: {marker: {color: 'red'}}, + }, { + target: 'b', + value: {marker: {color: 'blue'}} + }] }, { type: 'filter', operation: '>' @@ -510,7 +534,13 @@ describe('multiple traces with transforms:', function() { }); return Plotly.restyle(gd, { - 'transforms[0].style': { a: {marker: {color: 'green'}}, b: {marker: {color: 'red'}} }, + 'transforms[0].styles': [[{ + target: 'a', + value: {marker: {color: 'green'}}, + }, { + target: 'b', + value: {marker: {color: 'red'}} + }]], 'marker.opacity': [0.4, 0.6] }); }).then(function() {