diff --git a/src/plots/plots.js b/src/plots/plots.js index 38416678886..510b1752550 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -659,6 +659,51 @@ plots.linkSubplots = function(newFullData, newFullLayout, oldFullData, oldFullLa } }; +// This function clears any trace attributes with valType: color and +// no set dflt filed in the plot schema. This is needed because groupby (which +// is the only transform for which this currently applies) supplies parent +// trace defaults, then expanded trace defaults. The result is that `null` +// colors are default-supplied and inherited as a color instead of a null. +// The result is that expanded trace default colors have no effect, with +// the final result that groups are indistinguishable. This function clears +// those colors so that individual groupby groups get unique colors. +plots.clearExpandedTraceDefaultColors = function(trace) { + var colorAttrs, path, i; + + // This uses weird closure state in order to satisfy the linter rule + // that we can't create functions in a loop. + function locateColorAttrs(attr, attrName, attrs, level) { + path[level] = attrName; + path.length = level + 1; + if(attr.valType === 'color' && attr.dflt === undefined) { + colorAttrs.push(path.join('.')); + } + } + + path = []; + + // Get the cached colorAttrs: + colorAttrs = trace._module._colorAttrs; + + // Or else compute and cache the colorAttrs on the module: + if(!colorAttrs) { + trace._module._colorAttrs = colorAttrs = []; + PlotSchema.crawl( + trace._module.attributes, + locateColorAttrs + ); + } + + for(i = 0; i < colorAttrs.length; i++) { + var origprop = Lib.nestedProperty(trace, '_input.' + colorAttrs[i]); + + if(!origprop.get()) { + Lib.nestedProperty(trace, colorAttrs[i]).set(null); + } + } +}; + + plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { var i, fullTrace, trace; var modules = fullLayout._modules = [], @@ -694,8 +739,8 @@ plots.supplyDataDefaults = function(dataIn, dataOut, layout, fullLayout) { var expandedTraces = applyTransforms(fullTrace, dataOut, layout, fullLayout); for(var j = 0; j < expandedTraces.length; j++) { - var expandedTrace = expandedTraces[j], - fullExpandedTrace = plots.supplyTraceDefaults(expandedTrace, cnt, fullLayout, i); + var expandedTrace = expandedTraces[j]; + var fullExpandedTrace = plots.supplyTraceDefaults(expandedTrace, cnt, fullLayout, i); // mutate uid here using parent uid and expanded index // to promote consistency between update calls diff --git a/src/transforms/groupby.js b/src/transforms/groupby.js index ef1b78426b1..6590c428c26 100644 --- a/src/transforms/groupby.js +++ b/src/transforms/groupby.js @@ -10,6 +10,7 @@ var Lib = require('../lib'); var PlotSchema = require('../plot_api/plot_schema'); +var Plots = require('../plots/plots'); exports.moduleType = 'transform'; @@ -172,6 +173,8 @@ function transformOne(trace, state) { newTrace.name = groupName; + Plots.clearExpandedTraceDefaultColors(newTrace); + // there's no need to coerce styleLookup[groupName] here // as another round of supplyDefaults is done on the transformed traces newTrace = Lib.extendDeepNoArrays(newTrace, styleLookup[groupName] || {}); diff --git a/test/jasmine/tests/transform_groupby_test.js b/test/jasmine/tests/transform_groupby_test.js index 049a2856b5f..734622c0984 100644 --- a/test/jasmine/tests/transform_groupby_test.js +++ b/test/jasmine/tests/transform_groupby_test.js @@ -1,4 +1,5 @@ var Plotly = require('@lib/index'); +var Plots = require('@src/plots/plots'); var Lib = require('@src/lib'); var createGraphDiv = require('../assets/create_graph_div'); @@ -236,9 +237,38 @@ describe('groupby', function() { done(); }); }); + }); + + describe('many-to-many transforms', function() { + it('varies the color for each expanded trace', function() { + var uniqueColors = {}; + var dataOut = []; + var dataIn = [{ + y: [1, 2, 3], + transforms: [ + {type: 'filter', operation: '<', value: 4}, + {type: 'groupby', groups: ['a', 'b', 'c']} + ] + }, { + y: [4, 5, 6], + transforms: [ + {type: 'filter', operation: '<', value: 4}, + {type: 'groupby', groups: ['a', 'b', 'b']} + ] + }]; + Plots.supplyDataDefaults(dataIn, dataOut, {}, {}); + + for(var i = 0; i < dataOut.length; i++) { + uniqueColors[dataOut[i].marker.color] = true; + } + + // Confirm that five total colors exist: + expect(Object.keys(uniqueColors).length).toEqual(5); + }); }); + // these tests can be shortened, once the meaning of edge cases gets clarified describe('symmetry/degeneracy testing of one-to-many transforms on arbitrary arrays where there is no grouping (implicit 1):', function() { 'use strict'; @@ -662,6 +692,39 @@ describe('groupby', function() { it('passes with no groups', test(mockData0)); it('passes with empty groups', test(mockData1)); it('passes with falsey groups', test(mockData2)); + }); + + describe('expanded trace coloring', function() { + it('assigns unique colors to each group', function() { + var colors = []; + var dataOut = []; + var dataIn = [{ + y: [1, 2, 3], + transforms: [ + {type: 'filter', operation: '<', value: 4}, + {type: 'groupby', groups: ['a', 'b', 'c']} + ] + }, { + y: [4, 5, 6], + transforms: [ + {type: 'filter', operation: '<', value: 4}, + {type: 'groupby', groups: ['a', 'b', 'b']} + ] + }]; + + Plots.supplyDataDefaults(dataIn, dataOut, {}, {}); + for(var i = 0; i < dataOut.length; i++) { + colors.push(dataOut[i].marker.color); + } + + expect(colors).toEqual([ + '#1f77b4', + '#ff7f0e', + '#2ca02c', + '#d62728', + '#9467bd' + ]); + }); }); });