Skip to content

Commit 3962593

Browse files
committed
properly handle possibly negative z values (mask) and negative cmin/cmax (fall back to linear scale)
1 parent 36c49c5 commit 3962593

3 files changed

Lines changed: 69 additions & 24 deletions

File tree

src/components/colorbar/draw.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -957,6 +957,8 @@ function mockColorBarAxis(gd, opts, zrange) {
957957

958958
var cbAxisIn = {
959959
type: opts.type || 'linear',
960+
// Colorscale.calc guarantees zrange is strictly positive whenever
961+
// opts.type is still 'log' by the time we get here
960962
range: opts.type === 'log' ?
961963
[Math.log10(zrange[0]), Math.log10(zrange[1])] : zrange,
962964
tickmode: opts.tickmode,

src/components/colorscale/calc.js

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,24 @@ var isNumeric = require('fast-isnumeric');
55
var Lib = require('../../lib');
66
var extractOpts = require('./helpers').extractOpts;
77

8+
// mirrors Lib.aggNums's own recursion into nested (eg heatmap z) arrays, but
9+
// masks out non-positive values first - matching how cartesian log axes
10+
// exclude non-positive data from autorange (see findExtremes in
11+
// plots/cartesian/autorange.js)
12+
function maskNonPositive(vals) {
13+
if(Lib.isArrayOrTypedArray(vals[0])) return vals.map(maskNonPositive);
14+
15+
var out = new Array(vals.length);
16+
for(var i = 0; i < vals.length; i++) {
17+
var v = vals[i];
18+
out[i] = (isNumeric(v) && v > 0) ? v : undefined;
19+
}
20+
return out;
21+
}
22+
823
module.exports = function calc(gd, trace, opts) {
924
var fullLayout = gd._fullLayout;
10-
var vals = opts.vals;
25+
var rawVals = opts.vals;
1126
var containerStr = opts.containerStr;
1227

1328
var container = containerStr ?
@@ -20,6 +35,27 @@ module.exports = function calc(gd, trace, opts) {
2035
var max = cOpts.max;
2136
var mid = cOpts.mid;
2237

38+
var colorbar = cOpts.colorbar;
39+
var isLog = !!(colorbar && colorbar.type === 'log');
40+
41+
if(isLog) {
42+
// a log colorbar needs a strictly positive domain: an explicit
43+
// non-positive cmin/cmax can never be logged, and if this trace's
44+
// data is going to be scanned for an auto min/max but has no
45+
// positive values at all, there's nothing to scale from. Either way,
46+
// fall back to a linear colorbar instead of an undefined/negative range.
47+
var fullyPinned = !auto && isNumeric(min) && isNumeric(max);
48+
var hasExplicitNonPositive = (isNumeric(min) && min <= 0) || (isNumeric(max) && max <= 0);
49+
var hasPositiveData = fullyPinned || isNumeric(Lib.aggNums(Math.max, null, maskNonPositive(rawVals)));
50+
51+
if(hasExplicitNonPositive || !hasPositiveData) {
52+
isLog = false;
53+
colorbar.type = 'linear';
54+
}
55+
}
56+
57+
var vals = isLog ? maskNonPositive(rawVals) : rawVals;
58+
2359
var minVal = function() { return Lib.aggNums(Math.min, null, vals); };
2460
var maxVal = function() { return Lib.aggNums(Math.max, null, vals); };
2561

@@ -43,7 +79,9 @@ module.exports = function calc(gd, trace, opts) {
4379
}
4480
}
4581

46-
if(auto && mid !== undefined) {
82+
// a symmetric-around-cmid range is a linear-domain concept - skip it for
83+
// log colorbars rather than risk pushing a positive min/max non-positive
84+
if(auto && mid !== undefined && !isLog) {
4785
if(max - mid > mid - min) {
4886
min = mid - (max - mid);
4987
} else if(max - mid < mid - min) {

src/components/colorscale/helpers.js

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,14 @@ function extractScale(cont) {
118118
var cmax = cOpts.max;
119119

120120
// Check if a log type is defined on the colorbar
121+
// (Colorscale.calc guarantees cmin/cmax are strictly positive whenever
122+
// type is still 'log' by the time we get here - see maskNonPositive)
121123
var type = (cOpts.colorbar && cOpts.colorbar.type) || 'linear';
122124

123125
// Convert domain boundaries to base-10 log if required
124126
if(type === 'log') {
125-
if(cmin > 0) cmin = Math.log10(cmin);
126-
if(cmax > 0) cmax = Math.log10(cmax);
127+
cmin = Math.log10(cmin);
128+
cmax = Math.log10(cmax);
127129
}
128130

129131
var scl = cOpts.reversescale ?
@@ -217,30 +219,33 @@ function makeColorScaleFunc(specs, opts) {
217219
}
218220

219221
function makeColorScaleFuncFromTrace(trace, opts) {
220-
var baseColorFn = makeColorScaleFunc(extractScale(trace), opts);
221222
var cOpts = extractOpts(trace);
222223
var type = (cOpts.colorbar && cOpts.colorbar.type) || 'linear';
224+
var baseColorFn = makeColorScaleFunc(extractScale(trace), opts);
223225

224-
// Wrap the base generator if we need to dynamically apply log transformations
225-
if (type === 'log') {
226-
var wrappedFunc = function(v) {
227-
if (isNumeric(v)) {
228-
if (v <= 0) return 'rgba(0,0,0,0)'; // Logarithm of zero/negative is undefined
229-
return baseColorFn(Math.log10(v));
230-
} else if (tinycolor(v).isValid()) {
231-
return v;
232-
} else {
233-
return Color.defaultLine;
234-
}
235-
};
226+
if(type !== 'log') return baseColorFn;
236227

237-
// Preserve native domain and range methods for the draw components
238-
wrappedFunc.domain = baseColorFn.domain;
239-
wrappedFunc.range = baseColorFn.range;
240-
return wrappedFunc;
241-
}
228+
opts = opts || {};
229+
var noNumericCheck = opts.noNumericCheck;
230+
var returnArray = opts.returnArray;
231+
232+
// Wrap the base generator to dynamically apply log transformations,
233+
// matching the array/string return shape makeColorScaleFunc would have used
234+
var wrappedFunc = function(v) {
235+
if(noNumericCheck || isNumeric(v)) {
236+
// log of zero/negative is undefined - treat as out-of-range
237+
if(v > 0) return baseColorFn(Math.log10(v));
238+
return returnArray ? [0, 0, 0, 0] : 'rgba(0,0,0,0)';
239+
} else if(tinycolor(v).isValid()) {
240+
return v;
241+
}
242+
return Color.defaultLine;
243+
};
242244

243-
return baseColorFn;
245+
// Preserve native domain and range methods for the draw components
246+
wrappedFunc.domain = baseColorFn.domain;
247+
wrappedFunc.range = baseColorFn.range;
248+
return wrappedFunc;
244249
}
245250

246251
function colorArray2rbga(colorArray) {
@@ -261,4 +266,4 @@ module.exports = {
261266
flipScale: flipScale,
262267
makeColorScaleFunc: makeColorScaleFunc,
263268
makeColorScaleFuncFromTrace: makeColorScaleFuncFromTrace
264-
};
269+
};

0 commit comments

Comments
 (0)