Skip to content

Introducing Plotly.validate(data, layout) #697

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

Merged
merged 14 commits into from
Jul 13, 2016
Merged
32 changes: 18 additions & 14 deletions src/components/colorscale/is_valid_scale_array.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,24 @@ var tinycolor = require('tinycolor2');

module.exports = function isValidScaleArray(scl) {
var isValid = true,
highestVal = 0,
si;

if(!Array.isArray(scl)) return false;
else {
if(+scl[0][0] !== 0 || +scl[scl.length - 1][0] !== 1) return false;
for(var i = 0; i < scl.length; i++) {
si = scl[i];
if(si.length !== 2 || +si[0] < highestVal || !tinycolor(si[1]).isValid()) {
isValid = false;
break;
}
highestVal = +si[0];
highestVal = 0;

if(!Array.isArray(scl) || scl.length === 0) return false;
Copy link
Contributor

@mdtusz mdtusz Jun 28, 2016

Choose a reason for hiding this comment

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

We should check that scl.length > 1.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done in 09ed024


if(!scl[0] || !scl[scl.length - 1]) return false;

if(+scl[0][0] !== 0 || +scl[scl.length - 1][0] !== 1) return false;

for(var i = 0; i < scl.length; i++) {
var si = scl[i];

if(si.length !== 2 || +si[0] < highestVal || !tinycolor(si[1]).isValid()) {
isValid = false;
break;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nice cleanup. In fact, we could do away with isValid entirely and just return false here or return true at the end.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done in 09ed024

}
return isValid;

highestVal = +si[0];
}

return isValid;
};
1 change: 1 addition & 0 deletions src/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ exports.setPlotConfig = require('./plot_api/set_plot_config');
exports.register = Plotly.register;
exports.toImage = require('./plot_api/to_image');
exports.downloadImage = require('./snapshot/download');
exports.validate = require('./plot_api/validate');

// plot icons
exports.Icons = require('../build/ploticon');
Expand Down
69 changes: 58 additions & 11 deletions src/lib/coerce.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,16 +99,18 @@ exports.valObjects = {
// TODO 'values shouldn't be in there (edge case: 'dash' in Scatter)
otherOpts: ['dflt', 'noBlank', 'strict', 'arrayOk', 'values'],
coerceFunction: function(v, propOut, dflt, opts) {
if(opts.strict === true && typeof v !== 'string') {
propOut.set(dflt);
return;
}
if(typeof v !== 'string') {
var okToCoerce = (
typeof v === 'number' ||
v instanceof Date ||
typeof v === 'boolean'
);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@alexcjohnson are you ok with ⏫ ?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm OK with this, but actually, I'm coming around to your original position that we should only coerce numbers. My new argument against Date is that it will be altered if the plot is saved as JSON (plot.ly or otherwise) but also how likely is it that the default stringification is what the user wants for their date? And then 'boolean' just starts to look out of place.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done in ad8db59


var s = String(v);
if(v === undefined || (opts.noBlank === true && !s)) {
propOut.set(dflt);
if(opts.strict === true || !okToCoerce) propOut.set(dflt);
else propOut.set(String(v));
}
else propOut.set(s);
else if(opts.noBlank && !v) propOut.set(dflt);
else propOut.set(v);
}
},
color: {
Expand Down Expand Up @@ -162,11 +164,11 @@ exports.valObjects = {
subplotid: {
description: [
'An id string of a subplot type (given by dflt), optionally',
'followed by an integer >1. e.g. if dflt=\'geo\', we can have',
'followed by an integer >1. e.g. if dflt=\'geo\', we can have',
'\'geo\', \'geo2\', \'geo3\', ...'
].join(' '),
requiredOpts: [],
otherOpts: ['dflt'],
requiredOpts: ['dflt'],
otherOpts: [],
coerceFunction: function(v, propOut, dflt) {
var dlen = dflt.length;
if(typeof v === 'string' && v.substr(0, dlen) === dflt &&
Expand All @@ -175,6 +177,18 @@ exports.valObjects = {
return;
}
propOut.set(dflt);
},
validateFunction: function(v, opts) {
var dflt = opts.dflt,
dlen = dflt.length;

if(v === dflt) return true;
if(typeof v !== 'string') return false;
if(v.substr(0, dlen) === dflt && idRegex.test(v.substr(dlen))) {
return true;
}

return false;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@alexcjohnson I chose to write a validateFunction for the 'subplotid' val type (only 'subplotid' and 'info_array' now have validateFunction).

This felt like (1) less of a hassle than adding a base option and (2) less hacky than trying to patch into Lib.validate. I hope you're ok with ⏫ .

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, you made the right call. As long as there are good tests, which it looks like you added 🎉

}
},
flaglist: {
Expand Down Expand Up @@ -239,6 +253,20 @@ exports.valObjects = {
}

propOut.set(vOut);
},
validateFunction: function(v, opts) {
if(!Array.isArray(v)) return false;

var items = opts.items;

// valid when one item is valid (which is subject to debate)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd vote for being strict here (unless there's a reason otherwise).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done in a2f2160

for(var i = 0; i < items.length; i++) {
var isItemValid = exports.validate(v[i], opts.items[i]);

if(isItemValid) return true;
}

return false;
}
}
};
Expand Down Expand Up @@ -309,3 +337,22 @@ exports.coerceFont = function(coerce, attr, dfltObj) {

return out;
};

exports.validate = function(value, opts) {
var valObject = exports.valObjects[opts.valType];

if(opts.arrayOk && Array.isArray(value)) return true;

if(valObject.validateFunction) {
return valObject.validateFunction(value, opts);
}

var failed = {},
out = failed,
propMock = { set: function(v) { out = v; } };

// 'failed' just something mutable that won't be === anything else

valObject.coerceFunction(value, propMock, failed, opts);
Copy link
Member

@bpostlethwaite bpostlethwaite Jul 13, 2016

Choose a reason for hiding this comment

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

so the

failed = {}
out = failed
coerce(() => out = v)
out !== failed

dance is just checking to see if if propMock is fired by checking to see if the exising out === failed reference is no longer valid?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Exactly.

Copy link
Member

Choose a reason for hiding this comment

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

would something like

var called = false;
var propMock = {set: function() { called = true }

 valObject.coerceFunction(value, propMock, failed, opts);

return called;

also work?

Copy link
Contributor Author

@etpinard etpinard Jul 13, 2016

Choose a reason for hiding this comment

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

No. Because we want coerced values to be considered valid.

For example,

Lib.validate('1', { valType: 'number' });  // => true

even though 1 will be inserted in fullData.

In other words, out and failed will be linked to different refs when the coerce function makes propMock execute propMock.set(dflt).

Copy link
Member

Choose a reason for hiding this comment

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

right makes sense

return out !== failed;
};
1 change: 1 addition & 0 deletions src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ lib.valObjects = coerceModule.valObjects;
lib.coerce = coerceModule.coerce;
lib.coerce2 = coerceModule.coerce2;
lib.coerceFont = coerceModule.coerceFont;
lib.validate = coerceModule.validate;

var datesModule = require('./dates');
lib.dateTime2ms = datesModule.dateTime2ms;
Expand Down
156 changes: 156 additions & 0 deletions src/plot_api/validate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/**
* Copyright 2012-2016, 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 Lib = require('../lib');
var Plots = require('../plots/plots');
var PlotSchema = require('./plot_schema');

var isPlainObject = Lib.isPlainObject;

// validation error codes
var code2msgFunc = {
invisible: function(path) {
return 'trace ' + path + ' got defaulted to be not visible';
},
schema: function(path) {
return 'key ' + path.join('.') + ' is not part of the schema';
},
unused: function(path, valIn) {
var prefix = isPlainObject(valIn) ? 'container' : 'key';

return prefix + ' ' + path.join('.') + ' did not get coerced';
},
value: function(path, valIn) {
return 'key ' + path.join('.') + ' is set to an invalid value (' + valIn + ')';
}
};

module.exports = function valiate(data, layout) {
if(!Array.isArray(data)) {
throw new Error('data must be an array');
}

if(!isPlainObject(layout)) {
throw new Error('layout must be an object');
}

var gd = {
data: Lib.extendDeep([], data),
layout: Lib.extendDeep({}, layout)
};
Plots.supplyDefaults(gd);

var schema = PlotSchema.get();

var dataOut = gd._fullData,
len = data.length,
dataList = new Array(len);

for(var i = 0; i < len; i++) {
var traceIn = data[i];
var traceList = dataList[i] = [];

if(!isPlainObject(traceIn)) {
throw new Error('each data trace must be an object');
}

var traceOut = dataOut[i],
traceType = traceOut.type,
traceSchema = schema.traces[traceType].attributes;

// PlotSchema does something fancy with trace 'type', reset it here
// to make the trace schema compatible with Lib.validate.
traceSchema.type = {
valType: 'enumerated',
values: [traceType]
};

if(traceOut.visible === false && traceIn.visible !== false) {
traceList.push(format('invisible', i));
}

crawl(traceIn, traceOut, traceSchema, traceList);
}

var layoutOut = gd._fullLayout,
layoutSchema = fillLayoutSchema(schema, dataOut),
layoutList = [];

crawl(layout, layoutOut, layoutSchema, layoutList);

return {
data: dataList,
layout: layoutList
};
};

function crawl(objIn, objOut, schema, list, path) {
path = path || [];

var keys = Object.keys(objIn);

for(var i = 0; i < keys.length; i++) {
var k = keys[i];

var p = path.slice();
p.push(k);

var valIn = objIn[k],
valOut = objOut[k];

if(isPlainObject(valIn) && isPlainObject(valOut)) {
crawl(valIn, valOut, schema[k], list, p);
}
else if(!(k in schema)) {
list.push(format('schema', p));
}
else if(schema[k].items && Array.isArray(valIn)) {
var itemName = k.substr(0, k.length - 1);

for(var j = 0; j < valIn.length; j++) {
p[p.length - 1] = k + '[' + j + ']';

crawl(valIn[j], valOut[j], schema[k].items[itemName], list, p);
}
}
else if(!(k in objOut)) {
list.push(format('unused', p, valIn));
}
else if(!Lib.validate(valIn, schema[k])) {
list.push(format('value', p, valIn));
}
}

return list;
}

// the 'full' layout schema depends on the traces types presents
function fillLayoutSchema(schema, dataOut) {
for(var i = 0; i < dataOut.length; i++) {
var traceType = dataOut[i].type,
traceLayoutAttr = schema.traces[traceType].layoutAttributes;

if(traceLayoutAttr) {
Lib.extendFlat(schema.layout.layoutAttributes, traceLayoutAttr);
}
}

return schema.layout.layoutAttributes;
}

function format(code, path, valIn) {
return {
code: code,
path: path,
msg: code2msgFunc[code](path, valIn)
};
}
7 changes: 7 additions & 0 deletions test/jasmine/tests/colorscale_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ describe('Test colorscale:', function() {

it('should accept only array of 2-item arrays', function() {
expect(isValidScale('a')).toBe(false);
expect(isValidScale([])).toBe(false);
expect(isValidScale([null, undefined])).toBe(false);
expect(isValidScale([{}, [1, 'rgb(0, 0, 200']])).toBe(false);
expect(isValidScale([[0, 'rgb(200, 0, 0)'], {}])).toBe(false);
expect(isValidScale([[0, 'rgb(0, 0, 200)'], undefined])).toBe(false);
expect(isValidScale([null, [1, 'rgb(0, 0, 200)']])).toBe(false);
expect(isValidScale(['a', 'b'])).toBe(false);
expect(isValidScale(['a'])).toBe(false);
expect(isValidScale([['a'], ['b']])).toBe(false);

Expand Down
Loading