From 172933cf5aa09b64f8f09698250f2703d3a7f3f3 Mon Sep 17 00:00:00 2001 From: Antoine Roy-Gobeil Date: Wed, 30 Oct 2019 17:18:03 -0400 Subject: [PATCH 1/6] image: return a transparent pixel if its value is not numeric --- src/traces/image/plot.js | 9 ++++++++- test/image/baselines/image_non_numeric.png | Bin 0 -> 7310 bytes test/image/mocks/image_non_numeric.json | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 test/image/baselines/image_non_numeric.png create mode 100644 test/image/mocks/image_non_numeric.json diff --git a/src/traces/image/plot.js b/src/traces/image/plot.js index e0f6087f8a4..4acb1648b03 100644 --- a/src/traces/image/plot.js +++ b/src/traces/image/plot.js @@ -9,6 +9,7 @@ 'use strict'; var d3 = require('d3'); var Lib = require('../../lib'); +var isNumeric = require('fast-isnumeric'); var xmlnsNamespaces = require('../../constants/xmlns_namespaces'); var constants = require('./constants'); @@ -50,6 +51,7 @@ var scaler = function(trace) { return function(pixel) { var c = pixel.slice(0, n); for(var k = 0; k < n; k++) { + if(!c[k] || !isNumeric(c[k])) return false; c[k] = s[k](c[k]); } return c; @@ -142,7 +144,12 @@ module.exports.plot = function(gd, plotinfo, cdimage, imageLayer) { var jpx0 = jpx(j); var jpx1 = jpx(j + 1); if(jpx1 === jpx0 || isNaN(jpx1) || isNaN(jpx0) || !z[j][i]) continue; c = trace._scaler(z[j][i]); - context.fillStyle = trace.colormodel + '(' + fmt(c).join(',') + ')'; + if(c) { + context.fillStyle = trace.colormodel + '(' + fmt(c).join(',') + ')'; + } else { + // Return a transparent pixel + context.fillStyle = 'rgba(0,0,0,0)'; + } context.fillRect(ipx0, jpx0, ipx1 - ipx0, jpx1 - jpx0); } } diff --git a/test/image/baselines/image_non_numeric.png b/test/image/baselines/image_non_numeric.png new file mode 100644 index 0000000000000000000000000000000000000000..5225f6d17831f1130cb00a9a6675720c732488ff GIT binary patch literal 7310 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 Date: Thu, 31 Oct 2019 12:29:06 -0400 Subject: [PATCH 2/6] revise skip logic - fixes various failing tests --- src/traces/image/plot.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/traces/image/plot.js b/src/traces/image/plot.js index 4acb1648b03..37c6534d7bc 100644 --- a/src/traces/image/plot.js +++ b/src/traces/image/plot.js @@ -51,8 +51,9 @@ var scaler = function(trace) { return function(pixel) { var c = pixel.slice(0, n); for(var k = 0; k < n; k++) { - if(!c[k] || !isNumeric(c[k])) return false; - c[k] = s[k](c[k]); + var ck = c[k]; + if(!isNumeric(ck)) return false; + c[k] = s[k](ck); } return c; }; From 720db11edf607ba8643d75b00f2b4e7c9afac4aa Mon Sep 17 00:00:00 2001 From: archmoj Date: Thu, 31 Oct 2019 12:42:46 -0400 Subject: [PATCH 3/6] refactor image functions --- src/traces/image/constants.js | 1 - src/traces/image/defaults.js | 1 - src/traces/image/index.js | 2 +- src/traces/image/plot.js | 33 +++++++++++++++++---------------- src/traces/image/style.js | 1 - 5 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/traces/image/constants.js b/src/traces/image/constants.js index 865c340ff40..1a4570c3f1c 100644 --- a/src/traces/image/constants.js +++ b/src/traces/image/constants.js @@ -6,7 +6,6 @@ * LICENSE file in the root directory of this source tree. */ - 'use strict'; module.exports = { diff --git a/src/traces/image/defaults.js b/src/traces/image/defaults.js index ee55ee8859f..c2ebd3c3310 100644 --- a/src/traces/image/defaults.js +++ b/src/traces/image/defaults.js @@ -6,7 +6,6 @@ * LICENSE file in the root directory of this source tree. */ - 'use strict'; var Lib = require('../../lib'); diff --git a/src/traces/image/index.js b/src/traces/image/index.js index 69c58b5fed9..9ae0c9542be 100644 --- a/src/traces/image/index.js +++ b/src/traces/image/index.js @@ -12,7 +12,7 @@ module.exports = { attributes: require('./attributes'), supplyDefaults: require('./defaults'), calc: require('./calc'), - plot: require('./plot').plot, + plot: require('./plot'), style: require('./style'), hoverPoints: require('./hover'), eventData: require('./event_data'), diff --git a/src/traces/image/plot.js b/src/traces/image/plot.js index 37c6534d7bc..3b2d76cce2d 100644 --- a/src/traces/image/plot.js +++ b/src/traces/image/plot.js @@ -7,32 +7,31 @@ */ 'use strict'; + var d3 = require('d3'); var Lib = require('../../lib'); var isNumeric = require('fast-isnumeric'); var xmlnsNamespaces = require('../../constants/xmlns_namespaces'); var constants = require('./constants'); -module.exports = {}; +function scale(zero, factor, min, max) { + return function(c) { + c = (c - zero) * factor; + c = Lib.constrain(c, min, max); + return c; + }; +} + +function constrain(min, max) { + return function(c) { return Lib.constrain(c, min, max);}; +} // Generate a function to scale color components according to zmin/zmax and the colormodel -var scaler = function(trace) { +var makeScaler = function(trace) { var colormodel = trace.colormodel; var n = colormodel.length; var cr = constants.colormodel[colormodel]; - function scale(zero, factor, min, max) { - return function(c) { - c = (c - zero) * factor; - c = Lib.constrain(c, min, max); - return c; - }; - } - - function constrain(min, max) { - return function(c) { return Lib.constrain(c, min, max);}; - } - var s = []; // Loop over all color components for(var k = 0; k < n; k++) { @@ -58,7 +57,8 @@ var scaler = function(trace) { return c; }; }; -module.exports.plot = function(gd, plotinfo, cdimage, imageLayer) { + +module.exports = function plot(gd, plotinfo, cdimage, imageLayer) { var xa = plotinfo.xaxis; var ya = plotinfo.yaxis; @@ -132,10 +132,11 @@ module.exports.plot = function(gd, plotinfo, cdimage, imageLayer) { canvas.width = imageWidth; canvas.height = imageHeight; var context = canvas.getContext('2d'); + var ipx = function(i) {return Lib.constrain(Math.round(xa.c2p(x0 + i * dx) - left), 0, imageWidth);}; var jpx = function(j) {return Lib.constrain(Math.round(ya.c2p(y0 + j * dy) - top), 0, imageHeight);}; - trace._scaler = scaler(trace); + trace._scaler = makeScaler(trace); var fmt = constants.colormodel[trace.colormodel].fmt; var c; for(i = 0; i < cd0.w; i++) { diff --git a/src/traces/image/style.js b/src/traces/image/style.js index 9f98c4f4bd2..efb79761b32 100644 --- a/src/traces/image/style.js +++ b/src/traces/image/style.js @@ -6,7 +6,6 @@ * LICENSE file in the root directory of this source tree. */ - 'use strict'; var d3 = require('d3'); From 1a55816456787fa32083336ef7f30be7d31ed83c Mon Sep 17 00:00:00 2001 From: archmoj Date: Thu, 31 Oct 2019 15:16:40 -0400 Subject: [PATCH 4/6] move _scaler function from plot to calc step --- src/traces/image/attributes.js | 20 +++++++------- src/traces/image/calc.js | 48 ++++++++++++++++++++++++++++++++++ src/traces/image/plot.js | 46 -------------------------------- 3 files changed, 58 insertions(+), 56 deletions(-) diff --git a/src/traces/image/attributes.js b/src/traces/image/attributes.js index c41d7aa4e0c..1ff7ea48b81 100644 --- a/src/traces/image/attributes.js +++ b/src/traces/image/attributes.js @@ -41,13 +41,13 @@ module.exports = extendFlat({ zmin: { valType: 'info_array', items: [ - {valType: 'number', editType: 'plot'}, - {valType: 'number', editType: 'plot'}, - {valType: 'number', editType: 'plot'}, - {valType: 'number', editType: 'plot'} + {valType: 'number', editType: 'calc'}, + {valType: 'number', editType: 'calc'}, + {valType: 'number', editType: 'calc'}, + {valType: 'number', editType: 'calc'} ], role: 'info', - editType: 'plot', + editType: 'calc', description: [ 'Array defining the lower bound for each color component.', 'Note that the default value will depend on the colormodel.', @@ -57,13 +57,13 @@ module.exports = extendFlat({ zmax: { valType: 'info_array', items: [ - {valType: 'number', editType: 'plot'}, - {valType: 'number', editType: 'plot'}, - {valType: 'number', editType: 'plot'}, - {valType: 'number', editType: 'plot'} + {valType: 'number', editType: 'calc'}, + {valType: 'number', editType: 'calc'}, + {valType: 'number', editType: 'calc'}, + {valType: 'number', editType: 'calc'} ], role: 'info', - editType: 'plot', + editType: 'calc', description: [ 'Array defining the higher bound for each color component.', 'Note that the default value will depend on the colormodel.', diff --git a/src/traces/image/calc.js b/src/traces/image/calc.js index e826c3c2a5a..745d627a722 100644 --- a/src/traces/image/calc.js +++ b/src/traces/image/calc.js @@ -8,6 +8,9 @@ 'use strict'; +var Lib = require('../../lib'); +var constants = require('./constants'); +var isNumeric = require('fast-isnumeric'); var Axes = require('../../plots/cartesian/axes'); var maxRowLength = require('../../lib').maxRowLength; @@ -28,6 +31,7 @@ module.exports = function calc(gd, trace) { if(ya && ya.type === 'log') for(i = 0; i < h; i++) yrange.push(y0 + i * trace.dy); trace._extremes[xa._id] = Axes.findExtremes(xa, xrange); trace._extremes[ya._id] = Axes.findExtremes(ya, yrange); + trace._scaler = makeScaler(trace); var cd0 = { x0: x0, @@ -38,3 +42,47 @@ module.exports = function calc(gd, trace) { }; return [cd0]; }; + +function scale(zero, factor, min, max) { + return function(c) { + c = (c - zero) * factor; + c = Lib.constrain(c, min, max); + return c; + }; +} + +function constrain(min, max) { + return function(c) { return Lib.constrain(c, min, max);}; +} + +// Generate a function to scale color components according to zmin/zmax and the colormodel +function makeScaler(trace) { + var colormodel = trace.colormodel; + var n = colormodel.length; + var cr = constants.colormodel[colormodel]; + + var s = []; + // Loop over all color components + for(var k = 0; k < n; k++) { + if(cr.min[k] !== trace.zmin[k] || cr.max[k] !== trace.zmax[k]) { + s.push(scale( + trace.zmin[k], + (cr.max[k] - cr.min[k]) / (trace.zmax[k] - trace.zmin[k]), + cr.min[k], + cr.max[k] + )); + } else { + s.push(constrain(cr.min[k], cr.max[k])); + } + } + + return function(pixel) { + var c = pixel.slice(0, n); + for(var k = 0; k < n; k++) { + var ck = c[k]; + if(!isNumeric(ck)) return false; + c[k] = s[k](ck); + } + return c; + }; +} diff --git a/src/traces/image/plot.js b/src/traces/image/plot.js index 3b2d76cce2d..851c1772c11 100644 --- a/src/traces/image/plot.js +++ b/src/traces/image/plot.js @@ -10,54 +10,9 @@ var d3 = require('d3'); var Lib = require('../../lib'); -var isNumeric = require('fast-isnumeric'); var xmlnsNamespaces = require('../../constants/xmlns_namespaces'); var constants = require('./constants'); -function scale(zero, factor, min, max) { - return function(c) { - c = (c - zero) * factor; - c = Lib.constrain(c, min, max); - return c; - }; -} - -function constrain(min, max) { - return function(c) { return Lib.constrain(c, min, max);}; -} - -// Generate a function to scale color components according to zmin/zmax and the colormodel -var makeScaler = function(trace) { - var colormodel = trace.colormodel; - var n = colormodel.length; - var cr = constants.colormodel[colormodel]; - - var s = []; - // Loop over all color components - for(var k = 0; k < n; k++) { - if(cr.min[k] !== trace.zmin[k] || cr.max[k] !== trace.zmax[k]) { - s.push(scale( - trace.zmin[k], - (cr.max[k] - cr.min[k]) / (trace.zmax[k] - trace.zmin[k]), - cr.min[k], - cr.max[k] - )); - } else { - s.push(constrain(cr.min[k], cr.max[k])); - } - } - - return function(pixel) { - var c = pixel.slice(0, n); - for(var k = 0; k < n; k++) { - var ck = c[k]; - if(!isNumeric(ck)) return false; - c[k] = s[k](ck); - } - return c; - }; -}; - module.exports = function plot(gd, plotinfo, cdimage, imageLayer) { var xa = plotinfo.xaxis; var ya = plotinfo.yaxis; @@ -136,7 +91,6 @@ module.exports = function plot(gd, plotinfo, cdimage, imageLayer) { var ipx = function(i) {return Lib.constrain(Math.round(xa.c2p(x0 + i * dx) - left), 0, imageWidth);}; var jpx = function(j) {return Lib.constrain(Math.round(ya.c2p(y0 + j * dy) - top), 0, imageHeight);}; - trace._scaler = makeScaler(trace); var fmt = constants.colormodel[trace.colormodel].fmt; var c; for(i = 0; i < cd0.w; i++) { From d4fec74372d45d5738669838a66e0faf590830e2 Mon Sep 17 00:00:00 2001 From: archmoj Date: Thu, 31 Oct 2019 15:56:34 -0400 Subject: [PATCH 5/6] change colormodel from plot to calc and add scale reference array to the trace --- src/traces/image/attributes.js | 2 +- src/traces/image/calc.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/traces/image/attributes.js b/src/traces/image/attributes.js index 1ff7ea48b81..ac193e559a0 100644 --- a/src/traces/image/attributes.js +++ b/src/traces/image/attributes.js @@ -35,7 +35,7 @@ module.exports = extendFlat({ values: cm, dflt: 'rgb', role: 'info', - editType: 'plot', + editType: 'calc', description: 'Color model used to map the numerical color components described in `z` into colors.' }, zmin: { diff --git a/src/traces/image/calc.js b/src/traces/image/calc.js index 745d627a722..5d420d19239 100644 --- a/src/traces/image/calc.js +++ b/src/traces/image/calc.js @@ -61,18 +61,18 @@ function makeScaler(trace) { var n = colormodel.length; var cr = constants.colormodel[colormodel]; - var s = []; + trace._sArray = []; // Loop over all color components for(var k = 0; k < n; k++) { if(cr.min[k] !== trace.zmin[k] || cr.max[k] !== trace.zmax[k]) { - s.push(scale( + trace._sArray.push(scale( trace.zmin[k], (cr.max[k] - cr.min[k]) / (trace.zmax[k] - trace.zmin[k]), cr.min[k], cr.max[k] )); } else { - s.push(constrain(cr.min[k], cr.max[k])); + trace._sArray.push(constrain(cr.min[k], cr.max[k])); } } @@ -81,7 +81,7 @@ function makeScaler(trace) { for(var k = 0; k < n; k++) { var ck = c[k]; if(!isNumeric(ck)) return false; - c[k] = s[k](ck); + c[k] = trace._sArray[k](ck); } return c; }; From d6abde903a8988edc218b63498381d9d2fa472a5 Mon Sep 17 00:00:00 2001 From: archmoj Date: Thu, 31 Oct 2019 22:36:55 -0400 Subject: [PATCH 6/6] improve new image mock and refactor scale function in calc --- src/traces/image/calc.js | 6 +- test/image/baselines/image_non_numeric.png | Bin 7310 -> 19964 bytes test/image/mocks/image_non_numeric.json | 86 ++++++++++++++++++++- 3 files changed, 84 insertions(+), 8 deletions(-) diff --git a/src/traces/image/calc.js b/src/traces/image/calc.js index 5d420d19239..db3df881b78 100644 --- a/src/traces/image/calc.js +++ b/src/traces/image/calc.js @@ -43,11 +43,9 @@ module.exports = function calc(gd, trace) { return [cd0]; }; -function scale(zero, factor, min, max) { +function scale(zero, ratio, min, max) { return function(c) { - c = (c - zero) * factor; - c = Lib.constrain(c, min, max); - return c; + return Lib.constrain((c - zero) * ratio, min, max); }; } diff --git a/test/image/baselines/image_non_numeric.png b/test/image/baselines/image_non_numeric.png index 5225f6d17831f1130cb00a9a6675720c732488ff..78da6e369b1b6587a1c41127734c4eb68e0b84d9 100644 GIT binary patch literal 19964 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 literal 7310 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