diff --git a/.circleci/config.yml b/.circleci/config.yml index 1f31df70a85..ebebab6adde 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -292,6 +292,25 @@ jobs: paths: - plotly.js + make-baselines-b64: + parallelism: 4 + docker: + - image: circleci/python:3.8.9 + working_directory: ~/plotly.js + steps: + - attach_workspace: + at: ~/ + - run: + name: Install kaleido, plotly.io and required fonts + command: .circleci/env_image.sh + - run: + name: Create all png files + command: .circleci/test.sh make-baselines-b64 + - persist_to_workspace: + root: ~/ + paths: + - plotly.js + test-baselines: docker: - image: circleci/node:16.9.0 @@ -320,6 +339,20 @@ jobs: path: build destination: / + test-baselines-b64: + docker: + - image: circleci/node:16.9.0 + working_directory: ~/plotly.js + steps: + - attach_workspace: + at: ~/ + - run: + name: Compare pixels + command: .circleci/test.sh test-image ; find build -maxdepth 1 -type f -delete + - store_artifacts: + path: build + destination: / + test-baselines-mathjax3: docker: - image: circleci/node:16.9.0 @@ -512,6 +545,12 @@ workflows: - test-baselines-mathjax3: requires: - make-baselines-mathjax3 + - make-baselines-b64: + requires: + - install-and-cibuild + - test-baselines-b64: + requires: + - make-baselines-b64 - make-baselines: requires: - install-and-cibuild diff --git a/.circleci/env_image.sh b/.circleci/env_image.sh index 87d40fa2889..4b684aa1599 100755 --- a/.circleci/env_image.sh +++ b/.circleci/env_image.sh @@ -6,3 +6,5 @@ sudo cp -r .circleci/fonts/ /usr/share/ && \ sudo fc-cache -f && \ # install kaleido & plotly sudo python3 -m pip install kaleido==0.2.1 plotly==5.5.0 --progress-bar off +# install numpy i.e. to convert arrays to typed arrays +sudo python3 -m pip install numpy==1.24.2 diff --git a/.circleci/test.sh b/.circleci/test.sh index 32532182823..cda97917c29 100755 --- a/.circleci/test.sh +++ b/.circleci/test.sh @@ -106,6 +106,12 @@ case $1 in exit $EXIT_STATE ;; + make-baselines-b64) + SUITE=$(find $ROOT/test/image/mocks/ -type f -printf "%f\n" | sed 's/\.json$//1' | circleci tests split) + python3 test/image/make_baseline.py b64 $SUITE || EXIT_STATE=$? + exit $EXIT_STATE + ;; + make-baselines) SUITE=$(find $ROOT/test/image/mocks/ -type f -printf "%f\n" | sed 's/\.json$//1' | circleci tests split) python3 test/image/make_baseline.py $SUITE || EXIT_STATE=$? diff --git a/.eslintrc b/.eslintrc index b852aafd41d..5af89ce0f25 100644 --- a/.eslintrc +++ b/.eslintrc @@ -14,8 +14,12 @@ "Float32Array": true, "Float64Array": true, "Uint8Array": true, + "Int8Array": true, + "Uint8ClampedArray": true, "Int16Array": true, + "Uint16Array": true, "Int32Array": true, + "Uint32Array": true, "ArrayBuffer": true, "DataView": true, "SVGElement": false diff --git a/devtools/test_dashboard/server.js b/devtools/test_dashboard/server.js index b8e6103eba3..4c069e88379 100644 --- a/devtools/test_dashboard/server.js +++ b/devtools/test_dashboard/server.js @@ -23,6 +23,7 @@ if(!strict) { config.devtool = 'eval'; } +var mockFolder = constants.pathToTestImageMocks; // mock list getMockFiles() @@ -104,7 +105,7 @@ compiler.run(function(devtoolsErr, devtoolsStats) { function getMockFiles() { return new Promise(function(resolve, reject) { - fs.readdir(constants.pathToTestImageMocks, function(err, files) { + fs.readdir(mockFolder, function(err, files) { if(err) { reject(err); } else { @@ -116,7 +117,7 @@ function getMockFiles() { function readFiles(files) { var promises = files.map(function(file) { - var filePath = path.join(constants.pathToTestImageMocks, file); + var filePath = path.join(mockFolder, file); return readFilePromise(filePath); }); diff --git a/draftlogs/5230_add.md b/draftlogs/5230_add.md new file mode 100644 index 00000000000..b846e16184b --- /dev/null +++ b/draftlogs/5230_add.md @@ -0,0 +1 @@ + - Accept objects for encoded typedarrays in data_array valType [[#5230](https://github.com/plotly/plotly.js/pull/5230)] diff --git a/package-lock.json b/package-lock.json index e6dd94e1092..3d5bc91b039 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@turf/area": "^6.4.0", "@turf/bbox": "^6.4.0", "@turf/centroid": "^6.0.2", + "base64-arraybuffer": "^1.0.2", "canvas-fit": "^1.5.0", "color-alpha": "1.0.4", "color-normalize": "1.5.0", @@ -3306,6 +3307,14 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", @@ -15362,6 +15371,11 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==" + }, "base64-js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", diff --git a/package.json b/package.json index 161abfbf83a..f0d1c666e6f 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "test-requirejs": "node tasks/test_requirejs.js", "test-plain-obj": "node tasks/test_plain_obj.js", "test": "npm run test-jasmine -- --nowatch && npm run test-bundle && npm run test-image && npm run test-export && npm run test-syntax && npm run lint", + "b64": "python3 test/image/generate_b64_mocks.py && node devtools/test_dashboard/server.js", "mathjax3": "node devtools/test_dashboard/server.js --mathjax3", "mathjax3chtml": "node devtools/test_dashboard/server.js --mathjax3chtml", "strict": "node devtools/test_dashboard/server.js --strict", @@ -77,6 +78,7 @@ "@turf/area": "^6.4.0", "@turf/bbox": "^6.4.0", "@turf/centroid": "^6.0.2", + "base64-arraybuffer": "^1.0.2", "canvas-fit": "^1.5.0", "color-alpha": "1.0.4", "color-normalize": "1.5.0", diff --git a/src/components/colorscale/helpers.js b/src/components/colorscale/helpers.js index fed32703b22..bc476e0a9b5 100644 --- a/src/components/colorscale/helpers.js +++ b/src/components/colorscale/helpers.js @@ -13,7 +13,9 @@ function hasColorscale(trace, containerStr, colorKey) { var container = containerStr ? Lib.nestedProperty(trace, containerStr).get() || {} : trace; + var color = container[colorKey || 'color']; + if(color && color._inputArray) color = color._inputArray; var isArrayWithOneNumber = false; if(Lib.isArrayOrTypedArray(color)) { diff --git a/src/lib/array.js b/src/lib/array.js index 19463675a2d..6c5db031f09 100644 --- a/src/lib/array.js +++ b/src/lib/array.js @@ -1,4 +1,7 @@ 'use strict'; +var b64decode = require('base64-arraybuffer').decode; + +var isPlainObject = require('./is_plain_object'); var isArray = Array.isArray; @@ -48,6 +51,140 @@ exports.ensureArray = function(out, n) { return out; }; +var typedArrays = { + u1c: typeof Uint8ClampedArray === 'undefined' ? undefined : + Uint8ClampedArray, // not supported in numpy? + + i1: typeof Int8Array === 'undefined' ? undefined : + Int8Array, + + u1: typeof Uint8Array === 'undefined' ? undefined : + Uint8Array, + + i2: typeof Int16Array === 'undefined' ? undefined : + Int16Array, + + u2: typeof Uint16Array === 'undefined' ? undefined : + Uint16Array, + + i4: typeof Int32Array === 'undefined' ? undefined : + Int32Array, + + u4: typeof Uint32Array === 'undefined' ? undefined : + Uint32Array, + + f4: typeof Float32Array === 'undefined' ? undefined : + Float32Array, + + f8: typeof Float64Array === 'undefined' ? undefined : + Float64Array, + + /* TODO: potentially add Big Int + + i8: typeof BigInt64Array === 'undefined' ? undefined : + BigInt64Array, + + u8: typeof BigUint64Array === 'undefined' ? undefined : + BigUint64Array, + */ +}; + +typedArrays.uint8c = typedArrays.u1c; +typedArrays.uint8 = typedArrays.u1; +typedArrays.int8 = typedArrays.i1; +typedArrays.uint16 = typedArrays.u2; +typedArrays.int16 = typedArrays.i2; +typedArrays.uint32 = typedArrays.u4; +typedArrays.int32 = typedArrays.i4; +typedArrays.float32 = typedArrays.f4; +typedArrays.float64 = typedArrays.f8; + +function isArrayBuffer(a) { + return a.constructor === ArrayBuffer; +} +exports.isArrayBuffer = isArrayBuffer; + +exports.decodeTypedArraySpec = function(vIn) { + var out = []; + var v = coerceTypedArraySpec(vIn); + var dtype = v.dtype; + + var T = typedArrays[dtype]; + if(!T) throw new Error('Error in dtype: "' + dtype + '"'); + var BYTES_PER_ELEMENT = T.BYTES_PER_ELEMENT; + + var buffer = v.bdata; + if(!isArrayBuffer(buffer)) { + buffer = b64decode(buffer); + } + var shape = v.shape === undefined ? + // detect 1-d length + [buffer.byteLength / BYTES_PER_ELEMENT] : + // convert number to string and split to array + ('' + v.shape).split(','); + + shape.reverse(); // i.e. to match numpy order + var ndim = shape.length; + + var nj, j; + var ni = +shape[0]; + + var rowBytes = BYTES_PER_ELEMENT * ni; + var pos = 0; + + if(ndim === 1) { + out = new T(buffer); + } else if(ndim === 2) { + nj = +shape[1]; + for(j = 0; j < nj; j++) { + out[j] = new T(buffer, pos, ni); + pos += rowBytes; + } + } else if(ndim === 3) { + nj = +shape[1]; + var nk = +shape[2]; + for(var k = 0; k < nk; k++) { + out[k] = []; + for(j = 0; j < nj; j++) { + out[k][j] = new T(buffer, pos, ni); + pos += rowBytes; + } + } + } else { + throw new Error('ndim: ' + ndim + 'is not supported with the shape:"' + v.shape + '"'); + } + + // attach bdata, dtype & shape to array for json export + out.bdata = v.bdata; + out.dtype = v.dtype; + out.shape = shape.reverse().join(','); + + vIn._inputArray = out; + + return out; +}; + +exports.isTypedArraySpec = function(v) { + return ( + isPlainObject(v) && + v.hasOwnProperty('dtype') && (typeof v.dtype === 'string') && + + v.hasOwnProperty('bdata') && (typeof v.bdata === 'string' || isArrayBuffer(v.bdata)) && + + (v.shape === undefined || ( + v.hasOwnProperty('shape') && (typeof v.shape === 'string' || typeof v.shape === 'number') + )) + ); +}; + +function coerceTypedArraySpec(v) { + return { + bdata: v.bdata, + dtype: v.dtype, + shape: v.shape + }; +} + /* * TypedArray-compatible concatenation of n arrays * if all arrays are the same type it will preserve that type, diff --git a/src/lib/coerce.js b/src/lib/coerce.js index c679db05da9..5ad6dc61a2d 100644 --- a/src/lib/coerce.js +++ b/src/lib/coerce.js @@ -12,21 +12,46 @@ var nestedProperty = require('./nested_property'); var counterRegex = require('./regex').counter; var modHalf = require('./mod').modHalf; var isArrayOrTypedArray = require('./array').isArrayOrTypedArray; +var isTypedArraySpec = require('./array').isTypedArraySpec; +var decodeTypedArraySpec = require('./array').decodeTypedArraySpec; exports.valObjectMeta = { data_array: { // You can use *dflt=[] to force said array to exist though. description: [ 'An {array} of data.', - 'The value MUST be an {array}, or we ignore it.', - 'Note that typed arrays (e.g. Float32Array) are supported.' + 'The value must represent an {array} or it will be ignored,', + 'but this array can be provided in several forms:', + '(1) a regular {array} object', + '(2) a typed array (e.g. Float32Array)', + '(3) an object with keys dtype, bdata, and optionally shape.', + 'In this 3rd form, dtype is one of', + '*f8*, *f4*.', + '*i4*, *u4*,', + '*i2*, *u2*,', + '*i1*, *u1* or *u1c* for Uint8ClampedArray.', + 'In addition to shorthand `dtype` above one could also use the following forms:', + '*float64*, *float32*,', + '*int32*, *uint32*,', + '*int16*, *uint16*,', + '*int8*, *uint8* or *uint8c* for Uint8ClampedArray.', + '`bdata` is either a base64-encoded string or the ArrayBuffer of', + 'an integer or float typed array.', + 'For either multi-dimensional arrays you must also', + 'provide its dimensions separated by comma via `shape`.', + 'For example using `dtype`: *f4* and `shape`: *5,100* you can', + 'declare a 2-D array that has 5 rows and 100 columns', + 'containing float32 values i.e. 4 bits per value.', + '`shape` is optional for one dimensional arrays.' ].join(' '), requiredOpts: [], otherOpts: ['dflt'], coerceFunction: function(v, propOut, dflt) { - // TODO maybe `v: {type: 'float32', vals: [/* ... */]}` also - if(isArrayOrTypedArray(v)) propOut.set(v); - else if(dflt !== undefined) propOut.set(dflt); + propOut.set( + isArrayOrTypedArray(v) ? v : + isTypedArraySpec(v) ? decodeTypedArraySpec(v) : + dflt + ); } }, enumerated: { @@ -240,8 +265,14 @@ exports.valObjectMeta = { requiredOpts: [], otherOpts: ['dflt', 'values', 'arrayOk'], coerceFunction: function(v, propOut, dflt) { - if(v === undefined) propOut.set(dflt); - else propOut.set(v); + if(v === undefined) { + propOut.set(dflt); + } else { + propOut.set( + isTypedArraySpec(v) ? decodeTypedArraySpec(v) : + v + ); + } } }, info_array: { @@ -268,17 +299,19 @@ exports.valObjectMeta = { return out; } - var twoD = opts.dimensions === 2 || (opts.dimensions === '1-2' && Array.isArray(v) && Array.isArray(v[0])); + if(isTypedArraySpec(v)) v = decodeTypedArraySpec(v); - if(!Array.isArray(v)) { + if(!isArrayOrTypedArray(v)) { propOut.set(dflt); return; } + var twoD = opts.dimensions === 2 || (opts.dimensions === '1-2' && Array.isArray(v) && isArrayOrTypedArray(v[0])); + var items = opts.items; var vOut = []; var arrayItems = Array.isArray(items); - var arrayItems2D = arrayItems && twoD && Array.isArray(items[0]); + var arrayItems2D = arrayItems && twoD && isArrayOrTypedArray(items[0]); var innerItemsOnly = twoD && arrayItems && !arrayItems2D; var len = (arrayItems && !innerItemsOnly) ? items.length : v.length; @@ -289,7 +322,7 @@ exports.valObjectMeta = { if(twoD) { for(i = 0; i < len; i++) { vOut[i] = []; - row = Array.isArray(v[i]) ? v[i] : []; + row = isArrayOrTypedArray(v[i]) ? v[i] : []; if(innerItemsOnly) len2 = items.length; else if(arrayItems) len2 = items[i].length; else len2 = row.length; @@ -313,7 +346,7 @@ exports.valObjectMeta = { propOut.set(vOut); }, validateFunction: function(v, opts) { - if(!Array.isArray(v)) return false; + if(!isArrayOrTypedArray(v)) return false; var items = opts.items; var arrayItems = Array.isArray(items); @@ -325,7 +358,7 @@ exports.valObjectMeta = { // valid when all input items are valid for(var i = 0; i < v.length; i++) { if(twoD) { - if(!Array.isArray(v[i]) || (!opts.freeLength && v[i].length !== items[i].length)) { + if(!isArrayOrTypedArray(v[i]) || (!opts.freeLength && v[i].length !== items[i].length)) { return false; } for(var j = 0; j < v[i].length; j++) { @@ -368,15 +401,24 @@ exports.coerce = function(containerIn, containerOut, attributes, attribute, dflt if(dflt === undefined) dflt = opts.dflt; - /** - * arrayOk: value MAY be an array, then we do no value checking - * at this point, because it can be more complicated than the - * individual form (eg. some array vals can be numbers, even if the - * single values must be color strings) - */ - if(opts.arrayOk && isArrayOrTypedArray(v)) { - propOut.set(v); - return v; + if(opts.arrayOk) { + if(isArrayOrTypedArray(v)) { + /** + * arrayOk: value MAY be an array, then we do no value checking + * at this point, because it can be more complicated than the + * individual form (eg. some array vals can be numbers, even if the + * single values must be color strings) + */ + + propOut.set(v); + return v; + } else { + if(isTypedArraySpec(v)) { + v = decodeTypedArraySpec(v); + propOut.set(v); + return v; + } + } } var coerceFunction = exports.valObjectMeta[opts.valType].coerceFunction; diff --git a/src/lib/gl_format_color.js b/src/lib/gl_format_color.js index 1de4d7ba460..e42170c3d2d 100644 --- a/src/lib/gl_format_color.js +++ b/src/lib/gl_format_color.js @@ -31,6 +31,8 @@ function validateOpacity(opacityIn) { function formatColor(containerIn, opacityIn, len) { var colorIn = containerIn.color; + if(colorIn && colorIn._inputArray) colorIn = colorIn._inputArray; + var isArrayColorIn = isArrayOrTypedArray(colorIn); var isArrayOpacityIn = isArrayOrTypedArray(opacityIn); var cOpts = Colorscale.extractOpts(containerIn); diff --git a/src/lib/index.js b/src/lib/index.js index ebb1822d48c..4c76991249c 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -62,6 +62,7 @@ lib.toLogRange = require('./to_log_range'); lib.relinkPrivateKeys = require('./relink_private'); var arrayModule = require('./array'); +lib.isArrayBuffer = arrayModule.isArrayBuffer; lib.isTypedArray = arrayModule.isTypedArray; lib.isArrayOrTypedArray = arrayModule.isArrayOrTypedArray; lib.isArray1D = arrayModule.isArray1D; @@ -684,8 +685,8 @@ lib.getTargetArray = function(trace, transformOpts) { if(typeof target === 'string' && target) { var array = lib.nestedProperty(trace, target).get(); - return Array.isArray(array) ? array : false; - } else if(Array.isArray(target)) { + return lib.isArrayOrTypedArray(array) ? array : false; + } else if(lib.isArrayOrTypedArray(target)) { return target; } diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index dfdb0e5166d..fc65c7796bb 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -228,7 +228,7 @@ var getDataConversions = axes.getDataConversions = function(gd, trace, target, t // In the case of an array target, make a mock data array // and call supplyDefaults to the data type and // setup the data-to-calc method. - if(Array.isArray(d2cTarget)) { + if(Lib.isArrayOrTypedArray(d2cTarget)) { ax = { type: autoType(targetArray, undefined, { autotypenumbers: gd._fullLayout.autotypenumbers @@ -1288,7 +1288,7 @@ function arrayTicks(ax) { // without a text array, just format the given values as any other ticks // except with more precision to the numbers - if(!Array.isArray(text)) text = []; + if(!Lib.isArrayOrTypedArray(text)) text = []; for(var i = 0; i < vals.length; i++) { var vali = tickVal2l(vals[i]); @@ -1624,7 +1624,7 @@ axes.tickText = function(ax, x, hover, noSuffixPrefix) { var tickVal2l = axType === 'category' ? ax.d2l_noadd : ax.d2l; var i; - if(arrayMode && Array.isArray(ax.ticktext)) { + if(arrayMode && Lib.isArrayOrTypedArray(ax.ticktext)) { var rng = Lib.simpleMap(ax.range, ax.r2l); var minDiff = (Math.abs(rng[1] - rng[0]) - (ax._lBreaks || 0)) / 10000; @@ -1703,8 +1703,8 @@ axes.tickText = function(ax, x, hover, noSuffixPrefix) { axes.hoverLabelText = function(ax, values, hoverformat) { if(hoverformat) ax = Lib.extendFlat({}, ax, {hoverformat: hoverformat}); - var val = Array.isArray(values) ? values[0] : values; - var val2 = Array.isArray(values) ? values[1] : undefined; + var val = Lib.isArrayOrTypedArray(values) ? values[0] : values; + var val2 = Lib.isArrayOrTypedArray(values) ? values[1] : undefined; if(val2 !== undefined && val2 !== val) { return ( axes.hoverLabelText(ax, val, hoverformat) + ' - ' + diff --git a/src/plots/cartesian/category_order_defaults.js b/src/plots/cartesian/category_order_defaults.js index b75fa57898b..76c1e5c261a 100644 --- a/src/plots/cartesian/category_order_defaults.js +++ b/src/plots/cartesian/category_order_defaults.js @@ -1,5 +1,7 @@ 'use strict'; +var isTypedArraySpec = require('../../lib/array').isTypedArraySpec; + function findCategories(ax, opts) { var dataAttr = opts.dataAttr || ax._id.charAt(0); var lookup = {}; @@ -49,7 +51,8 @@ module.exports = function handleCategoryOrderDefaults(containerIn, containerOut, if(containerOut.type !== 'category') return; var arrayIn = containerIn.categoryarray; - var isValidArray = (Array.isArray(arrayIn) && arrayIn.length > 0); + var isValidArray = (Array.isArray(arrayIn) && arrayIn.length > 0) || + isTypedArraySpec(arrayIn); // override default 'categoryorder' value when non-empty array is supplied var orderDefault; diff --git a/src/plots/cartesian/tick_value_defaults.js b/src/plots/cartesian/tick_value_defaults.js index 92d83983186..68b9207ee62 100644 --- a/src/plots/cartesian/tick_value_defaults.js +++ b/src/plots/cartesian/tick_value_defaults.js @@ -2,6 +2,8 @@ var cleanTicks = require('./clean_ticks'); var isArrayOrTypedArray = require('../../lib').isArrayOrTypedArray; +var isTypedArraySpec = require('../../lib/array').isTypedArraySpec; +var decodeTypedArraySpec = require('../../lib/array').decodeTypedArraySpec; module.exports = function handleTickValueDefaults(containerIn, containerOut, coerce, axType, opts) { if(!opts) opts = {}; @@ -12,6 +14,8 @@ module.exports = function handleTickValueDefaults(containerIn, containerOut, coe function readInput(attr) { var v = cIn[attr]; + if(isTypedArraySpec(v)) v = decodeTypedArraySpec(v); + return ( v !== undefined ) ? v : (cOut._template || {})[attr]; diff --git a/src/plots/plots.js b/src/plots/plots.js index 5a6b5e0466c..3dcddc841f4 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -4,6 +4,7 @@ var d3 = require('@plotly/d3'); var timeFormatLocale = require('d3-time-format').timeFormatLocale; var formatLocale = require('d3-format').formatLocale; var isNumeric = require('fast-isnumeric'); +var b64encode = require('base64-arraybuffer'); var Registry = require('../registry'); var PlotSchema = require('../plot_api/plot_schema'); @@ -1356,7 +1357,10 @@ plots.supplyTraceDefaults = function(traceIn, traceOut, colorIndex, layout, trac } if(_module && _module.selectPoints) { - coerce('selectedpoints'); + var selectedpoints = coerce('selectedpoints'); + if(Lib.isTypedArray(selectedpoints)) { + traceOut.selectedpoints = Array.from(selectedpoints); + } } plots.supplyTransformDefaults(traceIn, traceOut, layout); @@ -2278,11 +2282,29 @@ plots.graphJson = function(gd, dataonly, mode, output, useDefaults, includeConfi return o; } - if(Array.isArray(d)) { + var dIsArray = Array.isArray(d); + var dIsTypedArray = Lib.isTypedArray(d); + + if((dIsArray || dIsTypedArray) && d.dtype && d.shape) { + var bdata = d.bdata; + return stripObj({ + dtype: d.dtype, + shape: d.shape, + + bdata: + // case of ArrayBuffer + Lib.isArrayBuffer(bdata) ? b64encode.encode(bdata) : + // case of b64 string + bdata + + }, keepFunction); + } + + if(dIsArray) { return d.map(function(x) {return stripObj(x, keepFunction);}); } - if(Lib.isTypedArray(d)) { + if(dIsTypedArray) { return Lib.simpleMap(d, Lib.identity); } diff --git a/src/plots/polar/set_convert.js b/src/plots/polar/set_convert.js index d150e61af5c..cc16cfc2b74 100644 --- a/src/plots/polar/set_convert.js +++ b/src/plots/polar/set_convert.js @@ -109,14 +109,6 @@ function setConvertAngular(ax, polarLayout) { var _d2c = function(v) { return ax.d2c(v, trace.thetaunit); }; if(arrayIn) { - if(Lib.isTypedArray(arrayIn) && axType === 'linear') { - if(len === arrayIn.length) { - return arrayIn; - } else if(arrayIn.subarray) { - return arrayIn.subarray(0, len); - } - } - arrayOut = new Array(len); for(i = 0; i < len; i++) { arrayOut[i] = _d2c(arrayIn[i]); diff --git a/src/plots/smith/layout_defaults.js b/src/plots/smith/layout_defaults.js index 70529ebc8e7..6eb26adc977 100644 --- a/src/plots/smith/layout_defaults.js +++ b/src/plots/smith/layout_defaults.js @@ -17,6 +17,9 @@ var constants = require('./constants'); var axisNames = constants.axisNames; var makeImagDflt = memoize(function(realTickvals) { + // TODO: handle this case outside supply defaults step + if(Lib.isTypedArray(realTickvals)) realTickvals = Array.from(realTickvals); + return realTickvals.slice().reverse().map(function(x) { return -x; }) .concat([0]) .concat(realTickvals); @@ -69,6 +72,9 @@ function handleDefaults(contIn, contOut, coerce, opts) { coerceAxis('tickvals', imagTickvalsDflt); } + // TODO: handle this case outside supply defaults step + if(Lib.isTypedArray(axOut.tickvals)) axOut.tickvals = Array.from(axOut.tickvals); + var dfltColor; var dfltFontColor; var dfltFontSize; diff --git a/src/traces/bar/cross_trace_calc.js b/src/traces/bar/cross_trace_calc.js index a63b4cc248b..6af4eb73a41 100644 --- a/src/traces/bar/cross_trace_calc.js +++ b/src/traces/bar/cross_trace_calc.js @@ -432,9 +432,9 @@ function setBarCenterAndWidth(pa, sieve) { var calcTrace = calcTraces[i]; var t = calcTrace[0].t; var poffset = t.poffset; - var poffsetIsArray = Array.isArray(poffset); + var poffsetIsArray = isArrayOrTypedArray(poffset); var barwidth = t.barwidth; - var barwidthIsArray = Array.isArray(barwidth); + var barwidthIsArray = isArrayOrTypedArray(barwidth); for(var j = 0; j < calcTrace.length; j++) { var calcBar = calcTrace[j]; @@ -478,8 +478,8 @@ function updatePositionAxis(pa, sieve, allowMinDtick) { var t = calcTrace0.t; var poffset = t.poffset; var barwidth = t.barwidth; - var poffsetIsArray = Array.isArray(poffset); - var barwidthIsArray = Array.isArray(barwidth); + var poffsetIsArray = isArrayOrTypedArray(poffset); + var barwidthIsArray = isArrayOrTypedArray(barwidth); for(j = 0; j < calcTrace.length; j++) { bar = calcTrace[j]; @@ -750,7 +750,7 @@ function collectExtents(calcTraces, pa) { cd[0].t.extents = extents; var poffset = cd[0].t.poffset; - var poffsetIsArray = Array.isArray(poffset); + var poffsetIsArray = isArrayOrTypedArray(poffset); for(j = 0; j < cd.length; j++) { var di = cd[j]; diff --git a/src/traces/bar/helpers.js b/src/traces/bar/helpers.js index 011c866b479..d1f30a23246 100644 --- a/src/traces/bar/helpers.js +++ b/src/traces/bar/helpers.js @@ -53,7 +53,7 @@ exports.coerceEnumerated = function(attributeDefinition, value, defaultValue) { exports.getValue = function(arrayOrScalar, index) { var value; - if(!Array.isArray(arrayOrScalar)) value = arrayOrScalar; + if(!isArrayOrTypedArray(arrayOrScalar)) value = arrayOrScalar; else if(index < arrayOrScalar.length) value = arrayOrScalar[index]; return value; }; diff --git a/src/traces/barpolar/calc.js b/src/traces/barpolar/calc.js index b9f605a4b92..1a532ab210c 100644 --- a/src/traces/barpolar/calc.js +++ b/src/traces/barpolar/calc.js @@ -2,6 +2,7 @@ var hasColorscale = require('../../components/colorscale/helpers').hasColorscale; var colorscaleCalc = require('../../components/colorscale/calc'); +var isArrayOrTypedArray = require('../../lib').isArrayOrTypedArray; var arraysToCalcdata = require('../bar/arrays_to_calcdata'); var setGroupPositions = require('../bar/cross_trace_calc').setGroupPositions; var calcSelection = require('../scatter/calc_selection'); @@ -33,7 +34,7 @@ function calc(gd, trace) { function d2c(attr) { var val = trace[attr]; if(val !== undefined) { - trace['_' + attr] = Array.isArray(val) ? + trace['_' + attr] = isArrayOrTypedArray(val) ? angularAxis.makeCalcdata(trace, attr) : angularAxis.d2c(val, trace.thetaunit); } diff --git a/src/traces/box/calc.js b/src/traces/box/calc.js index cbb045d189e..552de08018f 100644 --- a/src/traces/box/calc.js +++ b/src/traces/box/calc.js @@ -266,6 +266,7 @@ module.exports = function calc(gd, trace) { } } + if(trace.notched && Lib.isTypedArray(valArray)) valArray = Array.from(valArray); trace._extremes[valAxis._id] = Axes.findExtremes(valAxis, trace.notched ? valArray.concat([minLowerNotch, maxUpperNotch]) : valArray, {padded: true} diff --git a/src/traces/cone/convert.js b/src/traces/cone/convert.js index 42e1974c2b1..75a35c32477 100644 --- a/src/traces/cone/convert.js +++ b/src/traces/cone/convert.js @@ -6,6 +6,7 @@ var createConeMesh = require('../../../stackgl_modules').gl_cone3d.createConeMes var simpleMap = require('../../lib').simpleMap; var parseColorScale = require('../../lib/gl_format_color').parseColorScale; var extractOpts = require('../../components/colorscale').extractOpts; +var isArrayOrTypedArray = require('../../lib').isArrayOrTypedArray; var zip3 = require('../../plots/gl3d/zip3'); function Cone(scene, uid) { @@ -34,7 +35,7 @@ proto.handlePick = function(selection) { ]; var text = this.data.hovertext || this.data.text; - if(Array.isArray(text) && text[selectIndex] !== undefined) { + if(isArrayOrTypedArray(text) && text[selectIndex] !== undefined) { selection.textLabel = text[selectIndex]; } else if(text) { selection.textLabel = text; diff --git a/src/traces/contour/constraint_defaults.js b/src/traces/contour/constraint_defaults.js index d6804e0876a..2b0178d940f 100644 --- a/src/traces/contour/constraint_defaults.js +++ b/src/traces/contour/constraint_defaults.js @@ -8,6 +8,7 @@ var addOpacity = Color.addOpacity; var opacity = Color.opacity; var filterOps = require('../../constants/filter_ops'); +var isArrayOrTypedArray = require('../../lib').isArrayOrTypedArray; var CONSTRAINT_REDUCTION = filterOps.CONSTRAINT_REDUCTION; var COMPARISON_OPS2 = filterOps.COMPARISON_OPS2; @@ -50,7 +51,7 @@ function handleConstraintValueDefaults(coerce, contours) { // Requires an array of two numbers: coerce('contours.value', [0, 1]); - if(!Array.isArray(contours.value)) { + if(!isArrayOrTypedArray(contours.value)) { if(isNumeric(contours.value)) { zvalue = parseFloat(contours.value); contours.value = [zvalue, zvalue + 1]; @@ -73,7 +74,7 @@ function handleConstraintValueDefaults(coerce, contours) { coerce('contours.value', 0); if(!isNumeric(contours.value)) { - if(Array.isArray(contours.value)) { + if(isArrayOrTypedArray(contours.value)) { contours.value = parseFloat(contours.value[0]); } else { contours.value = 0; diff --git a/src/traces/contour/plot.js b/src/traces/contour/plot.js index e1728aca3c4..cf93b835612 100644 --- a/src/traces/contour/plot.js +++ b/src/traces/contour/plot.js @@ -403,7 +403,7 @@ exports.labelFormatter = function(gd, cd0) { } else { if(contours.type === 'constraint') { var value = contours.value; - if(Array.isArray(value)) { + if(Lib.isArrayOrTypedArray(value)) { formatAxis.range = [value[0], value[value.length - 1]]; } else formatAxis.range = [value, value]; } else { diff --git a/src/traces/funnel/defaults.js b/src/traces/funnel/defaults.js index f729bc21639..80f9745fac9 100644 --- a/src/traces/funnel/defaults.js +++ b/src/traces/funnel/defaults.js @@ -44,7 +44,7 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) { }); if(traceOut.textposition !== 'none' && !traceOut.texttemplate) { - coerce('textinfo', Array.isArray(text) ? 'text+value' : 'value'); + coerce('textinfo', Lib.isArrayOrTypedArray(text) ? 'text+value' : 'value'); } var markerColor = coerce('marker.color', defaultColor); diff --git a/src/traces/heatmap/hover.js b/src/traces/heatmap/hover.js index d28c2998c37..b67317b26eb 100644 --- a/src/traces/heatmap/hover.js +++ b/src/traces/heatmap/hover.js @@ -2,6 +2,7 @@ var Fx = require('../../components/fx'); var Lib = require('../../lib'); +var isArrayOrTypedArray = Lib.isArrayOrTypedArray; var Axes = require('../../plots/cartesian/axes'); var extractOpts = require('../../components/colorscale').extractOpts; @@ -96,9 +97,9 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, opts) { if(zVal === undefined && !trace.hoverongaps) return; var text; - if(Array.isArray(cd0.hovertext) && Array.isArray(cd0.hovertext[ny])) { + if(isArrayOrTypedArray(cd0.hovertext) && isArrayOrTypedArray(cd0.hovertext[ny])) { text = cd0.hovertext[ny][nx]; - } else if(Array.isArray(cd0.text) && Array.isArray(cd0.text[ny])) { + } else if(isArrayOrTypedArray(cd0.text) && isArrayOrTypedArray(cd0.text[ny])) { text = cd0.text[ny][nx]; } diff --git a/src/traces/heatmap/make_bound_array.js b/src/traces/heatmap/make_bound_array.js index 817ac4471a1..d6b30629cdb 100644 --- a/src/traces/heatmap/make_bound_array.js +++ b/src/traces/heatmap/make_bound_array.js @@ -22,7 +22,7 @@ module.exports = function makeBoundArray(trace, arrayIn, v0In, dvIn, numbricks, // and extend it linearly based on the last two points if(len <= numbricks) { // contour plots only want the centers - if(isContour || isGL2D) arrayOut = arrayIn.slice(0, numbricks); + if(isContour || isGL2D) arrayOut = Array.from(arrayIn).slice(0, numbricks); else if(numbricks === 1) { arrayOut = [arrayIn[0] - 0.5, arrayIn[0] + 0.5]; } else { diff --git a/src/traces/heatmap/xyz_defaults.js b/src/traces/heatmap/xyz_defaults.js index 5afcc20e708..1ded29e8811 100644 --- a/src/traces/heatmap/xyz_defaults.js +++ b/src/traces/heatmap/xyz_defaults.js @@ -13,7 +13,7 @@ module.exports = function handleXYZDefaults(traceIn, traceOut, coerce, layout, x if(z === undefined || !z.length) return 0; - if(Lib.isArray1D(traceIn.z)) { + if(Lib.isArray1D(z)) { x = coerce(xName); y = coerce(yName); diff --git a/src/traces/icicle/defaults.js b/src/traces/icicle/defaults.js index f98322e661f..70595b50695 100644 --- a/src/traces/icicle/defaults.js +++ b/src/traces/icicle/defaults.js @@ -41,7 +41,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout var text = coerce('text'); coerce('texttemplate'); - if(!traceOut.texttemplate) coerce('textinfo', Array.isArray(text) ? 'text+label' : 'label'); + if(!traceOut.texttemplate) coerce('textinfo', Lib.isArrayOrTypedArray(text) ? 'text+label' : 'label'); coerce('hovertext'); coerce('hovertemplate'); diff --git a/src/traces/image/hover.js b/src/traces/image/hover.js index 718d3fa0ba0..e03134b2d67 100644 --- a/src/traces/image/hover.js +++ b/src/traces/image/hover.js @@ -2,6 +2,7 @@ var Fx = require('../../components/fx'); var Lib = require('../../lib'); +var isArrayOrTypedArray = Lib.isArrayOrTypedArray; var constants = require('./constants'); module.exports = function hoverPoints(pointData, xval, yval) { @@ -54,9 +55,9 @@ module.exports = function hoverPoints(pointData, xval, yval) { } var text; - if(Array.isArray(trace.hovertext) && Array.isArray(trace.hovertext[ny])) { + if(isArrayOrTypedArray(trace.hovertext) && isArrayOrTypedArray(trace.hovertext[ny])) { text = trace.hovertext[ny][nx]; - } else if(Array.isArray(trace.text) && Array.isArray(trace.text[ny])) { + } else if(isArrayOrTypedArray(trace.text) && isArrayOrTypedArray(trace.text[ny])) { text = trace.text[ny][nx]; } diff --git a/src/traces/image/plot.js b/src/traces/image/plot.js index 1b6ab3d5fd9..4ed31e063d0 100644 --- a/src/traces/image/plot.js +++ b/src/traces/image/plot.js @@ -186,7 +186,11 @@ module.exports = function plot(gd, plotinfo, cdimage, imageLayer) { .then(function() { var href, canvas; if(trace._hasZ) { - canvas = drawMagnifiedPixelsOnCanvas(function(i, j) {return z[j][i];}); + canvas = drawMagnifiedPixelsOnCanvas(function(i, j) { + var _z = z[j][i]; + if(Lib.isTypedArray(_z)) _z = Array.from(_z); + return _z; + }); href = canvas.toDataURL('image/png'); } else if(trace._hasSource) { if(realImage) { diff --git a/src/traces/isosurface/convert.js b/src/traces/isosurface/convert.js index 5027cdc1d5b..94b9ef2b891 100644 --- a/src/traces/isosurface/convert.js +++ b/src/traces/isosurface/convert.js @@ -2,6 +2,7 @@ var createMesh = require('../../../stackgl_modules').gl_mesh3d; var parseColorScale = require('../../lib/gl_format_color').parseColorScale; +var isArrayOrTypedArray = require('../../lib').isArrayOrTypedArray; var str2RgbaArray = require('../../lib/str2rgbarray'); var extractOpts = require('../../components/colorscale').extractOpts; var zip3 = require('../../plots/gl3d/zip3'); @@ -59,7 +60,7 @@ proto.handlePick = function(selection) { ]; var text = this.data.hovertext || this.data.text; - if(Array.isArray(text) && text[selectIndex] !== undefined) { + if(isArrayOrTypedArray(text) && text[selectIndex] !== undefined) { selection.textLabel = text[selectIndex]; } else if(text) { selection.textLabel = text; diff --git a/src/traces/mesh3d/convert.js b/src/traces/mesh3d/convert.js index 3c38b148f85..7adddd93cb3 100644 --- a/src/traces/mesh3d/convert.js +++ b/src/traces/mesh3d/convert.js @@ -6,6 +6,7 @@ var alphaShape = require('../../../stackgl_modules').alpha_shape; var convexHull = require('../../../stackgl_modules').convex_hull; var parseColorScale = require('../../lib/gl_format_color').parseColorScale; +var isArrayOrTypedArray = require('../../lib').isArrayOrTypedArray; var str2RgbaArray = require('../../lib/str2rgbarray'); var extractOpts = require('../../components/colorscale').extractOpts; var zip3 = require('../../plots/gl3d/zip3'); @@ -37,7 +38,7 @@ proto.handlePick = function(selection) { } var text = this.data.hovertext || this.data.text; - if(Array.isArray(text) && text[selectIndex] !== undefined) { + if(isArrayOrTypedArray(text) && text[selectIndex] !== undefined) { selection.textLabel = text[selectIndex]; } else if(text) { selection.textLabel = text; diff --git a/src/traces/ohlc/calc.js b/src/traces/ohlc/calc.js index 831518652c5..c76a108e8bf 100644 --- a/src/traces/ohlc/calc.js +++ b/src/traces/ohlc/calc.js @@ -50,8 +50,8 @@ function calcCommon(gd, trace, origX, x, ya, ptFunc) { var l = ya.makeCalcdata(trace, 'low'); var c = ya.makeCalcdata(trace, 'close'); - var hasTextArray = Array.isArray(trace.text); - var hasHovertextArray = Array.isArray(trace.hovertext); + var hasTextArray = Lib.isArrayOrTypedArray(trace.text); + var hasHovertextArray = Lib.isArrayOrTypedArray(trace.hovertext); // we're optimists - before we have any changing data, assume increasing var increasing = true; diff --git a/src/traces/parcats/defaults.js b/src/traces/parcats/defaults.js index 6a0c436a26f..5d3319e4c3e 100644 --- a/src/traces/parcats/defaults.js +++ b/src/traces/parcats/defaults.js @@ -8,6 +8,7 @@ var handleArrayContainerDefaults = require('../../plots/array_container_defaults var attributes = require('./attributes'); var mergeLength = require('../parcoords/merge_length'); +var isTypedArraySpec = require('../../lib/array').isTypedArraySpec; function handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce) { coerce('line.shape'); @@ -44,7 +45,8 @@ function dimensionDefaults(dimensionIn, dimensionOut) { // Category level var arrayIn = dimensionIn.categoryarray; - var isValidArray = (Array.isArray(arrayIn) && arrayIn.length > 0); + var isValidArray = (Lib.isArrayOrTypedArray(arrayIn) && arrayIn.length > 0) || + isTypedArraySpec(arrayIn); var orderDefault; if(isValidArray) orderDefault = 'array'; diff --git a/src/traces/parcoords/parcoords.js b/src/traces/parcoords/parcoords.js index 8bce2766343..6a2c72d5a67 100644 --- a/src/traces/parcoords/parcoords.js +++ b/src/traces/parcoords/parcoords.js @@ -2,6 +2,7 @@ var d3 = require('@plotly/d3'); var Lib = require('../../lib'); +var isArrayOrTypedArray = Lib.isArrayOrTypedArray; var numberFormat = Lib.numberFormat; var rgba = require('color-rgba'); @@ -237,7 +238,7 @@ function viewModel(state, callbacks, model) { var key = dimension.label + (foundKey ? '__' + foundKey : ''); var specifiedConstraint = dimension.constraintrange; var filterRangeSpecified = specifiedConstraint && specifiedConstraint.length; - if(filterRangeSpecified && !Array.isArray(specifiedConstraint[0])) { + if(filterRangeSpecified && !isArrayOrTypedArray(specifiedConstraint[0])) { specifiedConstraint = [specifiedConstraint]; } var filterRange = filterRangeSpecified ? @@ -265,11 +266,13 @@ function viewModel(state, callbacks, model) { var ticktext; function makeTickItem(v, i) { return {val: v, text: ticktext[i]}; } function sortTickItem(a, b) { return a.val - b.val; } - if(Array.isArray(tickvals) && tickvals.length) { + if(isArrayOrTypedArray(tickvals) && tickvals.length) { + if(Lib.isTypedArray(tickvals)) tickvals = Array.from(tickvals); + ticktext = dimension.ticktext; // ensure ticktext and tickvals have same length - if(!Array.isArray(ticktext) || !ticktext.length) { + if(!isArrayOrTypedArray(ticktext) || !ticktext.length) { ticktext = tickvals.map(numberFormat(dimension.tickformat)); } else if(ticktext.length > tickvals.length) { ticktext = ticktext.slice(0, tickvals.length); diff --git a/src/traces/pie/defaults.js b/src/traces/pie/defaults.js index 98228a8a86e..ec66b67e08f 100644 --- a/src/traces/pie/defaults.js +++ b/src/traces/pie/defaults.js @@ -8,7 +8,7 @@ var handleText = require('../bar/defaults').handleText; var coercePattern = require('../../lib').coercePattern; function handleLabelsAndValues(labels, values) { - var hasLabels = Array.isArray(labels); + var hasLabels = Lib.isArrayOrTypedArray(labels); var hasValues = Lib.isArrayOrTypedArray(values); var len = Math.min( hasLabels ? labels.length : Infinity, @@ -86,7 +86,7 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) { var textData = coerce('text'); var textTemplate = coerce('texttemplate'); var textInfo; - if(!textTemplate) textInfo = coerce('textinfo', Array.isArray(textData) ? 'text+percent' : 'percent'); + if(!textTemplate) textInfo = coerce('textinfo', Lib.isArrayOrTypedArray(textData) ? 'text+percent' : 'percent'); coerce('hovertext'); coerce('hovertemplate'); diff --git a/src/traces/pie/helpers.js b/src/traces/pie/helpers.js index 25d35d09e42..1aab36fa8c7 100644 --- a/src/traces/pie/helpers.js +++ b/src/traces/pie/helpers.js @@ -21,7 +21,7 @@ exports.formatPieValue = function formatPieValue(v, separators) { }; exports.getFirstFilled = function getFirstFilled(array, indices) { - if(!Array.isArray(array)) return; + if(!Lib.isArrayOrTypedArray(array)) return; for(var i = 0; i < indices.length; i++) { var v = array[indices[i]]; if(v || v === 0 || v === '') return v; @@ -29,7 +29,7 @@ exports.getFirstFilled = function getFirstFilled(array, indices) { }; exports.castOption = function castOption(item, indices) { - if(Array.isArray(item)) return exports.getFirstFilled(item, indices); + if(Lib.isArrayOrTypedArray(item)) return exports.getFirstFilled(item, indices); else if(item) return item; }; diff --git a/src/traces/pie/plot.js b/src/traces/pie/plot.js index 33eb4813db5..0b868181e82 100644 --- a/src/traces/pie/plot.js +++ b/src/traces/pie/plot.js @@ -841,7 +841,7 @@ function getMaxPull(trace) { if(!maxPull) return 0; var j; - if(Array.isArray(maxPull)) { + if(Lib.isArrayOrTypedArray(maxPull)) { maxPull = 0; for(j = 0; j < trace.pull.length; j++) { if(trace.pull[j] > maxPull) maxPull = trace.pull[j]; @@ -874,7 +874,7 @@ function scootLabels(quadrants, trace) { if(newExtraY * yDiffSign > 0) thisPt.labelExtraY = newExtraY; // make sure this label doesn't overlap any slices - if(!Array.isArray(trace.pull)) return; // this can only happen with array pulls + if(!Lib.isArrayOrTypedArray(trace.pull)) return; // this can only happen with array pulls for(i = 0; i < wholeSide.length; i++) { otherPt = wholeSide[i]; diff --git a/src/traces/pointcloud/convert.js b/src/traces/pointcloud/convert.js index 7ed0b37c140..3f1ebb942f0 100644 --- a/src/traces/pointcloud/convert.js +++ b/src/traces/pointcloud/convert.js @@ -2,6 +2,7 @@ var createPointCloudRenderer = require('../../../stackgl_modules').gl_pointcloud2d; +var isArrayOrTypedArray = require('../../lib').isArrayOrTypedArray; var str2RGBArray = require('../../lib/str2rgbarray'); var findExtremes = require('../../plots/cartesian/autorange').findExtremes; var getTraceColor = require('../scatter/get_trace_color'); @@ -48,7 +49,7 @@ proto.handlePick = function(pickResult) { traceCoord: this.pickXYData ? [this.pickXYData[index * 2], this.pickXYData[index * 2 + 1]] : [this.pickXData[index], this.pickYData[index]], - textLabel: Array.isArray(this.textLabels) ? + textLabel: isArrayOrTypedArray(this.textLabels) ? this.textLabels[index] : this.textLabels, color: this.color, diff --git a/src/traces/scatter/defaults.js b/src/traces/scatter/defaults.js index 9c980ca69de..ce6a5fab07f 100644 --- a/src/traces/scatter/defaults.js +++ b/src/traces/scatter/defaults.js @@ -44,6 +44,10 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('hovertext'); coerce('mode', defaultMode); + if(subTypes.hasMarkers(traceOut)) { + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, {gradient: true}); + } + if(subTypes.hasLines(traceOut)) { handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce, {backoff: true}); handleLineShapeDefaults(traceIn, traceOut, coerce); @@ -51,10 +55,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('line.simplify'); } - if(subTypes.hasMarkers(traceOut)) { - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, {gradient: true}); - } - if(subTypes.hasText(traceOut)) { coerce('texttemplate'); handleTextDefaults(traceIn, traceOut, layout, coerce); diff --git a/src/traces/scatter/hover.js b/src/traces/scatter/hover.js index c86a5b902f8..e6fb76cdb36 100644 --- a/src/traces/scatter/hover.js +++ b/src/traces/scatter/hover.js @@ -199,7 +199,7 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { delete pointData.index; - if(trace.text && !Array.isArray(trace.text)) { + if(trace.text && !Lib.isArrayOrTypedArray(trace.text)) { pointData.text = String(trace.text); } else pointData.text = trace.name; diff --git a/src/traces/scatter/line_defaults.js b/src/traces/scatter/line_defaults.js index fe70e8f847a..bb70e25feb6 100644 --- a/src/traces/scatter/line_defaults.js +++ b/src/traces/scatter/line_defaults.js @@ -8,6 +8,7 @@ module.exports = function lineDefaults(traceIn, traceOut, defaultColor, layout, if(!opts) opts = {}; var markerColor = (traceIn.marker || {}).color; + if(markerColor && markerColor._inputArray) markerColor = markerColor._inputArray; coerce('line.color', defaultColor); diff --git a/src/traces/scatter/subtypes.js b/src/traces/scatter/subtypes.js index 780017eb5fa..ef463a51065 100644 --- a/src/traces/scatter/subtypes.js +++ b/src/traces/scatter/subtypes.js @@ -1,6 +1,7 @@ 'use strict'; var Lib = require('../../lib'); +var isTypedArraySpec = require('../../lib/array').isTypedArraySpec; module.exports = { hasLines: function(trace) { @@ -22,7 +23,10 @@ module.exports = { }, isBubble: function(trace) { - return Lib.isPlainObject(trace.marker) && - Lib.isArrayOrTypedArray(trace.marker.size); + var marker = trace.marker; + return Lib.isPlainObject(marker) && ( + Lib.isArrayOrTypedArray(marker.size) || + isTypedArraySpec(marker.size) + ); } }; diff --git a/src/traces/scatter3d/convert.js b/src/traces/scatter3d/convert.js index bea698d1e68..1c8d27509af 100644 --- a/src/traces/scatter3d/convert.js +++ b/src/traces/scatter3d/convert.js @@ -58,7 +58,7 @@ proto.handlePick = function(selection) { selection.textLabel = ''; if(this.textLabels) { - if(Array.isArray(this.textLabels)) { + if(Lib.isArrayOrTypedArray(this.textLabels)) { if(this.textLabels[ind] || this.textLabels[ind] === 0) { selection.textLabel = this.textLabels[ind]; } @@ -227,8 +227,11 @@ function convertPlotlyOptions(scene, data) { } // convert text - if(Array.isArray(data.text)) text = data.text; - else if(data.text !== undefined) { + if(Array.isArray(data.text)) { + text = data.text; + } else if(Lib.isTypedArray(data.text)) { + text = Array.from(data.text); + } else if(data.text !== undefined) { text = new Array(len); for(i = 0; i < len; i++) text[i] = data.text; } diff --git a/src/traces/scatter3d/defaults.js b/src/traces/scatter3d/defaults.js index 2cb922e5be6..06960264749 100644 --- a/src/traces/scatter3d/defaults.js +++ b/src/traces/scatter3d/defaults.js @@ -30,15 +30,15 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('mode'); + if(subTypes.hasMarkers(traceOut)) { + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, {noSelect: true, noAngle: true}); + } + if(subTypes.hasLines(traceOut)) { coerce('connectgaps'); handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce); } - if(subTypes.hasMarkers(traceOut)) { - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, {noSelect: true, noAngle: true}); - } - if(subTypes.hasText(traceOut)) { coerce('texttemplate'); handleTextDefaults(traceIn, traceOut, layout, coerce, {noSelect: true}); diff --git a/src/traces/scattercarpet/defaults.js b/src/traces/scattercarpet/defaults.js index b21f4e956df..0ca56773e37 100644 --- a/src/traces/scattercarpet/defaults.js +++ b/src/traces/scattercarpet/defaults.js @@ -41,16 +41,16 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout var defaultMode = len < constants.PTS_LINESONLY ? 'lines+markers' : 'lines'; coerce('mode', defaultMode); + if(subTypes.hasMarkers(traceOut)) { + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, {gradient: true}); + } + if(subTypes.hasLines(traceOut)) { handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce, {backoff: true}); handleLineShapeDefaults(traceIn, traceOut, coerce); coerce('connectgaps'); } - if(subTypes.hasMarkers(traceOut)) { - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, {gradient: true}); - } - if(subTypes.hasText(traceOut)) { handleTextDefaults(traceIn, traceOut, layout, coerce); } diff --git a/src/traces/scattergeo/calc.js b/src/traces/scattergeo/calc.js index cdb811146f6..da4e48c4817 100644 --- a/src/traces/scattergeo/calc.js +++ b/src/traces/scattergeo/calc.js @@ -6,6 +6,7 @@ var BADNUM = require('../../constants/numerical').BADNUM; var calcMarkerColorscale = require('../scatter/colorscale_calc'); var arraysToCalcdata = require('../scatter/arrays_to_calcdata'); var calcSelection = require('../scatter/calc_selection'); +var isArrayOrTypedArray = require('../../lib').isArrayOrTypedArray; var _ = require('../../lib')._; @@ -14,7 +15,7 @@ function isNonBlankString(v) { } module.exports = function calc(gd, trace) { - var hasLocationData = Array.isArray(trace.locations); + var hasLocationData = isArrayOrTypedArray(trace.locations); var len = hasLocationData ? trace.locations.length : trace._length; var calcTrace = new Array(len); diff --git a/src/traces/scattergeo/defaults.js b/src/traces/scattergeo/defaults.js index bee2a5d01fe..6f8adb11d98 100644 --- a/src/traces/scattergeo/defaults.js +++ b/src/traces/scattergeo/defaults.js @@ -50,15 +50,15 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('hovertemplate'); coerce('mode'); + if(subTypes.hasMarkers(traceOut)) { + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, {gradient: true}); + } + if(subTypes.hasLines(traceOut)) { handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce); coerce('connectgaps'); } - if(subTypes.hasMarkers(traceOut)) { - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, {gradient: true}); - } - if(subTypes.hasText(traceOut)) { coerce('texttemplate'); handleTextDefaults(traceIn, traceOut, layout, coerce); diff --git a/src/traces/scattergeo/plot.js b/src/traces/scattergeo/plot.js index 29d57b2c95a..9124900e8b3 100644 --- a/src/traces/scattergeo/plot.js +++ b/src/traces/scattergeo/plot.js @@ -72,7 +72,7 @@ function calcGeoJSON(calcTrace, fullLayout) { var len = trace._length; var i, calcPt; - if(Array.isArray(trace.locations)) { + if(Lib.isArrayOrTypedArray(trace.locations)) { var locationmode = trace.locationmode; var features = locationmode === 'geojson-id' ? geoUtils.extractTraceFeature(calcTrace) : diff --git a/src/traces/scattergl/convert.js b/src/traces/scattergl/convert.js index 66c43d753cb..5a477e493e7 100644 --- a/src/traces/scattergl/convert.js +++ b/src/traces/scattergl/convert.js @@ -6,6 +6,7 @@ var rgba = require('color-normalize'); var Registry = require('../../registry'); var Lib = require('../../lib'); +var isArrayOrTypedArray = Lib.isArrayOrTypedArray; var Drawing = require('../../components/drawing'); var AxisIDs = require('../../plots/cartesian/axis_ids'); @@ -54,7 +55,7 @@ function convertStyle(gd, trace) { opts.markerSel = convertMarkerSelection(gd, trace, trace.selected); opts.markerUnsel = convertMarkerSelection(gd, trace, trace.unselected); - if(!trace.unselected && Lib.isArrayOrTypedArray(trace.marker.opacity)) { + if(!trace.unselected && isArrayOrTypedArray(trace.marker.opacity)) { var mo = trace.marker.opacity; opts.markerUnsel.opacity = new Array(mo.length); for(i = 0; i < mo.length; i++) { @@ -102,7 +103,7 @@ function convertTextStyle(gd, trace) { var count = trace._length; var textfontIn = trace.textfont; var textpositionIn = trace.textposition; - var textPos = Array.isArray(textpositionIn) ? textpositionIn : [textpositionIn]; + var textPos = isArrayOrTypedArray(textpositionIn) ? textpositionIn : [textpositionIn]; var tfc = textfontIn.color; var tfs = textfontIn.size; var tff = textfontIn.family; @@ -130,7 +131,7 @@ function convertTextStyle(gd, trace) { optsOut.text.push(Lib.texttemplateString(txt(i), labels, d3locale, pointValues, d, meta)); } } else { - if(Array.isArray(trace.text) && trace.text.length < count) { + if(isArrayOrTypedArray(trace.text) && trace.text.length < count) { // if text array is shorter, we'll need to append to it, so let's slice to prevent mutating optsOut.text = trace.text.slice(); } else { @@ -138,7 +139,7 @@ function convertTextStyle(gd, trace) { } } // pad text array with empty strings - if(Array.isArray(optsOut.text)) { + if(isArrayOrTypedArray(optsOut.text)) { for(i = optsOut.text.length; i < count; i++) { optsOut.text[i] = ''; } @@ -174,7 +175,7 @@ function convertTextStyle(gd, trace) { } } - if(Array.isArray(tfc)) { + if(isArrayOrTypedArray(tfc)) { optsOut.color = new Array(count); for(i = 0; i < count; i++) { optsOut.color[i] = tfc[i]; @@ -183,7 +184,7 @@ function convertTextStyle(gd, trace) { optsOut.color = tfc; } - if(Lib.isArrayOrTypedArray(tfs) || Array.isArray(tff)) { + if(isArrayOrTypedArray(tfs) || isArrayOrTypedArray(tff)) { // if any textfont param is array - make render a batch optsOut.font = new Array(count); for(i = 0; i < count; i++) { @@ -191,12 +192,12 @@ function convertTextStyle(gd, trace) { fonti.size = ( Lib.isTypedArray(tfs) ? tfs[i] : - Array.isArray(tfs) ? ( + isArrayOrTypedArray(tfs) ? ( isNumeric(tfs[i]) ? tfs[i] : 0 ) : tfs ) * plotGlPixelRatio; - fonti.family = Array.isArray(tff) ? tff[i] : tff; + fonti.family = isArrayOrTypedArray(tff) ? tff[i] : tff; } } else { // if both are single values, make render fast single-value @@ -213,13 +214,13 @@ function convertMarkerStyle(gd, trace) { var optsOut = {}; var i; - var multiSymbol = Lib.isArrayOrTypedArray(optsIn.symbol); - var multiAngle = Lib.isArrayOrTypedArray(optsIn.angle); - var multiColor = Lib.isArrayOrTypedArray(optsIn.color); - var multiLineColor = Lib.isArrayOrTypedArray(optsIn.line.color); - var multiOpacity = Lib.isArrayOrTypedArray(optsIn.opacity); - var multiSize = Lib.isArrayOrTypedArray(optsIn.size); - var multiLineWidth = Lib.isArrayOrTypedArray(optsIn.line.width); + var multiSymbol = isArrayOrTypedArray(optsIn.symbol); + var multiAngle = isArrayOrTypedArray(optsIn.angle); + var multiColor = isArrayOrTypedArray(optsIn.color); + var multiLineColor = isArrayOrTypedArray(optsIn.line.color); + var multiOpacity = isArrayOrTypedArray(optsIn.opacity); + var multiSize = isArrayOrTypedArray(optsIn.size); + var multiLineWidth = isArrayOrTypedArray(optsIn.line.width); var isOpen; if(!multiSymbol) isOpen = helpers.isOpenSymbol(optsIn.symbol); @@ -236,28 +237,28 @@ function convertMarkerStyle(gd, trace) { var colors = formatColor(optsIn, optsIn.opacity, count); var borderColors = formatColor(optsIn.line, optsIn.opacity, count); - if(!Array.isArray(borderColors[0])) { + if(!isArrayOrTypedArray(borderColors[0])) { var borderColor = borderColors; borderColors = Array(count); for(i = 0; i < count; i++) { borderColors[i] = borderColor; } } - if(!Array.isArray(colors[0])) { + if(!isArrayOrTypedArray(colors[0])) { var color = colors; colors = Array(count); for(i = 0; i < count; i++) { colors[i] = color; } } - if(!Array.isArray(symbols)) { + if(!isArrayOrTypedArray(symbols)) { var symbol = symbols; symbols = Array(count); for(i = 0; i < count; i++) { symbols[i] = symbol; } } - if(!Array.isArray(angles)) { + if(!isArrayOrTypedArray(angles)) { var angle = angles; angles = Array(count); for(i = 0; i < count; i++) { @@ -642,12 +643,12 @@ function convertTextPosition(gd, trace, textOpts, markerOpts) { for(i = 0; i < count; i++) { var ms = markerOpts.sizes ? markerOpts.sizes[i] : markerOpts.size; - var fs = Array.isArray(fontOpts) ? fontOpts[i].size : fontOpts.size; + var fs = isArrayOrTypedArray(fontOpts) ? fontOpts[i].size : fontOpts.size; - var a = Array.isArray(align) ? + var a = isArrayOrTypedArray(align) ? (align.length > 1 ? align[i] : align[0]) : align; - var b = Array.isArray(baseline) ? + var b = isArrayOrTypedArray(baseline) ? (baseline.length > 1 ? baseline[i] : baseline[0]) : baseline; diff --git a/src/traces/scattergl/defaults.js b/src/traces/scattergl/defaults.js index a15ff09e254..b19c27bb117 100644 --- a/src/traces/scattergl/defaults.js +++ b/src/traces/scattergl/defaults.js @@ -39,17 +39,17 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('hovertemplate'); coerce('mode', defaultMode); + if(subTypes.hasMarkers(traceOut)) { + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, {noAngleRef: true, noStandOff: true}); + coerce('marker.line.width', isOpen || isBubble ? 1 : 0); + } + if(subTypes.hasLines(traceOut)) { coerce('connectgaps'); handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce); coerce('line.shape'); } - if(subTypes.hasMarkers(traceOut)) { - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, {noAngleRef: true, noStandOff: true}); - coerce('marker.line.width', isOpen || isBubble ? 1 : 0); - } - if(subTypes.hasText(traceOut)) { coerce('texttemplate'); handleTextDefaults(traceIn, traceOut, layout, coerce); diff --git a/src/traces/scattergl/edit_style.js b/src/traces/scattergl/edit_style.js index 7246fa1e641..961dee97cbc 100644 --- a/src/traces/scattergl/edit_style.js +++ b/src/traces/scattergl/edit_style.js @@ -23,7 +23,7 @@ function styleTextSelection(cd) { var stc = selOpts.color; var utc = unselOpts.color; var base = baseOpts.color; - var hasArrayBase = Array.isArray(base); + var hasArrayBase = Lib.isArrayOrTypedArray(base); opts.color = new Array(trace._length); for(i = 0; i < els.length; i++) { diff --git a/src/traces/scattergl/hover.js b/src/traces/scattergl/hover.js index 74efc4776b3..7d8a922f387 100644 --- a/src/traces/scattergl/hover.js +++ b/src/traces/scattergl/hover.js @@ -122,7 +122,7 @@ function calcHover(pointData, x, y, trace) { }; // that is single-item arrays_to_calcdata excerpt, since we are doing it for a single point and we don't have to do it beforehead for 1e6 points - di.tx = Array.isArray(trace.text) ? trace.text[id] : trace.text; + di.tx = Lib.isArrayOrTypedArray(trace.text) ? trace.text[id] : trace.text; di.htx = Array.isArray(trace.hovertext) ? trace.hovertext[id] : trace.hovertext; di.data = Array.isArray(trace.customdata) ? trace.customdata[id] : trace.customdata; di.tp = Array.isArray(trace.textposition) ? trace.textposition[id] : trace.textposition; diff --git a/src/traces/scattermapbox/defaults.js b/src/traces/scattermapbox/defaults.js index eb33d00cca9..4d59cce9f69 100644 --- a/src/traces/scattermapbox/defaults.js +++ b/src/traces/scattermapbox/defaults.js @@ -69,11 +69,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('mode'); coerce('below'); - if(subTypes.hasLines(traceOut)) { - handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce, {noDash: true}); - coerce('connectgaps'); - } - if(subTypes.hasMarkers(traceOut)) { handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, {noLine: true, noAngle: true}); @@ -88,6 +83,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } } + if(subTypes.hasLines(traceOut)) { + handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce, {noDash: true}); + coerce('connectgaps'); + } + var clusterMaxzoom = coerce2('cluster.maxzoom'); var clusterStep = coerce2('cluster.step'); var clusterColor = coerce2('cluster.color', (traceOut.marker && traceOut.marker.color) || defaultColor); diff --git a/src/traces/scatterpolar/defaults.js b/src/traces/scatterpolar/defaults.js index 5bf2c5e97b9..15b529f3c0e 100644 --- a/src/traces/scatterpolar/defaults.js +++ b/src/traces/scatterpolar/defaults.js @@ -29,16 +29,16 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) { coerce('hovertext'); if(traceOut.hoveron !== 'fills') coerce('hovertemplate'); + if(subTypes.hasMarkers(traceOut)) { + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, {gradient: true}); + } + if(subTypes.hasLines(traceOut)) { handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce, {backoff: true}); handleLineShapeDefaults(traceIn, traceOut, coerce); coerce('connectgaps'); } - if(subTypes.hasMarkers(traceOut)) { - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, {gradient: true}); - } - if(subTypes.hasText(traceOut)) { coerce('texttemplate'); handleTextDefaults(traceIn, traceOut, layout, coerce); @@ -70,6 +70,15 @@ function supplyDefaults(traceIn, traceOut, defaultColor, layout) { function handleRThetaDefaults(traceIn, traceOut, layout, coerce) { var r = coerce('r'); var theta = coerce('theta'); + + // TODO: handle this case outside supply defaults step + if(Lib.isTypedArray(r)) { + traceOut.r = r = Array.from(r); + } + if(Lib.isTypedArray(theta)) { + traceOut.theta = theta = Array.from(theta); + } + var len; if(r) { diff --git a/src/traces/scatterpolargl/defaults.js b/src/traces/scatterpolargl/defaults.js index 6b64bcb3179..d37497d2efa 100644 --- a/src/traces/scatterpolargl/defaults.js +++ b/src/traces/scatterpolargl/defaults.js @@ -29,15 +29,15 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('hovertext'); if(traceOut.hoveron !== 'fills') coerce('hovertemplate'); + if(subTypes.hasMarkers(traceOut)) { + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, {noAngleRef: true, noStandOff: true}); + } + if(subTypes.hasLines(traceOut)) { handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce); coerce('connectgaps'); } - if(subTypes.hasMarkers(traceOut)) { - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, {noAngleRef: true, noStandOff: true}); - } - if(subTypes.hasText(traceOut)) { coerce('texttemplate'); handleTextDefaults(traceIn, traceOut, layout, coerce); diff --git a/src/traces/scattersmith/defaults.js b/src/traces/scattersmith/defaults.js index d1001487803..622901b83c5 100644 --- a/src/traces/scattersmith/defaults.js +++ b/src/traces/scattersmith/defaults.js @@ -28,16 +28,16 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('hovertext'); if(traceOut.hoveron !== 'fills') coerce('hovertemplate'); + if(subTypes.hasMarkers(traceOut)) { + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, {gradient: true}); + } + if(subTypes.hasLines(traceOut)) { handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce, {backoff: true}); handleLineShapeDefaults(traceIn, traceOut, coerce); coerce('connectgaps'); } - if(subTypes.hasMarkers(traceOut)) { - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, {gradient: true}); - } - if(subTypes.hasText(traceOut)) { coerce('texttemplate'); handleTextDefaults(traceIn, traceOut, layout, coerce); @@ -75,6 +75,14 @@ function handleRealImagDefaults(traceIn, traceOut, layout, coerce) { len = Math.min(real.length, imag.length); } + // TODO: handle this case outside supply defaults step + if(Lib.isTypedArray(real)) { + traceOut.real = real = Array.from(real); + } + if(Lib.isTypedArray(imag)) { + traceOut.imag = imag = Array.from(imag); + } + traceOut._length = len; return len; } diff --git a/src/traces/scatterternary/defaults.js b/src/traces/scatterternary/defaults.js index cff521b24b9..0b7e3a0e630 100644 --- a/src/traces/scatterternary/defaults.js +++ b/src/traces/scatterternary/defaults.js @@ -55,16 +55,16 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout var defaultMode = len < constants.PTS_LINESONLY ? 'lines+markers' : 'lines'; coerce('mode', defaultMode); + if(subTypes.hasMarkers(traceOut)) { + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, {gradient: true}); + } + if(subTypes.hasLines(traceOut)) { handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce, {backoff: true}); handleLineShapeDefaults(traceIn, traceOut, coerce); coerce('connectgaps'); } - if(subTypes.hasMarkers(traceOut)) { - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, {gradient: true}); - } - if(subTypes.hasText(traceOut)) { coerce('texttemplate'); handleTextDefaults(traceIn, traceOut, layout, coerce); diff --git a/src/traces/sunburst/defaults.js b/src/traces/sunburst/defaults.js index 82b43f2c95a..9732f3f5cbd 100644 --- a/src/traces/sunburst/defaults.js +++ b/src/traces/sunburst/defaults.js @@ -47,7 +47,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout var text = coerce('text'); coerce('texttemplate'); - if(!traceOut.texttemplate) coerce('textinfo', Array.isArray(text) ? 'text+label' : 'label'); + if(!traceOut.texttemplate) coerce('textinfo', Lib.isArrayOrTypedArray(text) ? 'text+label' : 'label'); coerce('hovertext'); coerce('hovertemplate'); diff --git a/src/traces/surface/convert.js b/src/traces/surface/convert.js index c66689a8672..d2433c382bb 100644 --- a/src/traces/surface/convert.js +++ b/src/traces/surface/convert.js @@ -96,7 +96,7 @@ proto.handlePick = function(selection) { } var text = this.data.hovertext || this.data.text; - if(Array.isArray(text) && text[k] && text[k][j] !== undefined) { + if(isArrayOrTypedArray(text) && text[k] && text[k][j] !== undefined) { selection.textLabel = text[k][j]; } else if(text) { selection.textLabel = text; diff --git a/src/traces/table/data_preparation_helper.js b/src/traces/table/data_preparation_helper.js index 2fc0ee68ad8..fb737366dd3 100644 --- a/src/traces/table/data_preparation_helper.js +++ b/src/traces/table/data_preparation_helper.js @@ -3,6 +3,8 @@ var c = require('./constants'); var extendFlat = require('../../lib/extend').extendFlat; var isNumeric = require('fast-isnumeric'); +var isTypedArray = require('../../lib/array').isTypedArray; +var isArrayOrTypedArray = require('../../lib/array').isArrayOrTypedArray; // pure functions, don't alter but passes on `gd` and parts of `trace` without deep copying module.exports = function calc(gd, trace) { @@ -35,9 +37,13 @@ module.exports = function calc(gd, trace) { var headerRowBlocks = makeRowBlock(anchorToHeaderRowBlock, []); var rowBlocks = makeRowBlock(anchorToRowBlock, headerRowBlocks); var uniqueKeys = {}; - var columnOrder = trace._fullInput.columnorder.concat(slicer(cellsValues.map(function(d, i) {return i;}))); + + var columnOrder = trace._fullInput.columnorder; + if(isArrayOrTypedArray(columnOrder)) columnOrder = Array.from(columnOrder); + columnOrder = columnOrder.concat(slicer(cellsValues.map(function(d, i) {return i;}))); + var columnWidths = headerValues.map(function(d, i) { - var value = Array.isArray(trace.columnwidth) ? + var value = isArrayOrTypedArray(trace.columnwidth) ? trace.columnwidth[Math.min(i, trace.columnwidth.length - 1)] : trace.columnwidth; return isNumeric(value) ? Number(value) : 1; @@ -95,7 +101,7 @@ module.exports = function calc(gd, trace) { }; function arrayMax(maybeArray) { - if(Array.isArray(maybeArray)) { + if(isArrayOrTypedArray(maybeArray)) { var max = 0; for(var i = 0; i < maybeArray.length; i++) { max = Math.max(max, arrayMax(maybeArray[i])); @@ -115,7 +121,8 @@ function squareStringMatrix(matrixIn) { var maxLen = 0; var i; for(i = 0; i < matrix.length; i++) { - if(!Array.isArray(matrix[i])) matrix[i] = [matrix[i]]; + if(isTypedArray(matrix[i])) matrix[i] = Array.from(matrix[i]); + else if(!isArrayOrTypedArray(matrix[i])) matrix[i] = [matrix[i]]; minLen = Math.min(minLen, matrix[i].length); maxLen = Math.max(maxLen, matrix[i].length); } diff --git a/src/traces/table/plot.js b/src/traces/table/plot.js index 98480fa976a..972e5020903 100644 --- a/src/traces/table/plot.js +++ b/src/traces/table/plot.js @@ -600,9 +600,9 @@ function columnMoved(gd, calcdata, indices) { } function gridPick(spec, col, row) { - if(Array.isArray(spec)) { + if(Lib.isArrayOrTypedArray(spec)) { var column = spec[Math.min(col, spec.length - 1)]; - if(Array.isArray(column)) { + if(Lib.isArrayOrTypedArray(column)) { return column[Math.min(row, column.length - 1)]; } else { return column; diff --git a/src/traces/treemap/defaults.js b/src/traces/treemap/defaults.js index a9943f6ca43..930ca32d5ef 100644 --- a/src/traces/treemap/defaults.js +++ b/src/traces/treemap/defaults.js @@ -45,7 +45,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout var text = coerce('text'); coerce('texttemplate'); - if(!traceOut.texttemplate) coerce('textinfo', Array.isArray(text) ? 'text+label' : 'label'); + if(!traceOut.texttemplate) coerce('textinfo', Lib.isArrayOrTypedArray(text) ? 'text+label' : 'label'); coerce('hovertext'); coerce('hovertemplate'); diff --git a/src/traces/volume/convert.js b/src/traces/volume/convert.js index 7504a493d1f..c749a1eeb4b 100644 --- a/src/traces/volume/convert.js +++ b/src/traces/volume/convert.js @@ -3,6 +3,7 @@ var createMesh = require('../../../stackgl_modules').gl_mesh3d; var parseColorScale = require('../../lib/gl_format_color').parseColorScale; +var isArrayOrTypedArray = require('../../lib').isArrayOrTypedArray; var str2RgbaArray = require('../../lib/str2rgbarray'); var extractOpts = require('../../components/colorscale').extractOpts; var zip3 = require('../../plots/gl3d/zip3'); @@ -46,7 +47,7 @@ proto.handlePick = function(selection) { ]; var text = this.data.hovertext || this.data.text; - if(Array.isArray(text) && text[selectIndex] !== undefined) { + if(isArrayOrTypedArray(text) && text[selectIndex] !== undefined) { selection.textLabel = text[selectIndex]; } else if(text) { selection.textLabel = text; diff --git a/src/transforms/filter.js b/src/transforms/filter.js index e852a2a147f..6a05527db34 100644 --- a/src/transforms/filter.js +++ b/src/transforms/filter.js @@ -222,7 +222,7 @@ exports.calcTransform = function(gd, trace, opts) { function getFilterFunc(opts, d2c, targetCalendar) { var operation = opts.operation; var value = opts.value; - var hasArrayValue = Array.isArray(value); + var hasArrayValue = Lib.isArrayOrTypedArray(value); function isOperationIn(array) { return array.indexOf(operation) !== -1; diff --git a/test/image/convert_b64.py b/test/image/convert_b64.py new file mode 100644 index 00000000000..18bc4a0c197 --- /dev/null +++ b/test/image/convert_b64.py @@ -0,0 +1,105 @@ +import numpy +import base64 + +plotlyjsShortTypes = { + 'int8': 'i1', + 'uint8': 'u1', + 'int16': 'i2', + 'uint16': 'u2', + 'int32': 'i4', + 'uint32': 'u4', + 'float32': 'f4', + 'float64': 'f8' +} + +int8bounds = numpy.iinfo(numpy.int8) +int16bounds = numpy.iinfo(numpy.int16) +int32bounds = numpy.iinfo(numpy.int32) +uint8bounds = numpy.iinfo(numpy.uint8) +uint16bounds = numpy.iinfo(numpy.uint16) +uint32bounds = numpy.iinfo(numpy.uint32) + +skipKeys = [ + 'geojson', + 'layers', + 'range' +] + +def arraysToB64(obj, newObj) : + for key, val in obj.items() : + if key in skipKeys : + newObj[key] = val + elif isinstance(val, dict) : + newObj[key] = dict() + arraysToB64(val, newObj[key]) + elif isinstance(val, list) : + try : + arr = numpy.array(val) + except Exception : + newObj[key] = val + continue + + if arr.dtype == 'object' : + newList = list() + for v in val : + if isinstance(v, dict) : + newList.append(arraysToB64(v, dict())) + else : + newList.append(v) + + newObj[key] = newList + else : + # In a real application one does not need to convert + # small arrays like those that only have 2 items. + # But here we convert 2 item array to test typed arrays + # in more places. + + # skip converting arrays with no items + if(arr.ndim == 1 and arr.shape[0] < 1) : + newObj[key] = val + continue + + # convert default Big Ints until we could support them in plotly.js + if str(arr.dtype) == 'int64' : + max = arr.max() + min = arr.min() + if max <= int8bounds.max and min >= int8bounds.min : + arr = arr.astype(numpy.int8) + elif max <= int16bounds.max and min >= int16bounds.min : + arr = arr.astype(numpy.int16) + elif max <= int32bounds.max and min >= int32bounds.min : + arr = arr.astype(numpy.int32) + else : + newObj[key] = val + continue + + elif str(arr.dtype) == 'uint64' : + if max <= uint8bounds.max and min >= uint8bounds.min : + arr = arr.astype(numpy.uint8) + elif max <= uint16bounds.max and min >= uint16bounds.min : + arr = arr.astype(numpy.uint16) + elif max <= uint32bounds.max and min >= uint32bounds.min : + arr = arr.astype(numpy.uint32) + else : + newObj[key] = val + continue + + if str(arr.dtype) in plotlyjsShortTypes : + newObj[key] = { + 'dtype': plotlyjsShortTypes[str(arr.dtype)], + 'bdata': base64.b64encode(arr).decode('ascii') + } + + if(arr.ndim > 1) : + newObj[key]['shape'] = str(arr.shape)[1:-1] + + #print(val) + #print(newObj[key]) + #print('____________________') + else : + newObj[key] = val + + else : + newObj[key] = val + + return newObj diff --git a/test/image/generate_b64_mocks.py b/test/image/generate_b64_mocks.py new file mode 100644 index 00000000000..923f3af5669 --- /dev/null +++ b/test/image/generate_b64_mocks.py @@ -0,0 +1,50 @@ +import os +import sys +import json +import plotly.io as pio +from convert_b64 import arraysToB64 + +args = [] +if len(sys.argv) == 2 : + args = sys.argv[1].split() +elif len(sys.argv) > 1 : + args = sys.argv + +root = os.getcwd() +dirIn = os.path.join(root, 'test', 'image', 'mocks') +dirOut = dirIn + +print('output to', dirOut) + +ALL_MOCKS = [os.path.splitext(a)[0] for a in os.listdir(dirIn) if a.endswith('.json')] +ALL_MOCKS.sort() + +if len(args) > 0 : + allNames = [a for a in args if a in ALL_MOCKS] +else : + allNames = ALL_MOCKS + +if len(allNames) == 0 : + print('error: Nothing to create!') + sys.exit(1) + +for name in allNames : + outName = name + + with open(os.path.join(dirIn, name + '.json'), 'r') as _in : + fig = json.load(_in) + + before = json.dumps(fig, indent = 2) + + newFig = dict() + arraysToB64(fig, newFig) + fig = newFig + + after = json.dumps(fig, indent = 2) + + if before != after : + print(outName) + + _out = open(os.path.join(dirOut, outName + '.json'), 'w') + _out.write(json.dumps(fig, indent = 2)) + _out.close() diff --git a/test/image/make_baseline.js b/test/image/make_baseline.js index 11ab01dfad0..0666465311b 100644 --- a/test/image/make_baseline.js +++ b/test/image/make_baseline.js @@ -29,14 +29,20 @@ var getMockList = require('./assets/get_mock_list'); * * npm run baseline mathjax3 * - */ + * Generate or (re-generate) baselines using b64 typed arrays: + * + * npm run baseline b64 + * +*/ var argv = minimist(process.argv.slice(2), {}); var allMockList = []; -var mathjax3; +var mathjax3, b64; argv._.forEach(function(pattern) { - if(pattern === 'mathjax3') { + if(pattern === 'b64') { + b64 = true; + } else if(pattern === 'mathjax3') { mathjax3 = true; } else { var mockList = getMockList(pattern); @@ -67,7 +73,9 @@ console.log('Please wait for the process to complete.'); var p = spawn( 'python3', [ path.join('test', 'image', 'make_baseline.py'), - (mathjax3 ? 'mathjax3' : '') + '= ' + allMockList.join(' ') + (mathjax3 ? 'mathjax3' : '') + + (b64 ? 'b64' : '') + + '= ' + allMockList.join(' ') ] ); try { diff --git a/test/image/make_baseline.py b/test/image/make_baseline.py index 5e05b1d0801..3c0b50d1bc5 100644 --- a/test/image/make_baseline.py +++ b/test/image/make_baseline.py @@ -2,6 +2,7 @@ import sys import json import plotly.io as pio +from convert_b64 import arraysToB64 args = [] if len(sys.argv) == 2 : @@ -130,6 +131,12 @@ if 'height' in layout : height = layout['height'] + if 'b64' in sys.argv or 'b64=' in sys.argv or 'b64-json' in sys.argv : + newFig = dict() + arraysToB64(fig, newFig) + fig = newFig + if 'b64-json' in sys.argv and attempt == 0 : print(json.dumps(fig, indent = 2)) + try : pio.write_image( fig=fig, diff --git a/test/jasmine/tests/toimage_test.js b/test/jasmine/tests/toimage_test.js index bcfa50bd325..da4ffd5aba6 100644 --- a/test/jasmine/tests/toimage_test.js +++ b/test/jasmine/tests/toimage_test.js @@ -1,3 +1,8 @@ +var b64 = require('base64-arraybuffer'); +function b64encodeTypedArray(typedArray) { + return b64.encode(typedArray.buffer); +} + var Plotly = require('../../../lib/index'); var Lib = require('../../../src/lib'); @@ -291,6 +296,218 @@ describe('Plotly.toImage', function() { .then(done, done.fail); }); + it('export typed arrays as regular arrays', function(done) { + var x = new Float64Array([-1 / 3, 1 / 3]); + var y = new Float32Array([-1 / 3, 1 / 3]); + var z = [ + new Int16Array([-32768, 32767]), + new Uint16Array([65535, 0]) + ]; + + Plotly.newPlot(gd, [{ + type: 'surface', + x: x, + y: y, + z: z + }]) + .then(function(gd) { + var trace = gd._fullData[0]; + + expect(trace.visible).toEqual(true); + + expect(trace.x.slice()).toEqual(x); + expect(trace.y.slice()).toEqual(y); + expect(trace.z.slice()).toEqual(z); + + return Plotly.toImage(gd, imgOpts); + }) + .then(function(fig) { + var trace = JSON.parse(fig).data[0]; + + expect(trace.visible).toEqual(true); + + expect(trace.x).toEqual([-0.3333333333333333, 0.3333333333333333]); + expect(trace.y).toEqual([-0.3333333432674408, 0.3333333432674408]); + expect(trace.z).toEqual([[-32768, 32767], [65535, 0]]); + }) + .then(done, done.fail); + }); + + it('import & export 1d and 2d typed arrays', function(done) { + var allX = new Float64Array([-1 / 3, 0, 1 / 3]); + var allY = new Float32Array([1 / 3, -1 / 3]); + var allZ = new Uint16Array([0, 100, 200, 300, 400, 500]); + var x = b64encodeTypedArray(allX); + var y = b64encodeTypedArray(allY); + var z = b64encodeTypedArray(allZ); + + Plotly.newPlot(gd, [{ + type: 'surface', + x: {bdata: x, dtype: 'f8'}, + y: {bdata: y, dtype: 'f4'}, + z: {bdata: z, dtype: 'u2', shape: '2,3'} + }]) + .then(function(gd) { + var trace = gd._fullData[0]; + + expect(trace.visible).toEqual(true); + + expect(trace.x.slice()).toEqual(allX); + expect(trace.y.slice()).toEqual(allY); + expect(trace.z.slice()).toEqual([ + new Uint16Array([0, 100, 200]), + new Uint16Array([300, 400, 500]) + ]); + + return Plotly.toImage(gd, imgOpts); + }) + .then(function(fig) { + var trace = JSON.parse(fig).data[0]; + + expect(trace.visible).toEqual(true); + + expect(trace.x.bdata).toEqual('VVVVVVVV1b8AAAAAAAAAAFVVVVVVVdU/'); + expect(trace.y.bdata).toEqual('q6qqPquqqr4='); + expect(trace.z.bdata).toEqual('AABkAMgALAGQAfQB'); + + expect(trace.x.dtype).toEqual('f8'); + expect(trace.x.shape).toEqual('3'); + + expect(trace.y.dtype).toEqual('f4'); + expect(trace.y.shape).toEqual('2'); + + expect(trace.z.dtype).toEqual('u2'); + expect(trace.z.shape).toEqual('2,3'); + }) + .then(done, done.fail); + }); + + it('import buffer and export b64', function(done) { + var allX = new Float64Array([-1 / 3, 0, 1 / 3]); + var allY = new Float32Array([1 / 3, -1 / 3]); + var allZ = new Uint16Array([0, 100, 200, 300, 400, 500]); + var x = allX.buffer; + var y = allY.buffer; + var z = allZ.buffer; + + Plotly.newPlot(gd, [{ + type: 'surface', + x: {bdata: x, dtype: 'f8', shape: '3'}, + y: {bdata: y, dtype: 'f4', shape: '2'}, + z: {bdata: z, dtype: 'u2', shape: '2,3'} + }]) + .then(function(gd) { + var trace = gd._fullData[0]; + + expect(trace.visible).toEqual(true); + + expect(trace.x.slice()).toEqual(allX); + expect(trace.y.slice()).toEqual(allY); + expect(trace.z.slice()).toEqual([ + new Uint16Array([0, 100, 200]), + new Uint16Array([300, 400, 500]) + ]); + + return Plotly.toImage(gd, imgOpts); + }) + .then(function(fig) { + var trace = JSON.parse(fig).data[0]; + + expect(trace.visible).toEqual(true); + + expect(trace.x.bdata).toEqual('VVVVVVVV1b8AAAAAAAAAAFVVVVVVVdU/'); + expect(trace.y.bdata).toEqual('q6qqPquqqr4='); + expect(trace.z.bdata).toEqual('AABkAMgALAGQAfQB'); + + expect(trace.x.dtype).toEqual('f8'); + expect(trace.x.shape).toEqual('3'); + + expect(trace.y.dtype).toEqual('f4'); + expect(trace.y.shape).toEqual('2'); + + expect(trace.z.dtype).toEqual('u2'); + expect(trace.z.shape).toEqual('2,3'); + }) + .then(done, done.fail); + }); + + [ + 'scatter3d', + 'scattergl', + 'scatter' + ].forEach(function(type) { + it('import & export arrayOk marker.color and marker.size for ' + type, function(done) { + var is3D = type === 'scatter3d'; + + var allX = new Int16Array([-100, 200, -300, 400]); + var allY = new Uint16Array([100, 200, 300, 400]); + var allZ = new Int8Array([-120, -60, 0, 60]); + var allS = new Uint8ClampedArray([0, 60, 120, 240]); + var allC = new Uint8Array([0, 60, 120, 240]); + + var x = b64encodeTypedArray(allX); + var y = b64encodeTypedArray(allY); + var z = b64encodeTypedArray(allZ); + var s = b64encodeTypedArray(allS); + var c = b64encodeTypedArray(allC); + + Plotly.newPlot(gd, [{ + type: type, + x: {bdata: x, dtype: 'i2'}, + y: {bdata: y, dtype: 'u2'}, + z: {bdata: z, dtype: 'i1'}, + marker: { + color: {bdata: c, dtype: 'u1'}, + size: {bdata: s, dtype: 'u1c'} + } + }]) + .then(function(gd) { + var trace = gd._fullData[0]; + + expect(trace.visible).toEqual(true); + + expect(trace.x.slice()).toEqual(allX); + expect(trace.y.slice()).toEqual(allY); + if(is3D) expect(trace.z.slice()).toEqual(allZ); + expect(trace.marker.size.slice()).toEqual(allS); + expect(trace.marker.color.slice()).toEqual(allC); + expect(trace.line.color).toEqual('#1f77b4'); + + return Plotly.toImage(gd, imgOpts); + }) + .then(function(fig) { + var trace = JSON.parse(fig).data[0]; + + expect(trace.visible).toEqual(true); + + expect(trace.x.bdata).toEqual('nP/IANT+kAE='); + expect(trace.x.dtype).toEqual('i2'); + expect(trace.x.shape).toEqual('4'); + + expect(trace.y.bdata).toEqual('ZADIACwBkAE='); + expect(trace.y.dtype).toEqual('u2'); + expect(trace.y.shape).toEqual('4'); + + if(is3D) { + expect(trace.z.bdata).toEqual('iMQAPA=='); + expect(trace.z.dtype).toEqual('i1'); + expect(trace.z.shape).toEqual('4'); + } + + expect(trace.marker.size.bdata).toEqual('ADx48A=='); + expect(trace.marker.size.dtype).toEqual('u1c'); + expect(trace.marker.size.shape).toEqual('4'); + + expect(trace.marker.color.bdata).toEqual('ADx48A=='); + expect(trace.marker.color.dtype).toEqual('u1'); + expect(trace.marker.color.shape).toEqual('4'); + + expect(trace.marker.colorscale).toBeDefined(); + }) + .then(done, done.fail); + }); + }); + it('export computed margins', function(done) { Plotly.toImage(pieAutoMargin, imgOpts) .then(function(fig) { diff --git a/test/plot-schema.json b/test/plot-schema.json index 9ad961d8701..8cada16a333 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -479,7 +479,7 @@ "requiredOpts": [] }, "data_array": { - "description": "An {array} of data. The value MUST be an {array}, or we ignore it. Note that typed arrays (e.g. Float32Array) are supported.", + "description": "An {array} of data. The value must represent an {array} or it will be ignored, but this array can be provided in several forms: (1) a regular {array} object (2) a typed array (e.g. Float32Array) (3) an object with keys dtype, bdata, and optionally shape. In this 3rd form, dtype is one of *f8*, *f4*. *i4*, *u4*, *i2*, *u2*, *i1*, *u1* or *u1c* for Uint8ClampedArray. In addition to shorthand `dtype` above one could also use the following forms: *float64*, *float32*, *int32*, *uint32*, *int16*, *uint16*, *int8*, *uint8* or *uint8c* for Uint8ClampedArray. `bdata` is either a base64-encoded string or the ArrayBuffer of an integer or float typed array. For either multi-dimensional arrays you must also provide its dimensions separated by comma via `shape`. For example using `dtype`: *f4* and `shape`: *5,100* you can declare a 2-D array that has 5 rows and 100 columns containing float32 values i.e. 4 bits per value. `shape` is optional for one dimensional arrays.", "otherOpts": [ "dflt" ],