@@ -30,7 +30,7 @@ var dfltConfig = require('./plot_config');
30
30
* @returns {object } template: the extracted template - can then be used as
31
31
* `layout.template` in another figure.
32
32
*/
33
- module . exports = function makeTemplate ( figure ) {
33
+ exports . makeTemplate = function ( figure ) {
34
34
figure = Lib . extendDeep ( { _context : dfltConfig } , figure ) ;
35
35
Plots . supplyDefaults ( figure ) ;
36
36
var data = figure . data || [ ] ;
@@ -269,3 +269,187 @@ function getNextPath(parent, key, path) {
269
269
270
270
return nextPath ;
271
271
}
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