-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
Changes from 8 commits
e94eec3
9814edf
0b3040b
a5bc7fe
6c1938c
d304864
877077b
1958dc6
f620a22
a2c52fe
ad8db59
09ed024
a2f2160
54a6ed0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice cleanup. In fact, we could do away with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done in 09ed024 |
||
} | ||
return isValid; | ||
|
||
highestVal = +si[0]; | ||
} | ||
|
||
return isValid; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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' | ||
); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @alexcjohnson are you ok with ⏫ ? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: { | ||
|
@@ -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 && | ||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @alexcjohnson I chose to write a This felt like (1) less of a hassle than adding a There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: { | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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). There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} | ||
}; | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exactly. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would something like
also work? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 In other words, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. right makes sense |
||
return out !== failed; | ||
}; |
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) | ||
}; | ||
} |
There was a problem hiding this comment.
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
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done in 09ed024