Skip to content

Commit 1958dc6

Browse files
committed
add Plotly.validate:
- to determine whether user 'data' and 'layout' are valid in plotly.js
1 parent 877077b commit 1958dc6

File tree

3 files changed

+266
-0
lines changed

3 files changed

+266
-0
lines changed

src/core.js

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ exports.setPlotConfig = require('./plot_api/set_plot_config');
3333
exports.register = Plotly.register;
3434
exports.toImage = require('./plot_api/to_image');
3535
exports.downloadImage = require('./snapshot/download');
36+
exports.validate = require('./plot_api/validate');
3637

3738
// plot icons
3839
exports.Icons = require('../build/ploticon');

src/plot_api/validate.js

+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/**
2+
* Copyright 2012-2016, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
10+
'use strict';
11+
12+
13+
var Lib = require('../lib');
14+
var Plots = require('../plots/plots');
15+
var PlotSchema = require('./plot_schema');
16+
17+
var isPlainObject = Lib.isPlainObject;
18+
19+
// validation error codes
20+
var code2msgFunc = {
21+
invisible: function(path) {
22+
return 'trace ' + path + ' got defaulted to be not visible';
23+
},
24+
schema: function(path) {
25+
return 'key ' + path.join('.') + ' is not part of the schema';
26+
},
27+
unused: function(path, valIn) {
28+
var prefix = isPlainObject(valIn) ? 'container' : 'key';
29+
30+
return prefix + ' ' + path.join('.') + ' did not get coerced';
31+
},
32+
value: function(path, valIn) {
33+
return 'key ' + path.join('.') + ' is set to an invalid value (' + valIn + ')';
34+
}
35+
};
36+
37+
module.exports = function valiate(data, layout) {
38+
if(!Array.isArray(data)) {
39+
throw new Error('data must be an array');
40+
}
41+
42+
if(!isPlainObject(layout)) {
43+
throw new Error('layout must be an object');
44+
}
45+
46+
var gd = {
47+
data: Lib.extendDeep([], data),
48+
layout: Lib.extendDeep({}, layout)
49+
};
50+
Plots.supplyDefaults(gd);
51+
52+
var schema = PlotSchema.get();
53+
54+
var dataOut = gd._fullData,
55+
len = data.length,
56+
dataList = new Array(len);
57+
58+
for(var i = 0; i < len; i++) {
59+
var traceIn = data[i];
60+
var traceList = dataList[i] = [];
61+
62+
if(!isPlainObject(traceIn)) {
63+
throw new Error('each data trace must be an object');
64+
}
65+
66+
var traceOut = dataOut[i],
67+
traceType = traceOut.type,
68+
traceSchema = schema.traces[traceType].attributes;
69+
70+
// PlotSchema does something fancy with trace 'type', reset it here
71+
// to make the trace schema compatible with Lib.validate.
72+
traceSchema.type = {
73+
valType: 'enumerated',
74+
values: [traceType]
75+
};
76+
77+
if(traceOut.visible === false && traceIn.visible !== false) {
78+
traceList.push(format('invisible', i));
79+
}
80+
81+
crawl(traceIn, traceOut, traceSchema, traceList);
82+
}
83+
84+
var layoutOut = gd._fullLayout,
85+
layoutSchema = fillLayoutSchema(schema, dataOut),
86+
layoutList = [];
87+
88+
crawl(layout, layoutOut, layoutSchema, layoutList);
89+
90+
return {
91+
data: dataList,
92+
layout: layoutList
93+
};
94+
};
95+
96+
function crawl(objIn, objOut, schema, list, path) {
97+
path = path || [];
98+
99+
var keys = Object.keys(objIn);
100+
101+
for(var i = 0; i < keys.length; i++) {
102+
var k = keys[i];
103+
104+
var p = path.slice();
105+
p.push(k);
106+
107+
var valIn = objIn[k],
108+
valOut = objOut[k];
109+
110+
if(isPlainObject(valIn) && isPlainObject(valOut)) {
111+
crawl(valIn, valOut, schema[k], list, p);
112+
}
113+
else if(!(k in schema)) {
114+
list.push(format('schema', p));
115+
}
116+
else if(schema[k].items && Array.isArray(valIn)) {
117+
var itemName = k.substr(0, k.length - 1);
118+
119+
for(var j = 0; j < valIn.length; j++) {
120+
p[p.length - 1] = k + '[' + j + ']';
121+
122+
crawl(valIn[j], valOut[j], schema[k].items[itemName], list, p);
123+
}
124+
}
125+
else if(!(k in objOut)) {
126+
list.push(format('unused', p, valIn));
127+
}
128+
else if(!Lib.validate(valIn, schema[k])) {
129+
list.push(format('value', p, valIn));
130+
}
131+
}
132+
133+
return list;
134+
}
135+
136+
// the 'full' layout schema depends on the traces types presents
137+
function fillLayoutSchema(schema, dataOut) {
138+
for(var i = 0; i < dataOut.length; i++) {
139+
var traceType = dataOut[i].type,
140+
traceLayoutAttr = schema.traces[traceType].layoutAttributes;
141+
142+
if(traceLayoutAttr) {
143+
Lib.extendFlat(schema.layout.layoutAttributes, traceLayoutAttr);
144+
}
145+
}
146+
147+
return schema.layout.layoutAttributes;
148+
}
149+
150+
function format(code, path, valIn) {
151+
return {
152+
code: code,
153+
path: path,
154+
msg: code2msgFunc[code](path, valIn)
155+
};
156+
}

test/jasmine/tests/validate_test.js

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
var Plotly = require('@lib/index');
2+
3+
4+
describe('Plotly.validate', function() {
5+
6+
function assertErrorShape(out, dataSize, layoutSize) {
7+
var actualDataSize = out.data.map(function(t) {
8+
return t.length;
9+
});
10+
11+
expect(actualDataSize).toEqual(dataSize);
12+
expect(out.layout.length).toEqual(layoutSize);
13+
}
14+
15+
function assertErrorContent(obj, code, path) {
16+
expect(obj.code).toEqual(code);
17+
expect(obj.path).toEqual(path);
18+
19+
// TODO test msg
20+
}
21+
22+
it('should report when trace is defaulted to not be visible', function() {
23+
var out = Plotly.validate([{
24+
type: 'scatter'
25+
// missing 'x' and 'y
26+
}], {});
27+
28+
assertErrorShape(out, [1], 0);
29+
assertErrorContent(out.data[0][0], 'invisible', 0, '');
30+
});
31+
32+
it('should report when trace contains keys not part of the schema', function() {
33+
var out = Plotly.validate([{
34+
x: [1, 2, 3],
35+
markerColor: 'blue'
36+
}], {});
37+
38+
assertErrorShape(out, [1], 0);
39+
assertErrorContent(out.data[0][0], 'schema', ['markerColor'], '');
40+
});
41+
42+
it('should report when trace contains keys that are not coerced', function() {
43+
var out = Plotly.validate([{
44+
x: [1, 2, 3],
45+
mode: 'lines',
46+
marker: { color: 'blue' }
47+
}, {
48+
x: [1, 2, 3],
49+
mode: 'markers',
50+
marker: {
51+
color: 'blue',
52+
cmin: 10
53+
}
54+
}], {});
55+
56+
assertErrorShape(out, [1, 1], 0);
57+
expect(out.layout.length).toEqual(0);
58+
assertErrorContent(out.data[0][0], 'unused', ['marker'], '');
59+
assertErrorContent(out.data[1][0], 'unused', ['marker', 'cmin'], '');
60+
61+
});
62+
63+
it('should report when trace contains keys set to invalid values', function() {
64+
var out = Plotly.validate([{
65+
x: [1, 2, 3],
66+
mode: 'lines',
67+
line: { width: 'a big number' }
68+
}, {
69+
x: [1, 2, 3],
70+
mode: 'markers',
71+
marker: { color: 10 }
72+
}], {});
73+
74+
assertErrorShape(out, [1, 1], 0);
75+
assertErrorContent(out.data[0][0], 'value', ['line', 'width'], '');
76+
assertErrorContent(out.data[1][0], 'value', ['marker', 'color'], '');
77+
});
78+
79+
it('should work with isLinkedToArray attributes', function() {
80+
var out = Plotly.validate([], {
81+
annotations: [{
82+
text: 'some text'
83+
}, {
84+
arrowSymbol: 'cat'
85+
}],
86+
xaxis: {
87+
type: 'date',
88+
rangeselector: {
89+
buttons: [{
90+
label: '1 month',
91+
step: 'all',
92+
count: 10
93+
}, {
94+
title: '1 month'
95+
}]
96+
}
97+
},
98+
shapes: [{
99+
opacity: 'none'
100+
}]
101+
});
102+
103+
assertErrorShape(out, [], 4);
104+
assertErrorContent(out.layout[0], 'schema', ['annotations[1]', 'arrowSymbol'], '');
105+
assertErrorContent(out.layout[1], 'unused', ['xaxis', 'rangeselector', 'buttons[0]', 'count'], '');
106+
assertErrorContent(out.layout[2], 'schema', ['xaxis', 'rangeselector', 'buttons[1]', 'title'], '');
107+
assertErrorContent(out.layout[3], 'value', ['shapes[0]', 'opacity'], '');
108+
});
109+
});

0 commit comments

Comments
 (0)