@@ -30,7 +30,7 @@ var dfltConfig = require('./plot_config');
3030 * @returns {object } template: the extracted template - can then be used as
3131 * `layout.template` in another figure.
3232 */
33- module . exports = function makeTemplate ( figure ) {
33+ exports . makeTemplate = function ( figure ) {
3434 figure = Lib . extendDeep ( { _context : dfltConfig } , figure ) ;
3535 Plots . supplyDefaults ( figure ) ;
3636 var data = figure . data || [ ] ;
@@ -269,3 +269,187 @@ function getNextPath(parent, key, path) {
269269
270270 return nextPath ;
271271}
272+
273+ /**
274+ * validateTemplate: Test for consistency between the given figure and
275+ * a template, either already included in the figure or given separately.
276+ * Note that not every issue we identify here is necessarily a problem,
277+ * it depends on what you're using the template for.
278+ *
279+ * @param {object|DOM element } figure: the plot, with {data, layout} members,
280+ * to test the template against
281+ * @param {Optional(object) } template: the template, with its own {data, layout},
282+ * to test. If omitted, we will look for a template already attached as the
283+ * plot's `layout.template` attribute.
284+ *
285+ * @returns {array } array of error objects each containing:
286+ * - {string} code
287+ * error code ('missing', 'unused', 'reused', 'noLayout', 'noData')
288+ * - {string} msg
289+ * a full readable description of the issue.
290+ */
291+ exports . validateTemplate = function ( figureIn , template ) {
292+ var figure = Lib . extendDeep ( { } , {
293+ _context : dfltConfig ,
294+ data : figureIn . data ,
295+ layout : figureIn . layout
296+ } ) ;
297+ var layout = figure . layout || { } ;
298+ if ( ! isPlainObject ( template ) ) template = layout . template || { } ;
299+ var layoutTemplate = template . layout ;
300+ var dataTemplate = template . data ;
301+ var errorList = [ ] ;
302+
303+ figure . layout = layout ;
304+ figure . layout . template = template ;
305+ Plots . supplyDefaults ( figure ) ;
306+
307+ var fullLayout = figure . _fullLayout ;
308+ var fullData = figure . _fullData ;
309+
310+ if ( ! isPlainObject ( layoutTemplate ) ) {
311+ errorList . push ( { code : 'layout' } ) ;
312+ }
313+ else {
314+ // TODO: any need to look deeper than the first level of layout?
315+ // I don't think so, that gets all the subplot types which should be
316+ // sufficient.
317+ for ( var key in layoutTemplate ) {
318+ if ( key . indexOf ( 'defaults' ) === - 1 && isPlainObject ( layoutTemplate [ key ] ) &&
319+ ! hasMatchingKey ( fullLayout , key )
320+ ) {
321+ errorList . push ( { code : 'unused' , path : 'layout.' + key } ) ;
322+ }
323+ }
324+ }
325+
326+ if ( ! isPlainObject ( dataTemplate ) ) {
327+ errorList . push ( { code : 'data' } ) ;
328+ }
329+ else {
330+ var typeCount = { } ;
331+ var traceType ;
332+ for ( var i = 0 ; i < fullData . length ; i ++ ) {
333+ var fullTrace = fullData [ i ] ;
334+ traceType = fullTrace . type ;
335+ typeCount [ traceType ] = ( typeCount [ traceType ] || 0 ) + 1 ;
336+ if ( ! fullTrace . _fullInput . _template ) {
337+ // this takes care of the case of traceType in the data but not
338+ // the template
339+ errorList . push ( {
340+ code : 'missing' ,
341+ index : fullTrace . _fullInput . index ,
342+ traceType : traceType
343+ } ) ;
344+ }
345+ }
346+ for ( traceType in dataTemplate ) {
347+ var templateCount = dataTemplate [ traceType ] . length ;
348+ var dataCount = typeCount [ traceType ] || 0 ;
349+ if ( templateCount > dataCount ) {
350+ errorList . push ( {
351+ code : 'unused' ,
352+ traceType : traceType ,
353+ templateCount : templateCount ,
354+ dataCount : dataCount
355+ } ) ;
356+ }
357+ else if ( dataCount > templateCount ) {
358+ errorList . push ( {
359+ code : 'reused' ,
360+ traceType : traceType ,
361+ templateCount : templateCount ,
362+ dataCount : dataCount
363+ } ) ;
364+ }
365+ }
366+ }
367+
368+ // _template: false is when someone tried to modify an array item
369+ // but there was no template with matching name
370+ function crawlForMissingTemplates ( obj , path ) {
371+ for ( var key in obj ) {
372+ if ( key . charAt ( 0 ) === '_' ) continue ;
373+ var val = obj [ key ] ;
374+ var nextPath = getNextPath ( obj , key , path ) ;
375+ if ( isPlainObject ( val ) ) {
376+ if ( Array . isArray ( obj ) && val . _template === false && val . templateitemname ) {
377+ errorList . push ( {
378+ code : 'missing' ,
379+ path : nextPath ,
380+ templateitemname : val . templateitemname
381+ } ) ;
382+ }
383+ crawlForMissingTemplates ( val , nextPath ) ;
384+ }
385+ else if ( Array . isArray ( val ) && hasPlainObject ( val ) ) {
386+ crawlForMissingTemplates ( val , nextPath ) ;
387+ }
388+ }
389+ }
390+ crawlForMissingTemplates ( { data : fullData , layout : fullLayout } , '' ) ;
391+
392+ if ( errorList . length ) return errorList . map ( format ) ;
393+ } ;
394+
395+ function hasPlainObject ( arr ) {
396+ for ( var i = 0 ; i < arr . length ; i ++ ) {
397+ if ( isPlainObject ( arr [ i ] ) ) return true ;
398+ }
399+ }
400+
401+ function hasMatchingKey ( obj , key ) {
402+ if ( key in obj ) return true ;
403+ if ( getBaseKey ( key ) !== key ) return false ;
404+ for ( var key2 in obj ) {
405+ if ( getBaseKey ( key2 ) === key ) return true ;
406+ }
407+ }
408+
409+ function format ( opts ) {
410+ var msg ;
411+ switch ( opts . code ) {
412+ case 'data' :
413+ msg = 'The template has no key data.' ;
414+ break ;
415+ case 'layout' :
416+ msg = 'The template has no key layout.' ;
417+ break ;
418+ case 'missing' :
419+ if ( opts . path ) {
420+ msg = 'There are no templates for item ' + opts . path +
421+ ' with name ' + opts . templateitemname ;
422+ }
423+ else {
424+ msg = 'There are no templates for trace ' + opts . index +
425+ ', of type ' + opts . traceType + '.' ;
426+ }
427+ break ;
428+ case 'unused' :
429+ if ( opts . path ) {
430+ msg = 'The template item at ' + opts . path +
431+ ' was not used in constructing the plot.' ;
432+ }
433+ else if ( opts . dataCount ) {
434+ msg = 'Some of the templates of type ' + opts . traceType +
435+ ' were not used. The template has ' + opts . templateCount +
436+ ' traces, the data only has ' + opts . dataCount +
437+ ' of this type.' ;
438+ }
439+ else {
440+ msg = 'The template has ' + opts . templateCount +
441+ ' traces of type ' + opts . traceType +
442+ ' but there are none in the data.' ;
443+ }
444+ break ;
445+ case 'reused' :
446+ msg = 'Some of the templates of type ' + opts . traceType +
447+ ' were used more than once. The template has ' +
448+ opts . templateCount + ' traces, the data has ' +
449+ opts . dataCount + ' of this type.' ;
450+ break ;
451+ }
452+ opts . msg = msg ;
453+
454+ return opts ;
455+ }
0 commit comments