luci-app-statistics: adjust graph size fallback to defaults
[project/luci.git] / applications / luci-app-statistics / htdocs / luci-static / resources / statistics / rrdtool.js
1 'use strict';
2 'require baseclass';
3 'require fs';
4 'require uci';
5 'require tools.prng as random';
6
7 function subst(str, val) {
8 return str.replace(/%(H|pn|pi|dt|di|ds)/g, function(m, p1) {
9 switch (p1) {
10 case 'H': return val.host || '';
11 case 'pn': return val.plugin || '';
12 case 'pi': return val.pinst || '';
13 case 'dt': return val.dtype || '';
14 case 'di': return val.dinst || '';
15 case 'ds': return val.dsrc || '';
16 }
17 });
18 }
19
20 var i18n = L.Class.singleton({
21 title: function(host, plugin, pinst, dtype, dinst, user_title) {
22 var title = user_title || 'p=%s/pi=%s/dt=%s/di=%s'.format(
23 plugin,
24 pinst || '(nil)',
25 dtype || '(nil)',
26 dinst || '(nil)'
27 );
28
29 return subst(title, {
30 host: host,
31 plugin: plugin,
32 pinst: pinst,
33 dtype: dtype,
34 dinst: dinst
35 });
36 },
37
38 label: function(host, plugin, pinst, dtype, dinst, user_label) {
39 var label = user_label || 'dt=%s/%di=%s'.format(
40 dtype || '(nil)',
41 dinst || '(nil)'
42 );
43
44 return subst(label, {
45 host: host,
46 plugin: plugin,
47 pinst: pinst,
48 dtype: dtype,
49 dinst: dinst
50 });
51 },
52
53 ds: function(host, source) {
54 var label = source.title || 'dt=%s/di=%s/ds=%s'.format(
55 source.type || '(nil)',
56 source.instance || '(nil)',
57 source.ds || '(nil)'
58 );
59
60 return subst(label, {
61 host: host,
62 dtype: source.type,
63 dinst: source.instance,
64 dsrc: source.ds
65 }).replace(/:/g, '\\:');
66 }
67 });
68
69 var colors = L.Class.singleton({
70 fromString: function(s) {
71 if (typeof(s) != 'string' || !s.match(/^[0-9a-fA-F]{6}$/))
72 return null;
73
74 return [
75 parseInt(s.substring(0, 2), 16),
76 parseInt(s.substring(2, 4), 16),
77 parseInt(s.substring(4, 6), 16)
78 ];
79 },
80
81 asString: function(c) {
82 if (!Array.isArray(c) || c.length != 3)
83 return null;
84
85 return '%02x%02x%02x'.format(c[0], c[1], c[2]);
86 },
87
88 defined: function(i) {
89 var t = [
90 [230, 25, 75],
91 [245, 130, 48],
92 [255, 225, 25],
93 [60, 180, 75],
94 [70, 240, 240],
95 [0, 130, 200],
96 [0, 0, 128],
97 [170, 110, 40]
98 ];
99
100 return this.asString(t[i % t.length]);
101 },
102
103 random: function() {
104 var r = random.get(255),
105 g = random.get(255),
106 min = 0, max = 255;
107
108 if (r + g < 255)
109 min = 255 - r - g;
110 else
111 max = 511 - r - g;
112
113 var b = min + Math.floor(random.get() * (max - min));
114
115 return [ r, g, b ];
116 },
117
118 faded: function(fg, bg, alpha) {
119 fg = this.fromString(fg) || (this.asString(fg) ? fg : null);
120 bg = this.fromString(bg) || (this.asString(bg) ? bg : [255, 255, 255]);
121 alpha = !isNaN(alpha) ? +alpha : 0.25;
122
123 if (!fg)
124 return null;
125
126 return [
127 (alpha * fg[0]) + ((1.0 - alpha) * bg[0]),
128 (alpha * fg[1]) + ((1.0 - alpha) * bg[1]),
129 (alpha * fg[2]) + ((1.0 - alpha) * bg[2])
130 ];
131 }
132 });
133
134 var rrdtree = {},
135 graphdefs = {};
136
137 return baseclass.extend({
138 __init__: function() {
139 this.opts = {};
140 },
141
142 load: function() {
143 return Promise.all([
144 L.resolveDefault(fs.list('/www' + L.resource('statistics/rrdtool/definitions')), []),
145 fs.trimmed('/proc/sys/kernel/hostname'),
146 uci.load('luci_statistics')
147 ]).then(L.bind(function(data) {
148 var definitions = data[0],
149 hostname = data[1];
150
151 this.opts.host = uci.get('luci_statistics', 'collectd', 'Hostname') || hostname;
152 this.opts.timespan = uci.get('luci_statistics', 'rrdtool', 'default_timespan') || 3600;
153 this.opts.width = uci.get('luci_statistics', 'rrdtool', 'image_width') || 600;
154 this.opts.height = uci.get('luci_statistics', 'rrdtool', 'image_height') || 150;
155 this.opts.rrdpath = (uci.get('luci_statistics', 'collectd_rrdtool', 'DataDir') || '/tmp/rrd').replace(/\/$/, '');
156 this.opts.rrasingle = (uci.get('luci_statistics', 'collectd_rrdtool', 'RRASingle') == '1');
157 this.opts.rramax = (uci.get('luci_statistics', 'collectd_rrdtool', 'RRAMax') == '1');
158
159 graphdefs = {};
160
161 var tasks = [ this.scan() ];
162
163 for (var i = 0; i < definitions.length; i++) {
164 var m = definitions[i].name.match(/^(.+)\.js$/);
165
166 if (definitions[i].type != 'file' || m == null)
167 continue;
168
169 tasks.push(L.require('statistics.rrdtool.definitions.' + m[1]).then(L.bind(function(name, def) {
170 graphdefs[name] = def;
171 }, this, m[1])));
172 }
173
174 return Promise.all(tasks);
175 }, this));
176 },
177
178 ls: function() {
179 var dir = this.opts.rrdpath;
180
181 return L.resolveDefault(fs.list(dir), []).then(function(entries) {
182 var tasks = [];
183
184 for (var i = 0; i < entries.length; i++) {
185 if (entries[i].type != 'directory')
186 continue;
187
188 tasks.push(L.resolveDefault(fs.list(dir + '/' + entries[i].name), []).then(L.bind(function(entries) {
189 var tasks = [];
190
191 for (var j = 0; j < entries.length; j++) {
192 if (entries[j].type != 'directory')
193 continue;
194
195 tasks.push(L.resolveDefault(fs.list(dir + '/' + this.name + '/' + entries[j].name), []).then(L.bind(function(entries) {
196 return Object.assign(this, {
197 entries: entries.filter(function(e) {
198 return e.type == 'file' && e.name.match(/\.rrd$/);
199 })
200 });
201 }, entries[j])));
202 }
203
204 return Promise.all(tasks).then(L.bind(function(entries) {
205 return Object.assign(this, {
206 entries: entries
207 });
208 }, this));
209 }, entries[i])));
210 }
211
212 return Promise.all(tasks);
213 });
214 },
215
216 scan: function() {
217 return this.ls().then(L.bind(function(entries) {
218 rrdtree = {};
219
220 for (var i = 0; i < entries.length; i++) {
221 var hostInstance = entries[i].name;
222
223 rrdtree[hostInstance] = rrdtree[hostInstance] || {};
224
225 for (var j = 0; j < entries[i].entries.length; j++) {
226 var m = entries[i].entries[j].name.match(/^([^-]+)(?:-(.+))?$/);
227
228 if (!m)
229 continue;
230
231 var pluginName = m[1],
232 pluginInstance = m[2] || '';
233
234 rrdtree[hostInstance][pluginName] = rrdtree[hostInstance][pluginName] || {};
235 rrdtree[hostInstance][pluginName][pluginInstance] = rrdtree[hostInstance][pluginName][pluginInstance] || {};
236
237 for (var k = 0; k < entries[i].entries[j].entries.length; k++) {
238 var m = entries[i].entries[j].entries[k].name.match(/^([^-]+)(?:-(.+))?\.rrd$/);
239
240 if (!m)
241 continue;
242
243 var dataType = m[1],
244 dataInstance = m[2] || '';
245
246 rrdtree[hostInstance][pluginName][pluginInstance][dataType] = rrdtree[hostInstance][pluginName][pluginInstance][dataType] || [];
247 rrdtree[hostInstance][pluginName][pluginInstance][dataType].push(dataInstance);
248 }
249 }
250 }
251 }, this));
252 },
253
254 hostInstances: function() {
255 return Object.keys(rrdtree).sort();
256 },
257
258 pluginNames: function(hostInstance) {
259 return Object.keys(rrdtree[hostInstance] || {}).sort();
260 },
261
262 pluginInstances: function(hostInstance, pluginName) {
263 return Object.keys((rrdtree[hostInstance] || {})[pluginName] || {}).sort(function(a, b) {
264 var x = a.match(/^(\d+)\b/),
265 y = b.match(/^(\d+)\b/);
266
267 if (!x != !y)
268 return !x - !y;
269 else if (x && y && x[0] != y[0])
270 return +x[0] - +y[0];
271 else
272 return a > b;
273 });
274 },
275
276 dataTypes: function(hostInstance, pluginName, pluginInstance) {
277 return Object.keys(((rrdtree[hostInstance] || {})[pluginName] || {})[pluginInstance] || {}).sort();
278 },
279
280 dataInstances: function(hostInstance, pluginName, pluginInstance, dataType) {
281 return ((((rrdtree[hostInstance] || {})[pluginName] || {})[pluginInstance] || {})[dataType] || []).sort();
282 },
283
284 pluginTitle: function(pluginName) {
285 var def = graphdefs[pluginName];
286 return (def ? def.title : null) || pluginName;
287 },
288
289 hasDefinition: function(pluginName) {
290 return (graphdefs[pluginName] != null);
291 },
292
293 hasInstanceDetails: function(hostInstance, pluginName, pluginInstance) {
294 var def = graphdefs[pluginName];
295
296 if (!def || typeof(def.rrdargs) != 'function')
297 return false;
298
299 var optlist = this._forcelol(def.rrdargs(this, hostInstance, pluginName, pluginInstance, null, false));
300
301 for (var i = 0; i < optlist.length; i++)
302 if (optlist[i].detail)
303 return true;
304
305 return false;
306 },
307
308 _mkpath: function(host, plugin, plugin_instance, dtype, data_instance) {
309 var path = host + '/' + plugin;
310
311 if (plugin_instance != null && plugin_instance != '')
312 path += '-' + plugin_instance;
313
314 path += '/' + dtype;
315
316 if (data_instance != null && data_instance != '')
317 path += '-' + data_instance;
318
319 return path;
320 },
321
322 mkrrdpath: function(/* ... */) {
323 return '%s/%s.rrd'.format(
324 this.opts.rrdpath,
325 this._mkpath.apply(this, arguments)
326 ).replace(/[\\:]/g, '\\$&');
327 },
328
329 _forcelol: function(list) {
330 return L.isObject(list[0]) ? list : [ list ];
331 },
332
333 _rrdtool: function(def, rrd, timespan, width, height, cache) {
334 var cmdline = [
335 'graph', '-', '-a', 'PNG',
336 '-s', 'NOW-%s'.format(timespan || this.opts.timespan),
337 '-e', 'NOW-15',
338 '-w', width || this.opts.width,
339 '-h', height || this.opts.height
340 ];
341
342 for (var i = 0; i < def.length; i++) {
343 var opt = String(def[i]);
344
345 if (rrd)
346 opt = opt.replace(/\{file\}/g, rrd);
347
348 cmdline.push(opt);
349 }
350
351 if (L.isObject(cache)) {
352 var key = sfh(cmdline.join('\0'));
353
354 if (!cache.hasOwnProperty(key))
355 cache[key] = fs.exec_direct('/usr/bin/rrdtool', cmdline, 'blob', true);
356
357 return cache[key];
358 }
359
360 return fs.exec_direct('/usr/bin/rrdtool', cmdline, 'blob', true);
361 },
362
363 _generic: function(opts, host, plugin, plugin_instance, dtype, index) {
364 var defs = [],
365 gopts = this.opts,
366 _args = [],
367 _sources = [],
368 _stack_neg = [],
369 _stack_pos = [],
370 _longest_name = 0,
371 _has_totals = false;
372
373 /* use the plugin+instance+type as seed for the prng to ensure the
374 same pseudo-random color sequence for each render */
375 random.seed(sfh([plugin, plugin_instance || '', dtype || ''].join('.')));
376
377 function __def(source) {
378 var inst = source.sname,
379 rrd = source.rrd,
380 ds = source.ds || 'value';
381
382 _args.push(
383 'DEF:%s_avg_raw=%s:%s:AVERAGE'.format(inst, rrd, ds),
384 'CDEF:%s_avg=%s_avg_raw,%s'.format(inst, inst, source.transform_rpn)
385 );
386
387 if (!gopts.rrasingle)
388 _args.push(
389 'DEF:%s_min_raw=%s:%s:MIN'.format(inst, rrd, ds),
390 'CDEF:%s_min=%s_min_raw,%s'.format(inst, inst, source.transform_rpn),
391 'DEF:%s_max_raw=%s:%s:MAX'.format(inst, rrd, ds),
392 'CDEF:%s_max=%s_max_raw,%s'.format(inst, inst, source.transform_rpn)
393 );
394
395 _args.push(
396 'CDEF:%s_nnl=%s_avg,UN,0,%s_avg,IF'.format(inst, inst, inst)
397 );
398 }
399
400 function __cdef(source) {
401 var prev;
402
403 if (source.flip)
404 prev = _stack_neg[_stack_neg.length - 1];
405 else
406 prev = _stack_pos[_stack_pos.length - 1];
407
408 /* is first source in stack or overlay source: source_stk = source_nnl */
409 if (prev == null || source.overlay) {
410 /* create cdef statement for cumulative stack (no NaNs) and also
411 for display (preserving NaN where no points should be displayed) */
412 if (gopts.rrasingle || !gopts.rramax)
413 _args.push(
414 'CDEF:%s_stk=%s_nnl'.format(source.sname, source.sname),
415 'CDEF:%s_plot=%s_avg'.format(source.sname, source.sname)
416 );
417 else
418 _args.push(
419 'CDEF:%s_stk=%s_nnl'.format(source.sname, source.sname),
420 'CDEF:%s_plot=%s_max'.format(source.sname, source.sname)
421 );
422 }
423 /* is subsequent source without overlay: source_stk = source_nnl + previous_stk */
424 else {
425 /* create cdef statement */
426 if (gopts.rrasingle || !gopts.rramax)
427 _args.push(
428 'CDEF:%s_stk=%s_nnl,%s_stk,+'.format(source.sname, source.sname, prev),
429 'CDEF:%s_plot=%s_avg,%s_stk,+'.format(source.sname, source.sname, prev)
430 );
431 else
432 _args.push(
433 'CDEF:%s_stk=%s_nnl,%s_stk,+'.format(source.sname, source.sname, prev),
434 'CDEF:%s_plot=%s_max,%s_stk,+'.format(source.sname, source.sname, prev)
435 );
436 }
437
438 /* create multiply by minus one cdef if flip is enabled */
439 if (source.flip) {
440 _args.push('CDEF:%s_neg=%s_plot,-1,*'.format(source.sname, source.sname));
441
442 /* push to negative stack if overlay is disabled */
443 if (!source.overlay)
444 _stack_neg.push(source.sname);
445 }
446
447 /* no flipping, push to positive stack if overlay is disabled */
448 else if (!source.overlay) {
449 /* push to positive stack */
450 _stack_pos.push(source.sname);
451 }
452
453 /* calculate total amount of data if requested */
454 if (source.total)
455 _args.push(
456 'CDEF:%s_avg_sample=%s_avg,UN,0,%s_avg,IF,sample_len,*'.format(source.sname, source.sname, source.sname),
457 'CDEF:%s_avg_sum=PREV,UN,0,PREV,IF,%s_avg_sample,+'.format(source.sname, source.sname, source.sname)
458 );
459 }
460
461 /* local helper: create cdefs required for calculating total values */
462 function __cdef_totals() {
463 if (_has_totals)
464 _args.push(
465 'CDEF:mytime=%s_avg,TIME,TIME,IF'.format(_sources[0].sname),
466 'CDEF:sample_len_raw=mytime,PREV(mytime),-',
467 'CDEF:sample_len=sample_len_raw,UN,0,sample_len_raw,IF'
468 );
469 }
470
471 /* local helper: create line and area statements */
472 function __line(source) {
473 var line_color, area_color, legend, variable;
474
475 /* find colors: try source, then opts.colors; fall back to random color */
476 if (typeof(source.color) == 'string') {
477 line_color = source.color;
478 area_color = colors.fromString(line_color);
479 }
480 else if (typeof(opts.colors[source.name.replace(/\W/g, '_')]) == 'string') {
481 line_color = opts.colors[source.name.replace(/\W/g, '_')];
482 area_color = colors.fromString(line_color);
483 }
484 else {
485 area_color = colors.random();
486 line_color = colors.asString(area_color);
487 }
488
489 /* derive area background color from line color */
490 area_color = colors.asString(colors.faded(area_color));
491
492 /* choose source_plot or source_neg variable depending on flip state */
493 variable = source.flip ? 'neg' : 'plot';
494
495 /* create legend */
496 legend = '%%-%us'.format(_longest_name).format(source.title);
497
498 /* create area is not disabled */
499 if (!source.noarea)
500 _args.push('AREA:%s_%s#%s'.format(source.sname, variable, area_color));
501
502 /* create line statement */
503 _args.push('LINE%d:%s_%s#%s:%s'.format(
504 source.width || (source.noarea ? 2 : 1),
505 source.sname, variable, line_color, legend
506 ));
507 }
508
509 /* local helper: create gprint statements */
510 function __gprint(source) {
511 var numfmt = opts.number_format || '%6.1lf',
512 totfmt = opts.totals_format || '%5.1lf%s';
513
514 /* don't include MIN if rrasingle is enabled */
515 if (!gopts.rrasingle)
516 _args.push('GPRINT:%s_min:MIN:\tMin\\: %s'.format(source.sname, numfmt));
517
518 /* don't include AVERAGE if noavg option is set */
519 if (!source.noavg)
520 _args.push('GPRINT:%s_avg:AVERAGE:\tAvg\\: %s'.format(source.sname, numfmt));
521
522 /* don't include MAX if rrasingle is enabled */
523 if (!gopts.rrasingle)
524 _args.push('GPRINT:%s_max:MAX:\tMax\\: %s'.format(source.sname, numfmt));
525
526 /* include total count if requested else include LAST */
527 if (source.total)
528 _args.push('GPRINT:%s_avg_sum:LAST:(ca. %s Total)\\l'.format(source.sname, totfmt));
529 else
530 _args.push('GPRINT:%s_avg:LAST:\tLast\\: %s\\l'.format(source.sname, numfmt));
531 }
532
533 /*
534 * find all data sources
535 */
536
537 /* find data types */
538 var data_types = dtype ? [ dtype ] : (opts.data.types || []);
539
540 if (!(dtype || opts.data.types)) {
541 if (L.isObject(opts.data.instances))
542 data_types.push.apply(data_types, Object.keys(opts.data.instances));
543 else if (L.isObject(opts.data.sources))
544 data_types.push.apply(data_types, Object.keys(opts.data.sources));
545
546 }
547
548 /* iterate over data types */
549 for (var i = 0; i < data_types.length; i++) {
550 /* find instances */
551 var data_instances;
552
553 if (!opts.per_instance) {
554 if (L.isObject(opts.data.instances) && Array.isArray(opts.data.instances[data_types[i]]))
555 data_instances = opts.data.instances[data_types[i]];
556 else
557 data_instances = this.dataInstances(host, plugin, plugin_instance, data_types[i]);
558 }
559
560 if (!Array.isArray(data_instances) || data_instances.length == 0)
561 data_instances = [ '' ];
562
563 /* iterate over data instances */
564 for (var j = 0; j < data_instances.length; j++) {
565 /* construct combined data type / instance name */
566 var dname = data_types[i];
567
568 if (data_instances[j].length)
569 dname += '_' + data_instances[j];
570
571 /* find sources */
572 var data_sources = [ 'value' ];
573
574 if (L.isObject(opts.data.sources)) {
575 if (Array.isArray(opts.data.sources[dname]))
576 data_sources = opts.data.sources[dname];
577 else if (Array.isArray(opts.data.sources[data_types[i]]))
578 data_sources = opts.data.sources[data_types[i]];
579 }
580
581 /* iterate over data sources */
582 for (var k = 0; k < data_sources.length; k++) {
583 var dsname = data_types[i] + '_' + data_instances[j].replace(/\W/g, '_') + '_' + data_sources[k],
584 altname = data_types[i] + '__' + data_sources[k];
585
586 /* find datasource options */
587 var dopts = {};
588
589 if (L.isObject(opts.data.options)) {
590 if (L.isObject(opts.data.options[dsname]))
591 dopts = opts.data.options[dsname];
592 else if (L.isObject(opts.data.options[altname]))
593 dopts = opts.data.options[altname];
594 else if (L.isObject(opts.data.options[dname]))
595 dopts = opts.data.options[dname];
596 else if (L.isObject(opts.data.options[data_types[i]]))
597 dopts = opts.data.options[data_types[i]];
598 }
599
600 /* store values */
601 var source = {
602 rrd: dopts.rrd || this.mkrrdpath(host, plugin, plugin_instance, data_types[i], data_instances[j]),
603 color: dopts.color || colors.asString(colors.random()),
604 flip: dopts.flip || false,
605 total: dopts.total || false,
606 overlay: dopts.overlay || false,
607 transform_rpn: dopts.transform_rpn || '0,+',
608 noarea: dopts.noarea || false,
609 noavg: dopts.noavg || false,
610 title: dopts.title || null,
611 weight: dopts.weight || (dopts.negweight ? -+data_instances[j] : null) || (dopts.posweight ? +data_instances[j] : null) || null,
612 ds: data_sources[k],
613 type: data_types[i],
614 instance: data_instances[j],
615 index: _sources.length + 1,
616 sname: String(_sources.length + 1) + data_types[i]
617 };
618
619 _sources.push(source);
620
621 /* generate datasource title */
622 source.title = i18n.ds(host, source);
623
624 /* find longest name */
625 _longest_name = Math.max(_longest_name, source.title.length);
626
627 /* has totals? */
628 if (source.total)
629 _has_totals = true;
630 }
631 }
632 }
633
634 /*
635 * construct diagrams
636 */
637
638 /* if per_instance is enabled then find all instances from the first datasource in diagram */
639 /* if per_instance is disabled then use an empty pseudo instance and use model provided values */
640 var instances = [ '' ];
641
642 if (opts.per_instance)
643 instances = this.dataInstances(host, plugin, plugin_instance, _sources[0].type);
644
645 /* iterate over instances */
646 for (var i = 0; i < instances.length; i++) {
647 /* store title and vlabel */
648 _args.push(
649 '-t', i18n.title(host, plugin, plugin_instance, _sources[0].type, instances[i], opts.title),
650 '-v', i18n.label(host, plugin, plugin_instance, _sources[0].type, instances[i], opts.vlabel)
651 );
652
653 if (opts.y_max)
654 _args.push('-u', String(opts.y_max));
655
656 if (opts.y_min)
657 _args.push('-l', String(opts.y_min));
658
659 if (opts.units_exponent)
660 _args.push('-X', String(opts.units_exponent));
661
662 if (opts.alt_autoscale)
663 _args.push('-A');
664
665 if (opts.alt_autoscale_max)
666 _args.push('-M');
667
668 /* store additional rrd options */
669 if (Array.isArray(opts.rrdopts))
670 for (var j = 0; j < opts.rrdopts.length; j++)
671 _args.push(String(opts.rrdopts[j]));
672
673 /* sort sources */
674 _sources.sort(function(a, b) {
675 var x = a.weight || a.index || 0,
676 y = b.weight || b.index || 0;
677
678 return +x - +y;
679 });
680
681 /* define colors in order */
682 if (opts.ordercolor)
683 for (var j = 0; j < _sources.length; j++)
684 _sources[j].color = colors.defined(j);
685
686 /* create DEF statements for each instance */
687 for (var j = 0; j < _sources.length; j++) {
688 /* fixup properties for per instance mode... */
689 if (opts.per_instance) {
690 _sources[j].instance = instances[i];
691 _sources[j].rrd = this.mkrrdpath(host, plugin, plugin_instance, _sources[j].type, instances[i]);
692 }
693
694 __def(_sources[j]);
695 }
696
697 /* create CDEF required for calculating totals */
698 __cdef_totals();
699
700 /* create CDEF statements for each instance in reversed order */
701 for (var j = _sources.length - 1; j >= 0; j--)
702 __cdef(_sources[j]);
703
704 /* create LINE1, AREA and GPRINT statements for each instance */
705 for (var j = 0; j < _sources.length; j++) {
706 __line(_sources[j]);
707 __gprint(_sources[j]);
708 }
709
710 /* push arg stack to definition list */
711 defs.push(_args);
712
713 /* reset stacks */
714 _args = [];
715 _stack_pos = [];
716 _stack_neg = [];
717 }
718
719 return defs;
720 },
721
722 render: function(plugin, plugin_instance, is_index, hostname, timespan, width, height, cache) {
723 var pngs = [];
724
725 /* check for a whole graph handler */
726 var def = graphdefs[plugin];
727
728 if (def && typeof(def.rrdargs) == 'function') {
729 /* temporary image matrix */
730 var _images = [];
731
732 /* get diagram definitions */
733 var optlist = this._forcelol(def.rrdargs(this, hostname, plugin, plugin_instance, null, is_index));
734 for (var i = 0; i < optlist.length; i++) {
735 var opt = optlist[i];
736 if (!is_index || !opt.detail) {
737 _images[i] = [];
738
739 /* get diagram definition instances */
740 var diagrams = this._generic(opt, hostname, plugin, plugin_instance, null, i);
741
742 /* render all diagrams */
743 for (var j = 0; j < diagrams.length; j++) {
744 /* exec */
745 _images[i][j] = this._rrdtool(diagrams[j], null, timespan, width, height, cache);
746 }
747 }
748 }
749
750 /* remember images - XXX: fixme (will cause probs with asymmetric data) */
751 for (var y = 0; y < _images[0].length; y++)
752 for (var x = 0; x < _images.length; x++)
753 pngs.push(_images[x][y]);
754 }
755
756 return Promise.all(pngs);
757 }
758 });