diff --git a/src/components/colorscale/attributes.js b/src/components/colorscale/attributes.js index deab8b5b228..3df6baec2a5 100644 --- a/src/components/colorscale/attributes.js +++ b/src/components/colorscale/attributes.js @@ -9,6 +9,8 @@ 'use strict'; var colorbarAttrs = require('../colorbar/attributes'); +var counterRegex = require('../../lib/regex').counter; + var palettes = require('./scales.js').scales; var paletteStr = Object.keys(palettes); @@ -245,5 +247,22 @@ module.exports = function colorScaleAttrs(context, opts) { attrs.colorbar = colorbarAttrs; } + if(!opts.noColorAxis) { + attrs.coloraxis = { + valType: 'subplotid', + role: 'info', + regex: counterRegex('coloraxis'), + dflt: null, + editType: 'calc', + description: [ + 'Sets a reference to a shared color axis.', + 'References to these shared color axes are *coloraxis*, *coloraxis2*, *coloraxis3*, etc.', + 'Settings for these shared color axes are set in the layout, under', + '`layout.coloraxis`, `layout.coloraxis2`, etc.', + 'Note that multiple color scales can be linked to the same color axis.' + ].join(' ') + }; + } + return attrs; }; diff --git a/src/components/colorscale/defaults.js b/src/components/colorscale/defaults.js index 20070edf750..d7bef1487b8 100644 --- a/src/components/colorscale/defaults.js +++ b/src/components/colorscale/defaults.js @@ -15,20 +15,63 @@ var hasColorbar = require('../colorbar/has_colorbar'); var colorbarDefaults = require('../colorbar/defaults'); var isValidScale = require('./scales').isValid; +var traceIs = require('../../registry').traceIs; -function npMaybe(cont, prefix) { +function npMaybe(outerCont, prefix) { var containerStr = prefix.slice(0, prefix.length - 1); return prefix ? - Lib.nestedProperty(cont, containerStr).get() || {} : - cont; + Lib.nestedProperty(outerCont, containerStr).get() || {} : + outerCont; } -module.exports = function colorScaleDefaults(traceIn, traceOut, layout, coerce, opts) { +module.exports = function colorScaleDefaults(outerContIn, outerContOut, layout, coerce, opts) { var prefix = opts.prefix; var cLetter = opts.cLetter; - var containerIn = npMaybe(traceIn, prefix); - var containerOut = npMaybe(traceOut, prefix); - var template = npMaybe(traceOut._template || {}, prefix) || {}; + var inTrace = '_module' in outerContOut; + var containerIn = npMaybe(outerContIn, prefix); + var containerOut = npMaybe(outerContOut, prefix); + var template = npMaybe(outerContOut._template || {}, prefix) || {}; + + // colorScaleDefaults wrapper called if-ever we need to reset the colorscale + // attributes for containers that were linked to invalid color axes + var thisFn = function() { + delete outerContIn.coloraxis; + delete outerContOut.coloraxis; + return colorScaleDefaults(outerContIn, outerContOut, layout, coerce, opts); + }; + + if(inTrace) { + var colorAxes = layout._colorAxes || {}; + var colorAx = coerce(prefix + 'coloraxis'); + + if(colorAx) { + var colorbarVisuals = ( + traceIs(outerContOut, 'contour') && + Lib.nestedProperty(outerContOut, 'contours.coloring').get() + ) || 'heatmap'; + + var stash = colorAxes[colorAx]; + + if(stash) { + stash[2].push(thisFn); + + if(stash[0] !== colorbarVisuals) { + stash[0] = false; + Lib.warn([ + 'Ignoring coloraxis:', colorAx, 'setting', + 'as it is linked to incompatible colorscales.' + ].join(' ')); + } + } else { + // stash: + // - colorbar visual 'type' + // - colorbar options to help in Colorbar.draw + // - list of colorScaleDefaults wrapper functions + colorAxes[colorAx] = [colorbarVisuals, outerContOut, [thisFn]]; + } + return; + } + } var minIn = containerIn[cLetter + 'min']; var maxIn = containerIn[cLetter + 'max']; @@ -58,7 +101,7 @@ module.exports = function colorScaleDefaults(traceIn, traceOut, layout, coerce, // handles both the trace case where the dflt is listed in attributes and // the marker case where the dflt is determined by hasColorbar var showScaleDflt; - if(prefix) showScaleDflt = hasColorbar(containerIn); + if(prefix && inTrace) showScaleDflt = hasColorbar(containerIn); var showScale = coerce(prefix + 'showscale', showScaleDflt); if(showScale) colorbarDefaults(containerIn, containerOut, layout); diff --git a/src/components/colorscale/layout_attributes.js b/src/components/colorscale/layout_attributes.js index 75ec78007af..3612884c172 100644 --- a/src/components/colorscale/layout_attributes.js +++ b/src/components/colorscale/layout_attributes.js @@ -8,40 +8,63 @@ 'use strict'; +var extendFlat = require('../../lib/extend').extendFlat; + +var colorScaleAttrs = require('./attributes'); var scales = require('./scales').scales; var msg = 'Note that `autocolorscale` must be true for this attribute to work.'; module.exports = { editType: 'calc', - sequential: { - valType: 'colorscale', - dflt: scales.Reds, - role: 'style', - editType: 'calc', - description: [ - 'Sets the default sequential colorscale for positive values.', - msg - ].join(' ') - }, - sequentialminus: { - valType: 'colorscale', - dflt: scales.Blues, - role: 'style', + + colorscale: { editType: 'calc', - description: [ - 'Sets the default sequential colorscale for negative values.', - msg - ].join(' ') + + sequential: { + valType: 'colorscale', + dflt: scales.Reds, + role: 'style', + editType: 'calc', + description: [ + 'Sets the default sequential colorscale for positive values.', + msg + ].join(' ') + }, + sequentialminus: { + valType: 'colorscale', + dflt: scales.Blues, + role: 'style', + editType: 'calc', + description: [ + 'Sets the default sequential colorscale for negative values.', + msg + ].join(' ') + }, + diverging: { + valType: 'colorscale', + dflt: scales.RdBu, + role: 'style', + editType: 'calc', + description: [ + 'Sets the default diverging colorscale.', + msg + ].join(' ') + } }, - diverging: { - valType: 'colorscale', - dflt: scales.RdBu, - role: 'style', + + coloraxis: extendFlat({ + // not really a 'subplot' attribute container, + // but this is the flag we use to denote attributes that + // support yaxis, yaxis2, yaxis3, ... counters + _isSubplotObj: true, editType: 'calc', description: [ - 'Sets the default diverging colorscale.', - msg + '' ].join(' ') - } + }, colorScaleAttrs('', { + colorAttr: 'corresponding trace color array(s)', + noColorAxis: true, + showScaleDflt: true + })) }; diff --git a/src/components/colorscale/layout_defaults.js b/src/components/colorscale/layout_defaults.js index b05d56895c0..744e575de83 100644 --- a/src/components/colorscale/layout_defaults.js +++ b/src/components/colorscale/layout_defaults.js @@ -9,17 +9,41 @@ 'use strict'; var Lib = require('../../lib'); -var colorscaleAttrs = require('./layout_attributes'); var Template = require('../../plot_api/plot_template'); +var colorScaleAttrs = require('./layout_attributes'); +var colorScaleDefaults = require('./defaults'); + module.exports = function supplyLayoutDefaults(layoutIn, layoutOut) { - var colorscaleIn = layoutIn.colorscale; - var colorscaleOut = Template.newContainer(layoutOut, 'colorscale'); function coerce(attr, dflt) { - return Lib.coerce(colorscaleIn, colorscaleOut, colorscaleAttrs, attr, dflt); + return Lib.coerce(layoutIn, layoutOut, colorScaleAttrs, attr, dflt); } - coerce('sequential'); - coerce('sequentialminus'); - coerce('diverging'); + coerce('colorscale.sequential'); + coerce('colorscale.sequentialminus'); + coerce('colorscale.diverging'); + + var colorAxes = layoutOut._colorAxes; + var colorAxIn, colorAxOut; + + function coerceAx(attr, dflt) { + return Lib.coerce(colorAxIn, colorAxOut, colorScaleAttrs.coloraxis, attr, dflt); + } + + for(var k in colorAxes) { + var stash = colorAxes[k]; + + if(stash[0]) { + colorAxIn = layoutIn[k] || {}; + colorAxOut = Template.newContainer(layoutOut, k, 'coloraxis'); + colorAxOut._name = k; + colorScaleDefaults(colorAxIn, colorAxOut, layoutOut, coerceAx, {prefix: '', cLetter: 'c'}); + } else { + // re-coerce colorscale attributes w/o coloraxis + for(var i = 0; i < stash[2].length; i++) { + stash[2][i](); + } + delete layoutOut._colorAxes[k]; + } + } }; diff --git a/src/plot_api/plot_schema.js b/src/plot_api/plot_schema.js index 5dbe3fb4774..fd759a64c4a 100644 --- a/src/plot_api/plot_schema.js +++ b/src/plot_api/plot_schema.js @@ -544,24 +544,26 @@ function getLayoutAttributes() { var schema = _module.schema; if(schema && (schema.subplots || schema.layout)) { - /* - * Components with defined schema have already been merged in at register time - * but a few components define attributes that apply only to xaxis - * not yaxis (rangeselector, rangeslider) - delete from y schema. - * Note that the input attributes for xaxis/yaxis are the same object - * so it's not possible to only add them to xaxis from the start. - * If we ever have such asymmetry the other way, or anywhere else, - * we will need to extend both this code and mergeComponentAttrsToSubplot - * (which will not find yaxis only for example) - */ - + /* + * Components with defined schema have already been merged in at register time + * but a few components define attributes that apply only to xaxis + * not yaxis (rangeselector, rangeslider) - delete from y schema. + * Note that the input attributes for xaxis/yaxis are the same object + * so it's not possible to only add them to xaxis from the start. + * If we ever have such asymmetry the other way, or anywhere else, + * we will need to extend both this code and mergeComponentAttrsToSubplot + * (which will not find yaxis only for example) + */ var subplots = schema.subplots; if(subplots && subplots.xaxis && !subplots.yaxis) { - for(var xkey in subplots.xaxis) delete layoutAttributes.yaxis[xkey]; + for(var xkey in subplots.xaxis) { + delete layoutAttributes.yaxis[xkey]; + } } + } else if(_module.name === 'colorscale') { + extendDeepAll(layoutAttributes, _module.layoutAttributes); } else if(_module.layoutAttributes) { - // older style without schema need to be explicitly merged in now - + // older style without schema need to be explicitly merged in now insertAttrs(layoutAttributes, _module.layoutAttributes, _module.name); } } diff --git a/src/plots/layout_attributes.js b/src/plots/layout_attributes.js index ac204dbe8c6..66b55d13da8 100644 --- a/src/plots/layout_attributes.js +++ b/src/plots/layout_attributes.js @@ -11,7 +11,6 @@ var fontAttrs = require('./font_attributes'); var animationAttrs = require('./animation_attributes'); var colorAttrs = require('../components/color/attributes'); -var colorscaleAttrs = require('../components/colorscale/layout_attributes'); var padAttrs = require('./pad_attributes'); var extendFlat = require('../lib/extend').extendFlat; @@ -292,7 +291,6 @@ module.exports = { editType: 'calc', description: 'Sets the default trace colors.' }, - colorscale: colorscaleAttrs, datarevision: { valType: 'any', role: 'info', diff --git a/src/plots/plots.js b/src/plots/plots.js index de8951680ba..cbbc4e2bd9c 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -390,6 +390,8 @@ plots.supplyDefaults = function(gd, opts) { newFullLayout._firstScatter = {}; // for grouped bar/box/violin trace to share config across traces newFullLayout._alignmentOpts = {}; + // track color axes referenced in the data + newFullLayout._colorAxes = {}; // for traces to request a default rangeslider on their x axes // eg set `_requestRangeslider.x2 = true` for xaxis2 diff --git a/test/jasmine/bundle_tests/plotschema_test.js b/test/jasmine/bundle_tests/plotschema_test.js index a515f5523cd..b92a9ec9b62 100644 --- a/test/jasmine/bundle_tests/plotschema_test.js +++ b/test/jasmine/bundle_tests/plotschema_test.js @@ -136,7 +136,8 @@ describe('plot schema', function() { 'xaxis', 'yaxis', 'scene', 'geo', 'ternary', 'mapbox', 'polar', // not really a 'subplot' object but supports yaxis, yaxis2, yaxis3, // ... counters, so list it here - 'xaxis.rangeslider.yaxis' + 'xaxis.rangeslider.yaxis', + 'coloraxis' ]; // check if the subplot objects have '_isSubplotObj' @@ -146,7 +147,7 @@ describe('plot schema', function() { plotSchema.layout.layoutAttributes, astr + '.' + IS_SUBPLOT_OBJ ).get() - ).toBe(true); + ).toBe(true, astr); }); // check that no other object has '_isSubplotObj' diff --git a/test/jasmine/tests/colorscale_test.js b/test/jasmine/tests/colorscale_test.js index 896d4442434..fc919fb78bc 100644 --- a/test/jasmine/tests/colorscale_test.js +++ b/test/jasmine/tests/colorscale_test.js @@ -13,14 +13,12 @@ var destroyGraphDiv = require('../assets/destroy_graph_div'); var failTest = require('../assets/fail_test'); var supplyAllDefaults = require('../assets/supply_defaults'); -function _supply(trace, layout) { +function _supply(arg, layout) { var gd = { - data: [trace], + data: Array.isArray(arg) ? arg : [arg], layout: layout || {} }; - supplyAllDefaults(gd); - return gd; } @@ -259,7 +257,7 @@ describe('Test colorscale:', function() { } beforeEach(function() { - traceOut = {}; + traceOut = {_module: {}}; }); it('should set auto to true when min/max are valid', function() { @@ -330,7 +328,10 @@ describe('Test colorscale:', function() { } beforeEach(function() { - traceOut = { marker: {} }; + traceOut = { + marker: {}, + _module: {} + }; }); it('should coerce autocolorscale to true by default', function() { @@ -368,6 +369,117 @@ describe('Test colorscale:', function() { }); }); + describe('handleDefaults (coloraxis version)', function() { + it('should not coerced colorscale/colorbar attributes when referencing a shared color axis', function() { + var gd = _supply([ + {type: 'heatmap', z: [[0]]}, + {type: 'heatmap', z: [[2]], coloraxis: 'coloraxis'}, + {type: 'heatmap', z: [[2]], coloraxis: 'coloraxis'}, + ]); + + var fullData = gd._fullData; + var fullLayout = gd._fullLayout; + + var zAttrs = ['zauto', 'colorscale', 'reversescale']; + zAttrs.forEach(function(attr) { + expect(fullData[0][attr]).not.toBe(undefined, 'trace 0 ' + attr); + expect(fullData[1][attr]).toBe(undefined, 'trace 1 ' + attr); + expect(fullData[2][attr]).toBe(undefined, 'trace 2 ' + attr); + }); + + var cAttrs = ['cauto', 'colorscale', 'reversescale']; + cAttrs.forEach(function(attr) { + expect(fullLayout.coloraxis[attr]).not.toBe(undefined, 'coloraxis ' + attr); + }); + + expect(fullData[0].coloraxis).toBe(undefined); + expect(fullData[1].coloraxis).toBe('coloraxis'); + expect(fullData[2].coloraxis).toBe('coloraxis'); + expect(fullLayout.coloraxis.coloraxis).toBe(undefined); + expect(fullLayout.coloraxis.showscale).toBe(true, 'showscale is true by dflt in color axes'); + }); + + it('should keep track of all the color axes referenced in the traces', function() { + var gd = _supply([ + {type: 'heatmap', z: [[1]], coloraxis: 'coloraxis'}, + {y: [1], marker: {color: [1], coloraxis: 'coloraxis'}}, + {type: 'contour', z: [[1]], coloraxis: 'coloraxis3'}, + // invalid + {y: [1], marker: {color: [1], coloraxis: 'c1'}}, + // not coerced - visible:false trace + {marker: {color: [1], coloraxis: 'coloraxis2'}} + ], { + // not referenced in traces, shouldn't get coerced + coloraxis4: {colorscale: 'Viridis'} + }); + + var fullData = gd._fullData; + var fullLayout = gd._fullLayout; + + expect(fullData[0].coloraxis).toBe('coloraxis'); + expect(fullData[1].marker.coloraxis).toBe('coloraxis'); + expect(fullData[2].coloraxis).toBe('coloraxis3'); + expect(fullData[3].coloraxis).toBe(undefined); + + expect(fullData[0]._colorAx).toBe(fullLayout.coloraxis); + expect(fullData[1].marker._colorAx).toBe(fullLayout.coloraxis); + expect(fullData[2]._colorAx).toBe(fullLayout.coloraxis3); + + expect(fullLayout.coloraxis).not.toBe(undefined); + expect(fullLayout.coloraxis2).toBe(undefined); + expect(fullLayout.coloraxis3).not.toBe(undefined); + expect(fullLayout.coloraxis4).toBe(undefined); + + expect(Object.keys(fullLayout._colorAxes)).toEqual(['coloraxis', 'coloraxis3']); + expect(fullLayout._colorAxes.coloraxis[0]).toBe('heatmap'); + expect(fullLayout._colorAxes.coloraxis3[0]).toBe('fill'); + }); + + it('should log warning when trying to shared color axis with traces with incompatible color bars', function() { + spyOn(Lib, 'warn'); + + var gd = _supply([ + // ok + {type: 'heatmap', z: [[1]], coloraxis: 'coloraxis'}, + {y: [1], marker: {color: [1], coloraxis: 'coloraxis'}}, + {type: 'contour', z: [[1]], contours: {coloring: 'heatmap'}, coloraxis: 'coloraxis'}, + // invalid (coloring dflt is 'fill') + {type: 'heatmap', z: [[1]], coloraxis: 'coloraxis2'}, + {type: 'contour', z: [[1]], coloraxis: 'coloraxis2'}, + // invalid + {type: 'contour', z: [[1]], contours: {coloring: 'lines'}, coloraxis: 'coloraxis3'}, + {type: 'contour', z: [[1]], contours: {coloring: 'heatmap'}, coloraxis: 'coloraxis3'}, + // ok + {type: 'contour', z: [[1]], coloraxis: 'coloraxis4'}, + {type: 'contour', z: [[1]], coloraxis: 'coloraxis4'}, + // ok + {type: 'contour', z: [[1]], contours: {coloring: 'lines'}, coloraxis: 'coloraxis5'}, + {type: 'contour', z: [[1]], contours: {coloring: 'lines'}, coloraxis: 'coloraxis5'} + ]); + + var fullData = gd._fullData; + var fullLayout = gd._fullLayout; + + expect(Object.keys(fullLayout._colorAxes)).toEqual(['coloraxis', 'coloraxis4', 'coloraxis5']); + expect(fullLayout._colorAxes.coloraxis[0]).toBe('heatmap'); + expect(fullLayout._colorAxes.coloraxis4[0]).toBe('fill'); + expect(fullLayout._colorAxes.coloraxis5[0]).toBe('lines'); + expect(Lib.warn).toHaveBeenCalledTimes(2); + + var zAttrs = ['zauto', 'colorscale', 'reversescale']; + var withColorAx = [0, 1, 2, 7, 8, 9, 10]; + var woColorAx = [3, 4, 5, 6]; + zAttrs.forEach(function(attr) { + withColorAx.forEach(function(i) { + expect(fullData[i][attr]).toBe(undefined, 'trace ' + i + ' ' + attr); + }); + woColorAx.forEach(function(i) { + expect(fullData[i][attr]).not.toBe(undefined, 'trace ' + i + ' ' + attr); + }); + }); + }); + }); + describe('calc', function() { var calcColorscale = Colorscale.calc; var trace, z;