-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Templates #2761
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
Templates #2761
Changes from 1 commit
b8950c5
355fb09
8ddcfd6
b5ccfbe
86e09bb
3dee25c
5cca8d3
b3da4e7
8525953
e306d1c
8e2a321
4346611
fb489aa
bc21cc8
8490804
c2bcfe3
890a324
aed44dc
6df61e0
ef4c3cc
b1c6f0a
818cac9
15931cf
8598bc9
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 |
---|---|---|
|
@@ -30,7 +30,7 @@ var dfltConfig = require('./plot_config'); | |
* @returns {object} template: the extracted template - can then be used as | ||
* `layout.template` in another figure. | ||
*/ | ||
module.exports = function makeTemplate(figure) { | ||
exports.makeTemplate = function(figure) { | ||
figure = Lib.extendDeep({_context: dfltConfig}, figure); | ||
Plots.supplyDefaults(figure); | ||
var data = figure.data || []; | ||
|
@@ -269,3 +269,187 @@ function getNextPath(parent, key, path) { | |
|
||
return nextPath; | ||
} | ||
|
||
/** | ||
* validateTemplate: Test for consistency between the given figure and | ||
* a template, either already included in the figure or given separately. | ||
* Note that not every issue we identify here is necessarily a problem, | ||
* it depends on what you're using the template for. | ||
* | ||
* @param {object|DOM element} figure: the plot, with {data, layout} members, | ||
* to test the template against | ||
* @param {Optional(object)} template: the template, with its own {data, layout}, | ||
* to test. If omitted, we will look for a template already attached as the | ||
* plot's `layout.template` attribute. | ||
* | ||
* @returns {array} array of error objects each containing: | ||
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. THanks for making this have a similar API as 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. One difference is I didn't make the object fields consistent - they all have |
||
* - {string} code | ||
* error code ('missing', 'unused', 'reused', 'noLayout', 'noData') | ||
* - {string} msg | ||
* a full readable description of the issue. | ||
*/ | ||
exports.validateTemplate = function(figureIn, template) { | ||
var figure = Lib.extendDeep({}, { | ||
_context: dfltConfig, | ||
data: figureIn.data, | ||
layout: figureIn.layout | ||
}); | ||
var layout = figure.layout || {}; | ||
if(!isPlainObject(template)) template = layout.template || {}; | ||
var layoutTemplate = template.layout; | ||
var dataTemplate = template.data; | ||
var errorList = []; | ||
|
||
figure.layout = layout; | ||
figure.layout.template = template; | ||
Plots.supplyDefaults(figure); | ||
|
||
var fullLayout = figure._fullLayout; | ||
var fullData = figure._fullData; | ||
|
||
if(!isPlainObject(layoutTemplate)) { | ||
errorList.push({code: 'layout'}); | ||
} | ||
else { | ||
// TODO: any need to look deeper than the first level of layout? | ||
// I don't think so, that gets all the subplot types which should be | ||
// sufficient. | ||
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. What about 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. Hmm, good point. You're right, though it's a lot of complication (for example for 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. I don't this needs special handling. A comment about this would suffice. 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. actually wasn't all that hard - recursed into layout in 15931cf |
||
for(var key in layoutTemplate) { | ||
if(key.indexOf('defaults') === -1 && isPlainObject(layoutTemplate[key]) && | ||
!hasMatchingKey(fullLayout, key) | ||
) { | ||
errorList.push({code: 'unused', path: 'layout.' + key}); | ||
} | ||
} | ||
} | ||
|
||
if(!isPlainObject(dataTemplate)) { | ||
errorList.push({code: 'data'}); | ||
} | ||
else { | ||
var typeCount = {}; | ||
var traceType; | ||
for(var i = 0; i < fullData.length; i++) { | ||
var fullTrace = fullData[i]; | ||
traceType = fullTrace.type; | ||
typeCount[traceType] = (typeCount[traceType] || 0) + 1; | ||
if(!fullTrace._fullInput._template) { | ||
// this takes care of the case of traceType in the data but not | ||
// the template | ||
errorList.push({ | ||
code: 'missing', | ||
index: fullTrace._fullInput.index, | ||
traceType: traceType | ||
}); | ||
} | ||
} | ||
for(traceType in dataTemplate) { | ||
var templateCount = dataTemplate[traceType].length; | ||
var dataCount = typeCount[traceType] || 0; | ||
if(templateCount > dataCount) { | ||
errorList.push({ | ||
code: 'unused', | ||
traceType: traceType, | ||
templateCount: templateCount, | ||
dataCount: dataCount | ||
}); | ||
} | ||
else if(dataCount > templateCount) { | ||
errorList.push({ | ||
code: 'reused', | ||
traceType: traceType, | ||
templateCount: templateCount, | ||
dataCount: dataCount | ||
}); | ||
} | ||
} | ||
} | ||
|
||
// _template: false is when someone tried to modify an array item | ||
// but there was no template with matching name | ||
function crawlForMissingTemplates(obj, path) { | ||
for(var key in obj) { | ||
if(key.charAt(0) === '_') continue; | ||
var val = obj[key]; | ||
var nextPath = getNextPath(obj, key, path); | ||
if(isPlainObject(val)) { | ||
if(Array.isArray(obj) && val._template === false && val.templateitemname) { | ||
errorList.push({ | ||
code: 'missing', | ||
path: nextPath, | ||
templateitemname: val.templateitemname | ||
}); | ||
} | ||
crawlForMissingTemplates(val, nextPath); | ||
} | ||
else if(Array.isArray(val) && hasPlainObject(val)) { | ||
crawlForMissingTemplates(val, nextPath); | ||
} | ||
} | ||
} | ||
crawlForMissingTemplates({data: fullData, layout: fullLayout}, ''); | ||
|
||
if(errorList.length) return errorList.map(format); | ||
}; | ||
|
||
function hasPlainObject(arr) { | ||
for(var i = 0; i < arr.length; i++) { | ||
if(isPlainObject(arr[i])) return true; | ||
} | ||
} | ||
|
||
function hasMatchingKey(obj, key) { | ||
if(key in obj) return true; | ||
if(getBaseKey(key) !== key) return false; | ||
for(var key2 in obj) { | ||
if(getBaseKey(key2) === key) return true; | ||
} | ||
} | ||
|
||
function format(opts) { | ||
var msg; | ||
switch(opts.code) { | ||
case 'data': | ||
msg = 'The template has no key data.'; | ||
break; | ||
case 'layout': | ||
msg = 'The template has no key layout.'; | ||
break; | ||
case 'missing': | ||
if(opts.path) { | ||
msg = 'There are no templates for item ' + opts.path + | ||
' with name ' + opts.templateitemname; | ||
} | ||
else { | ||
msg = 'There are no templates for trace ' + opts.index + | ||
', of type ' + opts.traceType + '.'; | ||
} | ||
break; | ||
case 'unused': | ||
if(opts.path) { | ||
msg = 'The template item at ' + opts.path + | ||
' was not used in constructing the plot.'; | ||
} | ||
else if(opts.dataCount) { | ||
msg = 'Some of the templates of type ' + opts.traceType + | ||
' were not used. The template has ' + opts.templateCount + | ||
' traces, the data only has ' + opts.dataCount + | ||
' of this type.'; | ||
} | ||
else { | ||
msg = 'The template has ' + opts.templateCount + | ||
' traces of type ' + opts.traceType + | ||
' but there are none in the data.'; | ||
} | ||
break; | ||
case 'reused': | ||
msg = 'Some of the templates of type ' + opts.traceType + | ||
' were used more than once. The template has ' + | ||
opts.templateCount + ' traces, the data has ' + | ||
opts.dataCount + ' of this type.'; | ||
break; | ||
} | ||
opts.msg = msg; | ||
|
||
return opts; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -235,3 +235,106 @@ describe('template interactions', function() { | |
.then(done); | ||
}); | ||
}); | ||
|
||
describe('validateTemplate', function() { | ||
|
||
function checkValidate(mock, expected, countToCheck) { | ||
var template = mock.layout.template; | ||
var mockNoTemplate = Lib.extendDeep({}, mock); | ||
delete mockNoTemplate.layout.template; | ||
|
||
var out1 = Plotly.validateTemplate(mock); | ||
var out2 = Plotly.validateTemplate(mockNoTemplate, template); | ||
expect(out2).toEqual(out1); | ||
if(expected) { | ||
expect(countToCheck ? out1.slice(0, countToCheck) : out1) | ||
.toEqual(expected); | ||
} | ||
else { | ||
expect(out1).toBeUndefined(); | ||
} | ||
} | ||
|
||
var cleanMock = Lib.extendDeep({}, templateMock); | ||
cleanMock.layout.annotations.pop(); | ||
cleanMock.data.pop(); | ||
cleanMock.data.splice(1, 1); | ||
cleanMock.layout.template.data.bar.pop(); | ||
|
||
it('returns undefined when the template matches precisely', function() { | ||
checkValidate(cleanMock); | ||
}); | ||
|
||
it('catches all classes of regular issue', function() { | ||
var messyMock = Lib.extendDeep({}, templateMock); | ||
messyMock.data.push({type: 'box', x0: 1, y: [1, 2, 3]}); | ||
messyMock.layout.template.layout.geo = {projection: {type: 'orthographic'}}; | ||
messyMock.layout.template.layout.xaxis3 = {nticks: 50}; | ||
messyMock.layout.template.data.violin = [{fillcolor: '#000'}]; | ||
|
||
checkValidate(messyMock, [{ | ||
code: 'unused', | ||
path: 'layout.geo', | ||
msg: 'The template item at layout.geo was not used in constructing the plot.' | ||
}, { | ||
code: 'unused', | ||
path: 'layout.xaxis3', | ||
msg: 'The template item at layout.xaxis3 was not used in constructing the plot.' | ||
}, { | ||
code: 'missing', | ||
index: 5, | ||
traceType: 'box', | ||
msg: 'There are no templates for trace 5, of type box.' | ||
}, { | ||
code: 'reused', | ||
traceType: 'scatter', | ||
templateCount: 2, | ||
dataCount: 4, | ||
msg: 'Some of the templates of type scatter were used more than once.' + | ||
' The template has 2 traces, the data has 4 of this type.' | ||
}, { | ||
code: 'unused', | ||
traceType: 'bar', | ||
templateCount: 2, | ||
dataCount: 1, | ||
msg: 'Some of the templates of type bar were not used.' + | ||
' The template has 2 traces, the data only has 1 of this type.' | ||
}, { | ||
code: 'unused', | ||
traceType: 'violin', | ||
templateCount: 1, | ||
dataCount: 0, | ||
msg: 'The template has 1 traces of type violin' + | ||
' but there are none in the data.' | ||
}, { | ||
code: 'missing', | ||
path: 'layout.annotations[4]', | ||
templateitemname: 'nope', | ||
msg: 'There are no templates for item layout.annotations[4] with name nope' | ||
}]); | ||
}); | ||
|
||
it('catches missing template.data', function() { | ||
var noDataMock = Lib.extendDeep({}, cleanMock); | ||
delete noDataMock.layout.template.data; | ||
|
||
checkValidate(noDataMock, [{ | ||
code: 'data', | ||
msg: 'The template has no key data.' | ||
}], | ||
// check only the first error - we don't care about the specifics | ||
// uncovered after we already know there's no template.data | ||
1); | ||
}); | ||
|
||
it('catches missing template.data', function() { | ||
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. 🐄 catches missing template.layout. 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. good catch! 818cac9 |
||
var noLayoutMock = Lib.extendDeep({}, cleanMock); | ||
delete noLayoutMock.layout.template.layout; | ||
|
||
checkValidate(noLayoutMock, [{ | ||
code: 'layout', | ||
msg: 'The template has no key layout.' | ||
}], 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.
This case is just special because we were told to look for a template by name and it wasn't found.
Otherwise
_template
can benull
(no template was found but that's fine) orundefined
(we don't need to propagate the template into this container).