diff --git a/src/lib/array.js b/src/lib/array.js new file mode 100644 index 00000000000..a80f4b3f8ca --- /dev/null +++ b/src/lib/array.js @@ -0,0 +1,134 @@ +/** +* Copyright 2012-2018, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var isArray = Array.isArray; + +// IE9 fallbacks + +var ab = (typeof ArrayBuffer === 'undefined' || !ArrayBuffer.isView) ? + {isView: function() { return false; }} : + ArrayBuffer; + +var dv = (typeof DataView === 'undefined') ? + function() {} : + DataView; + +function isTypedArray(a) { + return ab.isView(a) && !(a instanceof dv); +} +exports.isTypedArray = isTypedArray; + +function isArrayOrTypedArray(a) { + return isArray(a) || isTypedArray(a); +} +exports.isArrayOrTypedArray = isArrayOrTypedArray; + +/* + * Test whether an input object is 1D. + * + * Assumes we already know the object is an array. + * + * Looks only at the first element, if the dimensionality is + * not consistent we won't figure that out here. + */ +function isArray1D(a) { + return !isArrayOrTypedArray(a[0]); +} +exports.isArray1D = isArray1D; + +/* + * Ensures an array has the right amount of storage space. If it doesn't + * exist, it creates an array. If it does exist, it returns it if too + * short or truncates it in-place. + * + * The goal is to just reuse memory to avoid a bit of excessive garbage + * collection. + */ +exports.ensureArray = function(out, n) { + // TODO: typed array support here? This is only used in + // traces/carpet/compute_control_points + if(!isArray(out)) out = []; + + // If too long, truncate. (If too short, it will grow + // automatically so we don't care about that case) + out.length = n; + + return out; +}; + +/* + * TypedArray-compatible concatenation of n arrays + * if all arrays are the same type it will preserve that type, + * otherwise it falls back on Array. + * Also tries to avoid copying, in case one array has zero length + * But never mutates an existing array + */ +exports.concat = function() { + var args = []; + var allArray = true; + var totalLen = 0; + + var _constructor, arg0, i, argi, posi, leni, out, j; + + for(i = 0; i < arguments.length; i++) { + argi = arguments[i]; + leni = argi.length; + if(leni) { + if(arg0) args.push(argi); + else { + arg0 = argi; + posi = leni; + } + + if(isArray(argi)) { + _constructor = false; + } + else { + allArray = false; + if(!totalLen) { + _constructor = argi.constructor; + } + else if(_constructor !== argi.constructor) { + // TODO: in principle we could upgrade here, + // ie keep typed array but convert all to Float64Array? + _constructor = false; + } + } + + totalLen += leni; + } + } + + if(!totalLen) return []; + if(!args.length) return arg0; + + if(allArray) return arg0.concat.apply(arg0, args); + if(_constructor) { + // matching typed arrays + out = new _constructor(totalLen); + out.set(arg0); + for(i = 0; i < args.length; i++) { + argi = args[i]; + out.set(argi, posi); + posi += argi.length; + } + return out; + } + + // mismatched types or Array + typed + out = new Array(totalLen); + for(j = 0; j < arg0.length; j++) out[j] = arg0[j]; + for(i = 0; i < args.length; i++) { + argi = args[i]; + for(j = 0; j < argi.length; j++) out[posi + j] = argi[j]; + posi += j; + } + return out; +}; diff --git a/src/lib/coerce.js b/src/lib/coerce.js index 421c9b4bd1c..87cc1473446 100644 --- a/src/lib/coerce.js +++ b/src/lib/coerce.js @@ -19,7 +19,7 @@ var nestedProperty = require('./nested_property'); var counterRegex = require('./regex').counter; var DESELECTDIM = require('../constants/interactions').DESELECTDIM; var modHalf = require('./mod').modHalf; -var isArrayOrTypedArray = require('./is_array').isArrayOrTypedArray; +var isArrayOrTypedArray = require('./array').isArrayOrTypedArray; exports.valObjectMeta = { data_array: { diff --git a/src/lib/ensure_array.js b/src/lib/ensure_array.js deleted file mode 100644 index 60c328da7be..00000000000 --- a/src/lib/ensure_array.js +++ /dev/null @@ -1,27 +0,0 @@ -/** -* Copyright 2012-2018, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - -'use strict'; - -/* - * Ensures an array has the right amount of storage space. If it doesn't - * exist, it creates an array. If it does exist, it returns it if too - * short or truncates it in-place. - * - * The goal is to just reuse memory to avoid a bit of excessive garbage - * collection. - */ -module.exports = function ensureArray(out, n) { - if(!Array.isArray(out)) out = []; - - // If too long, truncate. (If too short, it will grow - // automatically so we don't care about that case) - out.length = n; - - return out; -}; diff --git a/src/lib/gl_format_color.js b/src/lib/gl_format_color.js index 5f2e4b0348e..1fe622c8443 100644 --- a/src/lib/gl_format_color.js +++ b/src/lib/gl_format_color.js @@ -15,7 +15,7 @@ var rgba = require('color-normalize'); var Colorscale = require('../components/colorscale'); var colorDflt = require('../components/color/attributes').defaultLine; -var isArrayOrTypedArray = require('./is_array').isArrayOrTypedArray; +var isArrayOrTypedArray = require('./array').isArrayOrTypedArray; var colorDfltRgba = rgba(colorDflt); var opacityDflt = 1; diff --git a/src/lib/index.js b/src/lib/index.js index 0778748e09b..d0d2e34f802 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -24,17 +24,18 @@ lib.relativeAttr = require('./relative_attr'); lib.isPlainObject = require('./is_plain_object'); lib.toLogRange = require('./to_log_range'); lib.relinkPrivateKeys = require('./relink_private'); -lib.ensureArray = require('./ensure_array'); + +var arrayModule = require('./array'); +lib.isTypedArray = arrayModule.isTypedArray; +lib.isArrayOrTypedArray = arrayModule.isArrayOrTypedArray; +lib.isArray1D = arrayModule.isArray1D; +lib.ensureArray = arrayModule.ensureArray; +lib.concat = arrayModule.concat; var modModule = require('./mod'); lib.mod = modModule.mod; lib.modHalf = modModule.modHalf; -var isArrayModule = require('./is_array'); -lib.isTypedArray = isArrayModule.isTypedArray; -lib.isArrayOrTypedArray = isArrayModule.isArrayOrTypedArray; -lib.isArray1D = isArrayModule.isArray1D; - var coerceModule = require('./coerce'); lib.valObjectMeta = coerceModule.valObjectMeta; lib.coerce = coerceModule.coerce; diff --git a/src/lib/is_array.js b/src/lib/is_array.js deleted file mode 100644 index b8c5e1ae47c..00000000000 --- a/src/lib/is_array.js +++ /dev/null @@ -1,45 +0,0 @@ -/** -* Copyright 2012-2018, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - -'use strict'; - -// IE9 fallbacks - -var ab = (typeof ArrayBuffer === 'undefined' || !ArrayBuffer.isView) ? - {isView: function() { return false; }} : - ArrayBuffer; - -var dv = (typeof DataView === 'undefined') ? - function() {} : - DataView; - -function isTypedArray(a) { - return ab.isView(a) && !(a instanceof dv); -} - -function isArrayOrTypedArray(a) { - return Array.isArray(a) || isTypedArray(a); -} - -/* - * Test whether an input object is 1D. - * - * Assumes we already know the object is an array. - * - * Looks only at the first element, if the dimensionality is - * not consistent we won't figure that out here. - */ -function isArray1D(a) { - return !isArrayOrTypedArray(a[0]); -} - -module.exports = { - isTypedArray: isTypedArray, - isArrayOrTypedArray: isArrayOrTypedArray, - isArray1D: isArray1D -}; diff --git a/src/lib/nested_property.js b/src/lib/nested_property.js index a37bf5ca4ad..b88017445a0 100644 --- a/src/lib/nested_property.js +++ b/src/lib/nested_property.js @@ -10,7 +10,7 @@ 'use strict'; var isNumeric = require('fast-isnumeric'); -var isArrayOrTypedArray = require('./is_array').isArrayOrTypedArray; +var isArrayOrTypedArray = require('./array').isArrayOrTypedArray; /** * convert a string s (such as 'xaxis.range[0]') diff --git a/src/lib/relink_private.js b/src/lib/relink_private.js index e0c63b8fef4..19ef12a2f01 100644 --- a/src/lib/relink_private.js +++ b/src/lib/relink_private.js @@ -9,7 +9,7 @@ 'use strict'; -var isArrayOrTypedArray = require('./is_array').isArrayOrTypedArray; +var isArrayOrTypedArray = require('./array').isArrayOrTypedArray; var isPlainObject = require('./is_plain_object'); /** diff --git a/src/lib/stats.js b/src/lib/stats.js index 71c23d70375..1cfca38efc6 100644 --- a/src/lib/stats.js +++ b/src/lib/stats.js @@ -10,7 +10,7 @@ 'use strict'; var isNumeric = require('fast-isnumeric'); -var isArrayOrTypedArray = require('./is_array').isArrayOrTypedArray; +var isArrayOrTypedArray = require('./array').isArrayOrTypedArray; /** * aggNums() returns the result of an aggregate function applied to an array of diff --git a/src/traces/histogram/calc.js b/src/traces/histogram/calc.js index 1a60c9206e9..a03f3cec3ae 100644 --- a/src/traces/histogram/calc.js +++ b/src/traces/histogram/calc.js @@ -252,7 +252,7 @@ function calcAllAutoBins(gd, trace, pa, mainData, _overlayEdgeCase) { for(i = 0; i < traces.length; i++) { tracei = traces[i]; pos0 = tracei._pos0 = pa.makeCalcdata(tracei, mainData); - allPos = allPos.concat(pos0); + allPos = Lib.concat(allPos, pos0); delete tracei._autoBinFinished; if(trace.visible === true) { if(isFirstVisible) { diff --git a/test/jasmine/tests/histogram_test.js b/test/jasmine/tests/histogram_test.js index 1d86448b260..2cb91b71f26 100644 --- a/test/jasmine/tests/histogram_test.js +++ b/test/jasmine/tests/histogram_test.js @@ -536,6 +536,16 @@ describe('Test histogram', function() { expect(calcPositions(trace3)).toBeCloseToArray([1.1, 1.3], 5); }); + it('can handle TypedArrays', function() { + var trace1 = {x: new Float32Array([1, 2, 3, 4])}; + var trace2 = {x: new Float32Array([5, 5.5, 6, 6.5])}; + var trace3 = {x: new Float64Array([1, 1.1, 1.2, 1.3]), xaxis: 'x2'}; + var trace4 = {x: new Float64Array([1, 1.2, 1.4, 1.6]), yaxis: 'y2'}; + + expect(calcPositions(trace1, [trace2, trace3, trace4])).toEqual([1, 3, 5]); + expect(calcPositions(trace3)).toBeCloseToArray([1.1, 1.3], 5); + }); + describe('cumulative distribution functions', function() { var base = { x: [0, 5, 10, 15, 5, 10, 15, 10, 15, 15], diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index 6653cc9d46f..2ffe0710c55 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -2459,6 +2459,88 @@ describe('Test lib.js:', function() { expect(toContainer).toEqual(expected); }); }); + + describe('concat', function() { + var concat = Lib.concat; + + beforeEach(function() { + spyOn(Array.prototype, 'concat').and.callThrough(); + }); + + it('works with multiple Arrays', function() { + var res = concat([1], [[2], 3], [{a: 4}, 5, 6]); + expect(Array.prototype.concat.calls.count()).toBe(1); + + // note: can't `concat` in the `expect` if we want to count native + // `Array.concat calls`, because `toEqual` calls `Array.concat` + // profusely itself. + expect(res).toEqual([1, [2], 3, {a: 4}, 5, 6]); + }); + + it('works with some empty arrays', function() { + var a1 = [1]; + var res = concat(a1, [], [2, 3]); + expect(Array.prototype.concat.calls.count()).toBe(1); + expect(res).toEqual([1, 2, 3]); + expect(a1).toEqual([1]); // did not mutate a1 + + Array.prototype.concat.calls.reset(); + var a1b = concat(a1, []); + var a1c = concat([], a1b); + var a1d = concat([], a1c, []); + expect(Array.prototype.concat.calls.count()).toBe(0); + + expect(a1d).toEqual([1]); + // does not mutate a1, but *will* return it unchanged if it's the + // only one with data + expect(a1d).toBe(a1); + + expect(concat([], [0], [1, 0], [2, 0, 0])).toEqual([0, 1, 0, 2, 0, 0]); + + // a single typedArray will keep its identity (and type) + // even if other empty arrays don't match type. + var f1 = new Float32Array([1, 2]); + Array.prototype.concat.calls.reset(); + res = concat([], f1, new Float64Array([])); + expect(Array.prototype.concat.calls.count()).toBe(0); + expect(res).toBe(f1); + expect(f1).toEqual(new Float32Array([1, 2])); + }); + + it('works with all empty arrays', function() { + [[], [[]], [[], []], [[], [], [], []]].forEach(function(empties) { + Array.prototype.concat.calls.reset(); + var res = concat.apply(null, empties); + expect(Array.prototype.concat.calls.count()).toBe(0); + expect(res).toEqual([]); + }); + }); + + it('converts mismatched types to Array', function() { + [ + [[1, 2], new Float64Array([3, 4])], + [new Float64Array([1, 2]), [3, 4]], + [new Float64Array([1, 2]), new Float32Array([3, 4])] + ].forEach(function(mismatch) { + Array.prototype.concat.calls.reset(); + var res = concat.apply(null, mismatch); + // no concat - all entries moved over individually + expect(Array.prototype.concat.calls.count()).toBe(0); + expect(res).toEqual([1, 2, 3, 4]); + }); + }); + + it('concatenates matching TypedArrays preserving type', function() { + [Float32Array, Float64Array, Int16Array, Int32Array].forEach(function(Type, i) { + var v = i * 10; + Array.prototype.concat.calls.reset(); + var res = concat([], new Type([v]), new Type([v + 1, v]), new Type([v + 2, v, v])); + // no concat - uses `TypedArray.set` + expect(Array.prototype.concat.calls.count()).toBe(0); + expect(res).toEqual(new Type([v, v + 1, v, v + 2, v, v])); + }); + }); + }); }); describe('Queue', function() {