From 636e64437abb88dc1cbb939e37691e9db7aac226 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Thu, 16 Aug 2018 09:20:46 -0400 Subject: [PATCH 01/10] Added support for encoding TypedArrays as primitive objects (representation objects) A TypedArray representation object has two properties: `dtype` and `data`. `dtype` is a string indicating the type of the typed array (`'int8'`, `'float32'`, `'uint16'`, etc.) `data` is a primitive JavaScript object that stores the typed array data. It can be one of: - Standard JavaScript Array - ArrayBuffer - DataView - A base64 encoded string The representation objects may stand in for TypedArrays in `data_type` properties and in properties with `arrayOk: true`. The representation object is stored in `data`/`layout`, while the converted TypedArray is stored in `_fullData`/`_fullLayout` --- package.json | 1 + src/lib/coerce.js | 69 +++++++- src/lib/is_array.js | 9 +- .../image/mocks/typed_array_repr_scatter.json | 13 ++ .../tests/primitive_typed_array_repr_test.js | 148 ++++++++++++++++++ 5 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 test/image/mocks/typed_array_repr_scatter.json create mode 100644 test/jasmine/tests/primitive_typed_array_repr_test.js diff --git a/package.json b/package.json index 3cffcad41a2..c8513cf2066 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "@plotly/d3-sankey": "^0.5.0", "alpha-shape": "^1.0.0", "array-range": "^1.0.1", + "base64-arraybuffer": "^0.1.5", "canvas-fit": "^1.5.0", "color-normalize": "^1.0.3", "color-rgba": "^2.0.0", diff --git a/src/lib/coerce.js b/src/lib/coerce.js index a5d69fe22e2..138867f44cf 100644 --- a/src/lib/coerce.js +++ b/src/lib/coerce.js @@ -19,7 +19,11 @@ var nestedProperty = require('./nested_property'); var counterRegex = require('./regex').counter; var DESELECTDIM = require('../constants/interactions').DESELECTDIM; var wrap180 = require('./angles').wrap180; -var isArrayOrTypedArray = require('./is_array').isArrayOrTypedArray; +var isArray = require('./is_array'); +var isArrayOrTypedArray = isArray.isArrayOrTypedArray; +var isTypedArray = isArray.isTypedArray; +var isPrimitiveTypedArrayRepr = isArray.isPrimitiveTypedArrayRepr; +var b64 = require('base64-arraybuffer'); exports.valObjectMeta = { data_array: { @@ -33,8 +37,18 @@ exports.valObjectMeta = { 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); + if(isArrayOrTypedArray(v)) { + propOut.set(v); + } else if(isPrimitiveTypedArrayRepr(v)) { + var coercedV = primitiveTypedArrayReprToTypedArray(v); + if(coercedV === undefined && dflt !== undefined) { + propOut.set(dflt); + } else { + propOut.set(coercedV); + } + } else if(dflt !== undefined) { + propOut.set(dflt); + } } }, enumerated: { @@ -392,6 +406,10 @@ exports.coerce = function(containerIn, containerOut, attributes, attribute, dflt if(opts.arrayOk && isArrayOrTypedArray(v)) { propOut.set(v); return v; + } else if(opts.arrayOk && isPrimitiveTypedArrayRepr(v)) { + var coercedV = primitiveTypedArrayReprToTypedArray(v); + propOut.set(coercedV); + return coercedV; } var coerceFunction = exports.valObjectMeta[opts.valType].coerceFunction; @@ -521,3 +539,48 @@ function validate(value, opts) { return out !== failed; } exports.validate = validate; + +var dtypeStringToTypedarrayType = { + int8: Int8Array, + int16: Int16Array, + int32: Int32Array, + uint8: Uint8Array, + uint16: Uint16Array, + uint32: Uint32Array, + float32: Float32Array, + float64: Float64Array +}; + +/** + * Convert a primitive TypedArray representation object into a TypedArray + * @param {object} v: Object with `dtype` and `data` properties that + * represens a TypedArray. + * + * @returns {TypedArray} + */ +function primitiveTypedArrayReprToTypedArray(v) { + // v has dtype and data properties + + // Get TypedArray constructor type + var TypeArrayType = dtypeStringToTypedarrayType[v.dtype]; + + // Process data + var coercedV; + var data = v.data; + if(data instanceof ArrayBuffer) { + // data is an ArrayBuffer + coercedV = new TypeArrayType(data); + } else if(data.constructor === DataView) { + // data has a buffer property, where the buffer is an ArrayBuffer + coercedV = new TypeArrayType(data.buffer); + } else if(Array.isArray(data)) { + // data is a primitive array + coercedV = new TypeArrayType(data); + } else if(typeof data === 'string' || + data instanceof String) { + // data is a base64 encoded string + var buffer = b64.decode(data); + coercedV = new TypeArrayType(buffer); + } + return coercedV; +} diff --git a/src/lib/is_array.js b/src/lib/is_array.js index b8c5e1ae47c..3a300c6273b 100644 --- a/src/lib/is_array.js +++ b/src/lib/is_array.js @@ -38,8 +38,15 @@ function isArray1D(a) { return !isArrayOrTypedArray(a[0]); } +function isPrimitiveTypedArrayRepr(a) { + return (a !== undefined && a !== null && + typeof a === 'object' && + a.hasOwnProperty('dtype') && a.hasOwnProperty('data')); +} + module.exports = { isTypedArray: isTypedArray, isArrayOrTypedArray: isArrayOrTypedArray, - isArray1D: isArray1D + isArray1D: isArray1D, + isPrimitiveTypedArrayRepr: isPrimitiveTypedArrayRepr }; diff --git a/test/image/mocks/typed_array_repr_scatter.json b/test/image/mocks/typed_array_repr_scatter.json new file mode 100644 index 00000000000..cb275b09642 --- /dev/null +++ b/test/image/mocks/typed_array_repr_scatter.json @@ -0,0 +1,13 @@ +{ + "data": [{ + "type": "scatter", + "x": {"dtype": "float64", "data": [3, 2, 1]}, + "y": {"dtype": "float32", "data": "AABAQAAAAEAAAIA/"}, + "marker": { + "color": { + "dtype": "uint16", + "data": "AwACAAEA", + }, + } + }] +} diff --git a/test/jasmine/tests/primitive_typed_array_repr_test.js b/test/jasmine/tests/primitive_typed_array_repr_test.js new file mode 100644 index 00000000000..d56356100b9 --- /dev/null +++ b/test/jasmine/tests/primitive_typed_array_repr_test.js @@ -0,0 +1,148 @@ +var Lib = require('@src/lib'); +var supplyDefaults = require('../assets/supply_defaults'); +var isTypedArray = require('../../../src/lib/is_array').isTypedArray; +var b64 = require('base64-arraybuffer'); +var mock1 = require('@mocks/typed_array_repr_scatter.json'); + +var typedArraySpecs = [ + ['int8', new Int8Array([-128, -34, 1, 127])], + ['uint8', new Uint8Array([0, 1, 127, 255])], + ['int16', new Int16Array([-32768, -123, 345, 32767])], + ['uint16', new Uint16Array([0, 345, 32767, 65535])], + ['int32', new Int32Array([-2147483648, -123, 345, 32767, 2147483647])], + ['uint32', new Uint32Array([0, 345, 32767, 4294967295])], + ['float32', new Float32Array([1.2E-38, -2345.25, 2.7182818, 3.1415926, 2, 3.4E38])], + ['float64', new Float64Array([5.0E-324, 2.718281828459045, 3.141592653589793, 1.8E308])] +]; + +describe('Test TypedArray representations', function() { + 'use strict'; + + describe('ArrayBuffer', function() { + it('should accept representation as ArrayBuffer', function() { + typedArraySpecs.forEach(function(arraySpec) { + // Build data and confirm its type + var data = arraySpec[1].buffer; + expect(data.constructor).toEqual(ArrayBuffer); + + var repr = { + dtype: arraySpec[0], + data: data + }; + var gd = { + data: [{ + y: repr + }], + }; + + supplyDefaults(gd); + + expect(gd.data[0].y).toEqual(repr); + expect(gd._fullData[0].y).toEqual(arraySpec[1]); + }); + }); + }); + + describe('Array', function() { + it('should accept representation as Array', function() { + typedArraySpecs.forEach(function(arraySpec) { + // Build data and confirm its type + var data = Array.prototype.slice.call(arraySpec[1]); + expect(Array.isArray(data)).toEqual(true); + + var repr = { + dtype: arraySpec[0], + data: data + }; + var gd = { + data: [{ + y: repr + }], + }; + + supplyDefaults(gd); + + expect(gd.data[0].y).toEqual(repr); + expect(gd._fullData[0].y).toEqual(arraySpec[1]); + }); + }); + }); + + describe('DataView', function() { + it('should accept representation as DataView', function() { + typedArraySpecs.forEach(function(arraySpec) { + // Build data and confirm its type + var data = new DataView(arraySpec[1].buffer); + expect(data.constructor).toEqual(DataView); + + var repr = { + dtype: arraySpec[0], + data: data + }; + var gd = { + data: [{ + y: repr + }], + }; + + supplyDefaults(gd); + + expect(gd.data[0].y).toEqual(repr); + expect(gd._fullData[0].y).toEqual(arraySpec[1]); + }); + }); + }); + + describe('base64', function() { + it('should accept representation as base 64 string', function() { + typedArraySpecs.forEach(function(arraySpec) { + // Build data and confirm its type + var data = b64.encode(arraySpec[1].buffer); + expect(typeof data).toEqual('string'); + + var repr = { + dtype: arraySpec[0], + data: data + }; + var gd = { + data: [{ + y: repr + }], + }; + + supplyDefaults(gd); + expect(gd.data[0].y).toEqual(repr); + expect(gd._fullData[0].y).toEqual(arraySpec[1]); + }); + }); + }); + + describe('mock', function() { + it('should accept representation as base 64 and Array in Mock', function() { + + var gd = Lib.extendDeep({}, mock1); + supplyDefaults(gd); + + // Check x + // data_array property + expect(gd.data[0].x).toEqual({ + 'dtype': 'float64', + 'data': [3, 2, 1]}); + expect(gd._fullData[0].x).toEqual(new Float64Array([3, 2, 1])); + + // Check y + // data_array property + expect(gd.data[0].y).toEqual({ + 'dtype': 'float32', + 'data': 'AABAQAAAAEAAAIA/'}); + expect(gd._fullData[0].y).toEqual(new Float32Array([3, 2, 1])); + + // Check marker.color + // This is an arrayOk property not a data_array property + expect(gd.data[0].marker.color).toEqual({ + 'dtype': 'uint16', + 'data': 'AwACAAEA'}); + expect(gd._fullData[0].marker.color).toEqual(new Uint16Array([3, 2, 1])); + }); + }); +}); From 5030d3ac4629188133a1fbf432da5141e9f0d836 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Fri, 17 Aug 2018 08:29:41 -0400 Subject: [PATCH 02/10] Addressed minor review comments - Use `Lib.isPlainObject` - Renamed `data` -> `value` - Added `Uint8ClampedArray` - Committed updated package-lock.json No changes yet to the logical structure of where conversion happens --- package-lock.json | 3 +- src/lib/coerce.js | 30 +++++++------- src/lib/is_array.js | 6 +-- .../image/mocks/typed_array_repr_scatter.json | 6 +-- .../tests/primitive_typed_array_repr_test.js | 40 +++++++++---------- 5 files changed, 42 insertions(+), 43 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6872d852528..0a5e36e487f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -923,8 +923,7 @@ "base64-arraybuffer": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", - "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", - "dev": true + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" }, "base64-js": { "version": "1.2.3", diff --git a/src/lib/coerce.js b/src/lib/coerce.js index 138867f44cf..df10220684e 100644 --- a/src/lib/coerce.js +++ b/src/lib/coerce.js @@ -21,7 +21,6 @@ var DESELECTDIM = require('../constants/interactions').DESELECTDIM; var wrap180 = require('./angles').wrap180; var isArray = require('./is_array'); var isArrayOrTypedArray = isArray.isArrayOrTypedArray; -var isTypedArray = isArray.isTypedArray; var isPrimitiveTypedArrayRepr = isArray.isPrimitiveTypedArrayRepr; var b64 = require('base64-arraybuffer'); @@ -545,6 +544,7 @@ var dtypeStringToTypedarrayType = { int16: Int16Array, int32: Int32Array, uint8: Uint8Array, + uint8_clamped: Uint8ClampedArray, uint16: Uint16Array, uint32: Uint32Array, float32: Float32Array, @@ -566,20 +566,20 @@ function primitiveTypedArrayReprToTypedArray(v) { // Process data var coercedV; - var data = v.data; - if(data instanceof ArrayBuffer) { - // data is an ArrayBuffer - coercedV = new TypeArrayType(data); - } else if(data.constructor === DataView) { - // data has a buffer property, where the buffer is an ArrayBuffer - coercedV = new TypeArrayType(data.buffer); - } else if(Array.isArray(data)) { - // data is a primitive array - coercedV = new TypeArrayType(data); - } else if(typeof data === 'string' || - data instanceof String) { - // data is a base64 encoded string - var buffer = b64.decode(data); + var value = v.value; + if(value instanceof ArrayBuffer) { + // value is an ArrayBuffer + coercedV = new TypeArrayType(value); + } else if(value.constructor === DataView) { + // value has a buffer property, where the buffer is an ArrayBuffer + coercedV = new TypeArrayType(value.buffer); + } else if(Array.isArray(value)) { + // value is a primitive array + coercedV = new TypeArrayType(value); + } else if(typeof value === 'string' || + value instanceof String) { + // value is a base64 encoded string + var buffer = b64.decode(value); coercedV = new TypeArrayType(buffer); } return coercedV; diff --git a/src/lib/is_array.js b/src/lib/is_array.js index 3a300c6273b..e2f1588309a 100644 --- a/src/lib/is_array.js +++ b/src/lib/is_array.js @@ -5,6 +5,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ +var Lib = require('../lib'); 'use strict'; @@ -39,9 +40,8 @@ function isArray1D(a) { } function isPrimitiveTypedArrayRepr(a) { - return (a !== undefined && a !== null && - typeof a === 'object' && - a.hasOwnProperty('dtype') && a.hasOwnProperty('data')); + return (Lib.isPlainObject(a) && + a.hasOwnProperty('dtype') && a.hasOwnProperty('value')); } module.exports = { diff --git a/test/image/mocks/typed_array_repr_scatter.json b/test/image/mocks/typed_array_repr_scatter.json index cb275b09642..ff11abb37e7 100644 --- a/test/image/mocks/typed_array_repr_scatter.json +++ b/test/image/mocks/typed_array_repr_scatter.json @@ -1,12 +1,12 @@ { "data": [{ "type": "scatter", - "x": {"dtype": "float64", "data": [3, 2, 1]}, - "y": {"dtype": "float32", "data": "AABAQAAAAEAAAIA/"}, + "x": {"dtype": "float64", "value": [3, 2, 1]}, + "y": {"dtype": "float32", "value": "AABAQAAAAEAAAIA/"}, "marker": { "color": { "dtype": "uint16", - "data": "AwACAAEA", + "value": "AwACAAEA", }, } }] diff --git a/test/jasmine/tests/primitive_typed_array_repr_test.js b/test/jasmine/tests/primitive_typed_array_repr_test.js index d56356100b9..65b00f3a778 100644 --- a/test/jasmine/tests/primitive_typed_array_repr_test.js +++ b/test/jasmine/tests/primitive_typed_array_repr_test.js @@ -1,12 +1,12 @@ var Lib = require('@src/lib'); var supplyDefaults = require('../assets/supply_defaults'); -var isTypedArray = require('../../../src/lib/is_array').isTypedArray; var b64 = require('base64-arraybuffer'); var mock1 = require('@mocks/typed_array_repr_scatter.json'); var typedArraySpecs = [ ['int8', new Int8Array([-128, -34, 1, 127])], ['uint8', new Uint8Array([0, 1, 127, 255])], + ['uint8_clamped', new Uint8ClampedArray([0, 1, 127, 255])], ['int16', new Int16Array([-32768, -123, 345, 32767])], ['uint16', new Uint16Array([0, 345, 32767, 65535])], ['int32', new Int32Array([-2147483648, -123, 345, 32767, 2147483647])], @@ -21,13 +21,13 @@ describe('Test TypedArray representations', function() { describe('ArrayBuffer', function() { it('should accept representation as ArrayBuffer', function() { typedArraySpecs.forEach(function(arraySpec) { - // Build data and confirm its type - var data = arraySpec[1].buffer; - expect(data.constructor).toEqual(ArrayBuffer); + // Build value and confirm its type + var value = arraySpec[1].buffer; + expect(value.constructor).toEqual(ArrayBuffer); var repr = { dtype: arraySpec[0], - data: data + value: value }; var gd = { data: [{ @@ -46,13 +46,13 @@ describe('Test TypedArray representations', function() { describe('Array', function() { it('should accept representation as Array', function() { typedArraySpecs.forEach(function(arraySpec) { - // Build data and confirm its type - var data = Array.prototype.slice.call(arraySpec[1]); - expect(Array.isArray(data)).toEqual(true); + // Build value and confirm its type + var value = Array.prototype.slice.call(arraySpec[1]); + expect(Array.isArray(value)).toEqual(true); var repr = { dtype: arraySpec[0], - data: data + value: value }; var gd = { data: [{ @@ -71,13 +71,13 @@ describe('Test TypedArray representations', function() { describe('DataView', function() { it('should accept representation as DataView', function() { typedArraySpecs.forEach(function(arraySpec) { - // Build data and confirm its type - var data = new DataView(arraySpec[1].buffer); - expect(data.constructor).toEqual(DataView); + // Build value and confirm its type + var value = new DataView(arraySpec[1].buffer); + expect(value.constructor).toEqual(DataView); var repr = { dtype: arraySpec[0], - data: data + value: value }; var gd = { data: [{ @@ -96,13 +96,13 @@ describe('Test TypedArray representations', function() { describe('base64', function() { it('should accept representation as base 64 string', function() { typedArraySpecs.forEach(function(arraySpec) { - // Build data and confirm its type - var data = b64.encode(arraySpec[1].buffer); - expect(typeof data).toEqual('string'); + // Build value and confirm its type + var value = b64.encode(arraySpec[1].buffer); + expect(typeof value).toEqual('string'); var repr = { dtype: arraySpec[0], - data: data + value: value }; var gd = { data: [{ @@ -127,21 +127,21 @@ describe('Test TypedArray representations', function() { // data_array property expect(gd.data[0].x).toEqual({ 'dtype': 'float64', - 'data': [3, 2, 1]}); + 'value': [3, 2, 1]}); expect(gd._fullData[0].x).toEqual(new Float64Array([3, 2, 1])); // Check y // data_array property expect(gd.data[0].y).toEqual({ 'dtype': 'float32', - 'data': 'AABAQAAAAEAAAIA/'}); + 'value': 'AABAQAAAAEAAAIA/'}); expect(gd._fullData[0].y).toEqual(new Float32Array([3, 2, 1])); // Check marker.color // This is an arrayOk property not a data_array property expect(gd.data[0].marker.color).toEqual({ 'dtype': 'uint16', - 'data': 'AwACAAEA'}); + 'value': 'AwACAAEA'}); expect(gd._fullData[0].marker.color).toEqual(new Uint16Array([3, 2, 1])); }); }); From dfd44d5973c3ee6e9ac00d142d5fbdf2c3df28ce Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Fri, 17 Aug 2018 08:38:10 -0400 Subject: [PATCH 03/10] JSLint --- src/lib/is_array.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/is_array.js b/src/lib/is_array.js index e2f1588309a..08687acfe20 100644 --- a/src/lib/is_array.js +++ b/src/lib/is_array.js @@ -5,10 +5,11 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ -var Lib = require('../lib'); 'use strict'; +var Lib = require('../lib'); + // IE9 fallbacks var ab = (typeof ArrayBuffer === 'undefined' || !ArrayBuffer.isView) ? From ec217140ab929caa6eac4537a9d1c971081f0588 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Fri, 17 Aug 2018 10:10:22 -0400 Subject: [PATCH 04/10] Move TypedArray convertion logic to a new Plotly.import method, Revert changes to coerce.js --- src/lib/coerce.js | 69 +---------------- src/lib/index.js | 1 + src/plot_api/index.js | 1 + src/plot_api/plot_api.js | 77 +++++++++++++++++++ .../tests/primitive_typed_array_repr_test.js | 47 +++++------ 5 files changed, 103 insertions(+), 92 deletions(-) diff --git a/src/lib/coerce.js b/src/lib/coerce.js index df10220684e..a5d69fe22e2 100644 --- a/src/lib/coerce.js +++ b/src/lib/coerce.js @@ -19,10 +19,7 @@ var nestedProperty = require('./nested_property'); var counterRegex = require('./regex').counter; var DESELECTDIM = require('../constants/interactions').DESELECTDIM; var wrap180 = require('./angles').wrap180; -var isArray = require('./is_array'); -var isArrayOrTypedArray = isArray.isArrayOrTypedArray; -var isPrimitiveTypedArrayRepr = isArray.isPrimitiveTypedArrayRepr; -var b64 = require('base64-arraybuffer'); +var isArrayOrTypedArray = require('./is_array').isArrayOrTypedArray; exports.valObjectMeta = { data_array: { @@ -36,18 +33,8 @@ exports.valObjectMeta = { otherOpts: ['dflt'], coerceFunction: function(v, propOut, dflt) { // TODO maybe `v: {type: 'float32', vals: [/* ... */]}` also - if(isArrayOrTypedArray(v)) { - propOut.set(v); - } else if(isPrimitiveTypedArrayRepr(v)) { - var coercedV = primitiveTypedArrayReprToTypedArray(v); - if(coercedV === undefined && dflt !== undefined) { - propOut.set(dflt); - } else { - propOut.set(coercedV); - } - } else if(dflt !== undefined) { - propOut.set(dflt); - } + if(isArrayOrTypedArray(v)) propOut.set(v); + else if(dflt !== undefined) propOut.set(dflt); } }, enumerated: { @@ -405,10 +392,6 @@ exports.coerce = function(containerIn, containerOut, attributes, attribute, dflt if(opts.arrayOk && isArrayOrTypedArray(v)) { propOut.set(v); return v; - } else if(opts.arrayOk && isPrimitiveTypedArrayRepr(v)) { - var coercedV = primitiveTypedArrayReprToTypedArray(v); - propOut.set(coercedV); - return coercedV; } var coerceFunction = exports.valObjectMeta[opts.valType].coerceFunction; @@ -538,49 +521,3 @@ function validate(value, opts) { return out !== failed; } exports.validate = validate; - -var dtypeStringToTypedarrayType = { - int8: Int8Array, - int16: Int16Array, - int32: Int32Array, - uint8: Uint8Array, - uint8_clamped: Uint8ClampedArray, - uint16: Uint16Array, - uint32: Uint32Array, - float32: Float32Array, - float64: Float64Array -}; - -/** - * Convert a primitive TypedArray representation object into a TypedArray - * @param {object} v: Object with `dtype` and `data` properties that - * represens a TypedArray. - * - * @returns {TypedArray} - */ -function primitiveTypedArrayReprToTypedArray(v) { - // v has dtype and data properties - - // Get TypedArray constructor type - var TypeArrayType = dtypeStringToTypedarrayType[v.dtype]; - - // Process data - var coercedV; - var value = v.value; - if(value instanceof ArrayBuffer) { - // value is an ArrayBuffer - coercedV = new TypeArrayType(value); - } else if(value.constructor === DataView) { - // value has a buffer property, where the buffer is an ArrayBuffer - coercedV = new TypeArrayType(value.buffer); - } else if(Array.isArray(value)) { - // value is a primitive array - coercedV = new TypeArrayType(value); - } else if(typeof value === 'string' || - value instanceof String) { - // value is a base64 encoded string - var buffer = b64.decode(value); - coercedV = new TypeArrayType(buffer); - } - return coercedV; -} diff --git a/src/lib/index.js b/src/lib/index.js index abe8e1a8fa8..6bcbc7fff86 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -31,6 +31,7 @@ var isArrayModule = require('./is_array'); lib.isTypedArray = isArrayModule.isTypedArray; lib.isArrayOrTypedArray = isArrayModule.isArrayOrTypedArray; lib.isArray1D = isArrayModule.isArray1D; +lib.isPrimitiveTypedArrayRepr = isArrayModule.isPrimitiveTypedArrayRepr; var coerceModule = require('./coerce'); lib.valObjectMeta = coerceModule.valObjectMeta; diff --git a/src/plot_api/index.js b/src/plot_api/index.js index ac81c327b05..8c8acfcf95c 100644 --- a/src/plot_api/index.js +++ b/src/plot_api/index.js @@ -16,6 +16,7 @@ exports.restyle = main.restyle; exports.relayout = main.relayout; exports.redraw = main.redraw; exports.update = main.update; +exports.import = main.import; exports.react = main.react; exports.extendTraces = main.extendTraces; exports.prependTraces = main.prependTraces; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index d81255aa7f2..379fbe0af25 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -13,6 +13,7 @@ var d3 = require('d3'); var isNumeric = require('fast-isnumeric'); var hasHover = require('has-hover'); +var b64 = require('base64-arraybuffer'); var Lib = require('../lib'); var Events = require('../lib/events'); @@ -2156,6 +2157,82 @@ exports.update = function update(gd, traceUpdate, layoutUpdate, _traces) { }); }; +/** + * Convert a primitive TypedArray representation object into a TypedArray + * @param {object} v: Object with `dtype` and `data` properties that + * represens a TypedArray. + * + * @returns {TypedArray} + */ +var dtypeStringToTypedarrayType = { + int8: Int8Array, + int16: Int16Array, + int32: Int32Array, + uint8: Uint8Array, + uint8_clamped: Uint8ClampedArray, + uint16: Uint16Array, + uint32: Uint32Array, + float32: Float32Array, + float64: Float64Array +}; + +function primitiveTypedArrayReprToTypedArray(v) { + // v has dtype and data properties + + // Get TypedArray constructor type + var TypeArrayType = dtypeStringToTypedarrayType[v.dtype]; + + // Process data + var coercedV; + var value = v.value; + if(value instanceof ArrayBuffer) { + // value is an ArrayBuffer + coercedV = new TypeArrayType(value); + } else if(value.constructor === DataView) { + // value has a buffer property, where the buffer is an ArrayBuffer + coercedV = new TypeArrayType(value.buffer); + } else if(Array.isArray(value)) { + // value is a primitive array + coercedV = new TypeArrayType(value); + } else if(typeof value === 'string' || + value instanceof String) { + // value is a base64 encoded string + var buffer = b64.decode(value); + coercedV = new TypeArrayType(buffer); + } + return coercedV; +} + +function performImport(v) { + if(Lib.isPrimitiveTypedArrayRepr(v)) { + return primitiveTypedArrayReprToTypedArray(v); + } else if(Lib.isTypedArray(v)) { + return v; + } else if(Array.isArray(v)) { + return v.map(performImport); + } else if(Lib.isPlainObject(v)) { + var result = {}; + for(var k in v) { + if(v.hasOwnProperty(k)) { + result[k] = performImport(v[k]); + } + } + return result; + } else { + return v; + } +} + +/** + * Plotly.import: + * Import an object or array into... TODO + * @param v + * @returns {object} + */ +exports.import = function(v) { + return performImport(v); +}; + /** * Plotly.react: * A plot/update method that takes the full plot state (same API as plot/newPlot) diff --git a/test/jasmine/tests/primitive_typed_array_repr_test.js b/test/jasmine/tests/primitive_typed_array_repr_test.js index 65b00f3a778..65d815c000d 100644 --- a/test/jasmine/tests/primitive_typed_array_repr_test.js +++ b/test/jasmine/tests/primitive_typed_array_repr_test.js @@ -1,3 +1,4 @@ +var Plotly = require('@lib/index'); var Lib = require('@src/lib'); var supplyDefaults = require('../assets/supply_defaults'); var b64 = require('base64-arraybuffer'); @@ -29,16 +30,15 @@ describe('Test TypedArray representations', function() { dtype: arraySpec[0], value: value }; - var gd = { + var raw = { data: [{ y: repr }], }; - supplyDefaults(gd); + var gd = Plotly.import(raw); - expect(gd.data[0].y).toEqual(repr); - expect(gd._fullData[0].y).toEqual(arraySpec[1]); + expect(gd.data[0].y).toEqual(arraySpec[1]); }); }); }); @@ -54,16 +54,15 @@ describe('Test TypedArray representations', function() { dtype: arraySpec[0], value: value }; - var gd = { + var raw = { data: [{ y: repr }], }; - supplyDefaults(gd); + var gd = Plotly.import(raw); - expect(gd.data[0].y).toEqual(repr); - expect(gd._fullData[0].y).toEqual(arraySpec[1]); + expect(gd.data[0].y).toEqual(arraySpec[1]); }); }); }); @@ -79,16 +78,15 @@ describe('Test TypedArray representations', function() { dtype: arraySpec[0], value: value }; - var gd = { + var raw = { data: [{ y: repr }], }; - supplyDefaults(gd); + var gd = Plotly.import(raw); - expect(gd.data[0].y).toEqual(repr); - expect(gd._fullData[0].y).toEqual(arraySpec[1]); + expect(gd.data[0].y).toEqual(arraySpec[1]); }); }); }); @@ -104,45 +102,42 @@ describe('Test TypedArray representations', function() { dtype: arraySpec[0], value: value }; - var gd = { + var raw = { data: [{ y: repr }], }; - supplyDefaults(gd); - expect(gd.data[0].y).toEqual(repr); - expect(gd._fullData[0].y).toEqual(arraySpec[1]); + var gd = Plotly.import(raw); + expect(gd.data[0].y).toEqual(arraySpec[1]); }); }); }); describe('mock', function() { - it('should accept representation as base 64 and Array in Mock', function() { - - var gd = Lib.extendDeep({}, mock1); - supplyDefaults(gd); + it('should import representation as base 64 and Array in Mock', function() { + var gd = Plotly.import(mock1); // Check x // data_array property - expect(gd.data[0].x).toEqual({ + expect(mock1.data[0].x).toEqual({ 'dtype': 'float64', 'value': [3, 2, 1]}); - expect(gd._fullData[0].x).toEqual(new Float64Array([3, 2, 1])); + expect(gd.data[0].x).toEqual(new Float64Array([3, 2, 1])); // Check y // data_array property - expect(gd.data[0].y).toEqual({ + expect(mock1.data[0].y).toEqual({ 'dtype': 'float32', 'value': 'AABAQAAAAEAAAIA/'}); - expect(gd._fullData[0].y).toEqual(new Float32Array([3, 2, 1])); + expect(gd.data[0].y).toEqual(new Float32Array([3, 2, 1])); // Check marker.color // This is an arrayOk property not a data_array property - expect(gd.data[0].marker.color).toEqual({ + expect(mock1.data[0].marker.color).toEqual({ 'dtype': 'uint16', 'value': 'AwACAAEA'}); - expect(gd._fullData[0].marker.color).toEqual(new Uint16Array([3, 2, 1])); + expect(gd.data[0].marker.color).toEqual(new Uint16Array([3, 2, 1])); }); }); }); From 8de7b5a2c6d5885f1877c1411d1d77055983edce Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 10 Sep 2018 18:40:24 -0400 Subject: [PATCH 05/10] Add TypedArray types to .eslintrc --- .eslintrc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.eslintrc b/.eslintrc index 185abda5ef4..b8e364b0830 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 From 5107c97454bf3a0d1e17091db3ed2b47a02347a5 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 10 Sep 2018 18:50:41 -0400 Subject: [PATCH 06/10] Rename Plotly.import to Plotly.decode --- src/plot_api/index.js | 2 +- src/plot_api/plot_api.js | 12 ++++++------ .../tests/primitive_typed_array_repr_test.js | 14 ++++++-------- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/plot_api/index.js b/src/plot_api/index.js index 8c8acfcf95c..ef7f5d4e7fa 100644 --- a/src/plot_api/index.js +++ b/src/plot_api/index.js @@ -16,7 +16,7 @@ exports.restyle = main.restyle; exports.relayout = main.relayout; exports.redraw = main.redraw; exports.update = main.update; -exports.import = main.import; +exports.decode = main.decode; exports.react = main.react; exports.extendTraces = main.extendTraces; exports.prependTraces = main.prependTraces; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 379fbe0af25..9e5591ec303 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2203,18 +2203,18 @@ function primitiveTypedArrayReprToTypedArray(v) { return coercedV; } -function performImport(v) { +function performDecode(v) { if(Lib.isPrimitiveTypedArrayRepr(v)) { return primitiveTypedArrayReprToTypedArray(v); } else if(Lib.isTypedArray(v)) { return v; } else if(Array.isArray(v)) { - return v.map(performImport); + return v.map(performDecode); } else if(Lib.isPlainObject(v)) { var result = {}; for(var k in v) { if(v.hasOwnProperty(k)) { - result[k] = performImport(v[k]); + result[k] = performDecode(v[k]); } } return result; @@ -2224,13 +2224,13 @@ function performImport(v) { } /** - * Plotly.import: + * Plotly.decode: * Import an object or array into... TODO * @param v * @returns {object} */ -exports.import = function(v) { - return performImport(v); +exports.decode = function(v) { + return performDecode(v); }; /** diff --git a/test/jasmine/tests/primitive_typed_array_repr_test.js b/test/jasmine/tests/primitive_typed_array_repr_test.js index 65d815c000d..d9762720283 100644 --- a/test/jasmine/tests/primitive_typed_array_repr_test.js +++ b/test/jasmine/tests/primitive_typed_array_repr_test.js @@ -1,6 +1,4 @@ var Plotly = require('@lib/index'); -var Lib = require('@src/lib'); -var supplyDefaults = require('../assets/supply_defaults'); var b64 = require('base64-arraybuffer'); var mock1 = require('@mocks/typed_array_repr_scatter.json'); @@ -36,7 +34,7 @@ describe('Test TypedArray representations', function() { }], }; - var gd = Plotly.import(raw); + var gd = Plotly.decode(raw); expect(gd.data[0].y).toEqual(arraySpec[1]); }); @@ -60,7 +58,7 @@ describe('Test TypedArray representations', function() { }], }; - var gd = Plotly.import(raw); + var gd = Plotly.decode(raw); expect(gd.data[0].y).toEqual(arraySpec[1]); }); @@ -84,7 +82,7 @@ describe('Test TypedArray representations', function() { }], }; - var gd = Plotly.import(raw); + var gd = Plotly.decode(raw); expect(gd.data[0].y).toEqual(arraySpec[1]); }); @@ -108,16 +106,16 @@ describe('Test TypedArray representations', function() { }], }; - var gd = Plotly.import(raw); + var gd = Plotly.decode(raw); expect(gd.data[0].y).toEqual(arraySpec[1]); }); }); }); describe('mock', function() { - it('should import representation as base 64 and Array in Mock', function() { + it('should decode representation as base 64 and Array in Mock', function() { - var gd = Plotly.import(mock1); + var gd = Plotly.decode(mock1); // Check x // data_array property expect(mock1.data[0].x).toEqual({ From 5cdb828bdab2dae63a9b28fc7d0d2755e1e9f0c0 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 10 Sep 2018 19:29:13 -0400 Subject: [PATCH 07/10] Add fallback for browsers that don't support TypedArrays In this case the decoded value is undefined, but an error won't be thrown. --- src/lib/index.js | 2 +- src/lib/is_array.js | 4 +- src/plot_api/plot_api.js | 106 ++++++++++++++++++++++++--------------- 3 files changed, 69 insertions(+), 43 deletions(-) diff --git a/src/lib/index.js b/src/lib/index.js index 6bcbc7fff86..240a09a216a 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -31,7 +31,7 @@ var isArrayModule = require('./is_array'); lib.isTypedArray = isArrayModule.isTypedArray; lib.isArrayOrTypedArray = isArrayModule.isArrayOrTypedArray; lib.isArray1D = isArrayModule.isArray1D; -lib.isPrimitiveTypedArrayRepr = isArrayModule.isPrimitiveTypedArrayRepr; +lib.isTypedArrayEncoding = isArrayModule.isTypedArrayEncoding; var coerceModule = require('./coerce'); lib.valObjectMeta = coerceModule.valObjectMeta; diff --git a/src/lib/is_array.js b/src/lib/is_array.js index 08687acfe20..8f0b0603e28 100644 --- a/src/lib/is_array.js +++ b/src/lib/is_array.js @@ -40,7 +40,7 @@ function isArray1D(a) { return !isArrayOrTypedArray(a[0]); } -function isPrimitiveTypedArrayRepr(a) { +function isTypedArrayEncoding(a) { return (Lib.isPlainObject(a) && a.hasOwnProperty('dtype') && a.hasOwnProperty('value')); } @@ -49,5 +49,5 @@ module.exports = { isTypedArray: isTypedArray, isArrayOrTypedArray: isArrayOrTypedArray, isArray1D: isArray1D, - isPrimitiveTypedArrayRepr: isPrimitiveTypedArrayRepr + isTypedArrayEncoding: isTypedArrayEncoding }; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 9e5591ec303..2f6b9c95cc4 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2157,57 +2157,82 @@ exports.update = function update(gd, traceUpdate, layoutUpdate, _traces) { }); }; + +/** + * Create a mapping from dtype values to TypedArray types. A mapping is only + * added for TypedArray types supported by the current browser + */ +var dtypeStringToTypedarrayType = {}; +if(typeof Int8Array !== 'undefined') { + dtypeStringToTypedarrayType.int8 = Int8Array; +} +if(typeof Uint8Array !== 'undefined') { + dtypeStringToTypedarrayType.uint8 = Uint8Array; +} +if(typeof Uint8ClampedArray !== 'undefined') { + dtypeStringToTypedarrayType.uint8_clamped = Uint8ClampedArray; +} +if(typeof Int16Array !== 'undefined') { + dtypeStringToTypedarrayType.int16 = Int16Array; +} +if(typeof Uint16Array !== 'undefined') { + dtypeStringToTypedarrayType.uint16 = Uint16Array; +} +if(typeof Int32Array !== 'undefined') { + dtypeStringToTypedarrayType.int32 = Int32Array; +} +if(typeof Uint32Array !== 'undefined') { + dtypeStringToTypedarrayType.uint32 = Uint32Array; +} +if(typeof Float32Array !== 'undefined') { + dtypeStringToTypedarrayType.float32 = Float32Array; +} +if(typeof Float64Array !== 'undefined') { + dtypeStringToTypedarrayType.float64 = Float64Array; +} + /** - * Convert a primitive TypedArray representation object into a TypedArray - * @param {object} v: Object with `dtype` and `data` properties that - * represens a TypedArray. + * Convert a TypedArray encoding object into a TypedArray + * @param {object} v: Object with `dtype` and `value` properties that + * represents a TypedArray. * * @returns {TypedArray} */ -var dtypeStringToTypedarrayType = { - int8: Int8Array, - int16: Int16Array, - int32: Int32Array, - uint8: Uint8Array, - uint8_clamped: Uint8ClampedArray, - uint16: Uint16Array, - uint32: Uint32Array, - float32: Float32Array, - float64: Float64Array -}; - -function primitiveTypedArrayReprToTypedArray(v) { - // v has dtype and data properties - - // Get TypedArray constructor type - var TypeArrayType = dtypeStringToTypedarrayType[v.dtype]; +function decodeTypedArray(v) { - // Process data var coercedV; var value = v.value; - if(value instanceof ArrayBuffer) { - // value is an ArrayBuffer - coercedV = new TypeArrayType(value); - } else if(value.constructor === DataView) { - // value has a buffer property, where the buffer is an ArrayBuffer - coercedV = new TypeArrayType(value.buffer); - } else if(Array.isArray(value)) { - // value is a primitive array - coercedV = new TypeArrayType(value); - } else if(typeof value === 'string' || - value instanceof String) { - // value is a base64 encoded string - var buffer = b64.decode(value); - coercedV = new TypeArrayType(buffer); + var TypeArrayType = dtypeStringToTypedarrayType[v.dtype]; + + if(TypeArrayType) { + if(value instanceof ArrayBuffer) { + // value is an ArrayBuffer + coercedV = new TypeArrayType(value); + } else if(value.constructor === DataView) { + // value has a buffer property, where the buffer is an ArrayBuffer + coercedV = new TypeArrayType(value.buffer); + } else if(Array.isArray(value)) { + // value is a primitive array + coercedV = new TypeArrayType(value); + } else if(typeof value === 'string' || + value instanceof String) { + // value is a base64 encoded string + var buffer = b64.decode(value); + coercedV = new TypeArrayType(buffer); + } + } else { + // Either v.dtype was an invalid array type, or this browser doesn't + // support this typed array type. } return coercedV; } +/** + * Recursive helper function to perform decoding + */ function performDecode(v) { - if(Lib.isPrimitiveTypedArrayRepr(v)) { - return primitiveTypedArrayReprToTypedArray(v); - } else if(Lib.isTypedArray(v)) { - return v; + if(Lib.isTypedArrayEncoding(v)) { + return decodeTypedArray(v); } else if(Array.isArray(v)) { return v.map(performDecode); } else if(Lib.isPlainObject(v)) { @@ -2225,7 +2250,8 @@ function performDecode(v) { /** * Plotly.decode: - * Import an object or array into... TODO + * Recursively decode an object or array into a form supported by Plotly.js. + * * @param v * @returns {object} */ From 1ec957a8d65f0345c7ac972b369e785fc8d1c62b Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 10 Sep 2018 19:37:14 -0400 Subject: [PATCH 08/10] Convert encoded mock into test variable. This can't be a mock anymore because it is not valid as input to Plotly.plot without first passing through Plotly.decode --- .../image/mocks/typed_array_repr_scatter.json | 13 --------- ...pr_test.js => decode_typed_arrays_test.js} | 27 ++++++++++++++----- 2 files changed, 20 insertions(+), 20 deletions(-) delete mode 100644 test/image/mocks/typed_array_repr_scatter.json rename test/jasmine/tests/{primitive_typed_array_repr_test.js => decode_typed_arrays_test.js} (87%) diff --git a/test/image/mocks/typed_array_repr_scatter.json b/test/image/mocks/typed_array_repr_scatter.json deleted file mode 100644 index ff11abb37e7..00000000000 --- a/test/image/mocks/typed_array_repr_scatter.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "data": [{ - "type": "scatter", - "x": {"dtype": "float64", "value": [3, 2, 1]}, - "y": {"dtype": "float32", "value": "AABAQAAAAEAAAIA/"}, - "marker": { - "color": { - "dtype": "uint16", - "value": "AwACAAEA", - }, - } - }] -} diff --git a/test/jasmine/tests/primitive_typed_array_repr_test.js b/test/jasmine/tests/decode_typed_arrays_test.js similarity index 87% rename from test/jasmine/tests/primitive_typed_array_repr_test.js rename to test/jasmine/tests/decode_typed_arrays_test.js index d9762720283..1f8b21bbad8 100644 --- a/test/jasmine/tests/primitive_typed_array_repr_test.js +++ b/test/jasmine/tests/decode_typed_arrays_test.js @@ -1,6 +1,18 @@ var Plotly = require('@lib/index'); var b64 = require('base64-arraybuffer'); -var mock1 = require('@mocks/typed_array_repr_scatter.json'); +var encodedFigure = { + 'data': [{ + 'type': 'scatter', + 'x': {'dtype': 'float64', 'value': [3, 2, 1]}, + 'y': {'dtype': 'float32', 'value': 'AABAQAAAAEAAAIA/'}, + 'marker': { + 'color': { + 'dtype': 'uint16', + 'value': 'AwACAAEA', + }, + } + }] +}; var typedArraySpecs = [ ['int8', new Int8Array([-128, -34, 1, 127])], @@ -112,27 +124,28 @@ describe('Test TypedArray representations', function() { }); }); - describe('mock', function() { - it('should decode representation as base 64 and Array in Mock', function() { + describe('encoded figure', function() { + it('should decode representation as base 64 and Array in encoded figure', function() { + + var gd = Plotly.decode(encodedFigure); - var gd = Plotly.decode(mock1); // Check x // data_array property - expect(mock1.data[0].x).toEqual({ + expect(encodedFigure.data[0].x).toEqual({ 'dtype': 'float64', 'value': [3, 2, 1]}); expect(gd.data[0].x).toEqual(new Float64Array([3, 2, 1])); // Check y // data_array property - expect(mock1.data[0].y).toEqual({ + expect(encodedFigure.data[0].y).toEqual({ 'dtype': 'float32', 'value': 'AABAQAAAAEAAAIA/'}); expect(gd.data[0].y).toEqual(new Float32Array([3, 2, 1])); // Check marker.color // This is an arrayOk property not a data_array property - expect(mock1.data[0].marker.color).toEqual({ + expect(encodedFigure.data[0].marker.color).toEqual({ 'dtype': 'uint16', 'value': 'AwACAAEA'}); expect(gd.data[0].marker.color).toEqual(new Uint16Array([3, 2, 1])); From 2b9bda975f84667d1df54b28fe8a8267a9bd24fe Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 10 Sep 2018 20:11:36 -0400 Subject: [PATCH 09/10] Remove circular dependency --- src/lib/is_array.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/is_array.js b/src/lib/is_array.js index 8f0b0603e28..5269b28879e 100644 --- a/src/lib/is_array.js +++ b/src/lib/is_array.js @@ -8,7 +8,7 @@ 'use strict'; -var Lib = require('../lib'); +var isPlainObject = require('./is_plain_object'); // IE9 fallbacks @@ -41,7 +41,7 @@ function isArray1D(a) { } function isTypedArrayEncoding(a) { - return (Lib.isPlainObject(a) && + return (isPlainObject(a) && a.hasOwnProperty('dtype') && a.hasOwnProperty('value')); } From 669e8be7f919b593610fc5f87687a220d630a45d Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 11 Sep 2018 07:46:40 -0400 Subject: [PATCH 10/10] Added new top-level Plotly.encode function. This function inputs a Plotly object and outputs a copy where all TypedArray instances have been replace with JSON serializable representation objects. This function is the inverse of Plotly.decode --- src/plot_api/index.js | 1 + src/plot_api/plot_api.js | 139 +++++++++++++----- .../jasmine/tests/decode_typed_arrays_test.js | 10 +- 3 files changed, 115 insertions(+), 35 deletions(-) diff --git a/src/plot_api/index.js b/src/plot_api/index.js index ef7f5d4e7fa..e5cef72ba62 100644 --- a/src/plot_api/index.js +++ b/src/plot_api/index.js @@ -17,6 +17,7 @@ exports.relayout = main.relayout; exports.redraw = main.redraw; exports.update = main.update; exports.decode = main.decode; +exports.encode = main.encode; exports.react = main.react; exports.extendTraces = main.extendTraces; exports.prependTraces = main.prependTraces; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 421aac02372..3cab6704164 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2195,36 +2195,30 @@ exports.update = function update(gd, traceUpdate, layoutUpdate, _traces) { /** - * Create a mapping from dtype values to TypedArray types. A mapping is only - * added for TypedArray types supported by the current browser + * Get TypedArray type for a given dtype string + * @param {String} dtype: Data type string + * @returns {TypedArray} */ -var dtypeStringToTypedarrayType = {}; -if(typeof Int8Array !== 'undefined') { - dtypeStringToTypedarrayType.int8 = Int8Array; -} -if(typeof Uint8Array !== 'undefined') { - dtypeStringToTypedarrayType.uint8 = Uint8Array; -} -if(typeof Uint8ClampedArray !== 'undefined') { - dtypeStringToTypedarrayType.uint8_clamped = Uint8ClampedArray; -} -if(typeof Int16Array !== 'undefined') { - dtypeStringToTypedarrayType.int16 = Int16Array; -} -if(typeof Uint16Array !== 'undefined') { - dtypeStringToTypedarrayType.uint16 = Uint16Array; -} -if(typeof Int32Array !== 'undefined') { - dtypeStringToTypedarrayType.int32 = Int32Array; -} -if(typeof Uint32Array !== 'undefined') { - dtypeStringToTypedarrayType.uint32 = Uint32Array; -} -if(typeof Float32Array !== 'undefined') { - dtypeStringToTypedarrayType.float32 = Float32Array; -} -if(typeof Float64Array !== 'undefined') { - dtypeStringToTypedarrayType.float64 = Float64Array; +function getTypedArrayTypeForDtypeString(dtype) { + if(dtype === 'int8' && typeof Int8Array !== 'undefined') { + return Int8Array; + } else if(dtype === 'uint8' && typeof Uint8Array !== 'undefined') { + return Uint8Array; + } else if(dtype === 'uint8_clamped' && typeof Uint8ClampedArray !== 'undefined') { + return Uint8ClampedArray; + } else if(dtype === 'int16' && typeof Int16Array !== 'undefined') { + return Int16Array; + } else if(dtype === 'uint16' && typeof Uint16Array !== 'undefined') { + return Uint16Array; + } else if(dtype === 'int32' && typeof Int32Array !== 'undefined') { + return Int32Array; + } else if(dtype === 'uint32' && typeof Uint32Array !== 'undefined') { + return Uint32Array; + } else if(dtype === 'float32' && typeof Float32Array !== 'undefined') { + return Float32Array; + } else if(dtype === 'float64' && typeof Float64Array !== 'undefined') { + return Float64Array; + } } /** @@ -2238,7 +2232,7 @@ function decodeTypedArray(v) { var coercedV; var value = v.value; - var TypeArrayType = dtypeStringToTypedarrayType[v.dtype]; + var TypeArrayType = getTypedArrayTypeForDtypeString(v.dtype); if(TypeArrayType) { if(value instanceof ArrayBuffer) { @@ -2286,15 +2280,94 @@ function performDecode(v) { /** * Plotly.decode: - * Recursively decode an object or array into a form supported by Plotly.js. + * Attempt to recursively decode an object or array into a form supported + * by Plotly.js. This function is the inverse of Plotly.encode. * - * @param v - * @returns {object} + * @param {object} v: Value to be decoded + * @returns {object}: Decoded value */ exports.decode = function(v) { return performDecode(v); }; +/** + * Get data type string for TypedArray + * @param {TypedArray} v: A TypedArray instance + * @returns {String} + */ +function getDtypeStringForTypedArray(v) { + if(typeof Int8Array !== 'undefined' && v instanceof Int8Array) { + return 'int8'; + } else if(typeof Uint8Array !== 'undefined' && v instanceof Uint8Array) { + return 'uint8'; + } else if(typeof Uint8ClampedArray !== 'undefined' && v instanceof Uint8ClampedArray) { + return 'uint8_clamped'; + } else if(typeof Int16Array !== 'undefined' && v instanceof Int16Array) { + return 'int16'; + } else if(typeof Uint16Array !== 'undefined' && v instanceof Uint16Array) { + return 'uint16'; + } else if(typeof Int32Array !== 'undefined' && v instanceof Int32Array) { + return 'int32'; + } else if(typeof Uint32Array !== 'undefined' && v instanceof Uint32Array) { + return 'uint32'; + } else if(typeof Float32Array !== 'undefined' && v instanceof Float32Array) { + return 'float32'; + } else if(typeof Float64Array !== 'undefined' && v instanceof Float64Array) { + return 'float64'; + } +} + + +/** + * Convert a TypedArray instance into a JSON-serializable object that + * represents it. + * + * @param {TypedArray} v: A TypedArray instance + * + * @returns {object} Object with `dtype` and `value` properties that + * represents a TypedArray. + */ +function encodeTypedArray(v) { + var dtype = getDtypeStringForTypedArray(v); + var buffer = b64.encode(v.buffer); + return {'value': buffer, 'dtype': dtype}; +} + + +/** + * Recursive helper function to perform encoding + * @param v + */ +function performEncode(v) { + if(Lib.isTypedArray(v)) { + return encodeTypedArray(v); + } else if(Array.isArray(v)) { + return v.map(performEncode); + } else if(Lib.isPlainObject(v)) { + var result = {}; + for(var k in v) { + if(v.hasOwnProperty(k)) { + result[k] = performEncode(v[k]); + } + } + return result; + } else { + return v; + } +} + +/** + * Plotly.encode + * Recursively encode a Plotly.js object or array into a form that is JSON + * serializable + * + * @param {object} v: Value to be encode + * @returns {object}: Encoded value + */ +exports.encode = function(v) { + return performEncode(v); +}; + /** * Plotly.react: * A plot/update method that takes the full plot state (same API as plot/newPlot) diff --git a/test/jasmine/tests/decode_typed_arrays_test.js b/test/jasmine/tests/decode_typed_arrays_test.js index 1f8b21bbad8..ab57e38e8f1 100644 --- a/test/jasmine/tests/decode_typed_arrays_test.js +++ b/test/jasmine/tests/decode_typed_arrays_test.js @@ -3,7 +3,7 @@ var b64 = require('base64-arraybuffer'); var encodedFigure = { 'data': [{ 'type': 'scatter', - 'x': {'dtype': 'float64', 'value': [3, 2, 1]}, + 'x': {'dtype': 'float64', 'value': 'AAAAAAAACEAAAAAAAAAAQAAAAAAAAPA/'}, 'y': {'dtype': 'float32', 'value': 'AABAQAAAAEAAAIA/'}, 'marker': { 'color': { @@ -120,6 +120,9 @@ describe('Test TypedArray representations', function() { var gd = Plotly.decode(raw); expect(gd.data[0].y).toEqual(arraySpec[1]); + + // Re-encoding should produce the original encoding + expect(Plotly.encode(gd)).toEqual({data: [{y: repr}]}); }); }); }); @@ -133,7 +136,7 @@ describe('Test TypedArray representations', function() { // data_array property expect(encodedFigure.data[0].x).toEqual({ 'dtype': 'float64', - 'value': [3, 2, 1]}); + 'value': 'AAAAAAAACEAAAAAAAAAAQAAAAAAAAPA/'}); expect(gd.data[0].x).toEqual(new Float64Array([3, 2, 1])); // Check y @@ -149,6 +152,9 @@ describe('Test TypedArray representations', function() { 'dtype': 'uint16', 'value': 'AwACAAEA'}); expect(gd.data[0].marker.color).toEqual(new Uint16Array([3, 2, 1])); + + // Re-encode to make sure we obtain the original representation + expect(Plotly.encode(gd)).toEqual(encodedFigure); }); }); });