Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for encoding TypedArrays as primitive objects for serialization #2911

Closed
wants to merge 11 commits into from
4 changes: 4 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"@plotly/d3-sankey": "^0.5.0",
"alpha-shape": "^1.0.0",
"array-range": "^1.0.1",
"base64-arraybuffer": "^0.1.5",
archmoj marked this conversation as resolved.
Show resolved Hide resolved
"canvas-fit": "^1.5.0",
"color-normalize": "^1.3.0",
"convex-hull": "^1.0.3",
Expand Down
1 change: 1 addition & 0 deletions src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ var isArrayModule = require('./is_array');
lib.isTypedArray = isArrayModule.isTypedArray;
lib.isArrayOrTypedArray = isArrayModule.isArrayOrTypedArray;
lib.isArray1D = isArrayModule.isArray1D;
lib.isTypedArrayEncoding = isArrayModule.isTypedArrayEncoding;

var coerceModule = require('./coerce');
lib.valObjectMeta = coerceModule.valObjectMeta;
Expand Down
10 changes: 9 additions & 1 deletion src/lib/is_array.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

'use strict';

var isPlainObject = require('./is_plain_object');

// IE9 fallbacks

var ab = (typeof ArrayBuffer === 'undefined' || !ArrayBuffer.isView) ?
Expand Down Expand Up @@ -38,8 +40,14 @@ function isArray1D(a) {
return !isArrayOrTypedArray(a[0]);
}

function isTypedArrayEncoding(a) {
return (isPlainObject(a) &&
a.hasOwnProperty('dtype') && a.hasOwnProperty('value'));
}

module.exports = {
isTypedArray: isTypedArray,
isArrayOrTypedArray: isArrayOrTypedArray,
isArray1D: isArray1D
isArray1D: isArray1D,
isTypedArrayEncoding: isTypedArrayEncoding
};
2 changes: 2 additions & 0 deletions src/plot_api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ exports.restyle = main.restyle;
exports.relayout = main.relayout;
exports.redraw = main.redraw;
exports.update = main.update;
exports.decode = main.decode;
exports.encode = main.encode;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may start with underscore (i.e. kind of private) methods here.
For example:

exports._decode = main.decode;
exports._encode = main.encode;

If we want to expose these functionality, then using a more specific name could be considered.
For example:

exports.decodeArray = main.decode;
exports.encodeArray = main.encode;

exports.react = main.react;
exports.extendTraces = main.extendTraces;
exports.prependTraces = main.prependTraces;
Expand Down
176 changes: 176 additions & 0 deletions src/plot_api/plot_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -2192,6 +2193,181 @@ exports.update = function update(gd, traceUpdate, layoutUpdate, _traces) {
});
};


/**
* Get TypedArray type for a given dtype string
* @param {String} dtype: Data type string
* @returns {TypedArray}
*/
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;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may consider rewrite this:

function getArrayType(t) {
    return (typeof t !== 'undefined') ? t : undefined;
}
var validInt8Array = getArrayType(Int8Array);
var validUint8Array = getArrayType(Uint8Array);
...

/**
 * Get TypedArray type for a given dtype string
 * @param {String} dtype: Data type string
 * @returns {TypedArray}
 */
function getTypedArrayTypeForDtypeString(dtype) {
    switch(dtyle) {
        case 'int8':
            return validInt8Array;
        case 'uint8':
            return validUint8Array;
        ...
    }
}


/**
* Convert a TypedArray encoding object into a TypedArray
* @param {object} v: Object with `dtype` and `value` properties that
* represents a TypedArray.
*
* @returns {TypedArray}
*/
function decodeTypedArray(v) {

var coercedV;
var value = v.value;
var TypeArrayType = getTypedArrayTypeForDtypeString(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.isTypedArrayEncoding(v)) {
return decodeTypedArray(v);
} else if(Array.isArray(v)) {
return v.map(performDecode);
} else if(Lib.isPlainObject(v)) {
var result = {};
for(var k in v) {
if(v.hasOwnProperty(k)) {
result[k] = performDecode(v[k]);
}
}
return result;
} else {
return v;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can drop last else statement and simply return v;

}

/**
* Plotly.decode:
* Attempt to recursively decode an object or array into a form supported
* by Plotly.js. This function is the inverse of Plotly.encode.
*
* @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';
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be of interest to support BigInts as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Big integers could be useful for storing time in milliseconds.

}


/**
* 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)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we may list the keys using Object.getOwnPropertyNames and loop through them.

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)
Expand Down
Loading