diff --git a/src/components/colorscale/attributes.js b/src/components/colorscale/attributes.js index f1dad9b9573..014377c1c91 100644 --- a/src/components/colorscale/attributes.js +++ b/src/components/colorscale/attributes.js @@ -87,6 +87,8 @@ module.exports = function colorScaleAttrs(context, opts) { var auto = cLetter + 'auto'; var min = cLetter + 'min'; var max = cLetter + 'max'; + var mid = cLetter + 'mid'; + var autoFull = code(contextHead + auto); var minFull = code(contextHead + min); var maxFull = code(contextHead + max); var minmaxFull = minFull + ' and ' + maxFull; @@ -160,6 +162,21 @@ module.exports = function colorScaleAttrs(context, opts) { ].join('') }; + attrs[mid] = { + valType: 'number', + role: 'info', + dflt: null, + editType: 'calc', + impliedEdits: autoImpliedEdits, + description: [ + 'Sets the mid-point of the color domain by scaling ', minFull, + ' and/or ', maxFull, ' to be equidistant to this point.', + effectDesc, + ' Value should have the same units as ', colorAttrFull, '. ', + 'Has no effect when ', autoFull, ' is `false`.' + ].join('') + }; + attrs.colorscale = { valType: 'colorscale', role: 'style', diff --git a/src/components/colorscale/calc.js b/src/components/colorscale/calc.js index 46690b37ca0..6fb87c06b0c 100644 --- a/src/components/colorscale/calc.js +++ b/src/components/colorscale/calc.js @@ -23,9 +23,11 @@ module.exports = function calc(gd, trace, opts) { var autoAttr = cLetter + 'auto'; var minAttr = cLetter + 'min'; var maxAttr = cLetter + 'max'; + var midAttr = cLetter + 'mid'; var auto = container[autoAttr]; var min = container[minAttr]; var max = container[maxAttr]; + var mid = container[midAttr]; var scl = container.colorscale; if(auto !== false || min === undefined) { @@ -36,6 +38,15 @@ module.exports = function calc(gd, trace, opts) { max = Lib.aggNums(Math.max, null, vals); } + if(auto !== false && mid !== undefined) { + if(max - mid > mid - min) { + min = mid - (max - mid); + } + else if(max - mid < mid - min) { + max = mid + (mid - min); + } + } + if(min === max) { min -= 0.5; max += 0.5; diff --git a/src/components/colorscale/defaults.js b/src/components/colorscale/defaults.js index 387ecc787b9..75cd284b2be 100644 --- a/src/components/colorscale/defaults.js +++ b/src/components/colorscale/defaults.js @@ -33,9 +33,14 @@ module.exports = function colorScaleDefaults(traceIn, traceOut, layout, coerce, var minIn = containerIn[cLetter + 'min']; var maxIn = containerIn[cLetter + 'max']; var validMinMax = isNumeric(minIn) && isNumeric(maxIn) && (minIn < maxIn); - coerce(prefix + cLetter + 'auto', !validMinMax); - coerce(prefix + cLetter + 'min'); - coerce(prefix + cLetter + 'max'); + var auto = coerce(prefix + cLetter + 'auto', !validMinMax); + + if(auto) { + coerce(prefix + cLetter + 'mid'); + } else { + coerce(prefix + cLetter + 'min'); + coerce(prefix + cLetter + 'max'); + } // handles both the trace case (autocolorscale is false by default) and // the marker and marker.line case (autocolorscale is true by default) diff --git a/src/traces/scattermapbox/attributes.js b/src/traces/scattermapbox/attributes.js index 55bb3b7d2e2..8609904e1a2 100644 --- a/src/traces/scattermapbox/attributes.js +++ b/src/traces/scattermapbox/attributes.js @@ -93,6 +93,7 @@ module.exports = overrideAll({ cauto: markerAttrs.cauto, cmax: markerAttrs.cmax, cmin: markerAttrs.cmin, + cmid: markerAttrs.cmid, autocolorscale: markerAttrs.autocolorscale, reversescale: markerAttrs.reversescale, showscale: markerAttrs.showscale, diff --git a/test/image/baselines/cmid-zmid.png b/test/image/baselines/cmid-zmid.png new file mode 100644 index 00000000000..cb929f0f1e7 Binary files /dev/null and b/test/image/baselines/cmid-zmid.png differ diff --git a/test/image/mocks/cmid-zmid.json b/test/image/mocks/cmid-zmid.json new file mode 100644 index 00000000000..68b47fc7dbc --- /dev/null +++ b/test/image/mocks/cmid-zmid.json @@ -0,0 +1,43 @@ +{ + "data": [{ + "y": [1, 2, 1], + "mode": "markers", + "marker": { + "size": 20, + "color": [-1, 1, 10], + "cmid": "0", + "colorbar": { + "len": 0.3, + "y": "1", + "yanchor": "top", + "title": {"text": "marker.cmid=0", "side": "right"} + } + } + }, { + "type": "heatmap", + "z": [[1,2], [2, 3]], + "zmid": -1, + "colorbar": { + "len": 0.3, + "y": "0.5", + "yanchor": "middle", + "title": {"text": "zmid=-1", "side": "right"} + } + }, { + "type": "bar", + "y": [0.5, 1, 0.5], + "name": "marker.line.cmid=2", + "hoverlabel": {"namelength": -1}, + "marker": { + "line": { + "width": 2, + "color": [-1, -1, -10], + "cmid": 2 + } + } + }], + "layout": { + "title": {"text": "Traces with set cmid / zmid"}, + "showlegend": false + } +} diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index 837f1569f2d..c0cf010eab7 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -1195,6 +1195,77 @@ describe('Test plot api', function() { .then(done); }); + it('turns on cauto when cmid is edited', function(done) { + function _assert(msg, exp) { + return function() { + var mk = gd._fullData[0].marker; + for(var k in exp) { + expect(mk[k]).toBe(exp[k], [msg, k].join(' - ')); + } + }; + } + + function _restyle(arg) { + return function() { return Plotly.restyle(gd, arg); }; + } + + Plotly.plot(gd, [{ + mode: 'markers', + y: [1, 2, 1], + marker: { color: [1, -1, 4] } + }]) + .then(_assert('base', { + cauto: true, + cmid: undefined, + cmin: -1, + cmax: 4 + })) + .then(_restyle({'marker.cmid': 0})) + .then(_assert('set cmid=0', { + cauto: true, + cmid: 0, + cmin: -4, + cmax: 4 + })) + .then(_restyle({'marker.cmid': -2})) + .then(_assert('set cmid=-2', { + cauto: true, + cmid: -2, + cmin: -8, + cmax: 4 + })) + .then(_restyle({'marker.cmid': 2})) + .then(_assert('set cmid=2', { + cauto: true, + cmid: 2, + cmin: -1, + cmax: 5 + })) + .then(_restyle({'marker.cmin': 0})) + .then(_assert('set cmin=0', { + cauto: false, + cmid: undefined, + cmin: 0, + cmax: 5 + })) + .then(_restyle({'marker.cmax': 10})) + .then(_assert('set cmin=0 + cmax=10', { + cauto: false, + cmid: undefined, + cmin: 0, + cmax: 10 + })) + .then(_restyle({'marker.cauto': true, 'marker.cmid': null})) + .then(_assert('back to cauto=true', { + cauto: true, + cmid: undefined, + cmin: -1, + cmax: 4 + })) + .catch(failTest) + .then(done); + }); + it('turns off autobin when you edit bin specs', function(done) { // test retained (modified) for backward compat with new autobin logic var start0 = 0.2; diff --git a/test/jasmine/tests/scattergeo_test.js b/test/jasmine/tests/scattergeo_test.js index f84846d2714..fd87caddeac 100644 --- a/test/jasmine/tests/scattergeo_test.js +++ b/test/jasmine/tests/scattergeo_test.js @@ -90,7 +90,7 @@ describe('Test scattergeo defaults', function() { describe('Test scattergeo calc', function() { function _calc(opts) { - var base = { type: 'scattermapbox' }; + var base = { type: 'scattergeo' }; var trace = Lib.extendFlat({}, base, opts); var gd = { data: [trace] }; diff --git a/test/jasmine/tests/surface_test.js b/test/jasmine/tests/surface_test.js index 943189c325b..2d77fed1928 100644 --- a/test/jasmine/tests/surface_test.js +++ b/test/jasmine/tests/surface_test.js @@ -127,16 +127,14 @@ describe('Test surface', function() { it('should coerce \'c\' attributes with \'c\' values regardless of `\'z\' if \'c\' is present', function() { traceIn = { z: [[1, 2, 3], [2, 1, 2]], - zauto: false, zmin: 0, zmax: 10, - cauto: true, cmin: -10, cmax: 20 }; supplyDefaults(traceIn, traceOut, defaultColor, layout); - expect(traceOut.cauto).toEqual(true); + expect(traceOut.cauto).toEqual(false); expect(traceOut.cmin).toEqual(-10); expect(traceOut.cmax).toEqual(20); });