Skip to content

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

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

Closed
wants to merge 11 commits into from
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 @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
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 Lib = require('../lib');

// 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 isPrimitiveTypedArrayRepr(a) {
return (Lib.isPlainObject(a) &&
a.hasOwnProperty('dtype') && a.hasOwnProperty('value'));
}

module.exports = {
isTypedArray: isTypedArray,
isArrayOrTypedArray: isArrayOrTypedArray,
isArray1D: isArray1D
isArray1D: isArray1D,
isPrimitiveTypedArrayRepr: isPrimitiveTypedArrayRepr
};
1 change: 1 addition & 0 deletions src/plot_api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
77 changes: 77 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 @@ -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 = {
Copy link
Contributor

Choose a reason for hiding this comment

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

We'll need to append this list:

plotly.js/.eslintrc

Lines 12 to 21 in a8c6217

"globals": {
"Promise": true,
"Float32Array": true,
"Float64Array": true,
"Uint8Array": true,
"Int16Array": true,
"Int32Array": true,
"ArrayBuffer": true,
"DataView": true,
"SVGElement": false

to make npm run lint pass.

Copy link
Contributor

Choose a reason for hiding this comment

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

and we'll need to add fallbacks so that browsers w/o typed array support don't break.

We have a "test" for that:

npm run test-jasmine -- --bundleTest=ie9_test.js

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)) {
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] = 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)
Expand Down
13 changes: 13 additions & 0 deletions test/image/mocks/typed_array_repr_scatter.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"data": [{
"type": "scatter",
"x": {"dtype": "float64", "value": [3, 2, 1]},
"y": {"dtype": "float32", "value": "AABAQAAAAEAAAIA/"},
"marker": {
"color": {
"dtype": "uint16",
"value": "AwACAAEA",
},
}
}]
}
143 changes: 143 additions & 0 deletions test/jasmine/tests/primitive_typed_array_repr_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
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');

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])],
['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 value and confirm its type
var value = arraySpec[1].buffer;
expect(value.constructor).toEqual(ArrayBuffer);

var repr = {
dtype: arraySpec[0],
value: value
};
var raw = {
data: [{
y: repr
}],
};

var gd = Plotly.import(raw);

expect(gd.data[0].y).toEqual(arraySpec[1]);
});
});
});

describe('Array', function() {
it('should accept representation as Array', function() {
typedArraySpecs.forEach(function(arraySpec) {
// 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],
value: value
};
var raw = {
data: [{
y: repr
}],
};

var gd = Plotly.import(raw);

expect(gd.data[0].y).toEqual(arraySpec[1]);
});
});
});

describe('DataView', function() {
it('should accept representation as DataView', function() {
typedArraySpecs.forEach(function(arraySpec) {
// Build value and confirm its type
var value = new DataView(arraySpec[1].buffer);
expect(value.constructor).toEqual(DataView);

var repr = {
dtype: arraySpec[0],
value: value
};
var raw = {
data: [{
y: repr
}],
};

var gd = Plotly.import(raw);

expect(gd.data[0].y).toEqual(arraySpec[1]);
});
});
});

describe('base64', function() {
it('should accept representation as base 64 string', function() {
typedArraySpecs.forEach(function(arraySpec) {
// Build value and confirm its type
var value = b64.encode(arraySpec[1].buffer);
expect(typeof value).toEqual('string');

var repr = {
dtype: arraySpec[0],
value: value
};
var raw = {
data: [{
y: repr
}],
};

var gd = Plotly.import(raw);
expect(gd.data[0].y).toEqual(arraySpec[1]);
});
});
});

describe('mock', function() {
it('should import representation as base 64 and Array in Mock', function() {

var gd = Plotly.import(mock1);
// Check x
// data_array property
expect(mock1.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({
'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({
'dtype': 'uint16',
'value': 'AwACAAEA'});
expect(gd.data[0].marker.color).toEqual(new Uint16Array([3, 2, 1]));
});
});
});