@cryptotaxi247 / netdata-1 / commits / d160906f3

Refactoring dashboard.js, splitting monolithic file into multiple source files. (#4496)

* Mild cleanup of dashboard.js (#4461) * Balancing of else clauses * More cleanup, fixed nasty bug introduced by previous commit * More cleanup * More refactoring * Wip * More refactoring * Wip * Cleanup, wip * Refactor for-loop * Remove .hasOwnProperties, wip * Split dashboard.js into multiple files, wip #4479 * Fixed a bug in common min/max * Extracted more files from dashboard.js * Extracted more code, wip * Extract units-conversion.js and sparkline.js * Do not include morris.js, c3.js and raphael.js in dashbaord.js They are extracted as separate files but not included in dashboards.js * Removed refs to obsolete charting libraries #4492 * Extracted google-charts #4479 * Extracted google-charts * Removed refs to morris, c3, raphael from dashboard.html * Wip #4479 * Extracted more charting libraries, cleanup * Organize charting libs in subfolder * Apply WebStorm formatter (mostly adds missing braces) * Additional formatting with WebStorm #4461 * Extracted d3pie, peity, localstorage * Extracted options.js, fixed some issues with dependencies, wip * Extracted boot.js * Fixes in for loops #4461 * Removed extrtacted d3pie from main file * Extracted registry.js * Extracted alarms.js * Extracted error-handling.js * Extracted fast formatting to utils.js * Extracted chart libraries registration code * Extracted some js/css loading code into boot.js * Added some info on generating dashboard.js * Fixed issues reported by LGTM * Minor * added slitted dashboard.js to makefiles * updated merged dashboard.js * excluded paths from codacy * minor pending fixes * updated README.md for release 1.11

George Moschovitis committed Nov 8, 2018 at 02:42 UTC d160906f3e7f4ec024b643b577679bff5a40ac7e
34 files changed +18812 -7955
.codacy.yml
+4 -4
@@ -4,9 +4,9 @@ exclude_paths:
4 - collectors/python.d.plugin/python_modules/pyyaml3/**
5 - collectors/python.d.plugin/python_modules/urllib3/**
6 - collectors/python.d.plugin/python_modules/third_party/**
7 - - web/css/**
8 - - web/lib/**
9 - - web/old/**
10 - - web/gui/src/**
7 - collectors/node.d.plugin/node_modules/**
8 + - web/gui/css/**
9 + - web/gui/lib/**
10 + - web/gui/old/**
11 + - web/gui/src/**
12 - tests/**
README.md
+16 -44
@@ -64,6 +64,22 @@ netdata currently runs on **Linux**, **FreeBSD**, and **MacOS**.
64
65 ## News
66
67 +`Nov 6th, 2018` - **[netdata v1.11.0 released!](https://github.com/netdata/netdata/releases)**
68 +
69 + - New query engine, supporting statistical functions.
70 + - Fixed security issues identified by Red4Sec.com and Synacktiv.
71 + - New Data Collection Modules: `rethinkdbs`, `proxysql`, `litespeed`, `uwsgi`, `unbound`, `powerdns`, `dockerd`, `puppet`, `logind`, `adaptec_raid`, `megacli`, `spigotmc`, `boinc`, `w1sensor`, `monit`, `linux_power_supplies`.
72 + - Improved Data Collection Modules: `statsd.plugin`, `apps.plugin`, `freeipmi.plugin`, `proc.plugin`, `diskspace.plugin`, `freebsd.plugin`, `python.d.plugin`, `web_log`, `nginx_plus`, `ipfs`, `fail2ban`, `ceph`, `elasticsearch`, `nginx_plus`, `redis`,
73 + `beanstalk`, `mysql`, `varnish`, `couchdb`, `phpfpm`, `apache`, `icecast`, `mongodb`, `postgress`, `elasticsearch`, `mdstat`, `openvpn_log`, `snmp`, `nut`.
74 +
75 + - Added alarms for detecting abnormally high load average, `TCP` `SYN` and `TCP` accept queue overflows, network interfaces congestion and alarms for `bcache`, `mdstat`, `apcupsd`, `mysql`.
76 + - system alarms are now enabled on FreeBSD.
77 + - New notification methods: **rocket.chat**, **Microsoft Teams**, **syslog**, **fleep.io**, **Amazon SNS**.
78 +
79 + - and dozens more improvements, enhancements, features and compatibility fixes
80 +
81 +---
82 +
83 `Sep 18, 2018` - **netdata has its own organization**
84
85 Netdata used to be a [firehol.org](https://firehol.org) project, accessible as `firehol/netdata`.
@@ -82,50 +98,6 @@ Read the [netdata presentation](https://docs.google.com/presentation/d/18C8bCTbt
98
99 ---
100
85 -`Mar 27th, 2018` - **[netdata v1.10.0 released!](https://github.com/netdata/netdata/releases)**
86 -
87 - - new web server, a lot faster and more secure
88 - - updated all javascript libraries to their latest versions (fixed compatibility issues - now netdata chart can now be embedded on **Atlassian Confluence** pages and remain fully interactive!)
89 - - new plugins:
90 - - **BTRFS** (visualize BTRFS allocation with alarms)
91 - - **bcache** (monitor hybrid setups HDD + SSD)
92 - - **ceph**
93 - - **nginx plus**
94 - - **libreswan** (monitor the traffic of IPSEC tunnels)
95 - - **traefik**
96 - - **icecast**
97 - - **ntpd**
98 - - **httpcheck** (monitor any remote web server)
99 - - **portcheck** (monitor any remote TCP port)
100 - - **spring-boot** (monitor java spring-boot apps)
101 - - **dnsdist**
102 - - **Linux hugepages**
103 - - improved plugins:
104 - - **statsd**
105 - - **web_log**
106 - - **cgroups** for containers and VMs monitoring (netdata now supports **systemd-nspawn** and **kubernetes** - fixed security issue with `cgroup-network`)
107 - - **Linux memory**
108 - - **diskspace**
109 - - **network interfaces**
110 - - **postgres**
111 - - **rabbitmq**
112 - - **apps.plugin** (now it also tracks swap usage per process)
113 - - **haproxy**
114 - - **uptime**
115 - - **ksm** (kernel memory debupper)
116 - - **mdstat** (software raid)
117 - - **elasticsearch**
118 - - **apcupsd**
119 - - **dhcpd**
120 - - **fronius**
121 - - **stiebeletron**
122 - - new alarm notification methods
123 - - **alerta**
124 - - **IRC** (post on IRC channels)
125 - - and dozens more improvements, enhancements, features and compatibility fixes
126 -
127 ----
128 -
101 ## Features
102
103 <p align="center">
diagrams/Makefile.am
+10
@@ -1,4 +1,5 @@
1 # SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 MAINTAINERCLEANFILES= $(srcdir)/Makefile.in
4
5 dist_noinst_DATA = \
@@ -7,6 +8,15 @@ dist_noinst_DATA = \
8 netdata-for-ephemeral-nodes.xml \
9 netdata-proxies-example.xml \
10 netdata-overview.xml \
11 + data_structures/netdata_config.svg \
12 + data_structures/README.md \
13 + data_structures/registry.svg \
14 + data_structures/rrd.svg \
15 + data_structures/web.svg \
16 + data_structures/src/netdata_config.xml \
17 + data_structures/src/registry.xml \
18 + data_structures/src/rrd.xml \
19 + data_structures/src/web.xml \
20 $(NULL)
21
22 dist_noinst_SCRIPTS = \
web/gui/Makefile.am
+36 -1
@@ -3,9 +3,40 @@
3 # SPDX-License-Identifier: GPL-3.0-or-later
4 #
5 MAINTAINERCLEANFILES= $(srcdir)/Makefile.in
6 +CLEANFILES = \
7 + dashboard.js \
8 + version.txt \
9 + $(NULL)
10 +
11 +DASHBOARD_JS_FILES = \
12 + src/dashboard.js/prologue.js.inc \
13 + src/dashboard.js/utils.js \
14 + src/dashboard.js/error-handling.js \
15 + src/dashboard.js/compatibility.js \
16 + src/dashboard.js/xss.js \
17 + src/dashboard.js/colors.js \
18 + src/dashboard.js/units-conversion.js \
19 + src/dashboard.js/options.js \
20 + src/dashboard.js/localstorage.js \
21 + src/dashboard.js/charting/dygraph.js \
22 + src/dashboard.js/charting/sparkline.js \
23 + src/dashboard.js/charting/google-charts.js \
24 + src/dashboard.js/charting/gauge.js \
25 + src/dashboard.js/charting/easy-pie-chart.js \
26 + src/dashboard.js/charting/d3pie.js \
27 + src/dashboard.js/charting/d3.js \
28 + src/dashboard.js/charting/peity.js \
29 + src/dashboard.js/charting.js \
30 + src/dashboard.js/main.js \
31 + src/dashboard.js/alarms.js \
32 + src/dashboard.js/registry.js \
33 + src/dashboard.js/boot.js \
34 + src/dashboard.js/epilogue.js.inc \
35 + $(NULL)
36
37 dist_noinst_DATA = \
38 README.md \
39 + $(DASHBOARD_JS_FILES) \
40 $(NULL)
41
42 dist_web_DATA = \
@@ -103,6 +134,9 @@ dist_webimages_DATA = \
134 images/seo-performance-multi-size.icns \
135 $(NULL)
136
137 +dashboard.js: $(DASHBOARD_JS_FILES)
138 + if test -f $@; then rm -f $@; fi
139 + cat $(DASHBOARD_JS_FILES) > $@.tmp && mv $@.tmp $@
140
141 webwellknowndir=$(webdir)/.well-known
142 dist_webwellknown_DATA = \
@@ -120,4 +154,5 @@ version.txt:
154 test -s $@.tmp || echo 0 > $@.tmp
155 mv $@.tmp $@
156
123 -.PHONY: version.txt
157 +# regenerate these files, even if they are up to date
158 +.PHONY: version.txt dashboard.js
web/gui/README.md
+11 -2
@@ -1,3 +1,12 @@
1 -# Web GUI
1 +# Netdata Agent Web GUI
2
3 -A Graphical User Interface for visualizing data collected by Netdata.
3 +## Generating dashboard.js
4 +
5 +The monolithic `dashboards.js` file is automatically generated by concatenating the source files located in the `web/gui/src` directory by running the build script:
6 +
7 +```sh
8 +cd web/gui
9 +./build-dashboard-js.sh
10 +```
11 +
12 +After every change in the `src` directory, the `dashboard.js` file should be regenerated and commited to the repository.
web/gui/dashboard.html
+4 -6
@@ -56,7 +56,7 @@ This is a template for building custom dashboards. To build a dashboard you just
56 <li>You can host your dashboard anywhere.</li>
57 <li>You can add as many charts as you like.</li>
58 <li>You can have charts from many different netdata servers (add <pre>data-host="http://another.netdata.server:19999/"</pre> to each chart).</li>
59 - <li>You can use different chart libraries on the same page: <b>peity</b>, <b>sparkline</b>, <b>dygraph</b>, <b>google</b>, <b>morris</b></li>
59 + <li>You can use different chart libraries on the same page: <b>peity</b>, <b>sparkline</b>, <b>dygraph</b>, <b>google</b></li>
60 <li>You can customize each chart to your preferences. For each chart library most of their attributes can be given in <b>data-</b> attributes.</li>
61 <li>Each chart can have each own duration - it is controlled with the <b>data-after</b> attribute to give that many seconds of data.</li>
62 <li>Depending on the width of the chart and <b>data-after</b> attribute, netdata will automatically refresh the chart when it needs to be updated. For example giving 600 pixels for width for -600 seconds of data, using a chart library that needs 3 pixels per point, will yeld in a chart updated once every 3 seconds.</li>
@@ -555,11 +555,8 @@ NetData is a complete Google Visualization API provider.
555 <small>rendered in <span id="time303">X</span> ms</small>
556 </div>
557
558 -
559 -
560 -
561 -
562 -
558 +<!--
559 +
560 <hr>
561 <h1>Morris Charts</h1>
562 Unfortunatelly, Morris Charts are very slow. Here we force them to lower their detail to get acceptable results.
@@ -644,6 +641,7 @@ So, to avoid flashing the charts, we destroy and re-create the charts on each up
641 <small>rendered in <span id="time803">X</span> ms</small>
642 </div>
643
644 +-->
645
646 <hr>
647 <h1>d3pie Charts</h1>
web/gui/dashboard.js
+8433 -7894
@@ -1,7 +1,12 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +// DO NOT EDIT: This file is automatically generated from the source files in src/
4 +
5 // ----------------------------------------------------------------------------
6 // You can set the following variables before loading this script:
7
8 +// 'use strict';
9 +
10 /*global netdataNoDygraphs *//* boolean, disable dygraph charts
11 * (default: false) */
12 /*global netdataNoSparklines *//* boolean, disable sparkline charts
@@ -72,9441 +77,9975 @@
77 // ----------------------------------------------------------------------------
78 // global namespace
79
75 -var NETDATA = window.NETDATA || {};
80 +const NETDATA = window.NETDATA || {};
81
82 (function(window, document, $, undefined) {
83
79 - NETDATA.encodeURIComponent = function(s) {
80 - if(typeof(s) === 'string')
81 - return encodeURIComponent(s);
84 +NETDATA.encodeURIComponent = function (s) {
85 + if (typeof(s) === 'string') {
86 + return encodeURIComponent(s);
87 + }
88
83 - return s;
84 - };
89 + return s;
90 +};
91
86 - // ------------------------------------------------------------------------
87 - // compatibility fixes
92 +/// A heuristic for detecting slow devices.
93 +let isSlowDeviceResult = undefined;
94 +const isSlowDevice = function () {
95 + if (!isSlowDeviceResult) {
96 + return isSlowDeviceResult;
97 + }
98
89 - // fix IE issue with console
90 - if(!window.console) { window.console = { log: function(){} }; }
99 + try {
100 + let ua = navigator.userAgent.toLowerCase();
101
92 - // if string.endsWith is not defined, define it
93 - if(typeof String.prototype.endsWith !== 'function') {
94 - String.prototype.endsWith = function(s) {
95 - if(s.length > this.length) return false;
96 - return this.slice(-s.length) === s;
97 - };
102 + let iOS = /ipad|iphone|ipod/.test(ua) && !window.MSStream;
103 + let android = /android/.test(ua) && !window.MSStream;
104 + isSlowDeviceResult = (iOS || android);
105 + } catch (e) {
106 + isSlowDeviceResult = false;
107 }
108
100 - // if string.startsWith is not defined, define it
101 - if(typeof String.prototype.startsWith !== 'function') {
102 - String.prototype.startsWith = function(s) {
103 - if(s.length > this.length) return false;
104 - return this.slice(s.length) === s;
105 - };
106 - }
109 + return isSlowDeviceResult;
110 +};
111
108 - NETDATA.name2id = function(s) {
109 - return s
110 - .replace(/ /g, '_')
111 - .replace(/\(/g, '_')
112 - .replace(/\)/g, '_')
113 - .replace(/\./g, '_')
114 - .replace(/\//g, '_');
115 - };
112 +NETDATA.guid = function () {
113 + function s4() {
114 + return Math.floor((1 + Math.random()) * 0x10000)
115 + .toString(16)
116 + .substring(1);
117 + }
118
117 - // ----------------------------------------------------------------------------------------------------------------
118 - // XSS checks
119 -
120 - NETDATA.xss = {
121 - enabled: (typeof netdataCheckXSS === 'undefined')?false:netdataCheckXSS,
122 - enabled_for_data: (typeof netdataCheckXSS === 'undefined')?false:netdataCheckXSS,
123 -
124 - string: function (s) {
125 - return s.toString()
126 - .replace(/</g, '&lt;')
127 - .replace(/>/g, '&gt;')
128 - .replace(/"/g, '&quot;')
129 - .replace(/'/g, '&#39;');
130 - },
119 + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
120 +};
121
132 - object: function(name, obj, ignore_regex) {
133 - if(typeof ignore_regex !== 'undefined' && ignore_regex.test(name) === true) {
134 - // console.log('XSS: ignoring "' + name + '"');
135 - return obj;
122 +NETDATA.zeropad = function (x) {
123 + if (x > -10 && x < 10) {
124 + return '0' + x.toString();
125 + } else {
126 + return x.toString();
127 + }
128 +};
129 +
130 +NETDATA.seconds4human = function (seconds, options) {
131 + let default_options = {
132 + now: 'now',
133 + space: ' ',
134 + negative_suffix: 'ago',
135 + day: 'day',
136 + days: 'days',
137 + hour: 'hour',
138 + hours: 'hours',
139 + minute: 'min',
140 + minutes: 'mins',
141 + second: 'sec',
142 + seconds: 'secs',
143 + and: 'and'
144 + };
145 +
146 + if (typeof options !== 'object') {
147 + options = default_options;
148 + } else {
149 + for (const x in default_options) {
150 + if (typeof options[x] !== 'string') {
151 + options[x] = default_options[x];
152 }
153 + }
154 + }
155
138 - switch (typeof(obj)) {
139 - case 'string':
140 - var ret = this.string(obj);
141 - if(ret !== obj) console.log('XSS protection changed string ' + name + ' from "' + obj + '" to "' + ret + '"');
142 - return ret;
156 + if (typeof seconds === 'string') {
157 + seconds = parseInt(seconds, 10);
158 + }
159
144 - case 'object':
145 - if(obj === null) return obj;
160 + if (seconds === 0) {
161 + return options.now;
162 + }
163
147 - if(Array.isArray(obj) === true) {
148 - // console.log('checking array "' + name + '"');
164 + let suffix = '';
165 + if (seconds < 0) {
166 + seconds = -seconds;
167 + if (options.negative_suffix !== '') {
168 + suffix = options.space + options.negative_suffix;
169 + }
170 + }
171
150 - var len = obj.length;
151 - while(len--)
152 - obj[len] = this.object(name + '[' + len + ']', obj[len], ignore_regex);
153 - }
154 - else {
155 - // console.log('checking object "' + name + '"');
172 + let days = Math.floor(seconds / 86400);
173 + seconds -= (days * 86400);
174
157 - for(var i in obj) {
158 - if(obj.hasOwnProperty(i) === false) continue;
159 - if(this.string(i) !== i) {
160 - console.log('XSS protection removed invalid object member "' + name + '.' + i + '"');
161 - delete obj[i];
162 - }
163 - else
164 - obj[i] = this.object(name + '.' + i, obj[i], ignore_regex);
165 - }
166 - }
167 - return obj;
175 + let hours = Math.floor(seconds / 3600);
176 + seconds -= (hours * 3600);
177
169 - default:
170 - return obj;
171 - }
172 - },
178 + let minutes = Math.floor(seconds / 60);
179 + seconds -= (minutes * 60);
180
174 - checkOptional: function(name, obj, ignore_regex) {
175 - if(this.enabled === true) {
176 - //console.log('XSS: checking optional "' + name + '"...');
177 - return this.object(name, obj, ignore_regex);
178 - }
179 - return obj;
180 - },
181 + let strings = [];
182
182 - checkAlways: function(name, obj, ignore_regex) {
183 - //console.log('XSS: checking always "' + name + '"...');
184 - return this.object(name, obj, ignore_regex);
185 - },
183 + if (days > 1) {
184 + strings.push(days.toString() + options.space + options.days);
185 + } else if (days === 1) {
186 + strings.push(days.toString() + options.space + options.day);
187 + }
188
187 - checkData: function(name, obj, ignore_regex) {
188 - if(this.enabled_for_data === true) {
189 - //console.log('XSS: checking data "' + name + '"...');
190 - return this.object(name, obj, ignore_regex);
191 - }
192 - return obj;
193 - }
194 - };
189 + if (hours > 1) {
190 + strings.push(hours.toString() + options.space + options.hours);
191 + } else if (hours === 1) {
192 + strings.push(hours.toString() + options.space + options.hour);
193 + }
194
196 - // ----------------------------------------------------------------------------------------------------------------
197 - // Detect the netdata server
195 + if (minutes > 1) {
196 + strings.push(minutes.toString() + options.space + options.minutes);
197 + } else if (minutes === 1) {
198 + strings.push(minutes.toString() + options.space + options.minute);
199 + }
200 +
201 + if (seconds > 1) {
202 + strings.push(Math.floor(seconds).toString() + options.space + options.seconds);
203 + } else if (seconds === 1) {
204 + strings.push(Math.floor(seconds).toString() + options.space + options.second);
205 + }
206 +
207 + if (strings.length === 1) {
208 + return strings.pop() + suffix;
209 + }
210
199 - // http://stackoverflow.com/questions/984510/what-is-my-script-src-url
200 - // http://stackoverflow.com/questions/6941533/get-protocol-domain-and-port-from-url
201 - NETDATA._scriptSource = function() {
202 - var script = null;
211 + let last = strings.pop();
212 + return strings.join(", ") + " " + options.and + " " + last + suffix;
213 +};
214
204 - if(typeof document.currentScript !== 'undefined') {
205 - script = document.currentScript;
215 +// ----------------------------------------------------------------------------------------------------------------
216 +// element data attributes
217 +
218 +NETDATA.dataAttribute = function (element, attribute, def) {
219 + let key = 'data-' + attribute.toString();
220 + if (element.hasAttribute(key)) {
221 + let data = element.getAttribute(key);
222 +
223 + if (data === 'true') {
224 + return true;
225 }
207 - else {
208 - var all_scripts = document.getElementsByTagName('script');
209 - script = all_scripts[all_scripts.length - 1];
226 + if (data === 'false') {
227 + return false;
228 + }
229 + if (data === 'null') {
230 + return null;
231 }
232
212 - if (typeof script.getAttribute.length !== 'undefined')
213 - script = script.src;
214 - else
215 - script = script.getAttribute('src', -1);
216 -
217 - return script;
218 - };
233 + // Only convert to a number if it doesn't change the string
234 + if (data === +data + '') {
235 + return +data;
236 + }
237
220 - if(typeof netdataServer !== 'undefined')
221 - NETDATA.serverDefault = netdataServer;
222 - else {
223 - var s = NETDATA._scriptSource();
224 - if(s) NETDATA.serverDefault = s.replace(/\/dashboard.js(\?.*)?$/g, "");
225 - else {
226 - console.log('WARNING: Cannot detect the URL of the netdata server.');
227 - NETDATA.serverDefault = null;
238 + if (/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/.test(data)) {
239 + return JSON.parse(data);
240 }
241 +
242 + return data;
243 + } else {
244 + return def;
245 }
246 +};
247
231 - if(NETDATA.serverDefault === null)
232 - NETDATA.serverDefault = '';
233 - else if(NETDATA.serverDefault.slice(-1) !== '/')
234 - NETDATA.serverDefault += '/';
248 +NETDATA.dataAttributeBoolean = function (element, attribute, def) {
249 + let value = NETDATA.dataAttribute(element, attribute, def);
250
236 - if(typeof netdataServerStatic !== 'undefined' && netdataServerStatic !== null && netdataServerStatic !== '') {
237 - NETDATA.serverStatic = netdataServerStatic;
238 - if(NETDATA.serverStatic.slice(-1) !== '/')
239 - NETDATA.serverStatic += '/';
251 + if (value === true || value === false) // gmosx: Love this :)
252 + {
253 + return value;
254 }
241 - else {
242 - NETDATA.serverStatic = NETDATA.serverDefault;
243 - }
244 -
245 -
246 - // default URLs for all the external files we need
247 - // make them RELATIVE so that the whole thing can also be
248 - // installed under a web server
249 - NETDATA.jQuery = NETDATA.serverStatic + 'lib/jquery-2.2.4.min.js';
250 - NETDATA.peity_js = NETDATA.serverStatic + 'lib/jquery.peity-3.2.0.min.js';
251 - NETDATA.sparkline_js = NETDATA.serverStatic + 'lib/jquery.sparkline-2.1.2.min.js';
252 - NETDATA.easypiechart_js = NETDATA.serverStatic + 'lib/jquery.easypiechart-97b5824.min.js';
253 - NETDATA.gauge_js = NETDATA.serverStatic + 'lib/gauge-1.3.2.min.js';
254 - NETDATA.dygraph_js = NETDATA.serverStatic + 'lib/dygraph-c91c859.min.js';
255 - NETDATA.dygraph_smooth_js = NETDATA.serverStatic + 'lib/dygraph-smooth-plotter-c91c859.js';
256 - NETDATA.raphael_js = NETDATA.serverStatic + 'lib/raphael-2.2.4-min.js';
257 - NETDATA.c3_js = NETDATA.serverStatic + 'lib/c3-0.4.18.min.js';
258 - NETDATA.c3_css = NETDATA.serverStatic + 'css/c3-0.4.18.min.css';
259 - NETDATA.d3pie_js = NETDATA.serverStatic + 'lib/d3pie-0.2.1-netdata-3.js';
260 - NETDATA.d3_js = NETDATA.serverStatic + 'lib/d3-4.12.2.min.js';
261 - NETDATA.morris_js = NETDATA.serverStatic + 'lib/morris-0.5.1.min.js';
262 - NETDATA.morris_css = NETDATA.serverStatic + 'css/morris-0.5.1.css';
263 - NETDATA.google_js = 'https://www.google.com/jsapi';
264 -
265 - NETDATA.themes = {
266 - white: {
267 - bootstrap_css: NETDATA.serverStatic + 'css/bootstrap-3.3.7.css',
268 - dashboard_css: NETDATA.serverStatic + 'dashboard.css?v20180210-1',
269 - background: '#FFFFFF',
270 - foreground: '#000000',
271 - grid: '#F0F0F0',
272 - axis: '#F0F0F0',
273 - highlight: '#F5F5F5',
274 - colors: [ '#3366CC', '#DC3912', '#109618', '#FF9900', '#990099', '#DD4477',
275 - '#3B3EAC', '#66AA00', '#0099C6', '#B82E2E', '#AAAA11', '#5574A6',
276 - '#994499', '#22AA99', '#6633CC', '#E67300', '#316395', '#8B0707',
277 - '#329262', '#3B3EAC' ],
278 - easypiechart_track: '#f0f0f0',
279 - easypiechart_scale: '#dfe0e0',
280 - gauge_pointer: '#C0C0C0',
281 - gauge_stroke: '#F0F0F0',
282 - gauge_gradient: false,
283 - d3pie: {
284 - title: '#333333',
285 - subtitle: '#666666',
286 - footer: '#888888',
287 - other: '#aaaaaa',
288 - mainlabel: '#333333',
289 - percentage: '#dddddd',
290 - value: '#aaaa22',
291 - tooltip_bg: '#000000',
292 - tooltip_fg: '#efefef',
293 - segment_stroke: "#ffffff",
294 - gradient_color: '#000000'
295 - }
296 - },
297 - slate: {
298 - bootstrap_css: NETDATA.serverStatic + 'css/bootstrap-slate-flat-3.3.7.css?v20161229-1',
299 - dashboard_css: NETDATA.serverStatic + 'dashboard.slate.css?v20180210-1',
300 - background: '#272b30',
301 - foreground: '#C8C8C8',
302 - grid: '#283236',
303 - axis: '#283236',
304 - highlight: '#383838',
305 -/* colors: [ '#55bb33', '#ff2222', '#0099C6', '#faa11b', '#adbce0', '#DDDD00',
306 - '#4178ba', '#f58122', '#a5cc39', '#f58667', '#f5ef89', '#cf93c0',
307 - '#a5d18a', '#b8539d', '#3954a3', '#c8a9cf', '#c7de8a', '#fad20a',
308 - '#a6a479', '#a66da8' ],
309 -*/
310 - colors: [ '#66AA00', '#FE3912', '#3366CC', '#D66300', '#0099C6', '#DDDD00',
311 - '#5054e6', '#EE9911', '#BB44CC', '#e45757', '#ef0aef', '#CC7700',
312 - '#22AA99', '#109618', '#905bfd', '#f54882', '#4381bf', '#ff3737',
313 - '#329262', '#3B3EFF' ],
314 - easypiechart_track: '#373b40',
315 - easypiechart_scale: '#373b40',
316 - gauge_pointer: '#474b50',
317 - gauge_stroke: '#373b40',
318 - gauge_gradient: false,
319 - d3pie: {
320 - title: '#C8C8C8',
321 - subtitle: '#283236',
322 - footer: '#283236',
323 - other: '#283236',
324 - mainlabel: '#C8C8C8',
325 - percentage: '#dddddd',
326 - value: '#cccc44',
327 - tooltip_bg: '#272b30',
328 - tooltip_fg: '#C8C8C8',
329 - segment_stroke: "#283236",
330 - gradient_color: '#000000'
331 - }
332 - }
333 - };
255
335 - if(typeof netdataTheme !== 'undefined' && typeof NETDATA.themes[netdataTheme] !== 'undefined')
336 - NETDATA.themes.current = NETDATA.themes[netdataTheme];
337 - else
338 - NETDATA.themes.current = NETDATA.themes.white;
339 -
340 - NETDATA.colors = NETDATA.themes.current.colors;
341 -
342 - // these are the colors Google Charts are using
343 - // we have them here to attempt emulate their look and feel on the other chart libraries
344 - // http://there4.io/2012/05/02/google-chart-color-list/
345 - //NETDATA.colors = [ '#3366CC', '#DC3912', '#FF9900', '#109618', '#990099', '#3B3EAC', '#0099C6',
346 - // '#DD4477', '#66AA00', '#B82E2E', '#316395', '#994499', '#22AA99', '#AAAA11',
347 - // '#6633CC', '#E67300', '#8B0707', '#329262', '#5574A6', '#3B3EAC' ];
348 -
349 - // an alternative set
350 - // http://www.mulinblog.com/a-color-palette-optimized-for-data-visualization/
351 - // (blue) (red) (orange) (green) (pink) (brown) (purple) (yellow) (gray)
352 - //NETDATA.colors = [ '#5DA5DA', '#F15854', '#FAA43A', '#60BD68', '#F17CB0', '#B2912F', '#B276B2', '#DECF3F', '#4D4D4D' ];
353 -
354 - NETDATA.icons = {
355 - left: '<i class="fas fa-backward"></i>',
356 - reset: '<i class="fas fa-play"></i>',
357 - right: '<i class="fas fa-forward"></i>',
358 - zoomIn: '<i class="fas fa-plus"></i>',
359 - zoomOut: '<i class="fas fa-minus"></i>',
360 - resize: '<i class="fas fa-sort"></i>',
361 - lineChart: '<i class="fas fa-chart-line"></i>',
362 - areaChart: '<i class="fas fa-chart-area"></i>',
363 - noChart: '<i class="fas fa-chart-area"></i>',
364 - loading: '<i class="fas fa-sync-alt"></i>',
365 - noData: '<i class="fas fa-exclamation-triangle"></i>'
366 - };
256 + if (typeof(value) === 'string') {
257 + if (value === 'yes' || value === 'on') {
258 + return true;
259 + }
260
368 - if(typeof netdataIcons === 'object') {
369 - for(var icon in NETDATA.icons) {
370 - if(NETDATA.icons.hasOwnProperty(icon) && typeof(netdataIcons[icon]) === 'string')
371 - NETDATA.icons[icon] = netdataIcons[icon];
261 + if (value === '' || value === 'no' || value === 'off' || value === 'null') {
262 + return false;
263 }
264 +
265 + return def;
266 }
267
375 - if(typeof netdataSnapshotData === 'undefined')
376 - netdataSnapshotData = null;
268 + if (typeof(value) === 'number') {
269 + return value !== 0;
270 + }
271
378 - if(typeof netdataShowHelp === 'undefined')
379 - netdataShowHelp = true;
272 + return def;
273 +};
274
381 - if(typeof netdataShowAlarms === 'undefined')
382 - netdataShowAlarms = false;
275 +// ----------------------------------------------------------------------------------------------------------------
276 +// fast numbers formatting
277
384 - if(typeof netdataRegistryAfterMs !== 'number' || netdataRegistryAfterMs < 0)
385 - netdataRegistryAfterMs = 1500;
278 +NETDATA.fastNumberFormat = {
279 + formatters_fixed: [],
280 + formatters_zero_based: [],
281
387 - if(typeof netdataRegistry === 'undefined') {
388 - // backward compatibility
389 - netdataRegistry = (typeof netdataNoRegistry !== 'undefined' && netdataNoRegistry === false);
390 - }
391 - if(netdataRegistry === false && typeof netdataRegistryCallback === 'function')
392 - netdataRegistry = true;
282 + // this is the fastest and the preferred
283 + getIntlNumberFormat: function (min, max) {
284 + let key = max;
285 + if (min === max) {
286 + if (typeof this.formatters_fixed[key] === 'undefined') {
287 + this.formatters_fixed[key] = new Intl.NumberFormat(undefined, {
288 + // style: 'decimal',
289 + // minimumIntegerDigits: 1,
290 + // minimumSignificantDigits: 1,
291 + // maximumSignificantDigits: 1,
292 + useGrouping: true,
293 + minimumFractionDigits: min,
294 + maximumFractionDigits: max
295 + });
296 + }
297
298 + return this.formatters_fixed[key];
299 + } else if (min === 0) {
300 + if (typeof this.formatters_zero_based[key] === 'undefined') {
301 + this.formatters_zero_based[key] = new Intl.NumberFormat(undefined, {
302 + // style: 'decimal',
303 + // minimumIntegerDigits: 1,
304 + // minimumSignificantDigits: 1,
305 + // maximumSignificantDigits: 1,
306 + useGrouping: true,
307 + minimumFractionDigits: min,
308 + maximumFractionDigits: max
309 + });
310 + }
311
395 - // ----------------------------------------------------------------------------------------------------------------
396 - // detect if this is probably a slow device
312 + return this.formatters_zero_based[key];
313 + } else {
314 + // this is never used
315 + // it is added just for completeness
316 + return new Intl.NumberFormat(undefined, {
317 + // style: 'decimal',
318 + // minimumIntegerDigits: 1,
319 + // minimumSignificantDigits: 1,
320 + // maximumSignificantDigits: 1,
321 + useGrouping: true,
322 + minimumFractionDigits: min,
323 + maximumFractionDigits: max
324 + });
325 + }
326 + },
327
398 - var isSlowDeviceResult = undefined;
399 - var isSlowDevice = function() {
400 - if(isSlowDeviceResult !== undefined)
401 - return isSlowDeviceResult;
328 + // this respects locale
329 + getLocaleString: function (min, max) {
330 + let key = max;
331 + if (min === max) {
332 + if (typeof this.formatters_fixed[key] === 'undefined') {
333 + this.formatters_fixed[key] = {
334 + format: function (value) {
335 + return value.toLocaleString(undefined, {
336 + // style: 'decimal',
337 + // minimumIntegerDigits: 1,
338 + // minimumSignificantDigits: 1,
339 + // maximumSignificantDigits: 1,
340 + useGrouping: true,
341 + minimumFractionDigits: min,
342 + maximumFractionDigits: max
343 + });
344 + }
345 + };
346 + }
347
403 - try {
404 - var ua = navigator.userAgent.toLowerCase();
348 + return this.formatters_fixed[key];
349 + } else if (min === 0) {
350 + if (typeof this.formatters_zero_based[key] === 'undefined') {
351 + this.formatters_zero_based[key] = {
352 + format: function (value) {
353 + return value.toLocaleString(undefined, {
354 + // style: 'decimal',
355 + // minimumIntegerDigits: 1,
356 + // minimumSignificantDigits: 1,
357 + // maximumSignificantDigits: 1,
358 + useGrouping: true,
359 + minimumFractionDigits: min,
360 + maximumFractionDigits: max
361 + });
362 + }
363 + };
364 + }
365
406 - var iOS = /ipad|iphone|ipod/.test(ua) && !window.MSStream;
407 - var android = /android/.test(ua) && !window.MSStream;
408 - isSlowDeviceResult = (iOS === true || android === true);
409 - }
410 - catch (e) {
411 - isSlowDeviceResult = false;
366 + return this.formatters_zero_based[key];
367 + } else {
368 + return {
369 + format: function (value) {
370 + return value.toLocaleString(undefined, {
371 + // style: 'decimal',
372 + // minimumIntegerDigits: 1,
373 + // minimumSignificantDigits: 1,
374 + // maximumSignificantDigits: 1,
375 + useGrouping: true,
376 + minimumFractionDigits: min,
377 + maximumFractionDigits: max
378 + });
379 + }
380 + };
381 }
382 + },
383
414 - return isSlowDeviceResult;
415 - };
416 -
417 - // ----------------------------------------------------------------------------------------------------------------
418 - // the defaults for all charts
419 -
420 - // if the user does not specify any of these, the following will be used
421 -
422 - NETDATA.chartDefaults = {
423 - width: '100%', // the chart width - can be null
424 - height: '100%', // the chart height - can be null
425 - min_width: null, // the chart minimum width - can be null
426 - library: 'dygraph', // the graphing library to use
427 - method: 'average', // the grouping method
428 - before: 0, // panning
429 - after: -600, // panning
430 - pixels_per_point: 1, // the detail of the chart
431 - fill_luminance: 0.8 // luminance of colors in solid areas
432 - };
384 + // the fallback
385 + getFixed: function (min, max) {
386 + let key = max;
387 + if (min === max) {
388 + if (typeof this.formatters_fixed[key] === 'undefined') {
389 + this.formatters_fixed[key] = {
390 + format: function (value) {
391 + if (value === 0) {
392 + return "0";
393 + }
394 + return value.toFixed(max);
395 + }
396 + };
397 + }
398
434 - // ----------------------------------------------------------------------------------------------------------------
435 - // global options
399 + return this.formatters_fixed[key];
400 + } else if (min === 0) {
401 + if (typeof this.formatters_zero_based[key] === 'undefined') {
402 + this.formatters_zero_based[key] = {
403 + format: function (value) {
404 + if (value === 0) {
405 + return "0";
406 + }
407 + return value.toFixed(max);
408 + }
409 + };
410 + }
411
437 - NETDATA.options = {
438 - pauseCallback: null, // a callback when we are really paused
412 + return this.formatters_zero_based[key];
413 + } else {
414 + return {
415 + format: function (value) {
416 + if (value === 0) {
417 + return "0";
418 + }
419 + return value.toFixed(max);
420 + }
421 + };
422 + }
423 + },
424
440 - pause: false, // when enabled we don't auto-refresh the charts
425 + testIntlNumberFormat: function () {
426 + let value = 1.12345;
427 + let e1 = "1.12", e2 = "1,12";
428 + let s = "";
429
442 - targets: [], // an array of all the state objects that are
443 - // currently active (independently of their
444 - // viewport visibility)
430 + try {
431 + let x = new Intl.NumberFormat(undefined, {
432 + useGrouping: true,
433 + minimumFractionDigits: 2,
434 + maximumFractionDigits: 2
435 + });
436
446 - updated_dom: true, // when true, the DOM has been updated with
447 - // new elements we have to check.
437 + s = x.format(value);
438 + } catch (e) {
439 + s = "";
440 + }
441
449 - auto_refresher_fast_weight: 0, // this is the current time in ms, spent
450 - // rendering charts continuously.
451 - // used with .current.fast_render_timeframe
442 + // console.log('NumberFormat: ', s);
443 + return (s === e1 || s === e2);
444 + },
445
453 - page_is_visible: true, // when true, this page is visible
446 + testLocaleString: function () {
447 + let value = 1.12345;
448 + let e1 = "1.12", e2 = "1,12";
449 + let s = "";
450
455 - auto_refresher_stop_until: 0, // timestamp in ms - used internally, to stop the
456 - // auto-refresher for some time (when a chart is
457 - // performing pan or zoom, we need to stop refreshing
458 - // all other charts, to have the maximum speed for
459 - // rendering the chart that is panned or zoomed).
460 - // Used with .current.global_pan_sync_time
451 + try {
452 + s = value.toLocaleString(undefined, {
453 + useGrouping: true,
454 + minimumFractionDigits: 2,
455 + maximumFractionDigits: 2
456 + });
457 + } catch (e) {
458 + s = "";
459 + }
460 +
461 + // console.log('localeString: ', s);
462 + return (s === e1 || s === e2);
463 + },
464 +
465 + // on first run we decide which formatter to use
466 + get: function (min, max) {
467 + if (this.testIntlNumberFormat()) {
468 + // console.log('numberformat');
469 + this.get = this.getIntlNumberFormat;
470 + } else if (this.testLocaleString()) {
471 + // console.log('localestring');
472 + this.get = this.getLocaleString;
473 + } else {
474 + // console.log('fixed');
475 + this.get = this.getFixed;
476 + }
477 + return this.get(min, max);
478 + }
479 +};
480 +// Error Handling
481 +
482 +NETDATA.errorCodes = {
483 + 100: {message: "Cannot load chart library", alert: true},
484 + 101: {message: "Cannot load jQuery", alert: true},
485 + 402: {message: "Chart library not found", alert: false},
486 + 403: {message: "Chart library not enabled/is failed", alert: false},
487 + 404: {message: "Chart not found", alert: false},
488 + 405: {message: "Cannot download charts index from server", alert: true},
489 + 406: {message: "Invalid charts index downloaded from server", alert: true},
490 + 407: {message: "Cannot HELLO netdata server", alert: false},
491 + 408: {message: "Netdata servers sent invalid response to HELLO", alert: false},
492 + 409: {message: "Cannot ACCESS netdata registry", alert: false},
493 + 410: {message: "Netdata registry ACCESS failed", alert: false},
494 + 411: {message: "Netdata registry server send invalid response to DELETE ", alert: false},
495 + 412: {message: "Netdata registry DELETE failed", alert: false},
496 + 413: {message: "Netdata registry server send invalid response to SWITCH ", alert: false},
497 + 414: {message: "Netdata registry SWITCH failed", alert: false},
498 + 415: {message: "Netdata alarms download failed", alert: false},
499 + 416: {message: "Netdata alarms log download failed", alert: false},
500 + 417: {message: "Netdata registry server send invalid response to SEARCH ", alert: false},
501 + 418: {message: "Netdata registry SEARCH failed", alert: false}
502 +};
503 +
504 +NETDATA.errorLast = {
505 + code: 0,
506 + message: "",
507 + datetime: 0
508 +};
509 +
510 +NETDATA.error = function (code, msg) {
511 + NETDATA.errorLast.code = code;
512 + NETDATA.errorLast.message = msg;
513 + NETDATA.errorLast.datetime = Date.now();
514 +
515 + console.log("ERROR " + code + ": " + NETDATA.errorCodes[code].message + ": " + msg);
516 +
517 + let ret = true;
518 + if (typeof netdataErrorCallback === 'function') {
519 + ret = netdataErrorCallback('system', code, msg);
520 + }
521
462 - on_scroll_refresher_stop_until: 0, // timestamp in ms - used to stop evaluating
463 - // charts for some time, after a page scroll
522 + if (ret && NETDATA.errorCodes[code].alert) {
523 + alert("ERROR " + code + ": " + NETDATA.errorCodes[code].message + ": " + msg);
524 + }
525 +};
526
465 - last_page_resize: Date.now(), // the timestamp of the last resize request
527 +NETDATA.errorReset = function () {
528 + NETDATA.errorLast.code = 0;
529 + NETDATA.errorLast.message = "You are doing fine!";
530 + NETDATA.errorLast.datetime = 0;
531 +};
532 +// ------------------------------------------------------------------------
533 +// compatibility fixes
534
467 - last_page_scroll: 0, // the timestamp the last time the page was scrolled
535 +// fix IE issue with console
536 +if (!window.console) {
537 + window.console = {
538 + log: function () {
539 + }
540 + };
541 +}
542
469 - browser_timezone: 'unknown', // timezone detected by javascript
470 - server_timezone: 'unknown', // timezone reported by the server
543 +// if string.endsWith is not defined, define it
544 +if (typeof String.prototype.endsWith !== 'function') {
545 + String.prototype.endsWith = function (s) {
546 + if (s.length > this.length) {
547 + return false;
548 + }
549 + return this.slice(-s.length) === s;
550 + };
551 +}
552
472 - force_data_points: 0, // force the number of points to be returned for charts
473 - fake_chart_rendering: false, // when set to true, the dashboard will download data but will not render the charts
553 +// if string.startsWith is not defined, define it
554 +if (typeof String.prototype.startsWith !== 'function') {
555 + String.prototype.startsWith = function (s) {
556 + if (s.length > this.length) {
557 + return false;
558 + }
559 + return this.slice(s.length) === s;
560 + };
561 +}
562 +
563 +NETDATA.name2id = function (s) {
564 + return s
565 + .replace(/ /g, '_')
566 + .replace(/\(/g, '_')
567 + .replace(/\)/g, '_')
568 + .replace(/\./g, '_')
569 + .replace(/\//g, '_');
570 +};
571 +// ----------------------------------------------------------------------------------------------------------------
572 +// XSS checks
573 +
574 +NETDATA.xss = {
575 + enabled: (typeof netdataCheckXSS === 'undefined') ? false : netdataCheckXSS,
576 + enabled_for_data: (typeof netdataCheckXSS === 'undefined') ? false : netdataCheckXSS,
577 +
578 + string: function (s) {
579 + return s.toString()
580 + .replace(/</g, '&lt;')
581 + .replace(/>/g, '&gt;')
582 + .replace(/"/g, '&quot;')
583 + .replace(/'/g, '&#39;');
584 + },
585 +
586 + object: function (name, obj, ignore_regex) {
587 + if (typeof ignore_regex !== 'undefined' && ignore_regex.test(name)) {
588 + // console.log('XSS: ignoring "' + name + '"');
589 + return obj;
590 + }
591
475 - passive_events: null, // true if the browser supports passive events
592 + switch (typeof(obj)) {
593 + case 'string':
594 + const ret = this.string(obj);
595 + if (ret !== obj) {
596 + console.log('XSS protection changed string ' + name + ' from "' + obj + '" to "' + ret + '"');
597 + }
598 + return ret;
599
477 - // the current profile
478 - // we may have many...
479 - current: {
480 - units: 'auto', // can be 'auto' or 'original'
481 - temperature: 'celsius', // can be 'celsius' or 'fahrenheit'
482 - seconds_as_time: true, // show seconds as DDd:HH:MM:SS ?
483 - timezone: 'default', // the timezone to use, or 'default'
484 - user_set_server_timezone: 'default', // as set by the user on the dashboard
600 + case 'object':
601 + if (obj === null) {
602 + return obj;
603 + }
604
486 - legend_toolbox: true, // show the legend toolbox on charts
487 - resize_charts: true, // show the resize handler on charts
605 + if (Array.isArray(obj)) {
606 + // console.log('checking array "' + name + '"');
607
489 - pixels_per_point: isSlowDevice()?5:1, // the minimum pixels per point for all charts
490 - // increase this to speed javascript up
491 - // each chart library has its own limit too
492 - // the max of this and the chart library is used
493 - // the final is calculated every time, so a change
494 - // here will have immediate effect on the next chart
495 - // update
608 + let len = obj.length;
609 + while (len--) {
610 + obj[len] = this.object(name + '[' + len + ']', obj[len], ignore_regex);
611 + }
612 + } else {
613 + // console.log('checking object "' + name + '"');
614
497 - idle_between_charts: 100, // ms - how much time to wait between chart updates
615 + for (const i in obj) {
616 + if (obj.hasOwnProperty(i) === false) {
617 + continue;
618 + }
619 + if (this.string(i) !== i) {
620 + console.log('XSS protection removed invalid object member "' + name + '.' + i + '"');
621 + delete obj[i];
622 + } else {
623 + obj[i] = this.object(name + '.' + i, obj[i], ignore_regex);
624 + }
625 + }
626 + }
627 + return obj;
628
499 - fast_render_timeframe: 200, // ms - render continuously until this time of continuous
500 - // rendering has been reached
501 - // this setting is used to make it render e.g. 10
502 - // charts at once, sleep idle_between_charts time
503 - // and continue for another 10 charts.
629 + default:
630 + return obj;
631 + }
632 + },
633
505 - idle_between_loops: 500, // ms - if all charts have been updated, wait this
506 - // time before starting again.
634 + checkOptional: function (name, obj, ignore_regex) {
635 + if (this.enabled) {
636 + //console.log('XSS: checking optional "' + name + '"...');
637 + return this.object(name, obj, ignore_regex);
638 + }
639 + return obj;
640 + },
641
508 - idle_parallel_loops: 100, // ms - the time between parallel refresher updates
642 + checkAlways: function (name, obj, ignore_regex) {
643 + //console.log('XSS: checking always "' + name + '"...');
644 + return this.object(name, obj, ignore_regex);
645 + },
646
510 - idle_lost_focus: 500, // ms - when the window does not have focus, check
511 - // if focus has been regained, every this time
647 + checkData: function (name, obj, ignore_regex) {
648 + if (this.enabled_for_data) {
649 + //console.log('XSS: checking data "' + name + '"...');
650 + return this.object(name, obj, ignore_regex);
651 + }
652 + return obj;
653 + }
654 +};
655 +NETDATA.colorHex2Rgb = function (hex) {
656 + // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
657 + let shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
658 + hex = hex.replace(shorthandRegex, function (m, r, g, b) {
659 + return r + r + g + g + b + b;
660 + });
661
513 - global_pan_sync_time: 300, // ms - when you pan or zoom a chart, the background
514 - // auto-refreshing of charts is paused for this amount
515 - // of time
662 + let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
663 + return result ? {
664 + r: parseInt(result[1], 16),
665 + g: parseInt(result[2], 16),
666 + b: parseInt(result[3], 16)
667 + } : null;
668 +};
669 +
670 +NETDATA.colorLuminance = function (hex, lum) {
671 + // validate hex string
672 + hex = String(hex).replace(/[^0-9a-f]/gi, '');
673 + if (hex.length < 6) {
674 + hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
675 + }
676
517 - sync_selection_delay: 400, // ms - when you pan or zoom a chart, wait this amount
518 - // of time before setting up synchronized selections
519 - // on hover.
677 + lum = lum || 0;
678
521 - sync_selection: true, // enable or disable selection sync
679 + // convert to decimal and change luminosity
680 + let rgb = "#";
681 + for (let i = 0; i < 3; i++) {
682 + let c = parseInt(hex.substr(i * 2, 2), 16);
683 + c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16);
684 + rgb += ("00" + c).substr(c.length);
685 + }
686
523 - pan_and_zoom_delay: 50, // when panning or zooming, how ofter to update the chart
687 + return rgb;
688 +};
689 +NETDATA.unitsConversion = {
690 + keys: {}, // keys for data-common-units
691 + latest: {}, // latest selected units for data-common-units
692 +
693 + globalReset: function () {
694 + this.keys = {};
695 + this.latest = {};
696 + },
697 +
698 + scalableUnits: {
699 + 'packets/s': {
700 + 'pps': 1,
701 + 'Kpps': 1000,
702 + 'Mpps': 1000000
703 + },
704 + 'pps': {
705 + 'pps': 1,
706 + 'Kpps': 1000,
707 + 'Mpps': 1000000
708 + },
709 + 'kilobits/s': {
710 + 'bits/s': 1 / 1000,
711 + 'kilobits/s': 1,
712 + 'megabits/s': 1000,
713 + 'gigabits/s': 1000000,
714 + 'terabits/s': 1000000000
715 + },
716 + 'kilobytes/s': {
717 + 'bytes/s': 1 / 1024,
718 + 'kilobytes/s': 1,
719 + 'megabytes/s': 1024,
720 + 'gigabytes/s': 1024 * 1024,
721 + 'terabytes/s': 1024 * 1024 * 1024
722 + },
723 + 'KB/s': {
724 + 'B/s': 1 / 1024,
725 + 'KB/s': 1,
726 + 'MB/s': 1024,
727 + 'GB/s': 1024 * 1024,
728 + 'TB/s': 1024 * 1024 * 1024
729 + },
730 + 'KB': {
731 + 'B': 1 / 1024,
732 + 'KB': 1,
733 + 'MB': 1024,
734 + 'GB': 1024 * 1024,
735 + 'TB': 1024 * 1024 * 1024
736 + },
737 + 'MB': {
738 + 'B': 1 / (1024 * 1024),
739 + 'KB': 1 / 1024,
740 + 'MB': 1,
741 + 'GB': 1024,
742 + 'TB': 1024 * 1024,
743 + 'PB': 1024 * 1024 * 1024
744 + },
745 + 'GB': {
746 + 'B': 1 / (1024 * 1024 * 1024),
747 + 'KB': 1 / (1024 * 1024),
748 + 'MB': 1 / 1024,
749 + 'GB': 1,
750 + 'TB': 1024,
751 + 'PB': 1024 * 1024,
752 + 'EB': 1024 * 1024 * 1024
753 + }
754 + /*
755 + 'milliseconds': {
756 + 'seconds': 1000
757 + },
758 + 'seconds': {
759 + 'milliseconds': 0.001,
760 + 'seconds': 1,
761 + 'minutes': 60,
762 + 'hours': 3600,
763 + 'days': 86400
764 + }
765 + */
766 + },
767 +
768 + convertibleUnits: {
769 + 'Celsius': {
770 + 'Fahrenheit': {
771 + check: function (max) {
772 + void(max);
773 + return NETDATA.options.current.temperature === 'fahrenheit';
774 + },
775 + convert: function (value) {
776 + return value * 9 / 5 + 32;
777 + }
778 + }
779 + },
780 + 'celsius': {
781 + 'fahrenheit': {
782 + check: function (max) {
783 + void(max);
784 + return NETDATA.options.current.temperature === 'fahrenheit';
785 + },
786 + convert: function (value) {
787 + return value * 9 / 5 + 32;
788 + }
789 + }
790 + },
791 + 'seconds': {
792 + 'time': {
793 + check: function (max) {
794 + void(max);
795 + return NETDATA.options.current.seconds_as_time;
796 + },
797 + convert: function (seconds) {
798 + return NETDATA.unitsConversion.seconds2time(seconds);
799 + }
800 + }
801 + },
802 + 'milliseconds': {
803 + 'milliseconds': {
804 + check: function (max) {
805 + return NETDATA.options.current.seconds_as_time && max < 1000;
806 + },
807 + convert: function (milliseconds) {
808 + let tms = Math.round(milliseconds * 10);
809 + milliseconds = Math.floor(tms / 10);
810
525 - sync_pan_and_zoom: true, // enable or disable pan and zoom sync
811 + tms -= milliseconds * 10;
812
527 - pan_and_zoom_data_padding: true, // fetch more data for the master chart when panning or zooming
813 + return (milliseconds).toString() + '.' + tms.toString();
814 + }
815 + },
816 + 'seconds': {
817 + check: function (max) {
818 + return NETDATA.options.current.seconds_as_time && max >= 1000 && max < 60000;
819 + },
820 + convert: function (milliseconds) {
821 + milliseconds = Math.round(milliseconds);
822
529 - update_only_visible: true, // enable or disable visibility management / used for printing
823 + let seconds = Math.floor(milliseconds / 1000);
824 + milliseconds -= seconds * 1000;
825
531 - parallel_refresher: (isSlowDevice() === false), // enable parallel refresh of charts
826 + milliseconds = Math.round(milliseconds / 10);
827
533 - concurrent_refreshes: true, // when parallel_refresher is enabled, sync also the charts
828 + return seconds.toString() + '.'
829 + + NETDATA.zeropad(milliseconds);
830 + }
831 + },
832 + 'M:SS.ms': {
833 + check: function (max) {
834 + return NETDATA.options.current.seconds_as_time && max >= 60000;
835 + },
836 + convert: function (milliseconds) {
837 + milliseconds = Math.round(milliseconds);
838
535 - destroy_on_hide: (isSlowDevice() === true), // destroy charts when they are not visible
839 + let minutes = Math.floor(milliseconds / 60000);
840 + milliseconds -= minutes * 60000;
841
537 - show_help: netdataShowHelp, // when enabled the charts will show some help
538 - show_help_delay_show_ms: 500,
539 - show_help_delay_hide_ms: 0,
842 + let seconds = Math.floor(milliseconds / 1000);
843 + milliseconds -= seconds * 1000;
844
541 - eliminate_zero_dimensions: true, // do not show dimensions with just zeros
845 + milliseconds = Math.round(milliseconds / 10);
846
543 - stop_updates_when_focus_is_lost: true, // boolean - shall we stop auto-refreshes when document does not have user focus
544 - stop_updates_while_resizing: 1000, // ms - time to stop auto-refreshes while resizing the charts
847 + return minutes.toString() + ':'
848 + + NETDATA.zeropad(seconds) + '.'
849 + + NETDATA.zeropad(milliseconds);
850 + }
851 + }
852 + }
853 + },
854
546 - double_click_speed: 500, // ms - time between clicks / taps to detect double click/tap
855 + seconds2time: function (seconds) {
856 + seconds = Math.abs(seconds);
857
548 - smooth_plot: (isSlowDevice() === false), // enable smooth plot, where possible
858 + let days = Math.floor(seconds / 86400);
859 + seconds -= days * 86400;
860
550 - color_fill_opacity_line: 1.0,
551 - color_fill_opacity_area: 0.2,
552 - color_fill_opacity_stacked: 0.8,
861 + let hours = Math.floor(seconds / 3600);
862 + seconds -= hours * 3600;
863
554 - pan_and_zoom_factor: 0.25, // the increment when panning and zooming with the toolbox
555 - pan_and_zoom_factor_multiplier_control: 2.0,
556 - pan_and_zoom_factor_multiplier_shift: 3.0,
557 - pan_and_zoom_factor_multiplier_alt: 4.0,
864 + let minutes = Math.floor(seconds / 60);
865 + seconds -= minutes * 60;
866
559 - abort_ajax_on_scroll: false, // kill pending ajax page scroll
560 - async_on_scroll: false, // sync/async onscroll handler
561 - onscroll_worker_duration_threshold: 30, // time in ms, for async scroll handler
867 + seconds = Math.round(seconds);
868
563 - retries_on_data_failures: 3, // how many retries to make if we can't fetch chart data from the server
869 + let ms_txt = '';
870 + /*
871 + let ms = seconds - Math.floor(seconds);
872 + seconds -= ms;
873 + ms = Math.round(ms * 1000);
874
565 - setOptionCallback: function() { }
566 - },
875 + if (ms > 1) {
876 + if (ms < 10)
877 + ms_txt = '.00' + ms.toString();
878 + else if (ms < 100)
879 + ms_txt = '.0' + ms.toString();
880 + else
881 + ms_txt = '.' + ms.toString();
882 + }
883 + */
884 +
885 + return ((days > 0) ? days.toString() + 'd:' : '').toString()
886 + + NETDATA.zeropad(hours) + ':'
887 + + NETDATA.zeropad(minutes) + ':'
888 + + NETDATA.zeropad(seconds)
889 + + ms_txt;
890 + },
891 +
892 + // get a function that converts the units
893 + // + every time units are switched call the callback
894 + get: function (uuid, min, max, units, desired_units, common_units_name, switch_units_callback) {
895 + // validate the parameters
896 + if (typeof units === 'undefined') {
897 + units = 'undefined';
898 + }
899 +
900 + // check if we support units conversion
901 + if (typeof this.scalableUnits[units] === 'undefined' && typeof this.convertibleUnits[units] === 'undefined') {
902 + // we can't convert these units
903 + //console.log('DEBUG: ' + uuid.toString() + ' can\'t convert units: ' + units.toString());
904 + return function (value) {
905 + return value;
906 + };
907 + }
908
568 - debug: {
569 - show_boxes: false,
570 - main_loop: false,
571 - focus: false,
572 - visibility: false,
573 - chart_data_url: false,
574 - chart_errors: false, // remember to set it to false before merging
575 - chart_timing: false,
576 - chart_calls: false,
577 - libraries: false,
578 - dygraph: false,
579 - globalSelectionSync:false,
580 - globalPanAndZoom: false
909 + // check if the caller wants the original units
910 + if (typeof desired_units === 'undefined' || desired_units === null || desired_units === 'original' || desired_units === units) {
911 + //console.log('DEBUG: ' + uuid.toString() + ' original units wanted');
912 + switch_units_callback(units);
913 + return function (value) {
914 + return value;
915 + };
916 }
582 - };
917
584 - NETDATA.statistics = {
585 - refreshes_total: 0,
586 - refreshes_active: 0,
587 - refreshes_active_max: 0
588 - };
918 + // now we know we can convert the units
919 + // and the caller wants some kind of conversion
920
921 + let tunits = null;
922 + let tdivider = 0;
923
591 - // ----------------------------------------------------------------------------------------------------------------
924 + if (typeof this.scalableUnits[units] !== 'undefined') {
925 + // units that can be scaled
926 + // we decide a divider
927
593 - NETDATA.timeout = {
594 - // by default, these are just wrappers to setTimeout() / clearTimeout()
928 + // console.log('NETDATA.unitsConversion.get(' + units.toString() + ', ' + desired_units.toString() + ', function()) decide divider with min = ' + min.toString() + ', max = ' + max.toString());
929
596 - step: function(callback) {
597 - return window.setTimeout(callback, 1000 / 60);
598 - },
930 + if (desired_units === 'auto') {
931 + // the caller wants to auto-scale the units
932
600 - set: function(callback, delay) {
601 - return window.setTimeout(callback, delay);
602 - },
933 + // find the absolute maximum value that is rendered on the chart
934 + // based on this we decide the scale
935 + min = Math.abs(min);
936 + max = Math.abs(max);
937 + if (min > max) {
938 + max = min;
939 + }
940
604 - clear: function(id) {
605 - return window.clearTimeout(id);
606 - },
941 + // find the smallest scale that provides integers
942 + // for (x in this.scalableUnits[units]) {
943 + // if (this.scalableUnits[units].hasOwnProperty(x)) {
944 + // let m = this.scalableUnits[units][x];
945 + // if (m <= max && m > tdivider) {
946 + // tunits = x;
947 + // tdivider = m;
948 + // }
949 + // }
950 + // }
951 + const sunit = this.scalableUnits[units];
952 + for (const x of Object.keys(sunit)) {
953 + let m = sunit[x];
954 + if (m <= max && m > tdivider) {
955 + tunits = x;
956 + tdivider = m;
957 + }
958 + }
959
608 - init: function() {
609 - var custom = true;
960 + if (tunits === null || tdivider <= 0) {
961 + // we couldn't find one
962 + //console.log('DEBUG: ' + uuid.toString() + ' cannot find an auto-scaling candidate for units: ' + units.toString() + ' (max: ' + max.toString() + ')');
963 + switch_units_callback(units);
964 + return function (value) {
965 + return value;
966 + };
967 + }
968
611 - if(window.requestAnimationFrame) {
612 - this.step = function(callback) {
613 - return window.requestAnimationFrame(callback);
614 - };
969 + if (typeof common_units_name === 'string' && typeof uuid === 'string') {
970 + // the caller wants several charts to have the same units
971 + // data-common-units
972
616 - this.clear = function(handle) {
617 - return window.cancelAnimationFrame(handle.value);
618 - };
619 - }
620 - else if(window.webkitRequestAnimationFrame) {
621 - this.step = function(callback) {
622 - return window.webkitRequestAnimationFrame(callback);
623 - };
973 + let common_units_key = common_units_name + '-' + units;
974
625 - if(window.webkitCancelAnimationFrame) {
626 - this.clear = function (handle) {
627 - return window.webkitCancelAnimationFrame(handle.value);
628 - };
629 - }
630 - else if(window.webkitCancelRequestAnimationFrame) {
631 - this.clear = function (handle) {
632 - return window.webkitCancelRequestAnimationFrame(handle.value);
975 + // add our divider into the list of keys
976 + let t = this.keys[common_units_key];
977 + if (typeof t === 'undefined') {
978 + this.keys[common_units_key] = {};
979 + t = this.keys[common_units_key];
980 + }
981 + t[uuid] = {
982 + units: tunits,
983 + divider: tdivider
984 };
634 - }
635 - }
636 - else if(window.mozRequestAnimationFrame) {
637 - this.step = function(callback) {
638 - return window.mozRequestAnimationFrame(callback);
639 - };
985
641 - this.clear = function(handle) {
642 - return window.mozCancelRequestAnimationFrame(handle.value);
643 - };
644 - }
645 - else if(window.oRequestAnimationFrame) {
646 - this.step = function(callback) {
647 - return window.oRequestAnimationFrame(callback);
648 - };
649 -
650 - this.clear = function(handle) {
651 - return window.oCancelRequestAnimationFrame(handle.value);
652 - };
653 - }
654 - else if(window.msRequestAnimationFrame) {
655 - this.step = function(callback) {
656 - return window.msRequestAnimationFrame(callback);
657 - };
658 -
659 - this.clear = function(handle) {
660 - return window.msCancelRequestAnimationFrame(handle.value);
661 - };
662 - }
663 - else
664 - custom = false;
665 -
666 -
667 - if(custom === true) {
668 - // we have installed custom .step() / .clear() functions
669 - // overwrite the .set() too
670 -
671 - this.set = function(callback, delay) {
672 - var that = this;
986 + // find the max divider of all charts
987 + let common_units = t[uuid];
988 + for (const x in t) {
989 + if (t.hasOwnProperty(x) && t[x].divider > common_units.divider) {
990 + common_units = t[x];
991 + }
992 + }
993
674 - var start = Date.now(),
675 - handle = new Object();
994 + // save our common_max to the latest keys
995 + let latest = this.latest[common_units_key];
996 + if (typeof latest === 'undefined') {
997 + this.latest[common_units_key] = {};
998 + latest = this.latest[common_units_key];
999 + }
1000 + latest.units = common_units.units;
1001 + latest.divider = common_units.divider;
1002 +
1003 + tunits = latest.units;
1004 + tdivider = latest.divider;
1005 +
1006 + //console.log('DEBUG: ' + uuid.toString() + ' converted units: ' + units.toString() + ' to units: ' + tunits.toString() + ' with divider ' + tdivider.toString() + ', common-units=' + common_units_name.toString() + ((t[uuid].divider !== tdivider)?' USED COMMON, mine was ' + t[uuid].units:' set common').toString());
1007 +
1008 + // apply it to this chart
1009 + switch_units_callback(tunits);
1010 + return function (value) {
1011 + if (tdivider !== latest.divider) {
1012 + // another chart switched our common units
1013 + // we should switch them too
1014 + //console.log('DEBUG: ' + uuid + ' switching units due to a common-units change, from ' + tunits.toString() + ' to ' + latest.units.toString());
1015 + tunits = latest.units;
1016 + tdivider = latest.divider;
1017 + switch_units_callback(tunits);
1018 + }
1019
677 - function loop() {
678 - var current = Date.now(),
679 - delta = current - start;
1020 + return value / tdivider;
1021 + };
1022 + } else {
1023 + // the caller did not give data-common-units
1024 + // this chart auto-scales independently of all others
1025 + //console.log('DEBUG: ' + uuid.toString() + ' converted units: ' + units.toString() + ' to units: ' + tunits.toString() + ' with divider ' + tdivider.toString() + ', autonomously');
1026 +
1027 + switch_units_callback(tunits);
1028 + return function (value) {
1029 + return value / tdivider;
1030 + };
1031 + }
1032 + } else {
1033 + // the caller wants specific units
1034
681 - if(delta >= delay) {
682 - callback.call();
683 - }
684 - else {
685 - handle.value = that.step(loop);
1035 + if (typeof this.scalableUnits[units][desired_units] !== 'undefined') {
1036 + // all good, set the new units
1037 + tdivider = this.scalableUnits[units][desired_units];
1038 + // console.log('DEBUG: ' + uuid.toString() + ' converted units: ' + units.toString() + ' to units: ' + desired_units.toString() + ' with divider ' + tdivider.toString() + ', by reference');
1039 + switch_units_callback(desired_units);
1040 + return function (value) {
1041 + return value / tdivider;
1042 + };
1043 + } else {
1044 + // oops! switch back to original units
1045 + console.log('Units conversion from ' + units.toString() + ' to ' + desired_units.toString() + ' is not supported.');
1046 + switch_units_callback(units);
1047 + return function (value) {
1048 + return value;
1049 + };
1050 + }
1051 + }
1052 + } else if (typeof this.convertibleUnits[units] !== 'undefined') {
1053 + // units that can be converted
1054 + if (desired_units === 'auto') {
1055 + for (const x in this.convertibleUnits[units]) {
1056 + if (this.convertibleUnits[units].hasOwnProperty(x)) {
1057 + if (this.convertibleUnits[units][x].check(max)) {
1058 + //console.log('DEBUG: ' + uuid.toString() + ' converting ' + units.toString() + ' to: ' + x.toString());
1059 + switch_units_callback(x);
1060 + return this.convertibleUnits[units][x].convert;
1061 }
1062 }
1063 + }
1064
689 - handle.value = that.step(loop);
690 - return handle;
1065 + // none checked ok
1066 + //console.log('DEBUG: ' + uuid.toString() + ' no conversion available for ' + units.toString() + ' to: ' + desired_units.toString());
1067 + switch_units_callback(units);
1068 + return function (value) {
1069 + return value;
1070 + };
1071 + } else if (typeof this.convertibleUnits[units][desired_units] !== 'undefined') {
1072 + switch_units_callback(desired_units);
1073 + return this.convertibleUnits[units][desired_units].convert;
1074 + } else {
1075 + console.log('Units conversion from ' + units.toString() + ' to ' + desired_units.toString() + ' is not supported.');
1076 + switch_units_callback(units);
1077 + return function (value) {
1078 + return value;
1079 };
1080 }
1081 + } else {
1082 + // hm... did we forget to implement the new type?
1083 + console.log(`Unmatched unit conversion method for units ${units.toString()}`);
1084 + switch_units_callback(units);
1085 + return function (value) {
1086 + return value;
1087 + };
1088 }
694 - };
695 -
696 - NETDATA.timeout.init();
697 -
698 -
699 - // ----------------------------------------------------------------------------------------------------------------
700 - // local storage options
701 -
702 - NETDATA.localStorage = {
703 - default: {},
704 - current: {},
705 - callback: {} // only used for resetting back to defaults
706 - };
707 -
708 - NETDATA.localStorageTested = -1;
709 - NETDATA.localStorageTest = function() {
710 - if(NETDATA.localStorageTested !== -1)
711 - return NETDATA.localStorageTested;
712 -
713 - if(typeof Storage !== "undefined" && typeof localStorage === 'object') {
714 - var test = 'test';
715 - try {
716 - localStorage.setItem(test, test);
717 - localStorage.removeItem(test);
718 - NETDATA.localStorageTested = true;
719 - }
720 - catch (e) {
721 - NETDATA.localStorageTested = false;
722 - }
1089 + }
1090 +};
1091 +
1092 +NETDATA.icons = {
1093 + left: '<i class="fas fa-backward"></i>',
1094 + reset: '<i class="fas fa-play"></i>',
1095 + right: '<i class="fas fa-forward"></i>',
1096 + zoomIn: '<i class="fas fa-plus"></i>',
1097 + zoomOut: '<i class="fas fa-minus"></i>',
1098 + resize: '<i class="fas fa-sort"></i>',
1099 + lineChart: '<i class="fas fa-chart-line"></i>',
1100 + areaChart: '<i class="fas fa-chart-area"></i>',
1101 + noChart: '<i class="fas fa-chart-area"></i>',
1102 + loading: '<i class="fas fa-sync-alt"></i>',
1103 + noData: '<i class="fas fa-exclamation-triangle"></i>'
1104 +};
1105 +
1106 +if (typeof netdataIcons === 'object') {
1107 + // for (let icon in NETDATA.icons) {
1108 + // if (NETDATA.icons.hasOwnProperty(icon) && typeof(netdataIcons[icon]) === 'string')
1109 + // NETDATA.icons[icon] = netdataIcons[icon];
1110 + // }
1111 + for (const icon of Object.keys(NETDATA.icons)) {
1112 + if (typeof(netdataIcons[icon]) === 'string') {
1113 + NETDATA.icons[icon] = netdataIcons[icon]
1114 }
724 - else
725 - NETDATA.localStorageTested = false;
726 -
727 - return NETDATA.localStorageTested;
728 - };
729 -
730 - NETDATA.localStorageGet = function(key, def, callback) {
731 - var ret = def;
1115 + }
1116 +}
1117
733 - if(typeof NETDATA.localStorage.default[key.toString()] === 'undefined') {
734 - NETDATA.localStorage.default[key.toString()] = def;
735 - NETDATA.localStorage.callback[key.toString()] = callback;
736 - }
1118 +if (typeof netdataSnapshotData === 'undefined') {
1119 + netdataSnapshotData = null;
1120 +}
1121
738 - if(NETDATA.localStorageTest() === true) {
739 - try {
740 - // console.log('localStorage: loading "' + key.toString() + '"');
741 - ret = localStorage.getItem(key.toString());
742 - // console.log('netdata loaded: ' + key.toString() + ' = ' + ret.toString());
743 - if(ret === null || ret === 'undefined') {
744 - // console.log('localStorage: cannot load it, saving "' + key.toString() + '" with value "' + JSON.stringify(def) + '"');
745 - localStorage.setItem(key.toString(), JSON.stringify(def));
746 - ret = def;
747 - }
748 - else {
749 - // console.log('localStorage: got "' + key.toString() + '" with value "' + ret + '"');
750 - ret = JSON.parse(ret);
751 - // console.log('localStorage: loaded "' + key.toString() + '" as value ' + ret + ' of type ' + typeof(ret));
752 - }
753 - }
754 - catch(error) {
755 - console.log('localStorage: failed to read "' + key.toString() + '", using default: "' + def.toString() + '"');
756 - ret = def;
757 - }
758 - }
1122 +if (typeof netdataShowHelp === 'undefined') {
1123 + netdataShowHelp = true;
1124 +}
1125
760 - if(typeof ret === 'undefined' || ret === 'undefined') {
761 - console.log('localStorage: LOADED UNDEFINED "' + key.toString() + '" as value ' + ret + ' of type ' + typeof(ret));
762 - ret = def;
763 - }
1126 +if (typeof netdataShowAlarms === 'undefined') {
1127 + netdataShowAlarms = false;
1128 +}
1129
765 - NETDATA.localStorage.current[key.toString()] = ret;
766 - return ret;
767 - };
1130 +if (typeof netdataRegistryAfterMs !== 'number' || netdataRegistryAfterMs < 0) {
1131 + netdataRegistryAfterMs = 1500;
1132 +}
1133
769 - NETDATA.localStorageSet = function(key, value, callback) {
770 - if(typeof value === 'undefined' || value === 'undefined') {
771 - console.log('localStorage: ATTEMPT TO SET UNDEFINED "' + key.toString() + '" as value ' + value + ' of type ' + typeof(value));
772 - }
1134 +if (typeof netdataRegistry === 'undefined') {
1135 + // backward compatibility
1136 + netdataRegistry = (typeof netdataNoRegistry !== 'undefined' && netdataNoRegistry === false);
1137 +}
1138
774 - if(typeof NETDATA.localStorage.default[key.toString()] === 'undefined') {
775 - NETDATA.localStorage.default[key.toString()] = value;
776 - NETDATA.localStorage.current[key.toString()] = value;
777 - NETDATA.localStorage.callback[key.toString()] = callback;
778 - }
1139 +if (netdataRegistry === false && typeof netdataRegistryCallback === 'function') {
1140 + netdataRegistry = true;
1141 +}
1142
780 - if(NETDATA.localStorageTest() === true) {
781 - // console.log('localStorage: saving "' + key.toString() + '" with value "' + JSON.stringify(value) + '"');
782 - try {
783 - localStorage.setItem(key.toString(), JSON.stringify(value));
784 - }
785 - catch(e) {
786 - console.log('localStorage: failed to save "' + key.toString() + '" with value: "' + value.toString() + '"');
787 - }
788 - }
1143 +// ----------------------------------------------------------------------------------------------------------------
1144 +// the defaults for all charts
1145
790 - NETDATA.localStorage.current[key.toString()] = value;
791 - return value;
792 - };
1146 +// if the user does not specify any of these, the following will be used
1147
794 - NETDATA.localStorageGetRecursive = function(obj, prefix, callback) {
795 - var keys = Object.keys(obj);
796 - var len = keys.length;
797 - while(len--) {
798 - var i = keys[len];
1148 +NETDATA.chartDefaults = {
1149 + width: '100%', // the chart width - can be null
1150 + height: '100%', // the chart height - can be null
1151 + min_width: null, // the chart minimum width - can be null
1152 + library: 'dygraph', // the graphing library to use
1153 + method: 'average', // the grouping method
1154 + before: 0, // panning
1155 + after: -600, // panning
1156 + pixels_per_point: 1, // the detail of the chart
1157 + fill_luminance: 0.8 // luminance of colors in solid areas
1158 +};
1159
800 - if(typeof obj[i] === 'object') {
801 - //console.log('object ' + prefix + '.' + i.toString());
802 - NETDATA.localStorageGetRecursive(obj[i], prefix + '.' + i.toString(), callback);
803 - continue;
804 - }
1160 +// ----------------------------------------------------------------------------------------------------------------
1161 +// global options
1162
806 - obj[i] = NETDATA.localStorageGet(prefix + '.' + i.toString(), obj[i], callback);
807 - }
808 - };
1163 +NETDATA.options = {
1164 + pauseCallback: null, // a callback when we are really paused
1165
810 - NETDATA.setOption = function(key, value) {
811 - if(key.toString() === 'setOptionCallback') {
812 - if(typeof NETDATA.options.current.setOptionCallback === 'function') {
813 - NETDATA.options.current[key.toString()] = value;
814 - NETDATA.options.current.setOptionCallback();
815 - }
816 - }
817 - else if(NETDATA.options.current[key.toString()] !== value) {
818 - var name = 'options.' + key.toString();
1166 + pause: false, // when enabled we don't auto-refresh the charts
1167
820 - if(typeof NETDATA.localStorage.default[name.toString()] === 'undefined')
821 - console.log('localStorage: setOption() on unsaved option: "' + name.toString() + '", value: ' + value);
1168 + targets: [], // an array of all the state objects that are
1169 + // currently active (independently of their
1170 + // viewport visibility)
1171
823 - //console.log(NETDATA.localStorage);
824 - //console.log('setOption: setting "' + key.toString() + '" to "' + value + '" of type ' + typeof(value) + ' original type ' + typeof(NETDATA.options.current[key.toString()]));
825 - //console.log(NETDATA.options);
826 - NETDATA.options.current[key.toString()] = NETDATA.localStorageSet(name.toString(), value, null);
1172 + updated_dom: true, // when true, the DOM has been updated with
1173 + // new elements we have to check.
1174
828 - if(typeof NETDATA.options.current.setOptionCallback === 'function')
829 - NETDATA.options.current.setOptionCallback();
830 - }
1175 + auto_refresher_fast_weight: 0, // this is the current time in ms, spent
1176 + // rendering charts continuously.
1177 + // used with .current.fast_render_timeframe
1178
832 - return true;
833 - };
1179 + page_is_visible: true, // when true, this page is visible
1180
835 - NETDATA.getOption = function(key) {
836 - return NETDATA.options.current[key.toString()];
837 - };
1181 + auto_refresher_stop_until: 0, // timestamp in ms - used internally, to stop the
1182 + // auto-refresher for some time (when a chart is
1183 + // performing pan or zoom, we need to stop refreshing
1184 + // all other charts, to have the maximum speed for
1185 + // rendering the chart that is panned or zoomed).
1186 + // Used with .current.global_pan_sync_time
1187
839 - // read settings from local storage
840 - NETDATA.localStorageGetRecursive(NETDATA.options.current, 'options', null);
1188 + on_scroll_refresher_stop_until: 0, // timestamp in ms - used to stop evaluating
1189 + // charts for some time, after a page scroll
1190
842 - // always start with this option enabled.
843 - NETDATA.setOption('stop_updates_when_focus_is_lost', true);
1191 + last_page_resize: Date.now(), // the timestamp of the last resize request
1192
845 - NETDATA.resetOptions = function() {
846 - var keys = Object.keys(NETDATA.localStorage.default);
847 - var len = keys.length;
848 - while(len--) {
849 - var i = keys[len];
850 - var a = i.split('.');
1193 + last_page_scroll: 0, // the timestamp the last time the page was scrolled
1194
852 - if(a[0] === 'options') {
853 - if(a[1] === 'setOptionCallback') continue;
854 - if(typeof NETDATA.localStorage.default[i] === 'undefined') continue;
855 - if(NETDATA.options.current[i] === NETDATA.localStorage.default[i]) continue;
1195 + browser_timezone: 'unknown', // timezone detected by javascript
1196 + server_timezone: 'unknown', // timezone reported by the server
1197
857 - NETDATA.setOption(a[1], NETDATA.localStorage.default[i]);
858 - }
859 - else if(a[0] === 'chart_heights') {
860 - if(typeof NETDATA.localStorage.callback[i] === 'function' && typeof NETDATA.localStorage.default[i] !== 'undefined') {
861 - NETDATA.localStorage.callback[i](NETDATA.localStorage.default[i]);
862 - }
863 - }
864 - }
1198 + force_data_points: 0, // force the number of points to be returned for charts
1199 + fake_chart_rendering: false, // when set to true, the dashboard will download data but will not render the charts
1200
866 - NETDATA.dateTime.init(NETDATA.options.current.timezone);
867 - };
1201 + passive_events: null, // true if the browser supports passive events
1202
869 - // ----------------------------------------------------------------------------------------------------------------
1203 + // the current profile
1204 + // we may have many...
1205 + current: {
1206 + units: 'auto', // can be 'auto' or 'original'
1207 + temperature: 'celsius', // can be 'celsius' or 'fahrenheit'
1208 + seconds_as_time: true, // show seconds as DDd:HH:MM:SS ?
1209 + timezone: 'default', // the timezone to use, or 'default'
1210 + user_set_server_timezone: 'default', // as set by the user on the dashboard
1211
871 - if(NETDATA.options.debug.main_loop === true)
872 - console.log('welcome to NETDATA');
1212 + legend_toolbox: true, // show the legend toolbox on charts
1213 + resize_charts: true, // show the resize handler on charts
1214
874 - NETDATA.onresizeCallback = null;
875 - NETDATA.onresize = function() {
876 - NETDATA.options.last_page_resize = Date.now();
877 - NETDATA.onscroll();
1215 + pixels_per_point: isSlowDevice() ? 5 : 1, // the minimum pixels per point for all charts
1216 + // increase this to speed javascript up
1217 + // each chart library has its own limit too
1218 + // the max of this and the chart library is used
1219 + // the final is calculated every time, so a change
1220 + // here will have immediate effect on the next chart
1221 + // update
1222
879 - if(typeof NETDATA.onresizeCallback === 'function')
880 - NETDATA.onresizeCallback();
881 - };
1223 + idle_between_charts: 100, // ms - how much time to wait between chart updates
1224
883 - NETDATA.abort_all_refreshes = function() {
884 - var targets = NETDATA.options.targets;
885 - var len = targets.length;
1225 + fast_render_timeframe: 200, // ms - render continuously until this time of continuous
1226 + // rendering has been reached
1227 + // this setting is used to make it render e.g. 10
1228 + // charts at once, sleep idle_between_charts time
1229 + // and continue for another 10 charts.
1230
887 - while (len--) {
888 - if (targets[len].fetching_data === true) {
889 - if (typeof targets[len].xhr !== 'undefined') {
890 - targets[len].xhr.abort();
891 - targets[len].running = false;
892 - targets[len].fetching_data = false;
893 - }
894 - }
895 - }
896 - };
1231 + idle_between_loops: 500, // ms - if all charts have been updated, wait this
1232 + // time before starting again.
1233
898 - NETDATA.onscroll_start_delay = function() {
899 - NETDATA.options.last_page_scroll = Date.now();
1234 + idle_parallel_loops: 100, // ms - the time between parallel refresher updates
1235
901 - NETDATA.options.on_scroll_refresher_stop_until =
902 - NETDATA.options.last_page_scroll
903 - + ((NETDATA.options.current.async_on_scroll === true) ? 1000 : 0);
904 - };
1236 + idle_lost_focus: 500, // ms - when the window does not have focus, check
1237 + // if focus has been regained, every this time
1238
906 - NETDATA.onscroll_end_delay = function() {
907 - NETDATA.options.on_scroll_refresher_stop_until =
908 - Date.now()
909 - + ((NETDATA.options.current.async_on_scroll === true) ? NETDATA.options.current.onscroll_worker_duration_threshold : 0);
910 - };
1239 + global_pan_sync_time: 300, // ms - when you pan or zoom a chart, the background
1240 + // auto-refreshing of charts is paused for this amount
1241 + // of time
1242
912 - NETDATA.onscroll_updater_timeout_id = undefined;
913 - NETDATA.onscroll_updater = function() {
914 - NETDATA.globalSelectionSync.stop();
1243 + sync_selection_delay: 400, // ms - when you pan or zoom a chart, wait this amount
1244 + // of time before setting up synchronized selections
1245 + // on hover.
1246
916 - if(NETDATA.options.abort_ajax_on_scroll === true)
917 - NETDATA.abort_all_refreshes();
1247 + sync_selection: true, // enable or disable selection sync
1248
919 - // when the user scrolls he sees that we have
920 - // hidden all the not-visible charts
921 - // using this little function we try to switch
922 - // the charts back to visible quickly
1249 + pan_and_zoom_delay: 50, // when panning or zooming, how ofter to update the chart
1250
924 - if(NETDATA.intersectionObserver.enabled() === false) {
925 - if (NETDATA.options.current.parallel_refresher === false) {
926 - var targets = NETDATA.options.targets;
927 - var len = targets.length;
1251 + sync_pan_and_zoom: true, // enable or disable pan and zoom sync
1252
929 - while (len--)
930 - if (targets[len].running === false)
931 - targets[len].isVisible();
932 - }
933 - }
1253 + pan_and_zoom_data_padding: true, // fetch more data for the master chart when panning or zooming
1254
935 - NETDATA.onscroll_end_delay();
936 - };
1255 + update_only_visible: true, // enable or disable visibility management / used for printing
1256
938 - NETDATA.scrollUp = false;
939 - NETDATA.scrollY = window.scrollY;
940 - NETDATA.onscroll = function() {
941 - //console.log('onscroll() begin');
1257 + parallel_refresher: !isSlowDevice(), // enable parallel refresh of charts
1258
943 - NETDATA.onscroll_start_delay();
944 - NETDATA.chartRefresherReschedule();
1259 + concurrent_refreshes: true, // when parallel_refresher is enabled, sync also the charts
1260
946 - NETDATA.scrollUp = (window.scrollY > NETDATA.scrollY);
947 - NETDATA.scrollY = window.scrollY;
1261 + destroy_on_hide: isSlowDevice(), // destroy charts when they are not visible
1262
949 - if(NETDATA.onscroll_updater_timeout_id)
950 - NETDATA.timeout.clear(NETDATA.onscroll_updater_timeout_id);
1263 + show_help: netdataShowHelp, // when enabled the charts will show some help
1264 + show_help_delay_show_ms: 500,
1265 + show_help_delay_hide_ms: 0,
1266
952 - NETDATA.onscroll_updater_timeout_id = NETDATA.timeout.set(NETDATA.onscroll_updater, 0);
953 - //console.log('onscroll() end');
954 - };
1267 + eliminate_zero_dimensions: true, // do not show dimensions with just zeros
1268
956 - NETDATA.supportsPassiveEvents = function() {
957 - if(NETDATA.options.passive_events === null) {
958 - var supportsPassive = false;
959 - try {
960 - var opts = Object.defineProperty({}, 'passive', {
961 - get: function () {
962 - supportsPassive = true;
963 - }
964 - });
965 - window.addEventListener("test", null, opts);
966 - } catch (e) {
967 - console.log('browser does not support passive events');
968 - }
1269 + stop_updates_when_focus_is_lost: true, // boolean - shall we stop auto-refreshes when document does not have user focus
1270 + stop_updates_while_resizing: 1000, // ms - time to stop auto-refreshes while resizing the charts
1271
970 - NETDATA.options.passive_events = supportsPassive;
971 - }
1272 + double_click_speed: 500, // ms - time between clicks / taps to detect double click/tap
1273
973 - // console.log('passive ' + NETDATA.options.passive_events);
974 - return NETDATA.options.passive_events;
975 - };
1274 + smooth_plot: !isSlowDevice(), // enable smooth plot, where possible
1275
977 - window.addEventListener('resize', NETDATA.onresize, NETDATA.supportsPassiveEvents() ? { passive: true } : false);
978 - window.addEventListener('scroll', NETDATA.onscroll, NETDATA.supportsPassiveEvents() ? { passive: true } : false);
979 - // window.onresize = NETDATA.onresize;
980 - // window.onscroll = NETDATA.onscroll;
1276 + color_fill_opacity_line: 1.0,
1277 + color_fill_opacity_area: 0.2,
1278 + color_fill_opacity_stacked: 0.8,
1279
982 - // ----------------------------------------------------------------------------------------------------------------
983 - // Error Handling
984 -
985 - NETDATA.errorCodes = {
986 - 100: { message: "Cannot load chart library", alert: true },
987 - 101: { message: "Cannot load jQuery", alert: true },
988 - 402: { message: "Chart library not found", alert: false },
989 - 403: { message: "Chart library not enabled/is failed", alert: false },
990 - 404: { message: "Chart not found", alert: false },
991 - 405: { message: "Cannot download charts index from server", alert: true },
992 - 406: { message: "Invalid charts index downloaded from server", alert: true },
993 - 407: { message: "Cannot HELLO netdata server", alert: false },
994 - 408: { message: "Netdata servers sent invalid response to HELLO", alert: false },
995 - 409: { message: "Cannot ACCESS netdata registry", alert: false },
996 - 410: { message: "Netdata registry ACCESS failed", alert: false },
997 - 411: { message: "Netdata registry server send invalid response to DELETE ", alert: false },
998 - 412: { message: "Netdata registry DELETE failed", alert: false },
999 - 413: { message: "Netdata registry server send invalid response to SWITCH ", alert: false },
1000 - 414: { message: "Netdata registry SWITCH failed", alert: false },
1001 - 415: { message: "Netdata alarms download failed", alert: false },
1002 - 416: { message: "Netdata alarms log download failed", alert: false },
1003 - 417: { message: "Netdata registry server send invalid response to SEARCH ", alert: false },
1004 - 418: { message: "Netdata registry SEARCH failed", alert: false }
1005 - };
1006 - NETDATA.errorLast = {
1007 - code: 0,
1008 - message: "",
1009 - datetime: 0
1010 - };
1280 + pan_and_zoom_factor: 0.25, // the increment when panning and zooming with the toolbox
1281 + pan_and_zoom_factor_multiplier_control: 2.0,
1282 + pan_and_zoom_factor_multiplier_shift: 3.0,
1283 + pan_and_zoom_factor_multiplier_alt: 4.0,
1284
1012 - NETDATA.error = function(code, msg) {
1013 - NETDATA.errorLast.code = code;
1014 - NETDATA.errorLast.message = msg;
1015 - NETDATA.errorLast.datetime = Date.now();
1285 + abort_ajax_on_scroll: false, // kill pending ajax page scroll
1286 + async_on_scroll: false, // sync/async onscroll handler
1287 + onscroll_worker_duration_threshold: 30, // time in ms, for async scroll handler
1288
1017 - console.log("ERROR " + code + ": " + NETDATA.errorCodes[code].message + ": " + msg);
1289 + retries_on_data_failures: 3, // how many retries to make if we can't fetch chart data from the server
1290
1019 - var ret = true;
1020 - if(typeof netdataErrorCallback === 'function') {
1021 - ret = netdataErrorCallback('system', code, msg);
1291 + setOptionCallback: function () {
1292 }
1293 + },
1294
1024 - if(ret && NETDATA.errorCodes[code].alert)
1025 - alert("ERROR " + code + ": " + NETDATA.errorCodes[code].message + ": " + msg);
1026 - };
1027 -
1028 - NETDATA.errorReset = function() {
1029 - NETDATA.errorLast.code = 0;
1030 - NETDATA.errorLast.message = "You are doing fine!";
1031 - NETDATA.errorLast.datetime = 0;
1032 - };
1033 -
1034 - // ----------------------------------------------------------------------------------------------------------------
1035 - // fast numbers formatting
1036 -
1037 - NETDATA.fastNumberFormat = {
1038 - formatters_fixed: [],
1039 - formatters_zero_based: [],
1040 -
1041 - // this is the fastest and the preferred
1042 - getIntlNumberFormat: function(min, max) {
1043 - var key = max;
1044 - if(min === max) {
1045 - if(typeof this.formatters_fixed[key] === 'undefined')
1046 - this.formatters_fixed[key] = new Intl.NumberFormat(undefined, {
1047 - // style: 'decimal',
1048 - // minimumIntegerDigits: 1,
1049 - // minimumSignificantDigits: 1,
1050 - // maximumSignificantDigits: 1,
1051 - useGrouping: true,
1052 - minimumFractionDigits: min,
1053 - maximumFractionDigits: max
1054 - });
1055 -
1056 - return this.formatters_fixed[key];
1057 - }
1058 - else if(min === 0) {
1059 - if(typeof this.formatters_zero_based[key] === 'undefined')
1060 - this.formatters_zero_based[key] = new Intl.NumberFormat(undefined, {
1061 - // style: 'decimal',
1062 - // minimumIntegerDigits: 1,
1063 - // minimumSignificantDigits: 1,
1064 - // maximumSignificantDigits: 1,
1065 - useGrouping: true,
1066 - minimumFractionDigits: min,
1067 - maximumFractionDigits: max
1068 - });
1069 -
1070 - return this.formatters_zero_based[key];
1071 - }
1072 - else {
1073 - // this is never used
1074 - // it is added just for completeness
1075 - return new Intl.NumberFormat(undefined, {
1076 - // style: 'decimal',
1077 - // minimumIntegerDigits: 1,
1078 - // minimumSignificantDigits: 1,
1079 - // maximumSignificantDigits: 1,
1080 - useGrouping: true,
1081 - minimumFractionDigits: min,
1082 - maximumFractionDigits: max
1083 - });
1084 - }
1085 - },
1295 + debug: {
1296 + show_boxes: false,
1297 + main_loop: false,
1298 + focus: false,
1299 + visibility: false,
1300 + chart_data_url: false,
1301 + chart_errors: true, // remember to set it to false before merging
1302 + chart_timing: false,
1303 + chart_calls: false,
1304 + libraries: false,
1305 + dygraph: false,
1306 + globalSelectionSync: false,
1307 + globalPanAndZoom: false
1308 + }
1309 +};
1310
1087 - // this respects locale
1088 - getLocaleString: function(min, max) {
1089 - var key = max;
1090 - if(min === max) {
1091 - if(typeof this.formatters_fixed[key] === 'undefined')
1092 - this.formatters_fixed[key] = {
1093 - format: function (value) {
1094 - return value.toLocaleString(undefined, {
1095 - // style: 'decimal',
1096 - // minimumIntegerDigits: 1,
1097 - // minimumSignificantDigits: 1,
1098 - // maximumSignificantDigits: 1,
1099 - useGrouping: true,
1100 - minimumFractionDigits: min,
1101 - maximumFractionDigits: max
1102 - });
1103 - }
1104 - };
1311 +NETDATA.statistics = {
1312 + refreshes_total: 0,
1313 + refreshes_active: 0,
1314 + refreshes_active_max: 0
1315 +};
1316
1106 - return this.formatters_fixed[key];
1107 - }
1108 - else if(min === 0) {
1109 - if(typeof this.formatters_zero_based[key] === 'undefined')
1110 - this.formatters_zero_based[key] = {
1111 - format: function (value) {
1112 - return value.toLocaleString(undefined, {
1113 - // style: 'decimal',
1114 - // minimumIntegerDigits: 1,
1115 - // minimumSignificantDigits: 1,
1116 - // maximumSignificantDigits: 1,
1117 - useGrouping: true,
1118 - minimumFractionDigits: min,
1119 - maximumFractionDigits: max
1120 - });
1121 - }
1122 - };
1317 +// local storage options
1318
1124 - return this.formatters_zero_based[key];
1125 - }
1126 - else {
1127 - return {
1128 - format: function (value) {
1129 - return value.toLocaleString(undefined, {
1130 - // style: 'decimal',
1131 - // minimumIntegerDigits: 1,
1132 - // minimumSignificantDigits: 1,
1133 - // maximumSignificantDigits: 1,
1134 - useGrouping: true,
1135 - minimumFractionDigits: min,
1136 - maximumFractionDigits: max
1137 - });
1138 - }
1139 - };
1140 - }
1141 - },
1319 +NETDATA.localStorage = {
1320 + default: {},
1321 + current: {},
1322 + callback: {} // only used for resetting back to defaults
1323 +};
1324
1143 - // the fallback
1144 - getFixed: function(min, max) {
1145 - var key = max;
1146 - if(min === max) {
1147 - if(typeof this.formatters_fixed[key] === 'undefined')
1148 - this.formatters_fixed[key] = {
1149 - format: function (value) {
1150 - if(value === 0) return "0";
1151 - return value.toFixed(max);
1152 - }
1153 - };
1325 +NETDATA.localStorageTested = -1;
1326 +NETDATA.localStorageTest = function () {
1327 + if (NETDATA.localStorageTested !== -1) {
1328 + return NETDATA.localStorageTested;
1329 + }
1330
1155 - return this.formatters_fixed[key];
1156 - }
1157 - else if(min === 0) {
1158 - if(typeof this.formatters_zero_based[key] === 'undefined')
1159 - this.formatters_zero_based[key] = {
1160 - format: function (value) {
1161 - if(value === 0) return "0";
1162 - return value.toFixed(max);
1163 - }
1164 - };
1331 + if (typeof Storage !== "undefined" && typeof localStorage === 'object') {
1332 + let test = 'test';
1333 + try {
1334 + localStorage.setItem(test, test);
1335 + localStorage.removeItem(test);
1336 + NETDATA.localStorageTested = true;
1337 + } catch (e) {
1338 + NETDATA.localStorageTested = false;
1339 + }
1340 + } else {
1341 + NETDATA.localStorageTested = false;
1342 + }
1343
1166 - return this.formatters_zero_based[key];
1167 - }
1168 - else {
1169 - return {
1170 - format: function (value) {
1171 - if(value === 0) return "0";
1172 - return value.toFixed(max);
1173 - }
1174 - };
1175 - }
1176 - },
1344 + return NETDATA.localStorageTested;
1345 +};
1346
1178 - testIntlNumberFormat: function() {
1179 - var value = 1.12345;
1180 - var e1 = "1.12", e2 = "1,12";
1181 - var s = "";
1347 +NETDATA.localStorageGet = function (key, def, callback) {
1348 + let ret = def;
1349
1183 - try {
1184 - var x = new Intl.NumberFormat(undefined, {
1185 - useGrouping: true,
1186 - minimumFractionDigits: 2,
1187 - maximumFractionDigits: 2
1188 - });
1350 + if (typeof NETDATA.localStorage.default[key.toString()] === 'undefined') {
1351 + NETDATA.localStorage.default[key.toString()] = def;
1352 + NETDATA.localStorage.callback[key.toString()] = callback;
1353 + }
1354
1190 - s = x.format(value);
1191 - }
1192 - catch(e) {
1193 - s = "";
1355 + if (NETDATA.localStorageTest()) {
1356 + try {
1357 + // console.log('localStorage: loading "' + key.toString() + '"');
1358 + ret = localStorage.getItem(key.toString());
1359 + // console.log('netdata loaded: ' + key.toString() + ' = ' + ret.toString());
1360 + if (ret === null || ret === 'undefined') {
1361 + // console.log('localStorage: cannot load it, saving "' + key.toString() + '" with value "' + JSON.stringify(def) + '"');
1362 + localStorage.setItem(key.toString(), JSON.stringify(def));
1363 + ret = def;
1364 + } else {
1365 + // console.log('localStorage: got "' + key.toString() + '" with value "' + ret + '"');
1366 + ret = JSON.parse(ret);
1367 + // console.log('localStorage: loaded "' + key.toString() + '" as value ' + ret + ' of type ' + typeof(ret));
1368 }
1369 + } catch (error) {
1370 + console.log('localStorage: failed to read "' + key.toString() + '", using default: "' + def.toString() + '"');
1371 + ret = def;
1372 + }
1373 + }
1374
1196 - // console.log('NumberFormat: ', s);
1197 - return (s === e1 || s === e2);
1198 - },
1375 + if (typeof ret === 'undefined' || ret === 'undefined') {
1376 + console.log('localStorage: LOADED UNDEFINED "' + key.toString() + '" as value ' + ret + ' of type ' + typeof(ret));
1377 + ret = def;
1378 + }
1379
1200 - testLocaleString: function() {
1201 - var value = 1.12345;
1202 - var e1 = "1.12", e2 = "1,12";
1203 - var s = "";
1380 + NETDATA.localStorage.current[key.toString()] = ret;
1381 + return ret;
1382 +};
1383
1205 - try {
1206 - s = value.toLocaleString(undefined, {
1207 - useGrouping: true,
1208 - minimumFractionDigits: 2,
1209 - maximumFractionDigits: 2
1210 - });
1211 - }
1212 - catch(e) {
1213 - s = "";
1214 - }
1384 +NETDATA.localStorageSet = function (key, value, callback) {
1385 + if (typeof value === 'undefined' || value === 'undefined') {
1386 + console.log('localStorage: ATTEMPT TO SET UNDEFINED "' + key.toString() + '" as value ' + value + ' of type ' + typeof(value));
1387 + }
1388
1216 - // console.log('localeString: ', s);
1217 - return (s === e1 || s === e2);
1218 - },
1389 + if (typeof NETDATA.localStorage.default[key.toString()] === 'undefined') {
1390 + NETDATA.localStorage.default[key.toString()] = value;
1391 + NETDATA.localStorage.current[key.toString()] = value;
1392 + NETDATA.localStorage.callback[key.toString()] = callback;
1393 + }
1394
1220 - // on first run we decide which formatter to use
1221 - get: function(min, max) {
1222 - if(this.testIntlNumberFormat()) {
1223 - // console.log('numberformat');
1224 - this.get = this.getIntlNumberFormat;
1225 - }
1226 - else if(this.testLocaleString()) {
1227 - // console.log('localestring');
1228 - this.get = this.getLocaleString;
1229 - }
1230 - else {
1231 - // console.log('fixed');
1232 - this.get = this.getFixed;
1233 - }
1234 - return this.get(min, max);
1395 + if (NETDATA.localStorageTest()) {
1396 + // console.log('localStorage: saving "' + key.toString() + '" with value "' + JSON.stringify(value) + '"');
1397 + try {
1398 + localStorage.setItem(key.toString(), JSON.stringify(value));
1399 + } catch (e) {
1400 + console.log('localStorage: failed to save "' + key.toString() + '" with value: "' + value.toString() + '"');
1401 }
1236 - };
1237 -
1238 - // ----------------------------------------------------------------------------------------------------------------
1239 - // element data attributes
1240 -
1241 - NETDATA.dataAttribute = function(element, attribute, def) {
1242 - var key = 'data-' + attribute.toString();
1243 - if(element.hasAttribute(key) === true) {
1244 - var data = element.getAttribute(key);
1245 -
1246 - if(data === 'true') return true;
1247 - if(data === 'false') return false;
1248 - if(data === 'null') return null;
1402 + }
1403
1250 - // Only convert to a number if it doesn't change the string
1251 - if(data === +data + '') return +data;
1404 + NETDATA.localStorage.current[key.toString()] = value;
1405 + return value;
1406 +};
1407
1253 - if(/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/.test(data))
1254 - return JSON.parse(data);
1408 +NETDATA.localStorageGetRecursive = function (obj, prefix, callback) {
1409 + let keys = Object.keys(obj);
1410 + let len = keys.length;
1411 + while (len--) {
1412 + let i = keys[len];
1413
1256 - return data;
1414 + if (typeof obj[i] === 'object') {
1415 + //console.log('object ' + prefix + '.' + i.toString());
1416 + NETDATA.localStorageGetRecursive(obj[i], prefix + '.' + i.toString(), callback);
1417 + continue;
1418 }
1258 - else return def;
1259 - };
1419
1261 - NETDATA.dataAttributeBoolean = function(element, attribute, def) {
1262 - var value = NETDATA.dataAttribute(element, attribute, def);
1420 + obj[i] = NETDATA.localStorageGet(prefix + '.' + i.toString(), obj[i], callback);
1421 + }
1422 +};
1423
1264 - if(value === true || value === false)
1265 - return value;
1424 +NETDATA.setOption = function (key, value) {
1425 + if (key.toString() === 'setOptionCallback') {
1426 + if (typeof NETDATA.options.current.setOptionCallback === 'function') {
1427 + NETDATA.options.current[key.toString()] = value;
1428 + NETDATA.options.current.setOptionCallback();
1429 + }
1430 + } else if (NETDATA.options.current[key.toString()] !== value) {
1431 + let name = 'options.' + key.toString();
1432
1267 - if(typeof(value) === 'string') {
1268 - if(value === 'yes' || value === 'on')
1269 - return true;
1433 + if (typeof NETDATA.localStorage.default[name.toString()] === 'undefined') {
1434 + console.log('localStorage: setOption() on unsaved option: "' + name.toString() + '", value: ' + value);
1435 + }
1436
1271 - if(value === '' || value === 'no' || value === 'off' || value === 'null')
1272 - return false;
1437 + //console.log(NETDATA.localStorage);
1438 + //console.log('setOption: setting "' + key.toString() + '" to "' + value + '" of type ' + typeof(value) + ' original type ' + typeof(NETDATA.options.current[key.toString()]));
1439 + //console.log(NETDATA.options);
1440 + NETDATA.options.current[key.toString()] = NETDATA.localStorageSet(name.toString(), value, null);
1441
1274 - return def;
1442 + if (typeof NETDATA.options.current.setOptionCallback === 'function') {
1443 + NETDATA.options.current.setOptionCallback();
1444 }
1445 + }
1446
1277 - if(typeof(value) === 'number')
1278 - return value !== 0;
1279 -
1280 - return def;
1281 - };
1447 + return true;
1448 +};
1449
1283 - // ----------------------------------------------------------------------------------------------------------------
1284 - // commonMin & commonMax
1450 +NETDATA.getOption = function (key) {
1451 + return NETDATA.options.current[key.toString()];
1452 +};
1453
1286 - NETDATA.commonMin = {
1287 - keys: {},
1288 - latest: {},
1454 +// read settings from local storage
1455 +NETDATA.localStorageGetRecursive(NETDATA.options.current, 'options', null);
1456
1290 - globalReset: function() {
1291 - this.keys = {};
1292 - this.latest = {};
1293 - },
1457 +// always start with this option enabled.
1458 +NETDATA.setOption('stop_updates_when_focus_is_lost', true);
1459
1295 - get: function(state) {
1296 - if(typeof state.tmp.__commonMin === 'undefined') {
1297 - // get the commonMin setting
1298 - state.tmp.__commonMin = NETDATA.dataAttribute(state.element, 'common-min', null);
1299 - }
1460 +NETDATA.resetOptions = function () {
1461 + let keys = Object.keys(NETDATA.localStorage.default);
1462 + let len = keys.length;
1463
1301 - var min = state.data.min;
1302 - var name = state.tmp.__commonMin;
1464 + while (len--) {
1465 + let i = keys[len];
1466 + let a = i.split('.');
1467
1304 - if(name === null) {
1305 - // we don't need commonMin
1306 - //state.log('no need for commonMin');
1307 - return min;
1468 + if (a[0] === 'options') {
1469 + if (a[1] === 'setOptionCallback') {
1470 + continue;
1471 }
1309 -
1310 - var t = this.keys[name];
1311 - if(typeof t === 'undefined') {
1312 - // add our commonMin
1313 - this.keys[name] = {};
1314 - t = this.keys[name];
1472 + if (typeof NETDATA.localStorage.default[i] === 'undefined') {
1473 + continue;
1474 }
1316 -
1317 - var uuid = state.uuid;
1318 - if(typeof t[uuid] !== 'undefined') {
1319 - if(t[uuid] === min) {
1320 - //state.log('commonMin ' + state.tmp.__commonMin + ' not changed: ' + this.latest[name]);
1321 - return this.latest[name];
1322 - }
1323 - else if(min < this.latest[name]) {
1324 - //state.log('commonMin ' + state.tmp.__commonMin + ' increased: ' + min);
1325 - t[uuid] = min;
1326 - this.latest[name] = min;
1327 - return min;
1328 - }
1475 + if (NETDATA.options.current[i] === NETDATA.localStorage.default[i]) {
1476 + continue;
1477 }
1478
1331 - // add our min
1332 - t[uuid] = min;
1333 -
1334 - // find the common min
1335 - var m = min;
1336 - for(var i in t)
1337 - if(t.hasOwnProperty(i) && t[i] < m) m = t[i];
1338 -
1339 - //state.log('commonMin ' + state.tmp.__commonMin + ' updated: ' + m);
1340 - this.latest[name] = m;
1341 - return m;
1479 + NETDATA.setOption(a[1], NETDATA.localStorage.default[i]);
1480 + } else if (a[0] === 'chart_heights') {
1481 + if (typeof NETDATA.localStorage.callback[i] === 'function' && typeof NETDATA.localStorage.default[i] !== 'undefined') {
1482 + NETDATA.localStorage.callback[i](NETDATA.localStorage.default[i]);
1483 + }
1484 }
1343 - };
1485 + }
1486
1345 - NETDATA.commonMax = {
1346 - keys: {},
1347 - latest: {},
1487 + NETDATA.dateTime.init(NETDATA.options.current.timezone);
1488 +};
1489 +// dygraph
1490
1349 - globalReset: function() {
1350 - this.keys = {};
1351 - this.latest = {};
1352 - },
1491 +NETDATA.dygraph = {
1492 + smooth: false
1493 +};
1494
1354 - get: function(state) {
1355 - if(typeof state.tmp.__commonMax === 'undefined') {
1356 - // get the commonMax setting
1357 - state.tmp.__commonMax = NETDATA.dataAttribute(state.element, 'common-max', null);
1358 - }
1495 +NETDATA.dygraphToolboxPanAndZoom = function (state, after, before) {
1496 + if (after < state.netdata_first) {
1497 + after = state.netdata_first;
1498 + }
1499
1360 - var max = state.data.max;
1361 - var name = state.tmp.__commonMax;
1500 + if (before > state.netdata_last) {
1501 + before = state.netdata_last;
1502 + }
1503
1363 - if(name === null) {
1364 - // we don't need commonMax
1365 - //state.log('no need for commonMax');
1366 - return max;
1367 - }
1504 + state.setMode('zoom');
1505 + NETDATA.globalSelectionSync.stop();
1506 + NETDATA.globalSelectionSync.delay();
1507 + state.tmp.dygraph_user_action = true;
1508 + state.tmp.dygraph_force_zoom = true;
1509 + // state.log('toolboxPanAndZoom');
1510 + state.updateChartPanOrZoom(after, before);
1511 + NETDATA.globalPanAndZoom.setMaster(state, after, before);
1512 +};
1513 +
1514 +NETDATA.dygraphSetSelection = function (state, t) {
1515 + if (typeof state.tmp.dygraph_instance !== 'undefined') {
1516 + let r = state.calculateRowForTime(t);
1517 + if (r !== -1) {
1518 + state.tmp.dygraph_instance.setSelection(r);
1519 + return true;
1520 + } else {
1521 + state.tmp.dygraph_instance.clearSelection();
1522 + state.legendShowUndefined();
1523 + }
1524 + }
1525
1369 - var t = this.keys[name];
1370 - if(typeof t === 'undefined') {
1371 - // add our commonMax
1372 - this.keys[name] = {};
1373 - t = this.keys[name];
1526 + return false;
1527 +};
1528 +
1529 +NETDATA.dygraphClearSelection = function (state) {
1530 + if (typeof state.tmp.dygraph_instance !== 'undefined') {
1531 + state.tmp.dygraph_instance.clearSelection();
1532 + }
1533 + return true;
1534 +};
1535 +
1536 +NETDATA.dygraphSmoothInitialize = function (callback) {
1537 + $.ajax({
1538 + url: NETDATA.dygraph_smooth_js,
1539 + cache: true,
1540 + dataType: "script",
1541 + xhrFields: {withCredentials: true} // required for the cookie
1542 + })
1543 + .done(function () {
1544 + NETDATA.dygraph.smooth = true;
1545 + smoothPlotter.smoothing = 0.3;
1546 + })
1547 + .fail(function () {
1548 + NETDATA.dygraph.smooth = false;
1549 + })
1550 + .always(function () {
1551 + if (typeof callback === "function") {
1552 + return callback();
1553 }
1554 + });
1555 +};
1556
1376 - var uuid = state.uuid;
1377 - if(typeof t[uuid] !== 'undefined') {
1378 - if(t[uuid] === max) {
1379 - //state.log('commonMax ' + state.tmp.__commonMax + ' not changed: ' + this.latest[name]);
1380 - return this.latest[name];
1381 - }
1382 - else if(max > this.latest[name]) {
1383 - //state.log('commonMax ' + state.tmp.__commonMax + ' increased: ' + max);
1384 - t[uuid] = max;
1385 - this.latest[name] = max;
1386 - return max;
1557 +NETDATA.dygraphInitialize = function (callback) {
1558 + if (typeof netdataNoDygraphs === 'undefined' || !netdataNoDygraphs) {
1559 + $.ajax({
1560 + url: NETDATA.dygraph_js,
1561 + cache: true,
1562 + dataType: "script",
1563 + xhrFields: {withCredentials: true} // required for the cookie
1564 + })
1565 + .done(function () {
1566 + NETDATA.registerChartLibrary('dygraph', NETDATA.dygraph_js);
1567 + })
1568 + .fail(function () {
1569 + NETDATA.chartLibraries.dygraph.enabled = false;
1570 + NETDATA.error(100, NETDATA.dygraph_js);
1571 + })
1572 + .always(function () {
1573 + if (NETDATA.chartLibraries.dygraph.enabled && NETDATA.options.current.smooth_plot) {
1574 + NETDATA.dygraphSmoothInitialize(callback);
1575 + } else if (typeof callback === "function") {
1576 + return callback();
1577 }
1388 - }
1578 + });
1579 + } else {
1580 + NETDATA.chartLibraries.dygraph.enabled = false;
1581 + if (typeof callback === "function") {
1582 + return callback();
1583 + }
1584 + }
1585 +};
1586
1390 - // add our max
1391 - t[uuid] = max;
1587 +NETDATA.dygraphChartUpdate = function (state, data) {
1588 + let dygraph = state.tmp.dygraph_instance;
1589
1393 - // find the common max
1394 - var m = max;
1395 - for(var i in t)
1396 - if(t.hasOwnProperty(i) && t[i] > m) m = t[i];
1590 + if (typeof dygraph === 'undefined') {
1591 + return NETDATA.dygraphChartCreate(state, data);
1592 + }
1593
1398 - //state.log('commonMax ' + state.tmp.__commonMax + ' updated: ' + m);
1399 - this.latest[name] = m;
1400 - return m;
1401 - }
1594 + // when the chart is not visible, and hidden
1595 + // if there is a window resize, dygraph detects
1596 + // its element size as 0x0.
1597 + // this will make it re-appear properly
1598 +
1599 + if (state.tm.last_unhidden > state.tmp.dygraph_last_rendered) {
1600 + dygraph.resize();
1601 + }
1602 +
1603 + let options = {
1604 + file: data.result.data,
1605 + colors: state.chartColors(),
1606 + labels: data.result.labels,
1607 + //labelsDivWidth: state.chartWidth() - 70,
1608 + includeZero: state.tmp.dygraph_include_zero,
1609 + visibility: state.dimensions_visibility.selected2BooleanArray(state.data.dimension_names)
1610 };
1611
1404 - NETDATA.commonColors = {
1405 - keys: {},
1612 + if (state.tmp.dygraph_chart_type === 'stacked') {
1613 + if (options.includeZero && state.dimensions_visibility.countSelected() < options.visibility.length) {
1614 + options.includeZero = 0;
1615 + }
1616 + }
1617
1407 - globalReset: function() {
1408 - this.keys = {};
1409 - },
1618 + if (!NETDATA.chartLibraries.dygraph.isSparkline(state)) {
1619 + options.ylabel = state.units_current; // (state.units_desired === 'auto')?"":state.units_current;
1620 + }
1621
1411 - get: function(state, label) {
1412 - var ret = this.refill(state);
1622 + if (state.tmp.dygraph_force_zoom) {
1623 + if (NETDATA.options.debug.dygraph || state.debug) {
1624 + state.log('dygraphChartUpdate() forced zoom update');
1625 + }
1626
1414 - if(typeof ret.assigned[label] === 'undefined')
1415 - ret.assigned[label] = ret.available.shift();
1627 + options.dateWindow = (state.requested_padding !== null) ? [state.view_after, state.view_before] : null;
1628 + //options.isZoomedIgnoreProgrammaticZoom = true;
1629 + state.tmp.dygraph_force_zoom = false;
1630 + } else if (state.current.name !== 'auto') {
1631 + if (NETDATA.options.debug.dygraph || state.debug) {
1632 + state.log('dygraphChartUpdate() loose update');
1633 + }
1634 + } else {
1635 + if (NETDATA.options.debug.dygraph || state.debug) {
1636 + state.log('dygraphChartUpdate() strict update');
1637 + }
1638
1417 - return ret.assigned[label];
1418 - },
1639 + options.dateWindow = (state.requested_padding !== null) ? [state.view_after, state.view_before] : null;
1640 + //options.isZoomedIgnoreProgrammaticZoom = true;
1641 + }
1642
1420 - refill: function(state) {
1421 - var ret, len;
1643 + options.valueRange = state.tmp.dygraph_options.valueRange;
1644
1423 - if(typeof state.tmp.__commonColors === 'undefined')
1424 - ret = this.prepare(state);
1425 - else {
1426 - ret = this.keys[state.tmp.__commonColors];
1427 - if(typeof ret === 'undefined')
1428 - ret = this.prepare(state);
1429 - }
1645 + let oldMax = null, oldMin = null;
1646 + if (state.tmp.__commonMin !== null) {
1647 + state.data.min = state.tmp.dygraph_instance.axes_[0].extremeRange[0];
1648 + oldMin = options.valueRange[0] = NETDATA.commonMin.get(state);
1649 + }
1650 + if (state.tmp.__commonMax !== null) {
1651 + state.data.max = state.tmp.dygraph_instance.axes_[0].extremeRange[1];
1652 + oldMax = options.valueRange[1] = NETDATA.commonMax.get(state);
1653 + }
1654
1431 - if(ret.available.length === 0) {
1432 - if(ret.copy_theme === true || ret.custom.length === 0) {
1433 - // copy the theme colors
1434 - len = NETDATA.themes.current.colors.length;
1435 - while (len--)
1436 - ret.available.unshift(NETDATA.themes.current.colors[len]);
1437 - }
1655 + if (state.tmp.dygraph_smooth_eligible) {
1656 + if ((NETDATA.options.current.smooth_plot && state.tmp.dygraph_options.plotter !== smoothPlotter)
1657 + || (NETDATA.options.current.smooth_plot === false && state.tmp.dygraph_options.plotter === smoothPlotter)) {
1658 + NETDATA.dygraphChartCreate(state, data);
1659 + return;
1660 + }
1661 + }
1662
1439 - // copy the custom colors
1440 - len = ret.custom.length;
1441 - while (len--)
1442 - ret.available.unshift(ret.custom[len]);
1443 - }
1663 + if (netdataSnapshotData !== null && NETDATA.globalPanAndZoom.isActive() && NETDATA.globalPanAndZoom.isMaster(state) === false) {
1664 + // pan and zoom on snapshots
1665 + options.dateWindow = [NETDATA.globalPanAndZoom.force_after_ms, NETDATA.globalPanAndZoom.force_before_ms];
1666 + //options.isZoomedIgnoreProgrammaticZoom = true;
1667 + }
1668
1445 - state.colors_assigned = ret.assigned;
1446 - state.colors_available = ret.available;
1447 - state.colors_custom = ret.custom;
1669 + if (NETDATA.chartLibraries.dygraph.isLogScale(state)) {
1670 + if (Array.isArray(options.valueRange) && options.valueRange[0] <= 0) {
1671 + options.valueRange[0] = null;
1672 + }
1673 + }
1674
1449 - return ret;
1450 - },
1675 + dygraph.updateOptions(options);
1676
1452 - __read_custom_colors: function(state, ret) {
1453 - // add the user supplied colors
1454 - var c = NETDATA.dataAttribute(state.element, 'colors', undefined);
1455 - if (typeof c === 'string' && c.length > 0) {
1456 - c = c.split(' ');
1457 - var len = c.length;
1677 + let redraw = false;
1678 + if (oldMin !== null && oldMin > state.tmp.dygraph_instance.axes_[0].extremeRange[0]) {
1679 + state.data.min = state.tmp.dygraph_instance.axes_[0].extremeRange[0];
1680 + options.valueRange[0] = NETDATA.commonMin.get(state);
1681 + redraw = true;
1682 + }
1683 + if (oldMax !== null && oldMax < state.tmp.dygraph_instance.axes_[0].extremeRange[1]) {
1684 + state.data.max = state.tmp.dygraph_instance.axes_[0].extremeRange[1];
1685 + options.valueRange[1] = NETDATA.commonMax.get(state);
1686 + redraw = true;
1687 + }
1688
1459 - if (len > 0 && c[len - 1] === 'ONLY') {
1460 - len--;
1461 - ret.copy_theme = false;
1462 - }
1689 + if (redraw) {
1690 + // state.log('forcing redraw to adapt to common- min/max');
1691 + dygraph.updateOptions(options);
1692 + }
1693
1464 - while (len--)
1465 - ret.custom.unshift(c[len]);
1466 - }
1467 - },
1694 + state.tmp.dygraph_last_rendered = Date.now();
1695 + return true;
1696 +};
1697
1469 - prepare: function(state) {
1470 - var has_custom_colors = false;
1698 +NETDATA.dygraphChartCreate = function (state, data) {
1699 + if (NETDATA.options.debug.dygraph || state.debug) {
1700 + state.log('dygraphChartCreate()');
1701 + }
1702
1472 - if(typeof state.tmp.__commonColors === 'undefined') {
1473 - var defname = state.chart.context;
1703 + state.tmp.dygraph_chart_type = NETDATA.dataAttribute(state.element, 'dygraph-type', state.chart.chart_type);
1704 + if (state.tmp.dygraph_chart_type === 'stacked' && data.dimensions === 1) {
1705 + state.tmp.dygraph_chart_type = 'area';
1706 + }
1707 + if (state.tmp.dygraph_chart_type === 'stacked' && NETDATA.chartLibraries.dygraph.isLogScale(state)) {
1708 + state.tmp.dygraph_chart_type = 'area';
1709 + }
1710
1475 - // if this chart has data-colors=""
1476 - // we should use the chart uuid as the default key (private palette)
1477 - // (data-common-colors="NAME" will be used anyways)
1478 - var c = NETDATA.dataAttribute(state.element, 'colors', undefined);
1479 - if (typeof c === 'string' && c.length > 0) {
1480 - defname = state.uuid;
1481 - has_custom_colors = true;
1711 + let highlightCircleSize = NETDATA.chartLibraries.dygraph.isSparkline(state) ? 3 : 4;
1712 +
1713 + let smooth = NETDATA.dygraph.smooth
1714 + ? (NETDATA.dataAttributeBoolean(state.element, 'dygraph-smooth', (state.tmp.dygraph_chart_type === 'line' && NETDATA.chartLibraries.dygraph.isSparkline(state) === false)))
1715 + : false;
1716 +
1717 + state.tmp.dygraph_include_zero = NETDATA.dataAttribute(state.element, 'dygraph-includezero', (state.tmp.dygraph_chart_type === 'stacked'));
1718 + let drawAxis = NETDATA.dataAttributeBoolean(state.element, 'dygraph-drawaxis', true);
1719 +
1720 + state.tmp.dygraph_options = {
1721 + colors: NETDATA.dataAttribute(state.element, 'dygraph-colors', state.chartColors()),
1722 +
1723 + // leave a few pixels empty on the right of the chart
1724 + rightGap: NETDATA.dataAttribute(state.element, 'dygraph-rightgap', 5),
1725 + showRangeSelector: NETDATA.dataAttributeBoolean(state.element, 'dygraph-showrangeselector', false),
1726 + showRoller: NETDATA.dataAttributeBoolean(state.element, 'dygraph-showroller', false),
1727 + title: NETDATA.dataAttribute(state.element, 'dygraph-title', state.title),
1728 + titleHeight: NETDATA.dataAttribute(state.element, 'dygraph-titleheight', 19),
1729 + legend: NETDATA.dataAttribute(state.element, 'dygraph-legend', 'always'), // we need this to get selection events
1730 + labels: data.result.labels,
1731 + labelsDiv: NETDATA.dataAttribute(state.element, 'dygraph-labelsdiv', state.element_legend_childs.hidden),
1732 + //labelsDivStyles: NETDATA.dataAttribute(state.element, 'dygraph-labelsdivstyles', { 'fontSize':'1px' }),
1733 + //labelsDivWidth: NETDATA.dataAttribute(state.element, 'dygraph-labelsdivwidth', state.chartWidth() - 70),
1734 + labelsSeparateLines: NETDATA.dataAttributeBoolean(state.element, 'dygraph-labelsseparatelines', true),
1735 + labelsShowZeroValues: NETDATA.chartLibraries.dygraph.isLogScale(state) ? false : NETDATA.dataAttributeBoolean(state.element, 'dygraph-labelsshowzerovalues', true),
1736 + labelsKMB: false,
1737 + labelsKMG2: false,
1738 + showLabelsOnHighlight: NETDATA.dataAttributeBoolean(state.element, 'dygraph-showlabelsonhighlight', true),
1739 + hideOverlayOnMouseOut: NETDATA.dataAttributeBoolean(state.element, 'dygraph-hideoverlayonmouseout', true),
1740 + includeZero: state.tmp.dygraph_include_zero,
1741 + xRangePad: NETDATA.dataAttribute(state.element, 'dygraph-xrangepad', 0),
1742 + yRangePad: NETDATA.dataAttribute(state.element, 'dygraph-yrangepad', 1),
1743 + valueRange: NETDATA.dataAttribute(state.element, 'dygraph-valuerange', [null, null]),
1744 + ylabel: state.units_current, // (state.units_desired === 'auto')?"":state.units_current,
1745 + yLabelWidth: NETDATA.dataAttribute(state.element, 'dygraph-ylabelwidth', 12),
1746 +
1747 + // the function to plot the chart
1748 + plotter: null,
1749 +
1750 + // The width of the lines connecting data points.
1751 + // This can be used to increase the contrast or some graphs.
1752 + strokeWidth: NETDATA.dataAttribute(state.element, 'dygraph-strokewidth', ((state.tmp.dygraph_chart_type === 'stacked') ? 0.1 : ((smooth === true) ? 1.5 : 0.7))),
1753 + strokePattern: NETDATA.dataAttribute(state.element, 'dygraph-strokepattern', undefined),
1754 +
1755 + // The size of the dot to draw on each point in pixels (see drawPoints).
1756 + // A dot is always drawn when a point is "isolated",
1757 + // i.e. there is a missing point on either side of it.
1758 + // This also controls the size of those dots.
1759 + drawPoints: NETDATA.dataAttributeBoolean(state.element, 'dygraph-drawpoints', false),
1760 +
1761 + // Draw points at the edges of gaps in the data.
1762 + // This improves visibility of small data segments or other data irregularities.
1763 + drawGapEdgePoints: NETDATA.dataAttributeBoolean(state.element, 'dygraph-drawgapedgepoints', true),
1764 + connectSeparatedPoints: NETDATA.chartLibraries.dygraph.isLogScale(state) ? false : NETDATA.dataAttributeBoolean(state.element, 'dygraph-connectseparatedpoints', false),
1765 + pointSize: NETDATA.dataAttribute(state.element, 'dygraph-pointsize', 1),
1766 +
1767 + // enabling this makes the chart with little square lines
1768 + stepPlot: NETDATA.dataAttributeBoolean(state.element, 'dygraph-stepplot', false),
1769 +
1770 + // Draw a border around graph lines to make crossing lines more easily
1771 + // distinguishable. Useful for graphs with many lines.
1772 + strokeBorderColor: NETDATA.dataAttribute(state.element, 'dygraph-strokebordercolor', NETDATA.themes.current.background),
1773 + strokeBorderWidth: NETDATA.dataAttribute(state.element, 'dygraph-strokeborderwidth', (state.tmp.dygraph_chart_type === 'stacked') ? 0.0 : 0.0),
1774 + fillGraph: NETDATA.dataAttribute(state.element, 'dygraph-fillgraph', (state.tmp.dygraph_chart_type === 'area' || state.tmp.dygraph_chart_type === 'stacked')),
1775 + fillAlpha: NETDATA.dataAttribute(state.element, 'dygraph-fillalpha',
1776 + ((state.tmp.dygraph_chart_type === 'stacked')
1777 + ? NETDATA.options.current.color_fill_opacity_stacked
1778 + : NETDATA.options.current.color_fill_opacity_area)
1779 + ),
1780 + stackedGraph: NETDATA.dataAttribute(state.element, 'dygraph-stackedgraph', (state.tmp.dygraph_chart_type === 'stacked')),
1781 + stackedGraphNaNFill: NETDATA.dataAttribute(state.element, 'dygraph-stackedgraphnanfill', 'none'),
1782 + drawAxis: drawAxis,
1783 + axisLabelFontSize: NETDATA.dataAttribute(state.element, 'dygraph-axislabelfontsize', 10),
1784 + axisLineColor: NETDATA.dataAttribute(state.element, 'dygraph-axislinecolor', NETDATA.themes.current.axis),
1785 + axisLineWidth: NETDATA.dataAttribute(state.element, 'dygraph-axislinewidth', 1.0),
1786 + drawGrid: NETDATA.dataAttributeBoolean(state.element, 'dygraph-drawgrid', true),
1787 + gridLinePattern: NETDATA.dataAttribute(state.element, 'dygraph-gridlinepattern', null),
1788 + gridLineWidth: NETDATA.dataAttribute(state.element, 'dygraph-gridlinewidth', 1.0),
1789 + gridLineColor: NETDATA.dataAttribute(state.element, 'dygraph-gridlinecolor', NETDATA.themes.current.grid),
1790 + maxNumberWidth: NETDATA.dataAttribute(state.element, 'dygraph-maxnumberwidth', 8),
1791 + sigFigs: NETDATA.dataAttribute(state.element, 'dygraph-sigfigs', null),
1792 + digitsAfterDecimal: NETDATA.dataAttribute(state.element, 'dygraph-digitsafterdecimal', 2),
1793 + valueFormatter: NETDATA.dataAttribute(state.element, 'dygraph-valueformatter', undefined),
1794 + highlightCircleSize: NETDATA.dataAttribute(state.element, 'dygraph-highlightcirclesize', highlightCircleSize),
1795 + highlightSeriesOpts: NETDATA.dataAttribute(state.element, 'dygraph-highlightseriesopts', null), // TOO SLOW: { strokeWidth: 1.5 },
1796 + highlightSeriesBackgroundAlpha: NETDATA.dataAttribute(state.element, 'dygraph-highlightseriesbackgroundalpha', null), // TOO SLOW: (state.tmp.dygraph_chart_type === 'stacked')?0.7:0.5,
1797 + pointClickCallback: NETDATA.dataAttribute(state.element, 'dygraph-pointclickcallback', undefined),
1798 + visibility: state.dimensions_visibility.selected2BooleanArray(state.data.dimension_names),
1799 + logscale: NETDATA.chartLibraries.dygraph.isLogScale(state) ? 'y' : undefined,
1800 +
1801 + axes: {
1802 + x: {
1803 + pixelsPerLabel: NETDATA.dataAttribute(state.element, 'dygraph-xpixelsperlabel', 50),
1804 + ticker: Dygraph.dateTicker,
1805 + axisLabelWidth: NETDATA.dataAttribute(state.element, 'dygraph-xaxislabelwidth', 60),
1806 + drawAxis: NETDATA.dataAttributeBoolean(state.element, 'dygraph-drawxaxis', drawAxis),
1807 + axisLabelFormatter: function (d, gran) {
1808 + void(gran);
1809 + return NETDATA.dateTime.xAxisTimeString(d);
1810 }
1811 + },
1812 + y: {
1813 + logscale: NETDATA.chartLibraries.dygraph.isLogScale(state) ? true : undefined,
1814 + pixelsPerLabel: NETDATA.dataAttribute(state.element, 'dygraph-ypixelsperlabel', 15),
1815 + axisLabelWidth: NETDATA.dataAttribute(state.element, 'dygraph-yaxislabelwidth', 50),
1816 + drawAxis: NETDATA.dataAttributeBoolean(state.element, 'dygraph-drawyaxis', drawAxis),
1817 + axisLabelFormatter: function (y) {
1818 +
1819 + // unfortunately, we have to call this every single time
1820 + state.legendFormatValueDecimalsFromMinMax(
1821 + this.axes_[0].extremeRange[0],
1822 + this.axes_[0].extremeRange[1]
1823 + );
1824
1484 - // get the commonColors setting
1485 - state.tmp.__commonColors = NETDATA.dataAttribute(state.element, 'common-colors', defname);
1486 - }
1825 + let old_units = this.user_attrs_.ylabel;
1826 + let v = state.legendFormatValue(y);
1827 + let new_units = state.units_current;
1828 +
1829 + if (state.units_desired === 'auto' && typeof old_units !== 'undefined' && new_units !== old_units && !NETDATA.chartLibraries.dygraph.isSparkline(state)) {
1830 + // console.log(this);
1831 + // state.log('units discrepancy: old = ' + old_units + ', new = ' + new_units);
1832 + let len = this.plugins_.length;
1833 + while (len--) {
1834 + // console.log(this.plugins_[len]);
1835 + if (typeof this.plugins_[len].plugin.ylabel_div_ !== 'undefined'
1836 + && this.plugins_[len].plugin.ylabel_div_ !== null
1837 + && typeof this.plugins_[len].plugin.ylabel_div_.children !== 'undefined'
1838 + && this.plugins_[len].plugin.ylabel_div_.children !== null
1839 + && typeof this.plugins_[len].plugin.ylabel_div_.children[0].children !== 'undefined'
1840 + && this.plugins_[len].plugin.ylabel_div_.children[0].children !== null
1841 + ) {
1842 + this.plugins_[len].plugin.ylabel_div_.children[0].children[0].innerHTML = new_units;
1843 + this.user_attrs_.ylabel = new_units;
1844 + break;
1845 + }
1846 + }
1847
1488 - var name = state.tmp.__commonColors;
1489 - var ret = this.keys[name];
1848 + if (len < 0) {
1849 + state.log('units discrepancy, but cannot find dygraphs div to change: old = ' + old_units + ', new = ' + new_units);
1850 + }
1851 + }
1852
1491 - if(typeof ret === 'undefined') {
1492 - // add our commonMax
1493 - this.keys[name] = {
1494 - assigned: {}, // name-value of dimensions and their colors
1495 - available: [], // an array of colors available to be used
1496 - custom: [], // the array of colors defined by the user
1497 - charts: {}, // the charts linked to this
1498 - copy_theme: true
1499 - };
1500 - ret = this.keys[name];
1853 + return v;
1854 + }
1855 + }
1856 + },
1857 + legendFormatter: function (data) {
1858 + if (state.tmp.dygraph_mouse_down) {
1859 + return;
1860 }
1861
1503 - if(typeof ret.charts[state.uuid] === 'undefined') {
1504 - ret.charts[state.uuid] = state;
1862 + let elements = state.element_legend_childs;
1863
1506 - if(has_custom_colors === true)
1507 - this.__read_custom_colors(state, ret);
1864 + // if the hidden div is not there
1865 + // we are not managing the legend
1866 + if (elements.hidden === null) {
1867 + return;
1868 }
1869
1510 - return ret;
1511 - }
1512 - };
1870 + if (typeof data.x !== 'undefined') {
1871 + state.legendSetDate(data.x);
1872 + let i = data.series.length;
1873 + while (i--) {
1874 + let series = data.series[i];
1875 + if (series.isVisible) {
1876 + state.legendSetLabelValue(series.label, series.y);
1877 + } else {
1878 + state.legendSetLabelValue(series.label, null);
1879 + }
1880 + }
1881 + }
1882
1514 - // ----------------------------------------------------------------------------------------------------------------
1515 - // Chart Registry
1883 + return '';
1884 + },
1885 + drawCallback: function (dygraph, is_initial) {
1886
1517 - // When multiple charts need the same chart, we avoid downloading it
1518 - // multiple times (and having it in browser memory multiple time)
1519 - // by using this registry.
1887 + // the user has panned the chart and this is called to re-draw the chart
1888 + // 1. refresh this chart by adding data to it
1889 + // 2. notify all the other charts about the update they need
1890
1521 - // Every time we download a chart definition, we save it here with .add()
1522 - // Then we try to get it back with .get(). If that fails, we download it.
1891 + // to prevent an infinite loop (feedback), we use
1892 + // state.tmp.dygraph_user_action
1893 + // - when true, this is initiated by a user
1894 + // - when false, this is feedback
1895
1524 - NETDATA.fixHost = function(host) {
1525 - while(host.slice(-1) === '/')
1526 - host = host.substring(0, host.length - 1);
1896 + if (state.current.name !== 'auto' && state.tmp.dygraph_user_action) {
1897 + state.tmp.dygraph_user_action = false;
1898
1528 - return host;
1529 - };
1899 + let x_range = dygraph.xAxisRange();
1900 + let after = Math.round(x_range[0]);
1901 + let before = Math.round(x_range[1]);
1902
1531 - NETDATA.chartRegistry = {
1532 - charts: {},
1903 + if (NETDATA.options.debug.dygraph) {
1904 + state.log('dygraphDrawCallback(dygraph, ' + is_initial + '): mode ' + state.current.name + ' ' + (after / 1000).toString() + ' - ' + (before / 1000).toString());
1905 + //console.log(state);
1906 + }
1907
1534 - globalReset: function() {
1535 - this.charts = {};
1908 + if (before <= state.netdata_last && after >= state.netdata_first) {
1909 + // update only when we are within the data limits
1910 + state.updateChartPanOrZoom(after, before);
1911 + }
1912 + }
1913 },
1914 + zoomCallback: function (minDate, maxDate, yRanges) {
1915
1538 - add: function(host, id, data) {
1539 - if(typeof this.charts[host] === 'undefined')
1540 - this.charts[host] = {};
1916 + // the user has selected a range on the chart
1917 + // 1. refresh this chart by adding data to it
1918 + // 2. notify all the other charts about the update they need
1919
1542 - //console.log('added ' + host + '/' + id);
1543 - this.charts[host][id] = data;
1544 - },
1920 + void(yRanges);
1921
1546 - get: function(host, id) {
1547 - if(typeof this.charts[host] === 'undefined')
1548 - return null;
1922 + if (NETDATA.options.debug.dygraph) {
1923 + state.log('dygraphZoomCallback(): ' + state.current.name);
1924 + }
1925
1550 - if(typeof this.charts[host][id] === 'undefined')
1551 - return null;
1926 + NETDATA.globalSelectionSync.stop();
1927 + NETDATA.globalSelectionSync.delay();
1928 + state.setMode('zoom');
1929
1553 - //console.log('cached ' + host + '/' + id);
1554 - return this.charts[host][id];
1930 + // refresh it to the greatest possible zoom level
1931 + state.tmp.dygraph_user_action = true;
1932 + state.tmp.dygraph_force_zoom = true;
1933 + state.updateChartPanOrZoom(minDate, maxDate);
1934 },
1935 + highlightCallback: function (event, x, points, row, seriesName) {
1936 + void(seriesName);
1937
1557 - downloadAll: function(host, callback) {
1558 - host = NETDATA.fixHost(host);
1938 + state.pauseChart();
1939
1560 - var self = this;
1940 + // there is a bug in dygraph when the chart is zoomed enough
1941 + // the time it thinks is selected is wrong
1942 + // here we calculate the time t based on the row number selected
1943 + // which is ok
1944 + // let t = state.data_after + row * state.data_update_every;
1945 + // console.log('row = ' + row + ', x = ' + x + ', t = ' + t + ' ' + ((t === x)?'SAME':(Math.abs(x-t)<=state.data_update_every)?'SIMILAR':'DIFFERENT') + ', rows in db: ' + state.data_points + ' visible(x) = ' + state.timeIsVisible(x) + ' visible(t) = ' + state.timeIsVisible(t) + ' r(x) = ' + state.calculateRowForTime(x) + ' r(t) = ' + state.calculateRowForTime(t) + ' range: ' + state.data_after + ' - ' + state.data_before + ' real: ' + state.data.after + ' - ' + state.data.before + ' every: ' + state.data_update_every);
1946
1562 - function got_data(h, data, callback) {
1563 - if(data !== null) {
1564 - self.charts[h] = data.charts;
1947 + if (state.tmp.dygraph_mouse_down !== true) {
1948 + NETDATA.globalSelectionSync.sync(state, x);
1949 + }
1950
1566 - // update the server timezone in our options
1567 - if(typeof data.timezone === 'string')
1568 - NETDATA.options.server_timezone = data.timezone;
1569 - }
1570 - else NETDATA.error(406, h + '/api/v1/charts');
1951 + // fix legend zIndex using the internal structures of dygraph legend module
1952 + // this works, but it is a hack!
1953 + // state.tmp.dygraph_instance.plugins_[0].plugin.legend_div_.style.zIndex = 10000;
1954 + },
1955 + unhighlightCallback: function (event) {
1956 + void(event);
1957
1572 - if(typeof callback === 'function')
1573 - callback(data);
1958 + if (state.tmp.dygraph_mouse_down) {
1959 + return;
1960 }
1961
1576 - if(netdataSnapshotData !== null) {
1577 - got_data(host, netdataSnapshotData.charts, callback);
1578 - }
1579 - else {
1580 - $.ajax({
1581 - url: host + '/api/v1/charts',
1582 - async: true,
1583 - cache: false,
1584 - xhrFields: {withCredentials: true} // required for the cookie
1585 - })
1586 - .done(function (data) {
1587 - data = NETDATA.xss.checkOptional('/api/v1/charts', data);
1588 - got_data(host, data, callback);
1589 - })
1590 - .fail(function () {
1591 - NETDATA.error(405, host + '/api/v1/charts');
1592 -
1593 - if (typeof callback === 'function')
1594 - callback(null);
1595 - });
1962 + if (NETDATA.options.debug.dygraph || state.debug) {
1963 + state.log('dygraphUnhighlightCallback()');
1964 }
1597 - }
1598 - };
1599 -
1600 - // ----------------------------------------------------------------------------------------------------------------
1601 - // Global Pan and Zoom on charts
1602 -
1603 - // Using this structure are synchronize all the charts, so that
1604 - // when you pan or zoom one, all others are automatically refreshed
1605 - // to the same timespan.
1965
1607 - NETDATA.globalPanAndZoom = {
1608 - seq: 0, // timestamp ms
1609 - // every time a chart is panned or zoomed
1610 - // we set the timestamp here
1611 - // then we use it as a sequence number
1612 - // to find if other charts are synchronized
1613 - // to this time-range
1614 -
1615 - master: null, // the master chart (state), to which all others
1616 - // are synchronized
1617 -
1618 - force_before_ms: null, // the timespan to sync all other charts
1619 - force_after_ms: null,
1966 + state.unpauseChart();
1967 + NETDATA.globalSelectionSync.stop();
1968 + },
1969 + underlayCallback: function (canvas, area, g) {
1970
1621 - callback: null,
1971 + // the chart is about to be drawn
1972 + // this function renders global highlighted time-frame
1973
1623 - globalReset: function() {
1624 - this.clearMaster();
1625 - this.seq = 0;
1626 - this.master = null;
1627 - this.force_after_ms = null;
1628 - this.force_before_ms = null;
1629 - this.callback = null;
1630 - },
1974 + if (NETDATA.globalChartUnderlay.isActive()) {
1975 + let after = NETDATA.globalChartUnderlay.after;
1976 + let before = NETDATA.globalChartUnderlay.before;
1977
1632 - delay: function() {
1633 - if(NETDATA.options.debug.globalPanAndZoom === true)
1634 - console.log('globalPanAndZoom.delay()');
1978 + if (after < state.view_after) {
1979 + after = state.view_after;
1980 + }
1981
1636 - NETDATA.options.auto_refresher_stop_until = Date.now() + NETDATA.options.current.global_pan_sync_time;
1637 - },
1982 + if (before > state.view_before) {
1983 + before = state.view_before;
1984 + }
1985
1639 - // set a new master
1640 - setMaster: function(state, after, before) {
1641 - this.delay();
1986 + if (after < before) {
1987 + let bottom_left = g.toDomCoords(after, -20);
1988 + let top_right = g.toDomCoords(before, +20);
1989
1643 - if(NETDATA.options.current.sync_pan_and_zoom === false)
1644 - return;
1990 + let left = bottom_left[0];
1991 + let right = top_right[0];
1992
1646 - if(this.master === null) {
1647 - if(NETDATA.options.debug.globalPanAndZoom === true)
1648 - console.log('globalPanAndZoom.setMaster(' + state.id + ', ' + after + ', ' + before + ') SET MASTER');
1993 + canvas.fillStyle = NETDATA.themes.current.highlight;
1994 + canvas.fillRect(left, area.y, right - left, area.h);
1995 + }
1996 }
1650 - else if(this.master !== state) {
1651 - if(NETDATA.options.debug.globalPanAndZoom === true)
1652 - console.log('globalPanAndZoom.setMaster(' + state.id + ', ' + after + ', ' + before + ') CHANGED MASTER');
1997 + },
1998 + interactionModel: {
1999 + mousedown: function (event, dygraph, context) {
2000 + if (NETDATA.options.debug.dygraph || state.debug) {
2001 + state.log('interactionModel.mousedown()');
2002 + }
2003
1654 - this.master.resetChart(true, true);
1655 - }
2004 + state.tmp.dygraph_user_action = true;
2005
1657 - var now = Date.now();
1658 - this.master = state;
1659 - this.seq = now;
1660 - this.force_after_ms = after;
1661 - this.force_before_ms = before;
2006 + if (NETDATA.options.debug.dygraph) {
2007 + state.log('dygraphMouseDown()');
2008 + }
2009
1663 - if(typeof this.callback === 'function')
1664 - this.callback(true, after, before);
1665 - },
2010 + // Right-click should not initiate anything.
2011 + if (event.button && event.button === 2) {
2012 + return;
2013 + }
2014
1667 - // clear the master
1668 - clearMaster: function() {
1669 - if(NETDATA.options.debug.globalPanAndZoom === true)
1670 - console.log('globalPanAndZoom.clearMaster()');
2015 + NETDATA.globalSelectionSync.stop();
2016 + NETDATA.globalSelectionSync.delay();
2017
1672 - if(this.master !== null) {
1673 - var st = this.master;
1674 - this.master = null;
1675 - st.resetChart();
1676 - }
2018 + state.tmp.dygraph_mouse_down = true;
2019 + context.initializeMouseDown(event, dygraph, context);
2020
1678 - this.master = null;
1679 - this.seq = 0;
1680 - this.force_after_ms = null;
1681 - this.force_before_ms = null;
1682 - NETDATA.options.auto_refresher_stop_until = 0;
2021 + //console.log(event);
2022 + if (event.button && event.button === 1) {
2023 + if (event.shiftKey) {
2024 + //console.log('middle mouse button dragging (PAN)');
2025
1684 - if(typeof this.callback === 'function')
1685 - this.callback(false, 0, 0);
1686 - },
2026 + state.setMode('pan');
2027 + // NETDATA.globalSelectionSync.delay();
2028 + state.tmp.dygraph_highlight_after = null;
2029 + Dygraph.startPan(event, dygraph, context);
2030 + } else if (event.altKey || event.ctrlKey || event.metaKey) {
2031 + //console.log('middle mouse button highlight');
2032
1688 - // is the given state the master of the global
1689 - // pan and zoom sync?
1690 - isMaster: function(state) {
1691 - return (this.master === state);
1692 - },
2033 + if (!(event.offsetX && event.offsetY)) {
2034 + event.offsetX = event.layerX - event.target.offsetLeft;
2035 + event.offsetY = event.layerY - event.target.offsetTop;
2036 + }
2037 + state.tmp.dygraph_highlight_after = dygraph.toDataXCoord(event.offsetX);
2038 + Dygraph.startZoom(event, dygraph, context);
2039 + } else {
2040 + //console.log('middle mouse button selection for zoom (ZOOM)');
2041
1694 - // are we currently have a global pan and zoom sync?
1695 - isActive: function() {
1696 - return (this.master !== null && this.force_before_ms !== null && this.force_after_ms !== null && this.seq !== 0);
1697 - },
2042 + state.setMode('zoom');
2043 + // NETDATA.globalSelectionSync.delay();
2044 + state.tmp.dygraph_highlight_after = null;
2045 + Dygraph.startZoom(event, dygraph, context);
2046 + }
2047 + } else {
2048 + if (event.shiftKey) {
2049 + //console.log('left mouse button selection for zoom (ZOOM)');
2050
1699 - // check if a chart, other than the master
1700 - // needs to be refreshed, due to the global pan and zoom
1701 - shouldBeAutoRefreshed: function(state) {
1702 - if(this.master === null || this.seq === 0)
1703 - return false;
2051 + state.setMode('zoom');
2052 + // NETDATA.globalSelectionSync.delay();
2053 + state.tmp.dygraph_highlight_after = null;
2054 + Dygraph.startZoom(event, dygraph, context);
2055 + } else if (event.altKey || event.ctrlKey || event.metaKey) {
2056 + //console.log('left mouse button highlight');
2057
1705 - //if(state.needsRecreation())
1706 - // return true;
2058 + if (!(event.offsetX && event.offsetY)) {
2059 + event.offsetX = event.layerX - event.target.offsetLeft;
2060 + event.offsetY = event.layerY - event.target.offsetTop;
2061 + }
2062 + state.tmp.dygraph_highlight_after = dygraph.toDataXCoord(event.offsetX);
2063 + Dygraph.startZoom(event, dygraph, context);
2064 + } else {
2065 + //console.log('left mouse button dragging (PAN)');
2066
1708 - return (state.tm.pan_and_zoom_seq !== this.seq);
1709 - }
1710 - };
2067 + state.setMode('pan');
2068 + // NETDATA.globalSelectionSync.delay();
2069 + state.tmp.dygraph_highlight_after = null;
2070 + Dygraph.startPan(event, dygraph, context);
2071 + }
2072 + }
2073 + },
2074 + mousemove: function (event, dygraph, context) {
2075 + if (NETDATA.options.debug.dygraph || state.debug) {
2076 + state.log('interactionModel.mousemove()');
2077 + }
2078
1712 - // ----------------------------------------------------------------------------------------------------------------
1713 - // global chart underlay (time-frame highlighting)
1714 -
1715 - NETDATA.globalChartUnderlay = {
1716 - callback: null, // what to call when a highlighted range is setup
1717 - after: null, // highlight after this time
1718 - before: null, // highlight before this time
1719 - view_after: null, // the charts after_ms viewport when the highlight was setup
1720 - view_before: null, // the charts before_ms viewport, when the highlight was setup
1721 - state: null, // the chart the highlight was setup
1722 -
1723 - isActive: function() {
1724 - return (this.after !== null && this.before !== null);
1725 - },
2079 + if (state.tmp.dygraph_highlight_after !== null) {
2080 + //console.log('highlight selection...');
2081
1727 - hasViewport: function() {
1728 - return (this.state !== null && this.view_after !== null && this.view_before !== null);
1729 - },
2082 + NETDATA.globalSelectionSync.stop();
2083 + NETDATA.globalSelectionSync.delay();
2084
1731 - init: function(state, after, before, view_after, view_before) {
1732 - this.state = (typeof state !== 'undefined') ? state : null;
1733 - this.after = (typeof after !== 'undefined' && after !== null && after > 0) ? after : null;
1734 - this.before = (typeof before !== 'undefined' && before !== null && before > 0) ? before : null;
1735 - this.view_after = (typeof view_after !== 'undefined' && view_after !== null && view_after > 0) ? view_after : null;
1736 - this.view_before = (typeof view_before !== 'undefined' && view_before !== null && view_before > 0) ? view_before : null;
1737 - },
2085 + state.tmp.dygraph_user_action = true;
2086 + Dygraph.moveZoom(event, dygraph, context);
2087 + event.preventDefault();
2088 + } else if (context.isPanning) {
2089 + //console.log('panning...');
2090
1739 - setup: function() {
1740 - if(this.isActive() === true) {
1741 - if (this.state === null)
1742 - this.state = NETDATA.options.targets[0];
2091 + NETDATA.globalSelectionSync.stop();
2092 + NETDATA.globalSelectionSync.delay();
2093
1744 - if (typeof this.callback === 'function')
1745 - this.callback(true, this.after, this.before);
1746 - }
1747 - else {
1748 - if (typeof this.callback === 'function')
1749 - this.callback(false, 0, 0);
1750 - }
1751 - },
2094 + state.tmp.dygraph_user_action = true;
2095 + //NETDATA.globalSelectionSync.stop();
2096 + //NETDATA.globalSelectionSync.delay();
2097 + state.setMode('pan');
2098 + context.is2DPan = false;
2099 + Dygraph.movePan(event, dygraph, context);
2100 + } else if (context.isZooming) {
2101 + //console.log('zooming...');
2102
1753 - set: function(state, after, before, view_after, view_before) {
1754 - if(after > before) {
1755 - var t = after;
1756 - after = before;
1757 - before = t;
1758 - }
2103 + NETDATA.globalSelectionSync.stop();
2104 + NETDATA.globalSelectionSync.delay();
2105
1760 - this.init(state, after, before, view_after, view_before);
2106 + state.tmp.dygraph_user_action = true;
2107 + //NETDATA.globalSelectionSync.stop();
2108 + //NETDATA.globalSelectionSync.delay();
2109 + state.setMode('zoom');
2110 + Dygraph.moveZoom(event, dygraph, context);
2111 + }
2112 + },
2113 + mouseup: function (event, dygraph, context) {
2114 + state.tmp.dygraph_mouse_down = false;
2115
1762 - if (this.hasViewport() === true)
1763 - NETDATA.globalPanAndZoom.setMaster(this.state, this.view_after, this.view_before);
2116 + if (NETDATA.options.debug.dygraph || state.debug) {
2117 + state.log('interactionModel.mouseup()');
2118 + }
2119
1765 - this.setup();
1766 - },
2120 + if (state.tmp.dygraph_highlight_after !== null) {
2121 + //console.log('done highlight selection');
2122
1768 - clear: function() {
1769 - this.after = null;
1770 - this.before = null;
1771 - this.state = null;
1772 - this.view_after = null;
1773 - this.view_before = null;
2123 + NETDATA.globalSelectionSync.stop();
2124 + NETDATA.globalSelectionSync.delay();
2125
1775 - if(typeof this.callback === 'function')
1776 - this.callback(false, 0, 0);
1777 - },
2126 + if (!(event.offsetX && event.offsetY)) {
2127 + event.offsetX = event.layerX - event.target.offsetLeft;
2128 + event.offsetY = event.layerY - event.target.offsetTop;
2129 + }
2130
1779 - focus: function() {
1780 - if(this.isActive() === true && this.hasViewport() === true) {
1781 - if(this.state === null)
1782 - this.state = NETDATA.options.targets[0];
2131 + NETDATA.globalChartUnderlay.set(state
2132 + , state.tmp.dygraph_highlight_after
2133 + , dygraph.toDataXCoord(event.offsetX)
2134 + , state.view_after
2135 + , state.view_before
2136 + );
2137
1784 - if(NETDATA.globalPanAndZoom.isMaster(this.state) === true)
1785 - NETDATA.globalPanAndZoom.clearMaster();
2138 + state.tmp.dygraph_highlight_after = null;
2139
1787 - NETDATA.globalPanAndZoom.setMaster(this.state, this.view_after, this.view_before, true);
1788 - }
1789 - }
1790 - };
2140 + context.isZooming = false;
2141 + dygraph.clearZoomRect_();
2142 + dygraph.drawGraph_(false);
2143
1792 - // ----------------------------------------------------------------------------------------------------------------
1793 - // dimensions selection
2144 + // refresh all the charts immediately
2145 + NETDATA.options.auto_refresher_stop_until = 0;
2146 + } else if (context.isPanning) {
2147 + //console.log('done panning');
2148
1795 - // TODO
1796 - // move color assignment to dimensions, here
2149 + NETDATA.globalSelectionSync.stop();
2150 + NETDATA.globalSelectionSync.delay();
2151
1798 - var dimensionStatus = function(parent, label, name_div, value_div, color) {
1799 - this.enabled = false;
1800 - this.parent = parent;
1801 - this.label = label;
1802 - this.name_div = null;
1803 - this.value_div = null;
1804 - this.color = NETDATA.themes.current.foreground;
1805 - this.selected = (parent.unselected_count === 0);
1806 -
1807 - this.setOptions(name_div, value_div, color);
1808 - };
2152 + state.tmp.dygraph_user_action = true;
2153 + Dygraph.endPan(event, dygraph, context);
2154
1810 - dimensionStatus.prototype.invalidate = function() {
1811 - this.name_div = null;
1812 - this.value_div = null;
1813 - this.enabled = false;
1814 - };
2155 + // refresh all the charts immediately
2156 + NETDATA.options.auto_refresher_stop_until = 0;
2157 + } else if (context.isZooming) {
2158 + //console.log('done zomming');
2159
1816 - dimensionStatus.prototype.setOptions = function(name_div, value_div, color) {
1817 - this.color = color;
2160 + NETDATA.globalSelectionSync.stop();
2161 + NETDATA.globalSelectionSync.delay();
2162
1819 - if(this.name_div !== name_div) {
1820 - this.name_div = name_div;
1821 - this.name_div.title = this.label;
1822 - this.name_div.style.setProperty('color', this.color, 'important');
1823 - if(this.selected === false)
1824 - this.name_div.className = 'netdata-legend-name not-selected';
1825 - else
1826 - this.name_div.className = 'netdata-legend-name selected';
1827 - }
2163 + state.tmp.dygraph_user_action = true;
2164 + Dygraph.endZoom(event, dygraph, context);
2165
1829 - if(this.value_div !== value_div) {
1830 - this.value_div = value_div;
1831 - this.value_div.title = this.label;
1832 - this.value_div.style.setProperty('color', this.color, 'important');
1833 - if(this.selected === false)
1834 - this.value_div.className = 'netdata-legend-value not-selected';
1835 - else
1836 - this.value_div.className = 'netdata-legend-value selected';
1837 - }
2166 + // refresh all the charts immediately
2167 + NETDATA.options.auto_refresher_stop_until = 0;
2168 + }
2169 + },
2170 + click: function (event, dygraph, context) {
2171 + void(dygraph);
2172 + void(context);
2173
1839 - this.enabled = true;
1840 - this.setHandler();
1841 - };
2174 + if (NETDATA.options.debug.dygraph || state.debug) {
2175 + state.log('interactionModel.click()');
2176 + }
2177
1843 - dimensionStatus.prototype.setHandler = function() {
1844 - if(this.enabled === false) return;
2178 + event.preventDefault();
2179 + },
2180 + dblclick: function (event, dygraph, context) {
2181 + void(event);
2182 + void(dygraph);
2183 + void(context);
2184
1846 - var ds = this;
2185 + if (NETDATA.options.debug.dygraph || state.debug) {
2186 + state.log('interactionModel.dblclick()');
2187 + }
2188 + NETDATA.resetAllCharts(state);
2189 + },
2190 + wheel: function (event, dygraph, context) {
2191 + void(context);
2192
1848 - // this.name_div.onmousedown = this.value_div.onmousedown = function(e) {
1849 - this.name_div.onclick = this.value_div.onclick = function(e) {
1850 - e.preventDefault();
1851 - if(ds.isSelected()) {
1852 - // this is selected
1853 - if(e.shiftKey === true || e.ctrlKey === true) {
1854 - // control or shift key is pressed -> unselect this (except is none will remain selected, in which case select all)
1855 - ds.unselect();
2193 + if (NETDATA.options.debug.dygraph || state.debug) {
2194 + state.log('interactionModel.wheel()');
2195 + }
2196
1857 - if(ds.parent.countSelected() === 0)
1858 - ds.parent.selectAll();
2197 + // Take the offset of a mouse event on the dygraph canvas and
2198 + // convert it to a pair of percentages from the bottom left.
2199 + // (Not top left, bottom is where the lower value is.)
2200 + function offsetToPercentage(g, offsetX, offsetY) {
2201 + // This is calculating the pixel offset of the leftmost date.
2202 + let xOffset = g.toDomCoords(g.xAxisRange()[0], null)[0];
2203 + let yar0 = g.yAxisRange(0);
2204 +
2205 + // This is calculating the pixel of the highest value. (Top pixel)
2206 + let yOffset = g.toDomCoords(null, yar0[1])[1];
2207 +
2208 + // x y w and h are relative to the corner of the drawing area,
2209 + // so that the upper corner of the drawing area is (0, 0).
2210 + let x = offsetX - xOffset;
2211 + let y = offsetY - yOffset;
2212 +
2213 + // This is computing the rightmost pixel, effectively defining the
2214 + // width.
2215 + let w = g.toDomCoords(g.xAxisRange()[1], null)[0] - xOffset;
2216 +
2217 + // This is computing the lowest pixel, effectively defining the height.
2218 + let h = g.toDomCoords(null, yar0[0])[1] - yOffset;
2219 +
2220 + // Percentage from the left.
2221 + let xPct = w === 0 ? 0 : (x / w);
2222 + // Percentage from the top.
2223 + let yPct = h === 0 ? 0 : (y / h);
2224 +
2225 + // The (1-) part below changes it from "% distance down from the top"
2226 + // to "% distance up from the bottom".
2227 + return [xPct, (1 - yPct)];
2228 }
1860 - else {
1861 - // no key is pressed -> select only this (except if it is the only selected already, in which case select all)
1862 - if(ds.parent.countSelected() === 1) {
1863 - ds.parent.selectAll();
2229 +
2230 + // Adjusts [x, y] toward each other by zoomInPercentage%
2231 + // Split it so the left/bottom axis gets xBias/yBias of that change and
2232 + // tight/top gets (1-xBias)/(1-yBias) of that change.
2233 + //
2234 + // If a bias is missing it splits it down the middle.
2235 + function zoomRange(g, zoomInPercentage, xBias, yBias) {
2236 + xBias = xBias || 0.5;
2237 + yBias = yBias || 0.5;
2238 +
2239 + function adjustAxis(axis, zoomInPercentage, bias) {
2240 + let delta = axis[1] - axis[0];
2241 + let increment = delta * zoomInPercentage;
2242 + let foo = [increment * bias, increment * (1 - bias)];
2243 +
2244 + return [axis[0] + foo[0], axis[1] - foo[1]];
2245 }
1865 - else {
1866 - ds.parent.selectNone();
1867 - ds.select();
2246 +
2247 + let yAxes = g.yAxisRanges();
2248 + let newYAxes = [];
2249 + for (let i = 0; i < yAxes.length; i++) {
2250 + newYAxes[i] = adjustAxis(yAxes[i], zoomInPercentage, yBias);
2251 }
2252 +
2253 + return adjustAxis(g.xAxisRange(), zoomInPercentage, xBias);
2254 }
1870 - }
1871 - else {
1872 - // this is not selected
1873 - if(e.shiftKey === true || e.ctrlKey === true) {
1874 - // control or shift key is pressed -> select this too
1875 - ds.select();
1876 - }
1877 - else {
1878 - // no key is pressed -> select only this
1879 - ds.parent.selectNone();
1880 - ds.select();
1881 - }
1882 - }
2255
1884 - ds.parent.state.redrawChart();
1885 - }
1886 - };
2256 + if (event.altKey || event.shiftKey) {
2257 + state.tmp.dygraph_user_action = true;
2258
1888 - dimensionStatus.prototype.select = function() {
1889 - if(this.enabled === false) return;
2259 + NETDATA.globalSelectionSync.stop();
2260 + NETDATA.globalSelectionSync.delay();
2261
1891 - this.name_div.className = 'netdata-legend-name selected';
1892 - this.value_div.className = 'netdata-legend-value selected';
1893 - this.selected = true;
1894 - };
2262 + // http://dygraphs.com/gallery/interaction-api.js
2263 + let normal_def;
2264 + if (typeof event.wheelDelta === 'number' && !isNaN(event.wheelDelta))
2265 + // chrome
2266 + {
2267 + normal_def = event.wheelDelta / 40;
2268 + } else
2269 + // firefox
2270 + {
2271 + normal_def = event.deltaY * -1.2;
2272 + }
2273
1896 - dimensionStatus.prototype.unselect = function() {
1897 - if(this.enabled === false) return;
2274 + let normal = (event.detail) ? event.detail * -1 : normal_def;
2275 + let percentage = normal / 50;
2276
1899 - this.name_div.className = 'netdata-legend-name not-selected';
1900 - this.value_div.className = 'netdata-legend-value hidden';
1901 - this.selected = false;
1902 - };
2277 + if (!(event.offsetX && event.offsetY)) {
2278 + event.offsetX = event.layerX - event.target.offsetLeft;
2279 + event.offsetY = event.layerY - event.target.offsetTop;
2280 + }
2281
1904 - dimensionStatus.prototype.isSelected = function() {
1905 - return(this.enabled === true && this.selected === true);
1906 - };
2282 + let percentages = offsetToPercentage(dygraph, event.offsetX, event.offsetY);
2283 + let xPct = percentages[0];
2284 + let yPct = percentages[1];
2285
1908 - // ----------------------------------------------------------------------------------------------------------------
2286 + let new_x_range = zoomRange(dygraph, percentage, xPct, yPct);
2287 + let after = new_x_range[0];
2288 + let before = new_x_range[1];
2289
1910 - var dimensionsVisibility = function(state) {
1911 - this.state = state;
1912 - this.len = 0;
1913 - this.dimensions = {};
1914 - this.selected_count = 0;
1915 - this.unselected_count = 0;
1916 - };
2290 + let first = state.netdata_first + state.data_update_every;
2291 + let last = state.netdata_last + state.data_update_every;
2292
1918 - dimensionsVisibility.prototype.dimensionAdd = function(label, name_div, value_div, color) {
1919 - if(typeof this.dimensions[label] === 'undefined') {
1920 - this.len++;
1921 - this.dimensions[label] = new dimensionStatus(this, label, name_div, value_div, color);
1922 - }
1923 - else
1924 - this.dimensions[label].setOptions(name_div, value_div, color);
2293 + if (before > last) {
2294 + after -= (before - last);
2295 + before = last;
2296 + }
2297 + if (after < first) {
2298 + after = first;
2299 + }
2300
1926 - return this.dimensions[label];
1927 - };
1928 -
1929 - dimensionsVisibility.prototype.dimensionGet = function(label) {
1930 - return this.dimensions[label];
1931 - };
1932 -
1933 - dimensionsVisibility.prototype.invalidateAll = function() {
1934 - var keys = Object.keys(this.dimensions);
1935 - var len = keys.length;
1936 - while(len--)
1937 - this.dimensions[keys[len]].invalidate();
1938 - };
1939 -
1940 - dimensionsVisibility.prototype.selectAll = function() {
1941 - var keys = Object.keys(this.dimensions);
1942 - var len = keys.length;
1943 - while(len--)
1944 - this.dimensions[keys[len]].select();
1945 - };
1946 -
1947 - dimensionsVisibility.prototype.countSelected = function() {
1948 - var selected = 0;
1949 - var keys = Object.keys(this.dimensions);
1950 - var len = keys.length;
1951 - while(len--)
1952 - if(this.dimensions[keys[len]].isSelected()) selected++;
1953 -
1954 - return selected;
1955 - };
1956 -
1957 - dimensionsVisibility.prototype.selectNone = function() {
1958 - var keys = Object.keys(this.dimensions);
1959 - var len = keys.length;
1960 - while(len--)
1961 - this.dimensions[keys[len]].unselect();
1962 - };
1963 -
1964 - dimensionsVisibility.prototype.selected2BooleanArray = function(array) {
1965 - var ret = [];
1966 - this.selected_count = 0;
1967 - this.unselected_count = 0;
1968 -
1969 - var len = array.length;
1970 - while(len--) {
1971 - var ds = this.dimensions[array[len]];
1972 - if(typeof ds === 'undefined') {
1973 - // console.log(array[i] + ' is not found');
1974 - ret.unshift(false);
1975 - }
1976 - else if(ds.isSelected()) {
1977 - ret.unshift(true);
1978 - this.selected_count++;
1979 - }
1980 - else {
1981 - ret.unshift(false);
1982 - this.unselected_count++;
1983 - }
1984 - }
1985 -
1986 - if(this.selected_count === 0 && this.unselected_count !== 0) {
1987 - this.selectAll();
1988 - return this.selected2BooleanArray(array);
1989 - }
1990 -
1991 - return ret;
1992 - };
1993 -
1994 -
1995 - // ----------------------------------------------------------------------------------------------------------------
1996 - // date/time conversion
2301 + state.setMode('zoom');
2302 + state.updateChartPanOrZoom(after, before, function () {
2303 + dygraph.updateOptions({dateWindow: [after, before]});
2304 + });
2305
1998 - NETDATA.dateTime = {
1999 - using_timezone: false,
2306 + event.preventDefault();
2307 + }
2308 + },
2309 + touchstart: function (event, dygraph, context) {
2310 + state.tmp.dygraph_mouse_down = true;
2311
2001 - // these are the old netdata functions
2002 - // we fallback to these, if the new ones fail
2312 + if (NETDATA.options.debug.dygraph || state.debug) {
2313 + state.log('interactionModel.touchstart()');
2314 + }
2315
2004 - localeDateStringNative: function(d) {
2005 - return d.toLocaleDateString();
2006 - },
2316 + state.tmp.dygraph_user_action = true;
2317 + state.setMode('zoom');
2318 + state.pauseChart();
2319
2008 - localeTimeStringNative: function(d) {
2009 - return d.toLocaleTimeString();
2010 - },
2320 + NETDATA.globalSelectionSync.stop();
2321 + NETDATA.globalSelectionSync.delay();
2322
2012 - xAxisTimeStringNative: function(d) {
2013 - return NETDATA.zeropad(d.getHours()) + ":"
2014 - + NETDATA.zeropad(d.getMinutes()) + ":"
2015 - + NETDATA.zeropad(d.getSeconds());
2016 - },
2323 + Dygraph.defaultInteractionModel.touchstart(event, dygraph, context);
2324
2018 - // initialize the new date/time conversion
2019 - // functions.
2020 - // if this fails, we fallback to the above
2021 - init: function(timezone) {
2022 - //console.log('init with timezone: ' + timezone);
2325 + // we overwrite the touch directions at the end, to overwrite
2326 + // the internal default of dygraph
2327 + context.touchDirections = {x: true, y: false};
2328
2024 - // detect browser timezone
2025 - try {
2026 - NETDATA.options.browser_timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
2027 - }
2028 - catch(e) {
2029 - console.log('failed to detect browser timezone: ' + e.toString());
2030 - NETDATA.options.browser_timezone = 'cannot-detect-it';
2031 - }
2329 + state.dygraph_last_touch_start = Date.now();
2330 + state.dygraph_last_touch_move = 0;
2331
2033 - var ret = false;
2332 + if (typeof event.touches[0].pageX === 'number') {
2333 + state.dygraph_last_touch_page_x = event.touches[0].pageX;
2334 + } else {
2335 + state.dygraph_last_touch_page_x = 0;
2336 + }
2337 + },
2338 + touchmove: function (event, dygraph, context) {
2339 + if (NETDATA.options.debug.dygraph || state.debug) {
2340 + state.log('interactionModel.touchmove()');
2341 + }
2342
2035 - try {
2036 - var dateOptions ={
2037 - localeMatcher: 'best fit',
2038 - formatMatcher: 'best fit',
2039 - weekday: 'short',
2040 - year: 'numeric',
2041 - month: 'short',
2042 - day: '2-digit'
2043 - };
2343 + NETDATA.globalSelectionSync.stop();
2344 + NETDATA.globalSelectionSync.delay();
2345
2045 - var timeOptions = {
2046 - localeMatcher: 'best fit',
2047 - hour12: false,
2048 - formatMatcher: 'best fit',
2049 - hour: '2-digit',
2050 - minute: '2-digit',
2051 - second: '2-digit'
2052 - };
2346 + state.tmp.dygraph_user_action = true;
2347 + Dygraph.defaultInteractionModel.touchmove(event, dygraph, context);
2348
2054 - var xAxisOptions = {
2055 - localeMatcher: 'best fit',
2056 - hour12: false,
2057 - formatMatcher: 'best fit',
2058 - hour: '2-digit',
2059 - minute: '2-digit',
2060 - second: '2-digit'
2061 - };
2349 + state.dygraph_last_touch_move = Date.now();
2350 + },
2351 + touchend: function (event, dygraph, context) {
2352 + state.tmp.dygraph_mouse_down = false;
2353
2063 - if(typeof timezone === 'string' && timezone !== '' && timezone !== 'default') {
2064 - dateOptions.timeZone = timezone;
2065 - timeOptions.timeZone = timezone;
2066 - timeOptions.timeZoneName = 'short';
2067 - xAxisOptions.timeZone = timezone;
2068 - this.using_timezone = true;
2069 - }
2070 - else {
2071 - timezone = 'default';
2072 - this.using_timezone = false;
2354 + if (NETDATA.options.debug.dygraph || state.debug) {
2355 + state.log('interactionModel.touchend()');
2356 }
2357
2075 - this.dateFormat = new Intl.DateTimeFormat(navigator.language, dateOptions);
2076 - this.timeFormat = new Intl.DateTimeFormat(navigator.language, timeOptions);
2077 - this.xAxisFormat = new Intl.DateTimeFormat(navigator.language, xAxisOptions);
2358 + NETDATA.globalSelectionSync.stop();
2359 + NETDATA.globalSelectionSync.delay();
2360
2079 - this.localeDateString = function(d) {
2080 - return this.dateFormat.format(d);
2081 - };
2361 + state.tmp.dygraph_user_action = true;
2362 + Dygraph.defaultInteractionModel.touchend(event, dygraph, context);
2363
2083 - this.localeTimeString = function(d) {
2084 - return this.timeFormat.format(d);
2085 - };
2364 + // if it didn't move, it is a selection
2365 + if (state.dygraph_last_touch_move === 0 && state.dygraph_last_touch_page_x !== 0) {
2366 + NETDATA.globalSelectionSync.dont_sync_before = 0;
2367 + NETDATA.globalSelectionSync.setMaster(state);
2368
2087 - this.xAxisTimeString = function(d) {
2088 - return this.xAxisFormat.format(d);
2089 - };
2369 + // internal api of dygraph
2370 + let pct = (state.dygraph_last_touch_page_x - (dygraph.plotter_.area.x + state.element.getBoundingClientRect().left)) / dygraph.plotter_.area.w;
2371 + console.log('pct: ' + pct.toString());
2372
2091 - //var d = new Date();
2092 - //var t = this.dateFormat.format(d) + ' ' + this.timeFormat.format(d) + ' ' + this.xAxisFormat.format(d);
2373 + let t = Math.round(state.view_after + (state.view_before - state.view_after) * pct);
2374 + if (NETDATA.dygraphSetSelection(state, t)) {
2375 + NETDATA.globalSelectionSync.sync(state, t);
2376 + }
2377 + }
2378
2094 - ret = true;
2095 - }
2096 - catch(e) {
2097 - console.log('Cannot setup Date/Time formatting: ' + e.toString());
2379 + // if it was double tap within double click time, reset the charts
2380 + let now = Date.now();
2381 + if (typeof state.dygraph_last_touch_end !== 'undefined') {
2382 + if (state.dygraph_last_touch_move === 0) {
2383 + let dt = now - state.dygraph_last_touch_end;
2384 + if (dt <= NETDATA.options.current.double_click_speed) {
2385 + NETDATA.resetAllCharts(state);
2386 + }
2387 + }
2388 + }
2389
2099 - timezone = 'default';
2100 - this.localeDateString = this.localeDateStringNative;
2101 - this.localeTimeString = this.localeTimeStringNative;
2102 - this.xAxisTimeString = this.xAxisTimeStringNative;
2103 - this.using_timezone = false;
2390 + // remember the timestamp of the last touch end
2391 + state.dygraph_last_touch_end = now;
2392
2105 - ret = false;
2393 + // refresh all the charts immediately
2394 + NETDATA.options.auto_refresher_stop_until = 0;
2395 }
2107 -
2108 - // save it
2109 - //console.log('init setOption timezone: ' + timezone);
2110 - NETDATA.setOption('timezone', timezone);
2111 -
2112 - return ret;
2396 }
2397 };
2115 - NETDATA.dateTime.init(NETDATA.options.current.timezone);
2116 -
2117 -
2118 - // ----------------------------------------------------------------------------------------------------------------
2119 - // units conversion
2120 -
2121 - NETDATA.unitsConversion = {
2122 - keys: {}, // keys for data-common-units
2123 - latest: {}, // latest selected units for data-common-units
2398
2125 - globalReset: function() {
2126 - this.keys = {};
2127 - this.latest = {};
2128 - },
2399 + if (NETDATA.chartLibraries.dygraph.isLogScale(state)) {
2400 + if (Array.isArray(state.tmp.dygraph_options.valueRange) && state.tmp.dygraph_options.valueRange[0] <= 0) {
2401 + state.tmp.dygraph_options.valueRange[0] = null;
2402 + }
2403 + }
2404
2130 - scalableUnits: {
2131 - 'packets/s': {
2132 - 'pps': 1,
2133 - 'Kpps': 1000,
2134 - 'Mpps': 1000000
2135 - },
2136 - 'pps': {
2137 - 'pps': 1,
2138 - 'Kpps': 1000,
2139 - 'Mpps': 1000000
2140 - },
2141 - 'kilobits/s': {
2142 - 'bits/s': 1 / 1000,
2143 - 'kilobits/s': 1,
2144 - 'megabits/s': 1000,
2145 - 'gigabits/s': 1000000,
2146 - 'terabits/s': 1000000000
2147 - },
2148 - 'kilobytes/s': {
2149 - 'bytes/s': 1 / 1024,
2150 - 'kilobytes/s': 1,
2151 - 'megabytes/s': 1024,
2152 - 'gigabytes/s': 1024 * 1024,
2153 - 'terabytes/s': 1024 * 1024 * 1024
2154 - },
2155 - 'KB/s': {
2156 - 'B/s': 1 / 1024,
2157 - 'KB/s': 1,
2158 - 'MB/s': 1024,
2159 - 'GB/s': 1024 * 1024,
2160 - 'TB/s': 1024 * 1024 * 1024
2161 - },
2162 - 'KB': {
2163 - 'B': 1 / 1024,
2164 - 'KB': 1,
2165 - 'MB': 1024,
2166 - 'GB': 1024 * 1024,
2167 - 'TB': 1024 * 1024 * 1024
2168 - },
2169 - 'MB': {
2170 - 'B': 1 / (1024 * 1024),
2171 - 'KB': 1 / 1024,
2172 - 'MB': 1,
2173 - 'GB': 1024,
2174 - 'TB': 1024 * 1024,
2175 - 'PB': 1024 * 1024 * 1024
2176 - },
2177 - 'GB': {
2178 - 'B': 1 / (1024 * 1024 * 1024),
2179 - 'KB': 1 / (1024 * 1024),
2180 - 'MB': 1 / 1024,
2181 - 'GB': 1,
2182 - 'TB': 1024,
2183 - 'PB': 1024 * 1024,
2184 - 'EB': 1024 * 1024 * 1024
2185 - }
2186 - /*
2187 - 'milliseconds': {
2188 - 'seconds': 1000
2189 - },
2190 - 'seconds': {
2191 - 'milliseconds': 0.001,
2192 - 'seconds': 1,
2193 - 'minutes': 60,
2194 - 'hours': 3600,
2195 - 'days': 86400
2196 - }
2197 - */
2198 - },
2405 + if (NETDATA.chartLibraries.dygraph.isSparkline(state)) {
2406 + state.tmp.dygraph_options.drawGrid = false;
2407 + state.tmp.dygraph_options.drawAxis = false;
2408 + state.tmp.dygraph_options.title = undefined;
2409 + state.tmp.dygraph_options.ylabel = undefined;
2410 + state.tmp.dygraph_options.yLabelWidth = 0;
2411 + //state.tmp.dygraph_options.labelsDivWidth = 120;
2412 + //state.tmp.dygraph_options.labelsDivStyles.width = '120px';
2413 + state.tmp.dygraph_options.labelsSeparateLines = true;
2414 + state.tmp.dygraph_options.rightGap = 0;
2415 + state.tmp.dygraph_options.yRangePad = 1;
2416 + state.tmp.dygraph_options.axes.x.drawAxis = false;
2417 + state.tmp.dygraph_options.axes.y.drawAxis = false;
2418 + }
2419
2200 - convertibleUnits: {
2201 - 'Celsius': {
2202 - 'Fahrenheit': {
2203 - check: function(max) { void(max); return NETDATA.options.current.temperature === 'fahrenheit'; },
2204 - convert: function(value) { return value * 9 / 5 + 32; }
2205 - }
2206 - },
2207 - 'celsius': {
2208 - 'fahrenheit': {
2209 - check: function(max) { void(max); return NETDATA.options.current.temperature === 'fahrenheit'; },
2210 - convert: function(value) { return value * 9 / 5 + 32; }
2211 - }
2212 - },
2213 - 'seconds': {
2214 - 'time': {
2215 - check: function (max) { void(max); return NETDATA.options.current.seconds_as_time; },
2216 - convert: function(seconds) { return NETDATA.unitsConversion.seconds2time(seconds); }
2217 - }
2218 - },
2219 - 'milliseconds': {
2220 - 'milliseconds': {
2221 - check: function (max) { return NETDATA.options.current.seconds_as_time && max < 1000; },
2222 - convert: function(milliseconds) {
2223 - var tms = Math.round(milliseconds * 10);
2224 - milliseconds = Math.floor(tms / 10);
2420 + if (smooth) {
2421 + state.tmp.dygraph_smooth_eligible = true;
2422
2226 - tms -= milliseconds * 10;
2423 + if (NETDATA.options.current.smooth_plot) {
2424 + state.tmp.dygraph_options.plotter = smoothPlotter;
2425 + }
2426 + }
2427 + else {
2428 + state.tmp.dygraph_smooth_eligible = false;
2429 + }
2430
2228 - return (milliseconds).toString() + '.' + tms.toString();
2229 - }
2230 - },
2231 - 'seconds': {
2232 - check: function (max) { return NETDATA.options.current.seconds_as_time && max >= 1000 && max < 60000; },
2233 - convert: function(milliseconds) {
2234 - milliseconds = Math.round(milliseconds);
2431 + if (netdataSnapshotData !== null && NETDATA.globalPanAndZoom.isActive() && NETDATA.globalPanAndZoom.isMaster(state) === false) {
2432 + // pan and zoom on snapshots
2433 + state.tmp.dygraph_options.dateWindow = [NETDATA.globalPanAndZoom.force_after_ms, NETDATA.globalPanAndZoom.force_before_ms];
2434 + //state.tmp.dygraph_options.isZoomedIgnoreProgrammaticZoom = true;
2435 + }
2436
2236 - var seconds = Math.floor(milliseconds / 1000);
2237 - milliseconds -= seconds * 1000;
2437 + state.tmp.dygraph_instance = new Dygraph(state.element_chart,
2438 + data.result.data, state.tmp.dygraph_options);
2439
2239 - milliseconds = Math.round(milliseconds / 10);
2440 + state.tmp.dygraph_force_zoom = false;
2441 + state.tmp.dygraph_user_action = false;
2442 + state.tmp.dygraph_last_rendered = Date.now();
2443 + state.tmp.dygraph_highlight_after = null;
2444
2241 - return seconds.toString() + '.'
2242 - + NETDATA.zeropad(milliseconds);
2243 - }
2244 - },
2245 - 'M:SS.ms': {
2246 - check: function (max) { return NETDATA.options.current.seconds_as_time && max >= 60000; },
2247 - convert: function(milliseconds) {
2248 - milliseconds = Math.round(milliseconds);
2445 + if (state.tmp.dygraph_options.valueRange[0] === null && state.tmp.dygraph_options.valueRange[1] === null) {
2446 + if (typeof state.tmp.dygraph_instance.axes_[0].extremeRange !== 'undefined') {
2447 + state.tmp.__commonMin = NETDATA.dataAttribute(state.element, 'common-min', null);
2448 + state.tmp.__commonMax = NETDATA.dataAttribute(state.element, 'common-max', null);
2449 + } else {
2450 + state.log('incompatible version of Dygraph detected');
2451 + state.tmp.__commonMin = null;
2452 + state.tmp.__commonMax = null;
2453 + }
2454 + } else {
2455 + // if the user gave a valueRange, respect it
2456 + state.tmp.__commonMin = null;
2457 + state.tmp.__commonMax = null;
2458 + }
2459
2250 - var minutes = Math.floor(milliseconds / 60000);
2251 - milliseconds -= minutes * 60000;
2460 + return true;
2461 +};
2462 +// ----------------------------------------------------------------------------------------------------------------
2463 +// sparkline
2464
2253 - var seconds = Math.floor(milliseconds / 1000);
2254 - milliseconds -= seconds * 1000;
2465 +NETDATA.sparklineInitialize = function (callback) {
2466 + if (typeof netdataNoSparklines === 'undefined' || !netdataNoSparklines) {
2467 + $.ajax({
2468 + url: NETDATA.sparkline_js,
2469 + cache: true,
2470 + dataType: "script",
2471 + xhrFields: {withCredentials: true} // required for the cookie
2472 + })
2473 + .done(function () {
2474 + NETDATA.registerChartLibrary('sparkline', NETDATA.sparkline_js);
2475 + })
2476 + .fail(function () {
2477 + NETDATA.chartLibraries.sparkline.enabled = false;
2478 + NETDATA.error(100, NETDATA.sparkline_js);
2479 + })
2480 + .always(function () {
2481 + if (typeof callback === "function") {
2482 + return callback();
2483 + }
2484 + });
2485 + } else {
2486 + NETDATA.chartLibraries.sparkline.enabled = false;
2487 + if (typeof callback === "function") {
2488 + return callback();
2489 + }
2490 + }
2491 +};
2492 +
2493 +NETDATA.sparklineChartUpdate = function (state, data) {
2494 + state.sparkline_options.width = state.chartWidth();
2495 + state.sparkline_options.height = state.chartHeight();
2496 +
2497 + $(state.element_chart).sparkline(data.result, state.sparkline_options);
2498 + return true;
2499 +};
2500 +
2501 +NETDATA.sparklineChartCreate = function (state, data) {
2502 + let type = NETDATA.dataAttribute(state.element, 'sparkline-type', 'line');
2503 + let lineColor = NETDATA.dataAttribute(state.element, 'sparkline-linecolor', state.chartCustomColors()[0]);
2504 + let fillColor = NETDATA.dataAttribute(state.element, 'sparkline-fillcolor', ((state.chart.chart_type === 'line') ? NETDATA.themes.current.background : NETDATA.colorLuminance(lineColor, NETDATA.chartDefaults.fill_luminance)));
2505 + let chartRangeMin = NETDATA.dataAttribute(state.element, 'sparkline-chartrangemin', undefined);
2506 + let chartRangeMax = NETDATA.dataAttribute(state.element, 'sparkline-chartrangemax', undefined);
2507 + let composite = NETDATA.dataAttribute(state.element, 'sparkline-composite', undefined);
2508 + let enableTagOptions = NETDATA.dataAttribute(state.element, 'sparkline-enabletagoptions', undefined);
2509 + let tagOptionPrefix = NETDATA.dataAttribute(state.element, 'sparkline-tagoptionprefix', undefined);
2510 + let tagValuesAttribute = NETDATA.dataAttribute(state.element, 'sparkline-tagvaluesattribute', undefined);
2511 + let disableHiddenCheck = NETDATA.dataAttribute(state.element, 'sparkline-disablehiddencheck', undefined);
2512 + let defaultPixelsPerValue = NETDATA.dataAttribute(state.element, 'sparkline-defaultpixelspervalue', undefined);
2513 + let spotColor = NETDATA.dataAttribute(state.element, 'sparkline-spotcolor', undefined);
2514 + let minSpotColor = NETDATA.dataAttribute(state.element, 'sparkline-minspotcolor', undefined);
2515 + let maxSpotColor = NETDATA.dataAttribute(state.element, 'sparkline-maxspotcolor', undefined);
2516 + let spotRadius = NETDATA.dataAttribute(state.element, 'sparkline-spotradius', undefined);
2517 + let valueSpots = NETDATA.dataAttribute(state.element, 'sparkline-valuespots', undefined);
2518 + let highlightSpotColor = NETDATA.dataAttribute(state.element, 'sparkline-highlightspotcolor', undefined);
2519 + let highlightLineColor = NETDATA.dataAttribute(state.element, 'sparkline-highlightlinecolor', undefined);
2520 + let lineWidth = NETDATA.dataAttribute(state.element, 'sparkline-linewidth', undefined);
2521 + let normalRangeMin = NETDATA.dataAttribute(state.element, 'sparkline-normalrangemin', undefined);
2522 + let normalRangeMax = NETDATA.dataAttribute(state.element, 'sparkline-normalrangemax', undefined);
2523 + let drawNormalOnTop = NETDATA.dataAttribute(state.element, 'sparkline-drawnormalontop', undefined);
2524 + let xvalues = NETDATA.dataAttribute(state.element, 'sparkline-xvalues', undefined);
2525 + let chartRangeClip = NETDATA.dataAttribute(state.element, 'sparkline-chartrangeclip', undefined);
2526 + let chartRangeMinX = NETDATA.dataAttribute(state.element, 'sparkline-chartrangeminx', undefined);
2527 + let chartRangeMaxX = NETDATA.dataAttribute(state.element, 'sparkline-chartrangemaxx', undefined);
2528 + let disableInteraction = NETDATA.dataAttributeBoolean(state.element, 'sparkline-disableinteraction', false);
2529 + let disableTooltips = NETDATA.dataAttributeBoolean(state.element, 'sparkline-disabletooltips', false);
2530 + let disableHighlight = NETDATA.dataAttributeBoolean(state.element, 'sparkline-disablehighlight', false);
2531 + let highlightLighten = NETDATA.dataAttribute(state.element, 'sparkline-highlightlighten', 1.4);
2532 + let highlightColor = NETDATA.dataAttribute(state.element, 'sparkline-highlightcolor', undefined);
2533 + let tooltipContainer = NETDATA.dataAttribute(state.element, 'sparkline-tooltipcontainer', undefined);
2534 + let tooltipClassname = NETDATA.dataAttribute(state.element, 'sparkline-tooltipclassname', undefined);
2535 + let tooltipFormat = NETDATA.dataAttribute(state.element, 'sparkline-tooltipformat', undefined);
2536 + let tooltipPrefix = NETDATA.dataAttribute(state.element, 'sparkline-tooltipprefix', undefined);
2537 + let tooltipSuffix = NETDATA.dataAttribute(state.element, 'sparkline-tooltipsuffix', ' ' + state.units_current);
2538 + let tooltipSkipNull = NETDATA.dataAttributeBoolean(state.element, 'sparkline-tooltipskipnull', true);
2539 + let tooltipValueLookups = NETDATA.dataAttribute(state.element, 'sparkline-tooltipvaluelookups', undefined);
2540 + let tooltipFormatFieldlist = NETDATA.dataAttribute(state.element, 'sparkline-tooltipformatfieldlist', undefined);
2541 + let tooltipFormatFieldlistKey = NETDATA.dataAttribute(state.element, 'sparkline-tooltipformatfieldlistkey', undefined);
2542 + let numberFormatter = NETDATA.dataAttribute(state.element, 'sparkline-numberformatter', function (n) {
2543 + return n.toFixed(2);
2544 + });
2545 + let numberDigitGroupSep = NETDATA.dataAttribute(state.element, 'sparkline-numberdigitgroupsep', undefined);
2546 + let numberDecimalMark = NETDATA.dataAttribute(state.element, 'sparkline-numberdecimalmark', undefined);
2547 + let numberDigitGroupCount = NETDATA.dataAttribute(state.element, 'sparkline-numberdigitgroupcount', undefined);
2548 + let animatedZooms = NETDATA.dataAttributeBoolean(state.element, 'sparkline-animatedzooms', false);
2549
2256 - milliseconds = Math.round(milliseconds / 10);
2550 + if (spotColor === 'disable') {
2551 + spotColor = '';
2552 + }
2553 + if (minSpotColor === 'disable') {
2554 + minSpotColor = '';
2555 + }
2556 + if (maxSpotColor === 'disable') {
2557 + maxSpotColor = '';
2558 + }
2559
2258 - return minutes.toString() + ':'
2259 - + NETDATA.zeropad(seconds) + '.'
2260 - + NETDATA.zeropad(milliseconds);
2261 - }
2560 + // state.log('sparkline type ' + type + ', lineColor: ' + lineColor + ', fillColor: ' + fillColor);
2561 +
2562 + state.sparkline_options = {
2563 + type: type,
2564 + lineColor: lineColor,
2565 + fillColor: fillColor,
2566 + chartRangeMin: chartRangeMin,
2567 + chartRangeMax: chartRangeMax,
2568 + composite: composite,
2569 + enableTagOptions: enableTagOptions,
2570 + tagOptionPrefix: tagOptionPrefix,
2571 + tagValuesAttribute: tagValuesAttribute,
2572 + disableHiddenCheck: disableHiddenCheck,
2573 + defaultPixelsPerValue: defaultPixelsPerValue,
2574 + spotColor: spotColor,
2575 + minSpotColor: minSpotColor,
2576 + maxSpotColor: maxSpotColor,
2577 + spotRadius: spotRadius,
2578 + valueSpots: valueSpots,
2579 + highlightSpotColor: highlightSpotColor,
2580 + highlightLineColor: highlightLineColor,
2581 + lineWidth: lineWidth,
2582 + normalRangeMin: normalRangeMin,
2583 + normalRangeMax: normalRangeMax,
2584 + drawNormalOnTop: drawNormalOnTop,
2585 + xvalues: xvalues,
2586 + chartRangeClip: chartRangeClip,
2587 + chartRangeMinX: chartRangeMinX,
2588 + chartRangeMaxX: chartRangeMaxX,
2589 + disableInteraction: disableInteraction,
2590 + disableTooltips: disableTooltips,
2591 + disableHighlight: disableHighlight,
2592 + highlightLighten: highlightLighten,
2593 + highlightColor: highlightColor,
2594 + tooltipContainer: tooltipContainer,
2595 + tooltipClassname: tooltipClassname,
2596 + tooltipChartTitle: state.title,
2597 + tooltipFormat: tooltipFormat,
2598 + tooltipPrefix: tooltipPrefix,
2599 + tooltipSuffix: tooltipSuffix,
2600 + tooltipSkipNull: tooltipSkipNull,
2601 + tooltipValueLookups: tooltipValueLookups,
2602 + tooltipFormatFieldlist: tooltipFormatFieldlist,
2603 + tooltipFormatFieldlistKey: tooltipFormatFieldlistKey,
2604 + numberFormatter: numberFormatter,
2605 + numberDigitGroupSep: numberDigitGroupSep,
2606 + numberDecimalMark: numberDecimalMark,
2607 + numberDigitGroupCount: numberDigitGroupCount,
2608 + animatedZooms: animatedZooms,
2609 + width: state.chartWidth(),
2610 + height: state.chartHeight()
2611 + };
2612 +
2613 + $(state.element_chart).sparkline(data.result, state.sparkline_options);
2614 +
2615 + return true;
2616 +};
2617 +// google charts
2618 +
2619 +NETDATA.googleInitialize = function (callback) {
2620 + if (typeof netdataNoGoogleCharts === 'undefined' || !netdataNoGoogleCharts) {
2621 + $.ajax({
2622 + url: NETDATA.google_js,
2623 + cache: true,
2624 + dataType: "script",
2625 + xhrFields: {withCredentials: true} // required for the cookie
2626 + })
2627 + .done(function () {
2628 + NETDATA.registerChartLibrary('google', NETDATA.google_js);
2629 + google.load('visualization', '1.1', {
2630 + 'packages': ['corechart', 'controls'],
2631 + 'callback': callback
2632 + });
2633 + })
2634 + .fail(function () {
2635 + NETDATA.chartLibraries.google.enabled = false;
2636 + NETDATA.error(100, NETDATA.google_js);
2637 + if (typeof callback === "function") {
2638 + return callback();
2639 }
2640 + });
2641 + } else {
2642 + NETDATA.chartLibraries.google.enabled = false;
2643 + if (typeof callback === "function") {
2644 + return callback();
2645 + }
2646 + }
2647 +};
2648 +
2649 +NETDATA.googleChartUpdate = function (state, data) {
2650 + let datatable = new google.visualization.DataTable(data.result);
2651 + state.google_instance.draw(datatable, state.google_options);
2652 + return true;
2653 +};
2654 +
2655 +NETDATA.googleChartCreate = function (state, data) {
2656 + let datatable = new google.visualization.DataTable(data.result);
2657 +
2658 + state.google_options = {
2659 + colors: state.chartColors(),
2660 +
2661 + // do not set width, height - the chart resizes itself
2662 + //width: state.chartWidth(),
2663 + //height: state.chartHeight(),
2664 + lineWidth: 1,
2665 + title: state.title,
2666 + fontSize: 11,
2667 + hAxis: {
2668 + // title: "Time of Day",
2669 + // format:'HH:mm:ss',
2670 + viewWindowMode: 'maximized',
2671 + slantedText: false,
2672 + format: 'HH:mm:ss',
2673 + textStyle: {
2674 + fontSize: 9
2675 + },
2676 + gridlines: {
2677 + color: '#EEE'
2678 }
2679 },
2265 -
2266 - seconds2time: function(seconds) {
2267 - seconds = Math.abs(seconds);
2268 -
2269 - var days = Math.floor(seconds / 86400);
2270 - seconds -= days * 86400;
2271 -
2272 - var hours = Math.floor(seconds / 3600);
2273 - seconds -= hours * 3600;
2274 -
2275 - var minutes = Math.floor(seconds / 60);
2276 - seconds -= minutes * 60;
2277 -
2278 - seconds = Math.round(seconds);
2279 -
2280 - var ms_txt = '';
2281 - /*
2282 - var ms = seconds - Math.floor(seconds);
2283 - seconds -= ms;
2284 - ms = Math.round(ms * 1000);
2285 -
2286 - if(ms > 1) {
2287 - if(ms < 10)
2288 - ms_txt = '.00' + ms.toString();
2289 - else if(ms < 100)
2290 - ms_txt = '.0' + ms.toString();
2291 - else
2292 - ms_txt = '.' + ms.toString();
2680 + vAxis: {
2681 + title: state.units_current,
2682 + viewWindowMode: 'pretty',
2683 + minValue: -0.1,
2684 + maxValue: 0.1,
2685 + direction: 1,
2686 + textStyle: {
2687 + fontSize: 9
2688 + },
2689 + gridlines: {
2690 + color: '#EEE'
2691 }
2294 - */
2295 -
2296 - return ((days > 0)?days.toString() + 'd:':'').toString()
2297 - + NETDATA.zeropad(hours) + ':'
2298 - + NETDATA.zeropad(minutes) + ':'
2299 - + NETDATA.zeropad(seconds)
2300 - + ms_txt;
2692 },
2302 -
2303 - // get a function that converts the units
2304 - // + every time units are switched call the callback
2305 - get: function(uuid, min, max, units, desired_units, common_units_name, switch_units_callback) {
2306 - // validate the parameters
2307 - if(typeof units === 'undefined')
2308 - units = 'undefined';
2309 -
2310 - // check if we support units conversion
2311 - if(typeof this.scalableUnits[units] === 'undefined' && typeof this.convertibleUnits[units] === 'undefined') {
2312 - // we can't convert these units
2313 - //console.log('DEBUG: ' + uuid.toString() + ' can\'t convert units: ' + units.toString());
2314 - return function(value) { return value; };
2693 + chartArea: {
2694 + width: '65%',
2695 + height: '80%'
2696 + },
2697 + focusTarget: 'category',
2698 + annotation: {
2699 + '1': {
2700 + style: 'line'
2701 }
2316 -
2317 - // check if the caller wants the original units
2318 - if(typeof desired_units === 'undefined' || desired_units === null || desired_units === 'original' || desired_units === units) {
2319 - //console.log('DEBUG: ' + uuid.toString() + ' original units wanted');
2320 - switch_units_callback(units);
2321 - return function(value) { return value; };
2702 + },
2703 + pointsVisible: 0,
2704 + titlePosition: 'out',
2705 + titleTextStyle: {
2706 + fontSize: 11
2707 + },
2708 + tooltip: {
2709 + isHtml: false,
2710 + ignoreBounds: true,
2711 + textStyle: {
2712 + fontSize: 9
2713 }
2714 + },
2715 + curveType: 'function',
2716 + areaOpacity: 0.3,
2717 + isStacked: false
2718 + };
2719 +
2720 + switch (state.chart.chart_type) {
2721 + case "area":
2722 + state.google_options.vAxis.viewWindowMode = 'maximized';
2723 + state.google_options.areaOpacity = NETDATA.options.current.color_fill_opacity_area;
2724 + state.google_instance = new google.visualization.AreaChart(state.element_chart);
2725 + break;
2726 +
2727 + case "stacked":
2728 + state.google_options.isStacked = true;
2729 + state.google_options.areaOpacity = NETDATA.options.current.color_fill_opacity_stacked;
2730 + state.google_options.vAxis.viewWindowMode = 'maximized';
2731 + state.google_options.vAxis.minValue = null;
2732 + state.google_options.vAxis.maxValue = null;
2733 + state.google_instance = new google.visualization.AreaChart(state.element_chart);
2734 + break;
2735 +
2736 + default:
2737 + case "line":
2738 + state.google_options.lineWidth = 2;
2739 + state.google_instance = new google.visualization.LineChart(state.element_chart);
2740 + break;
2741 + }
2742
2324 - // now we know we can convert the units
2325 - // and the caller wants some kind of conversion
2326 -
2327 - var tunits = null;
2328 - var tdivider = 0;
2329 - var x;
2330 -
2331 - if(typeof this.scalableUnits[units] !== 'undefined') {
2332 - // units that can be scaled
2333 - // we decide a divider
2334 -
2335 - // console.log('NETDATA.unitsConversion.get(' + units.toString() + ', ' + desired_units.toString() + ', function()) decide divider with min = ' + min.toString() + ', max = ' + max.toString());
2336 -
2337 - if (desired_units === 'auto') {
2338 - // the caller wants to auto-scale the units
2339 -
2340 - // find the absolute maximum value that is rendered on the chart
2341 - // based on this we decide the scale
2342 - min = Math.abs(min);
2343 - max = Math.abs(max);
2344 - if (min > max) max = min;
2345 -
2346 - // find the smallest scale that provides integers
2347 - for (x in this.scalableUnits[units]) {
2348 - if (this.scalableUnits[units].hasOwnProperty(x)) {
2349 - var m = this.scalableUnits[units][x];
2350 - if (m <= max && m > tdivider) {
2351 - tunits = x;
2352 - tdivider = m;
2353 - }
2354 - }
2355 - }
2356 -
2357 - if(tunits === null || tdivider <= 0) {
2358 - // we couldn't find one
2359 - //console.log('DEBUG: ' + uuid.toString() + ' cannot find an auto-scaling candidate for units: ' + units.toString() + ' (max: ' + max.toString() + ')');
2360 - switch_units_callback(units);
2361 - return function(value) { return value; };
2362 - }
2363 -
2364 - if(typeof common_units_name === 'string' && typeof uuid === 'string') {
2365 - // the caller wants several charts to have the same units
2366 - // data-common-units
2367 -
2368 - var common_units_key = common_units_name + '-' + units;
2369 -
2370 - // add our divider into the list of keys
2371 - var t = this.keys[common_units_key];
2372 - if(typeof t === 'undefined') {
2373 - this.keys[common_units_key] = {};
2374 - t = this.keys[common_units_key];
2375 - }
2376 - t[uuid] = {
2377 - units: tunits,
2378 - divider: tdivider
2379 - };
2380 -
2381 - // find the max divider of all charts
2382 - var common_units = t[uuid];
2383 - for(x in t) {
2384 - if (t.hasOwnProperty(x) && t[x].divider > common_units.divider)
2385 - common_units = t[x];
2386 - }
2387 -
2388 - // save our common_max to the latest keys
2389 - var latest = this.latest[common_units_key];
2390 - if(typeof latest === 'undefined') {
2391 - this.latest[common_units_key] = {};
2392 - latest = this.latest[common_units_key];
2393 - }
2394 - latest.units = common_units.units;
2395 - latest.divider = common_units.divider;
2396 -
2397 - tunits = latest.units;
2398 - tdivider = latest.divider;
2399 -
2400 - //console.log('DEBUG: ' + uuid.toString() + ' converted units: ' + units.toString() + ' to units: ' + tunits.toString() + ' with divider ' + tdivider.toString() + ', common-units=' + common_units_name.toString() + ((t[uuid].divider !== tdivider)?' USED COMMON, mine was ' + t[uuid].units:' set common').toString());
2401 -
2402 - // apply it to this chart
2403 - switch_units_callback(tunits);
2404 - return function(value) {
2405 - if(tdivider !== latest.divider) {
2406 - // another chart switched our common units
2407 - // we should switch them too
2408 - //console.log('DEBUG: ' + uuid + ' switching units due to a common-units change, from ' + tunits.toString() + ' to ' + latest.units.toString());
2409 - tunits = latest.units;
2410 - tdivider = latest.divider;
2411 - switch_units_callback(tunits);
2412 - }
2413 -
2414 - return value / tdivider;
2415 - };
2416 - }
2417 - else {
2418 - // the caller did not give data-common-units
2419 - // this chart auto-scales independently of all others
2420 - //console.log('DEBUG: ' + uuid.toString() + ' converted units: ' + units.toString() + ' to units: ' + tunits.toString() + ' with divider ' + tdivider.toString() + ', autonomously');
2743 + state.google_instance.draw(datatable, state.google_options);
2744 + return true;
2745 +};
2746 +// gauge.js
2747
2422 - switch_units_callback(tunits);
2423 - return function (value) { return value / tdivider; };
2424 - }
2425 - }
2426 - else {
2427 - // the caller wants specific units
2428 -
2429 - if(typeof this.scalableUnits[units][desired_units] !== 'undefined') {
2430 - // all good, set the new units
2431 - tdivider = this.scalableUnits[units][desired_units];
2432 - // console.log('DEBUG: ' + uuid.toString() + ' converted units: ' + units.toString() + ' to units: ' + desired_units.toString() + ' with divider ' + tdivider.toString() + ', by reference');
2433 - switch_units_callback(desired_units);
2434 - return function (value) { return value / tdivider; };
2435 - }
2436 - else {
2437 - // oops! switch back to original units
2438 - console.log('Units conversion from ' + units.toString() + ' to ' + desired_units.toString() + ' is not supported.');
2439 - switch_units_callback(units);
2440 - return function (value) { return value; };
2441 - }
2442 - }
2443 - }
2444 - else if(typeof this.convertibleUnits[units] !== 'undefined') {
2445 - // units that can be converted
2446 - if(desired_units === 'auto') {
2447 - for(x in this.convertibleUnits[units]) {
2448 - if (this.convertibleUnits[units].hasOwnProperty(x)) {
2449 - if (this.convertibleUnits[units][x].check(max)) {
2450 - //console.log('DEBUG: ' + uuid.toString() + ' converting ' + units.toString() + ' to: ' + x.toString());
2451 - switch_units_callback(x);
2452 - return this.convertibleUnits[units][x].convert;
2453 - }
2454 - }
2455 - }
2456 -
2457 - // none checked ok
2458 - //console.log('DEBUG: ' + uuid.toString() + ' no conversion available for ' + units.toString() + ' to: ' + desired_units.toString());
2459 - switch_units_callback(units);
2460 - return function (value) { return value; };
2461 - }
2462 - else if(typeof this.convertibleUnits[units][desired_units] !== 'undefined') {
2463 - switch_units_callback(desired_units);
2464 - return this.convertibleUnits[units][desired_units].convert;
2465 - }
2466 - else {
2467 - console.log('Units conversion from ' + units.toString() + ' to ' + desired_units.toString() + ' is not supported.');
2468 - switch_units_callback(units);
2469 - return function (value) { return value; };
2748 +NETDATA.gaugeInitialize = function (callback) {
2749 + if (typeof netdataNoGauge === 'undefined' || !netdataNoGauge) {
2750 + $.ajax({
2751 + url: NETDATA.gauge_js,
2752 + cache: true,
2753 + dataType: "script",
2754 + xhrFields: {withCredentials: true} // required for the cookie
2755 + })
2756 + .done(function () {
2757 + NETDATA.registerChartLibrary('gauge', NETDATA.gauge_js);
2758 + })
2759 + .fail(function () {
2760 + NETDATA.chartLibraries.gauge.enabled = false;
2761 + NETDATA.error(100, NETDATA.gauge_js);
2762 + })
2763 + .always(function () {
2764 + if (typeof callback === "function") {
2765 + return callback();
2766 }
2471 - }
2472 - else {
2473 - // hm... did we forget to implement the new type?
2474 - console.log('Unmatched unit conversion method for units ' + units.toString());
2475 - switch_units_callback(units);
2476 - return function (value) { return value; };
2477 - }
2767 + })
2768 + }
2769 + else {
2770 + NETDATA.chartLibraries.gauge.enabled = false;
2771 + if (typeof callback === "function") {
2772 + return callback();
2773 }
2479 - };
2480 -
2481 - // ----------------------------------------------------------------------------------------------------------------
2482 - // global selection sync
2483 -
2484 - NETDATA.globalSelectionSync = {
2485 - state: null,
2486 - dont_sync_before: 0,
2487 - last_t: 0,
2488 - slaves: [],
2489 - timeout_id: undefined,
2490 -
2491 - globalReset: function() {
2492 - this.stop();
2493 - this.state = null;
2494 - this.dont_sync_before = 0;
2495 - this.last_t = 0;
2496 - this.slaves = [];
2497 - this.timeout_id = undefined;
2498 - },
2774 + }
2775 +};
2776
2500 - active: function() {
2501 - return (this.state !== null);
2502 - },
2777 +NETDATA.gaugeAnimation = function (state, status) {
2778 + let speed = 32;
2779
2504 - // return true if global selection sync can be enabled now
2505 - enabled: function() {
2506 - // console.log('enabled()');
2507 - // can we globally apply selection sync?
2508 - if(NETDATA.options.current.sync_selection === false)
2509 - return false;
2780 + if (typeof status === 'boolean' && status === false) {
2781 + speed = 1000000000;
2782 + } else if (typeof status === 'number') {
2783 + speed = status;
2784 + }
2785
2511 - return (this.dont_sync_before <= Date.now());
2512 - },
2786 + // console.log('gauge speed ' + speed);
2787 + state.tmp.gauge_instance.animationSpeed = speed;
2788 + state.tmp.___gaugeOld__.speed = speed;
2789 +};
2790
2514 - // set the global selection sync master
2515 - setMaster: function(state) {
2516 - if(this.enabled() === false) {
2517 - this.stop();
2518 - return;
2519 - }
2520 -
2521 - if(this.state === state)
2522 - return;
2791 +NETDATA.gaugeSet = function (state, value, min, max) {
2792 + if (typeof value !== 'number') {
2793 + value = 0;
2794 + }
2795 + if (typeof min !== 'number') {
2796 + min = 0;
2797 + }
2798 + if (typeof max !== 'number') {
2799 + max = 0;
2800 + }
2801 + if (value > max) {
2802 + max = value;
2803 + }
2804 + if (value < min) {
2805 + min = value;
2806 + }
2807 + if (min > max) {
2808 + let t = min;
2809 + min = max;
2810 + max = t;
2811 + }
2812 + else if (min === max) {
2813 + max = min + 1;
2814 + }
2815
2524 - if(this.state !== null)
2525 - this.stop();
2816 + state.legendFormatValueDecimalsFromMinMax(min, max);
2817
2527 - if(NETDATA.options.debug.globalSelectionSync === true)
2528 - console.log('globalSelectionSync.setMaster(' + state.id + ')');
2818 + // gauge.js has an issue if the needle
2819 + // is smaller than min or larger than max
2820 + // when we set the new values
2821 + // the needle will go crazy
2822
2530 - state.selected = true;
2531 - this.state = state;
2532 - this.last_t = 0;
2823 + // to prevent it, we always feed it
2824 + // with a percentage, so that the needle
2825 + // is always between min and max
2826 + let pcent = (value - min) * 100 / (max - min);
2827
2534 - // find all slaves
2535 - var targets = NETDATA.intersectionObserver.targets();
2536 - this.slaves = [];
2537 - var len = targets.length;
2538 - while(len--) {
2539 - var st = targets[len];
2540 - if (this.state !== st && st.globalSelectionSyncIsEligible() === true)
2541 - this.slaves.push(st);
2542 - }
2828 + // bug fix for gauge.js 1.3.1
2829 + // if the value is the absolute min or max, the chart is broken
2830 + if (pcent < 0.001) {
2831 + pcent = 0.001;
2832 + }
2833 + if (pcent > 99.999) {
2834 + pcent = 99.999;
2835 + }
2836
2544 - // this.delay(100);
2545 - },
2837 + state.tmp.gauge_instance.set(pcent);
2838 + // console.log('gauge set ' + pcent + ', value ' + value + ', min ' + min + ', max ' + max);
2839
2547 - // stop global selection sync
2548 - stop: function() {
2549 - if(this.state !== null) {
2550 - if(NETDATA.options.debug.globalSelectionSync === true)
2551 - console.log('globalSelectionSync.stop()');
2840 + state.tmp.___gaugeOld__.value = value;
2841 + state.tmp.___gaugeOld__.min = min;
2842 + state.tmp.___gaugeOld__.max = max;
2843 +};
2844
2553 - var len = this.slaves.length;
2554 - while (len--)
2555 - this.slaves[len].clearSelection();
2845 +NETDATA.gaugeSetLabels = function (state, value, min, max) {
2846 + if (state.tmp.___gaugeOld__.valueLabel !== value) {
2847 + state.tmp.___gaugeOld__.valueLabel = value;
2848 + state.tmp.gaugeChartLabel.innerText = state.legendFormatValue(value);
2849 + }
2850 + if (state.tmp.___gaugeOld__.minLabel !== min) {
2851 + state.tmp.___gaugeOld__.minLabel = min;
2852 + state.tmp.gaugeChartMin.innerText = state.legendFormatValue(min);
2853 + }
2854 + if (state.tmp.___gaugeOld__.maxLabel !== max) {
2855 + state.tmp.___gaugeOld__.maxLabel = max;
2856 + state.tmp.gaugeChartMax.innerText = state.legendFormatValue(max);
2857 + }
2858 +};
2859
2557 - this.state.clearSelection();
2860 +NETDATA.gaugeClearSelection = function (state, force) {
2861 + if (typeof state.tmp.gaugeEvent !== 'undefined' && typeof state.tmp.gaugeEvent.timer !== 'undefined') {
2862 + NETDATA.timeout.clear(state.tmp.gaugeEvent.timer);
2863 + state.tmp.gaugeEvent.timer = undefined;
2864 + }
2865
2559 - this.last_t = 0;
2560 - this.slaves = [];
2561 - this.state = null;
2562 - }
2563 - },
2866 + if (state.isAutoRefreshable() && state.data !== null && force !== true) {
2867 + NETDATA.gaugeChartUpdate(state, state.data);
2868 + } else {
2869 + NETDATA.gaugeAnimation(state, false);
2870 + NETDATA.gaugeSetLabels(state, null, null, null);
2871 + NETDATA.gaugeSet(state, null, null, null);
2872 + }
2873
2565 - // delay global selection sync for some time
2566 - delay: function(ms) {
2567 - if(NETDATA.options.current.sync_selection === true) {
2568 - if(NETDATA.options.debug.globalSelectionSync === true)
2569 - console.log('globalSelectionSync.delay()');
2874 + NETDATA.gaugeAnimation(state, true);
2875 + return true;
2876 +};
2877
2571 - if(typeof ms === 'number')
2572 - this.dont_sync_before = Date.now() + ms;
2573 - else
2574 - this.dont_sync_before = Date.now() + NETDATA.options.current.sync_selection_delay;
2575 - }
2576 - },
2878 +NETDATA.gaugeSetSelection = function (state, t) {
2879 + if (state.timeIsVisible(t) !== true) {
2880 + return NETDATA.gaugeClearSelection(state, true);
2881 + }
2882
2578 - __syncSlaves: function() {
2579 - if(NETDATA.globalSelectionSync.enabled() === true) {
2580 - if(NETDATA.options.debug.globalSelectionSync === true)
2581 - console.log('globalSelectionSync.__syncSlaves()');
2883 + let slot = state.calculateRowForTime(t);
2884 + if (slot < 0 || slot >= state.data.result.length) {
2885 + return NETDATA.gaugeClearSelection(state, true);
2886 + }
2887
2583 - var t = NETDATA.globalSelectionSync.last_t;
2584 - var len = NETDATA.globalSelectionSync.slaves.length;
2585 - while (len--)
2586 - NETDATA.globalSelectionSync.slaves[len].setSelection(t);
2888 + if (typeof state.tmp.gaugeEvent === 'undefined') {
2889 + state.tmp.gaugeEvent = {
2890 + timer: undefined,
2891 + value: 0,
2892 + min: 0,
2893 + max: 0
2894 + };
2895 + }
2896
2588 - this.timeout_id = undefined;
2589 - }
2590 - },
2897 + let value = state.data.result[state.data.result.length - 1 - slot];
2898 + let min = (state.tmp.gaugeMin === null) ? NETDATA.commonMin.get(state) : state.tmp.gaugeMin;
2899 + let max = (state.tmp.gaugeMax === null) ? NETDATA.commonMax.get(state) : state.tmp.gaugeMax;
2900
2592 - // sync all the visible charts to the given time
2593 - // this is to be called from the chart libraries
2594 - sync: function(state, t) {
2595 - if(NETDATA.options.current.sync_selection === true) {
2596 - if(NETDATA.options.debug.globalSelectionSync === true)
2597 - console.log('globalSelectionSync.sync(' + state.id + ', ' + t.toString() + ')');
2901 + // make sure it is zero based
2902 + // but only if it has not been set by the user
2903 + if (state.tmp.gaugeMin === null && min > 0) {
2904 + min = 0;
2905 + }
2906 + if (state.tmp.gaugeMax === null && max < 0) {
2907 + max = 0;
2908 + }
2909
2599 - this.setMaster(state);
2910 + state.tmp.gaugeEvent.value = value;
2911 + state.tmp.gaugeEvent.min = min;
2912 + state.tmp.gaugeEvent.max = max;
2913 + NETDATA.gaugeSetLabels(state, value, min, max);
2914
2601 - if(t === this.last_t)
2602 - return;
2915 + if (state.tmp.gaugeEvent.timer === undefined) {
2916 + NETDATA.gaugeAnimation(state, false);
2917
2604 - this.last_t = t;
2918 + state.tmp.gaugeEvent.timer = NETDATA.timeout.set(function () {
2919 + state.tmp.gaugeEvent.timer = undefined;
2920 + NETDATA.gaugeSet(state, state.tmp.gaugeEvent.value, state.tmp.gaugeEvent.min, state.tmp.gaugeEvent.max);
2921 + }, 0);
2922 + }
2923
2606 - if (state.foreign_element_selection !== null)
2607 - state.foreign_element_selection.innerText = NETDATA.dateTime.localeDateString(t) + ' ' + NETDATA.dateTime.localeTimeString(t);
2924 + return true;
2925 +};
2926
2609 - if (this.timeout_id)
2610 - NETDATA.timeout.clear(this.timeout_id);
2927 +NETDATA.gaugeChartUpdate = function (state, data) {

This file is too large to show in full.

web/gui/index.html
+4 -4
@@ -1072,9 +1072,9 @@
1072
1073 if(!found) {
1074 if(machines)
1075 - el += '<li><a href="https://github.com/netdata/netdata/wiki/mynetdata-menu-item" style="color: #666;" target="_blank">your netdata server list is empty...</a></li>';
1075 + el += '<li><a href="https://github.com/netdata/netdata/tree/master/registry#netdata-registry" style="color: #666;" target="_blank">your netdata server list is empty...</a></li>';
1076 else
1077 - el += '<li><a href="https://github.com/netdata/netdata/wiki/mynetdata-menu-item" style="color: #666;" target="_blank">failed to contact the registry...</a></li>';
1077 + el += '<li><a href="https://github.com/netdata/netdata/tree/master/registry#netdata-registry" style="color: #666;" target="_blank">failed to contact the registry...</a></li>';
1078
1079 a1 += '<li><a href="#" onClick="return false;">&nbsp;</a></li>';
1080
@@ -1101,7 +1101,7 @@
1101 el += '<li role="separator" class="divider"></li>';
1102 a1 += '<li role="separator" class="divider"></li>';
1103
1104 - el += '<li><a href="https://github.com/netdata/netdata/wiki/mynetdata-menu-item" style="color: #999;" target="_blank">What is this?</a></li>';
1104 + el += '<li><a href="https://github.com/netdata/netdata/tree/master/registry#netdata-registry" style="color: #999;" target="_blank">What is this?</a></li>';
1105 a1 += '<li><a href="#" style="color: #999;" onclick="switchRegistryModalHandler(); return false;"><i class="fas fa-cog" style="color: #999;"></i></a></li>';
1106
1107 document.getElementById('mynetdata_servers').innerHTML = el;
@@ -2184,7 +2184,7 @@
2184 function alarmsUpdateModal() {
2185 var active = '<h3>Raised Alarms</h3><table class="table">';
2186 var all = '<h3>All Running Alarms</h3><div class="panel-group" id="alarms_all_accordion" role="tablist" aria-multiselectable="true">';
2187 - var footer = '<hr/><a href="https://github.com/netdata/netdata/wiki/Generating-Badges" target="_blank">netdata badges</a> refresh automatically. Their color indicates the state of the alarm: <span style="color: #e05d44"><b>&nbsp;red&nbsp;</b></span> is critical, <span style="color:#fe7d37"><b>&nbsp;orange&nbsp;</b></span> is warning, <span style="color: #4c1"><b>&nbsp;bright green&nbsp;</b></span> is ok, <span style="color: #9f9f9f"><b>&nbsp;light grey&nbsp;</b></span> is undefined (i.e. no data or no status), <span style="color: #000"><b>&nbsp;black&nbsp;</b></span> is not initialized. You can copy and paste their URLs to embed them in any web page.<br/>netdata can send notifications for these alarms. Check <a href="https://github.com/netdata/netdata/blob/master/health/notifications/health_alarm_notify.conf">this configuration file</a> for more information.';
2187 + var footer = '<hr/><a href="https://github.com/netdata/netdata/tree/master/web/api/badges#netdata-badges" target="_blank">netdata badges</a> refresh automatically. Their color indicates the state of the alarm: <span style="color: #e05d44"><b>&nbsp;red&nbsp;</b></span> is critical, <span style="color:#fe7d37"><b>&nbsp;orange&nbsp;</b></span> is warning, <span style="color: #4c1"><b>&nbsp;bright green&nbsp;</b></span> is ok, <span style="color: #9f9f9f"><b>&nbsp;light grey&nbsp;</b></span> is undefined (i.e. no data or no status), <span style="color: #000"><b>&nbsp;black&nbsp;</b></span> is not initialized. You can copy and paste their URLs to embed them in any web page.<br/>netdata can send notifications for these alarms. Check <a href="https://github.com/netdata/netdata/blob/master/health/notifications/health_alarm_notify.conf">this configuration file</a> for more information.';
2188
2189 loadClipboard(function() {});
2190
web/gui/src/dashboard.js/alarms.js new
+422
@@ -0,0 +1,422 @@
1 +
2 +// Registry of netdata hosts
3 +
4 +NETDATA.alarms = {
5 + onclick: null, // the callback to handle the click - it will be called with the alarm log entry
6 + chart_div_offset: -50, // give that space above the chart when scrolling to it
7 + chart_div_id_prefix: 'chart_', // the chart DIV IDs have this prefix (they should be NETDATA.name2id(chart.id))
8 + chart_div_animation_duration: 0,// the duration of the animation while scrolling to a chart
9 +
10 + ms_penalty: 0, // the time penalty of the next alarm
11 + ms_between_notifications: 500, // firefox moves the alarms off-screen (above, outside the top of the screen)
12 + // if alarms are shown faster than: one per 500ms
13 +
14 + update_every: 10000, // the time in ms between alarm checks
15 +
16 + notifications: false, // when true, the browser supports notifications (may not be granted though)
17 + last_notification_id: 0, // the id of the last alarm_log we have raised an alarm for
18 + first_notification_id: 0, // the id of the first alarm_log entry for this session
19 + // this is used to prevent CLEAR notifications for past events
20 + // notifications_shown: [],
21 +
22 + server: null, // the server to connect to for fetching alarms
23 + current: null, // the list of raised alarms - updated in the background
24 +
25 + // a callback function to call every time the list of raised alarms is refreshed
26 + callback: (typeof netdataAlarmsActiveCallback === 'function') ? netdataAlarmsActiveCallback : null,
27 +
28 + // a callback function to call every time a notification is shown
29 + // the return value is used to decide if the notification will be shown
30 + notificationCallback: (typeof netdataAlarmsNotifCallback === 'function') ? netdataAlarmsNotifCallback : null,
31 +
32 + recipients: null, // the list (array) of recipients to show alarms for, or null
33 +
34 + recipientMatches: function (to_string, wanted_array) {
35 + if (typeof wanted_array === 'undefined' || wanted_array === null || Array.isArray(wanted_array) === false) {
36 + return true;
37 + }
38 +
39 + let r = ' ' + to_string.toString() + ' ';
40 + let len = wanted_array.length;
41 + while (len--) {
42 + if (r.indexOf(' ' + wanted_array[len] + ' ') >= 0) {
43 + return true;
44 + }
45 + }
46 +
47 + return false;
48 + },
49 +
50 + activeForRecipients: function () {
51 + let active = {};
52 + let data = NETDATA.alarms.current;
53 +
54 + if (typeof data === 'undefined' || data === null) {
55 + return active;
56 + }
57 +
58 + for (let x in data.alarms) {
59 + if (!data.alarms.hasOwnProperty(x)) {
60 + continue;
61 + }
62 +
63 + let alarm = data.alarms[x];
64 + if ((alarm.status === 'WARNING' || alarm.status === 'CRITICAL') && NETDATA.alarms.recipientMatches(alarm.recipient, NETDATA.alarms.recipients)) {
65 + active[x] = alarm;
66 + }
67 + }
68 +
69 + return active;
70 + },
71 +
72 + notify: function (entry) {
73 + // console.log('alarm ' + entry.unique_id);
74 +
75 + if (entry.updated) {
76 + // console.log('alarm ' + entry.unique_id + ' has been updated by another alarm');
77 + return;
78 + }
79 +
80 + let value_string = entry.value_string;
81 +
82 + if (NETDATA.alarms.current !== null) {
83 + // get the current value_string
84 + let t = NETDATA.alarms.current.alarms[entry.chart + '.' + entry.name];
85 + if (typeof t !== 'undefined' && entry.status === t.status && typeof t.value_string !== 'undefined') {
86 + value_string = t.value_string;
87 + }
88 + }
89 +
90 + let name = entry.name.replace(/_/g, ' ');
91 + let status = entry.status.toLowerCase();
92 + let title = name + ' = ' + value_string.toString();
93 + let tag = entry.alarm_id;
94 + let icon = 'images/seo-performance-128.png';
95 + let interaction = false;
96 + let data = entry;
97 + let show = true;
98 +
99 + // console.log('alarm ' + entry.unique_id + ' ' + entry.chart + '.' + entry.name + ' is ' + entry.status);
100 +
101 + switch (entry.status) {
102 + case 'REMOVED':
103 + show = false;
104 + break;
105 +
106 + case 'UNDEFINED':
107 + return;
108 +
109 + case 'UNINITIALIZED':
110 + return;
111 +
112 + case 'CLEAR':
113 + if (entry.unique_id < NETDATA.alarms.first_notification_id) {
114 + // console.log('alarm ' + entry.unique_id + ' is not current');
115 + return;
116 + }
117 + if (entry.old_status === 'UNINITIALIZED' || entry.old_status === 'UNDEFINED') {
118 + // console.log('alarm' + entry.unique_id + ' switch to CLEAR from ' + entry.old_status);
119 + return;
120 + }
121 + if (entry.no_clear_notification) {
122 + // console.log('alarm' + entry.unique_id + ' is CLEAR but has no_clear_notification flag');
123 + return;
124 + }
125 + title = name + ' back to normal (' + value_string.toString() + ')';
126 + icon = 'images/check-mark-2-128-green.png';
127 + interaction = false;
128 + break;
129 +
130 + case 'WARNING':
131 + if (entry.old_status === 'CRITICAL') {
132 + status = 'demoted to ' + entry.status.toLowerCase();
133 + }
134 +
135 + icon = 'images/alert-128-orange.png';
136 + interaction = false;
137 + break;
138 +
139 + case 'CRITICAL':
140 + if (entry.old_status === 'WARNING') {
141 + status = 'escalated to ' + entry.status.toLowerCase();
142 + }
143 +
144 + icon = 'images/alert-128-red.png';
145 + interaction = true;
146 + break;
147 +
148 + default:
149 + console.log('invalid alarm status ' + entry.status);
150 + return;
151 + }
152 +
153 + // filter recipients
154 + if (show) {
155 + show = NETDATA.alarms.recipientMatches(entry.recipient, NETDATA.alarms.recipients);
156 + }
157 +
158 + /*
159 + // cleanup old notifications with the same alarm_id as this one
160 + // it does not seem to work on any web browser - so notifications cannot be removed
161 +
162 + let len = NETDATA.alarms.notifications_shown.length;
163 + while (len--) {
164 + let n = NETDATA.alarms.notifications_shown[len];
165 + if (n.data.alarm_id === entry.alarm_id) {
166 + console.log('removing old alarm ' + n.data.unique_id);
167 +
168 + // close the notification
169 + n.close.bind(n);
170 +
171 + // remove it from the array
172 + NETDATA.alarms.notifications_shown.splice(len, 1);
173 + len = NETDATA.alarms.notifications_shown.length;
174 + }
175 + }
176 + */
177 +
178 + if (show) {
179 + if (typeof NETDATA.alarms.notificationCallback === 'function') {
180 + show = NETDATA.alarms.notificationCallback(entry);
181 + }
182 +
183 + if (show) {
184 + setTimeout(function () {
185 + // show this notification
186 + // console.log('new notification: ' + title);
187 + let n = new Notification(title, {
188 + body: entry.hostname + ' - ' + entry.chart + ' (' + entry.family + ') - ' + status + ': ' + entry.info,
189 + tag: tag,
190 + requireInteraction: interaction,
191 + icon: NETDATA.serverStatic + icon,
192 + data: data
193 + });
194 +
195 + n.onclick = function (event) {
196 + event.preventDefault();
197 + NETDATA.alarms.onclick(event.target.data);
198 + };
199 +
200 + // console.log(n);
201 + // NETDATA.alarms.notifications_shown.push(n);
202 + // console.log(entry);
203 + }, NETDATA.alarms.ms_penalty);
204 +
205 + NETDATA.alarms.ms_penalty += NETDATA.alarms.ms_between_notifications;
206 + }
207 + }
208 + },
209 +
210 + scrollToChart: function (chart_id) {
211 + if (typeof chart_id === 'string') {
212 + let offset = $('#' + NETDATA.alarms.chart_div_id_prefix + NETDATA.name2id(chart_id)).offset();
213 + if (typeof offset !== 'undefined') {
214 + $('html, body').animate({scrollTop: offset.top + NETDATA.alarms.chart_div_offset}, NETDATA.alarms.chart_div_animation_duration);
215 + return true;
216 + }
217 + }
218 + return false;
219 + },
220 +
221 + scrollToAlarm: function (alarm) {
222 + if (typeof alarm === 'object') {
223 + let ret = NETDATA.alarms.scrollToChart(alarm.chart);
224 +
225 + if (ret && NETDATA.options.page_is_visible === false) {
226 + window.focus();
227 + }
228 + // alert('netdata dashboard will now scroll to chart: ' + alarm.chart + '\n\nThis alarm opened to bring the browser window in front of the screen. Click on the dashboard to prevent it from appearing again.');
229 + }
230 +
231 + },
232 +
233 + notifyAll: function () {
234 + // console.log('FETCHING ALARM LOG');
235 + NETDATA.alarms.get_log(NETDATA.alarms.last_notification_id, function (data) {
236 + // console.log('ALARM LOG FETCHED');
237 +
238 + if (data === null || typeof data !== 'object') {
239 + console.log('invalid alarms log response');
240 + return;
241 + }
242 +
243 + if (data.length === 0) {
244 + console.log('received empty alarm log');
245 + return;
246 + }
247 +
248 + // console.log('received alarm log of ' + data.length + ' entries, from ' + data[data.length - 1].unique_id.toString() + ' to ' + data[0].unique_id.toString());
249 +
250 + data.sort(function (a, b) {
251 + if (a.unique_id > b.unique_id) {
252 + return -1;
253 + }
254 + if (a.unique_id < b.unique_id) {
255 + return 1;
256 + }
257 + return 0;
258 + });
259 +
260 + NETDATA.alarms.ms_penalty = 0;
261 +
262 + let len = data.length;
263 + while (len--) {
264 + if (data[len].unique_id > NETDATA.alarms.last_notification_id) {
265 + NETDATA.alarms.notify(data[len]);
266 + }
267 + //else
268 + // console.log('ignoring alarm (older) with id ' + data[len].unique_id.toString());
269 + }
270 +
271 + NETDATA.alarms.last_notification_id = data[0].unique_id;
272 +
273 + if (typeof netdataAlarmsRemember === 'undefined' || netdataAlarmsRemember) {
274 + NETDATA.localStorageSet('last_notification_id', NETDATA.alarms.last_notification_id, null);
275 + }
276 + // console.log('last notification id = ' + NETDATA.alarms.last_notification_id);
277 + })
278 + },
279 +
280 + check_notifications: function () {
281 + // returns true if we should fire 1+ notifications
282 +
283 + if (NETDATA.alarms.notifications !== true) {
284 + // console.log('web notifications are not available');
285 + return false;
286 + }
287 +
288 + if (Notification.permission !== 'granted') {
289 + // console.log('web notifications are not granted');
290 + return false;
291 + }
292 +
293 + if (typeof NETDATA.alarms.current !== 'undefined' && typeof NETDATA.alarms.current.alarms === 'object') {
294 + // console.log('can do alarms: old id = ' + NETDATA.alarms.last_notification_id + ' new id = ' + NETDATA.alarms.current.latest_alarm_log_unique_id);
295 +
296 + if (NETDATA.alarms.current.latest_alarm_log_unique_id > NETDATA.alarms.last_notification_id) {
297 + // console.log('new alarms detected');
298 + return true;
299 + }
300 + //else console.log('no new alarms');
301 + }
302 + // else console.log('cannot process alarms');
303 +
304 + return false;
305 + },
306 +
307 + get: function (what, callback) {
308 + $.ajax({
309 + url: NETDATA.alarms.server + '/api/v1/alarms?' + what.toString(),
310 + async: true,
311 + cache: false,
312 + headers: {
313 + 'Cache-Control': 'no-cache, no-store',
314 + 'Pragma': 'no-cache'
315 + },
316 + xhrFields: {withCredentials: true} // required for the cookie
317 + })
318 + .done(function (data) {
319 + data = NETDATA.xss.checkOptional('/api/v1/alarms', data /*, '.*\.(calc|calc_parsed|warn|warn_parsed|crit|crit_parsed)$' */);
320 +
321 + if (NETDATA.alarms.first_notification_id === 0 && typeof data.latest_alarm_log_unique_id === 'number') {
322 + NETDATA.alarms.first_notification_id = data.latest_alarm_log_unique_id;
323 + }
324 +
325 + if (typeof callback === 'function') {
326 + return callback(data);
327 + }
328 + })
329 + .fail(function () {
330 + NETDATA.error(415, NETDATA.alarms.server);
331 +
332 + if (typeof callback === 'function') {
333 + return callback(null);
334 + }
335 + });
336 + },
337 +
338 + update_forever: function () {
339 + if (netdataShowAlarms !== true || netdataSnapshotData !== null) {
340 + return;
341 + }
342 +
343 + NETDATA.alarms.get('active', function (data) {
344 + if (data !== null) {
345 + NETDATA.alarms.current = data;
346 +
347 + if (NETDATA.alarms.check_notifications()) {
348 + NETDATA.alarms.notifyAll();
349 + }
350 +
351 + if (typeof NETDATA.alarms.callback === 'function') {
352 + NETDATA.alarms.callback(data);
353 + }
354 +
355 + // Health monitoring is disabled on this netdata
356 + if (data.status === false) {
357 + return;
358 + }
359 + }
360 +
361 + setTimeout(NETDATA.alarms.update_forever, NETDATA.alarms.update_every);
362 + });
363 + },
364 +
365 + get_log: function (last_id, callback) {
366 + // console.log('fetching all log after ' + last_id.toString());
367 + $.ajax({
368 + url: NETDATA.alarms.server + '/api/v1/alarm_log?after=' + last_id.toString(),
369 + async: true,
370 + cache: false,
371 + headers: {
372 + 'Cache-Control': 'no-cache, no-store',
373 + 'Pragma': 'no-cache'
374 + },
375 + xhrFields: {withCredentials: true} // required for the cookie
376 + })
377 + .done(function (data) {
378 + data = NETDATA.xss.checkOptional('/api/v1/alarm_log', data);
379 +
380 + if (typeof callback === 'function') {
381 + return callback(data);
382 + }
383 + })
384 + .fail(function () {
385 + NETDATA.error(416, NETDATA.alarms.server);
386 +
387 + if (typeof callback === 'function') {
388 + return callback(null);
389 + }
390 + });
391 + },
392 +
393 + init: function () {
394 + NETDATA.alarms.server = NETDATA.fixHost(NETDATA.serverDefault);
395 +
396 + if (typeof netdataAlarmsRemember === 'undefined' || netdataAlarmsRemember) {
397 + NETDATA.alarms.last_notification_id =
398 + NETDATA.localStorageGet('last_notification_id', NETDATA.alarms.last_notification_id, null);
399 + }
400 +
401 + if (NETDATA.alarms.onclick === null) {
402 + NETDATA.alarms.onclick = NETDATA.alarms.scrollToAlarm;
403 + }
404 +
405 + if (typeof netdataAlarmsRecipients !== 'undefined' && Array.isArray(netdataAlarmsRecipients)) {
406 + NETDATA.alarms.recipients = netdataAlarmsRecipients;
407 + }
408 +
409 + if (netdataShowAlarms) {
410 + NETDATA.alarms.update_forever();
411 +
412 + if ('Notification' in window) {
413 + // console.log('notifications available');
414 + NETDATA.alarms.notifications = true;
415 +
416 + if (Notification.permission === 'default') {
417 + Notification.requestPermission();
418 + }
419 + }
420 + }
421 + }
422 +};
web/gui/src/dashboard.js/boot.js new
+142
@@ -0,0 +1,142 @@
1 +
2 +// Load required JS libraries and CSS
3 +
4 +NETDATA.requiredJs = [
5 + {
6 + url: NETDATA.serverStatic + 'lib/bootstrap-3.3.7.min.js',
7 + async: false,
8 + isAlreadyLoaded: function () {
9 + // check if bootstrap is loaded
10 + if (typeof $().emulateTransitionEnd === 'function') {
11 + return true;
12 + } else {
13 + return typeof netdataNoBootstrap !== 'undefined' && netdataNoBootstrap;
14 + }
15 + }
16 + },
17 + {
18 + url: NETDATA.serverStatic + 'lib/fontawesome-all-5.0.1.min.js',
19 + async: true,
20 + isAlreadyLoaded: function () {
21 + return typeof netdataNoFontAwesome !== 'undefined' && netdataNoFontAwesome;
22 + }
23 + },
24 + {
25 + url: NETDATA.serverStatic + 'lib/perfect-scrollbar-0.6.15.min.js',
26 + isAlreadyLoaded: function () {
27 + return false;
28 + }
29 + }
30 +];
31 +
32 +NETDATA.requiredCSS = [
33 + {
34 + url: NETDATA.themes.current.bootstrap_css,
35 + isAlreadyLoaded: function () {
36 + return typeof netdataNoBootstrap !== 'undefined' && netdataNoBootstrap;
37 + }
38 + },
39 + {
40 + url: NETDATA.themes.current.dashboard_css,
41 + isAlreadyLoaded: function () {
42 + return false;
43 + }
44 + }
45 +];
46 +
47 +NETDATA.loadedRequiredJs = 0;
48 +NETDATA.loadRequiredJs = function (index, callback) {
49 + if (index >= NETDATA.requiredJs.length) {
50 + if (typeof callback === 'function') {
51 + return callback();
52 + }
53 + return;
54 + }
55 +
56 + if (NETDATA.requiredJs[index].isAlreadyLoaded()) {
57 + NETDATA.loadedRequiredJs++;
58 + NETDATA.loadRequiredJs(++index, callback);
59 + return;
60 + }
61 +
62 + if (NETDATA.options.debug.main_loop) {
63 + console.log('loading ' + NETDATA.requiredJs[index].url);
64 + }
65 +
66 + let async = true;
67 + if (typeof NETDATA.requiredJs[index].async !== 'undefined' && NETDATA.requiredJs[index].async === false) {
68 + async = false;
69 + }
70 +
71 + $.ajax({
72 + url: NETDATA.requiredJs[index].url,
73 + cache: true,
74 + dataType: "script",
75 + xhrFields: {withCredentials: true} // required for the cookie
76 + })
77 + .done(function () {
78 + if (NETDATA.options.debug.main_loop) {
79 + console.log('loaded ' + NETDATA.requiredJs[index].url);
80 + }
81 + })
82 + .fail(function () {
83 + alert('Cannot load required JS library: ' + NETDATA.requiredJs[index].url);
84 + })
85 + .always(function () {
86 + NETDATA.loadedRequiredJs++;
87 +
88 + // if (async === false)
89 + if (!async) {
90 + NETDATA.loadRequiredJs(++index, callback);
91 + }
92 + });
93 +
94 + // if (async === true)
95 + if (async) {
96 + NETDATA.loadRequiredJs(++index, callback);
97 + }
98 +};
99 +
100 +NETDATA.loadRequiredCSS = function (index) {
101 + if (index >= NETDATA.requiredCSS.length) {
102 + return;
103 + }
104 +
105 + if (NETDATA.requiredCSS[index].isAlreadyLoaded()) {
106 + NETDATA.loadRequiredCSS(++index);
107 + return;
108 + }
109 +
110 + if (NETDATA.options.debug.main_loop) {
111 + console.log('loading ' + NETDATA.requiredCSS[index].url);
112 + }
113 +
114 + NETDATA._loadCSS(NETDATA.requiredCSS[index].url);
115 + NETDATA.loadRequiredCSS(++index);
116 +};
117 +
118 +// Boot it!
119 +
120 +if (typeof netdataPrepCallback === 'function') {
121 + netdataPrepCallback();
122 +}
123 +
124 +NETDATA.errorReset();
125 +NETDATA.loadRequiredCSS(0);
126 +
127 +NETDATA._loadjQuery(function () {
128 + NETDATA.loadRequiredJs(0, function () {
129 + if (typeof $().emulateTransitionEnd !== 'function') {
130 + // bootstrap is not available
131 + NETDATA.options.current.show_help = false;
132 + }
133 +
134 + if (typeof netdataDontStart === 'undefined' || !netdataDontStart) {
135 + if (NETDATA.options.debug.main_loop) {
136 + console.log('starting chart refresh thread');
137 + }
138 +
139 + NETDATA.start();
140 + }
141 + });
142 +});
web/gui/src/dashboard.js/charting.js new
+450
@@ -0,0 +1,450 @@
1 +
2 +// Charts Libraries Registration
3 +
4 +NETDATA.chartLibraries = {
5 + "dygraph": {
6 + initialize: NETDATA.dygraphInitialize,
7 + create: NETDATA.dygraphChartCreate,
8 + update: NETDATA.dygraphChartUpdate,
9 + resize: function (state) {
10 + if (typeof state.tmp.dygraph_instance !== 'undefined' && typeof state.tmp.dygraph_instance.resize === 'function') {
11 + state.tmp.dygraph_instance.resize();
12 + }
13 + },
14 + setSelection: NETDATA.dygraphSetSelection,
15 + clearSelection: NETDATA.dygraphClearSelection,
16 + toolboxPanAndZoom: NETDATA.dygraphToolboxPanAndZoom,
17 + initialized: false,
18 + enabled: true,
19 + xssRegexIgnore: new RegExp('^/api/v1/data\.result.data$'),
20 + format: function (state) {
21 + void(state);
22 + return 'json';
23 + },
24 + options: function (state) {
25 + return 'ms' + '%7C' + 'flip' + (this.isLogScale(state) ? ('%7C' + 'abs') : '').toString();
26 + },
27 + legend: function (state) {
28 + return (this.isSparkline(state) === false && NETDATA.dataAttributeBoolean(state.element, 'legend', true) === true) ? 'right-side' : null;
29 + },
30 + autoresize: function (state) {
31 + void(state);
32 + return true;
33 + },
34 + max_updates_to_recreate: function (state) {
35 + void(state);
36 + return 5000;
37 + },
38 + track_colors: function (state) {
39 + void(state);
40 + return true;
41 + },
42 + pixels_per_point: function (state) {
43 + return (this.isSparkline(state) === false) ? 3 : 2;
44 + },
45 + isSparkline: function (state) {
46 + if (typeof state.tmp.dygraph_sparkline === 'undefined') {
47 + state.tmp.dygraph_sparkline = (this.theme(state) === 'sparkline');
48 + }
49 + return state.tmp.dygraph_sparkline;
50 + },
51 + isLogScale: function (state) {
52 + if (typeof state.tmp.dygraph_logscale === 'undefined') {
53 + state.tmp.dygraph_logscale = (this.theme(state) === 'logscale');
54 + }
55 + return state.tmp.dygraph_logscale;
56 + },
57 + theme: function (state) {
58 + if (typeof state.tmp.dygraph_theme === 'undefined') {
59 + state.tmp.dygraph_theme = NETDATA.dataAttribute(state.element, 'dygraph-theme', 'default');
60 + }
61 + return state.tmp.dygraph_theme;
62 + },
63 + container_class: function (state) {
64 + if (this.legend(state) !== null) {
65 + return 'netdata-container-with-legend';
66 + }
67 + return 'netdata-container';
68 + }
69 + },
70 + "sparkline": {
71 + initialize: NETDATA.sparklineInitialize,
72 + create: NETDATA.sparklineChartCreate,
73 + update: NETDATA.sparklineChartUpdate,
74 + resize: null,
75 + setSelection: undefined, // function(state, t) { void(state); return true; },
76 + clearSelection: undefined, // function(state) { void(state); return true; },
77 + toolboxPanAndZoom: null,
78 + initialized: false,
79 + enabled: true,
80 + xssRegexIgnore: new RegExp('^/api/v1/data\.result$'),
81 + format: function (state) {
82 + void(state);
83 + return 'array';
84 + },
85 + options: function (state) {
86 + void(state);
87 + return 'flip' + '%7C' + 'abs';
88 + },
89 + legend: function (state) {
90 + void(state);
91 + return null;
92 + },
93 + autoresize: function (state) {
94 + void(state);
95 + return false;
96 + },
97 + max_updates_to_recreate: function (state) {
98 + void(state);
99 + return 5000;
100 + },
101 + track_colors: function (state) {
102 + void(state);
103 + return false;
104 + },
105 + pixels_per_point: function (state) {
106 + void(state);
107 + return 3;
108 + },
109 + container_class: function (state) {
110 + void(state);
111 + return 'netdata-container';
112 + }
113 + },
114 + "peity": {
115 + initialize: NETDATA.peityInitialize,
116 + create: NETDATA.peityChartCreate,
117 + update: NETDATA.peityChartUpdate,
118 + resize: null,
119 + setSelection: undefined, // function(state, t) { void(state); return true; },
120 + clearSelection: undefined, // function(state) { void(state); return true; },
121 + toolboxPanAndZoom: null,
122 + initialized: false,
123 + enabled: true,
124 + xssRegexIgnore: new RegExp('^/api/v1/data\.result$'),
125 + format: function (state) {
126 + void(state);
127 + return 'ssvcomma';
128 + },
129 + options: function (state) {
130 + void(state);
131 + return 'null2zero' + '%7C' + 'flip' + '%7C' + 'abs';
132 + },
133 + legend: function (state) {
134 + void(state);
135 + return null;
136 + },
137 + autoresize: function (state) {
138 + void(state);
139 + return false;
140 + },
141 + max_updates_to_recreate: function (state) {
142 + void(state);
143 + return 5000;
144 + },
145 + track_colors: function (state) {
146 + void(state);
147 + return false;
148 + },
149 + pixels_per_point: function (state) {
150 + void(state);
151 + return 3;
152 + },
153 + container_class: function (state) {
154 + void(state);
155 + return 'netdata-container';
156 + }
157 + },
158 + // "morris": {
159 + // initialize: NETDATA.morrisInitialize,
160 + // create: NETDATA.morrisChartCreate,
161 + // update: NETDATA.morrisChartUpdate,
162 + // resize: null,
163 + // setSelection: undefined, // function(state, t) { void(state); return true; },
164 + // clearSelection: undefined, // function(state) { void(state); return true; },
165 + // toolboxPanAndZoom: null,
166 + // initialized: false,
167 + // enabled: true,
168 + // xssRegexIgnore: new RegExp('^/api/v1/data\.result.data$'),
169 + // format: function(state) { void(state); return 'json'; },
170 + // options: function(state) { void(state); return 'objectrows' + '%7C' + 'ms'; },
171 + // legend: function(state) { void(state); return null; },
172 + // autoresize: function(state) { void(state); return false; },
173 + // max_updates_to_recreate: function(state) { void(state); return 50; },
174 + // track_colors: function(state) { void(state); return false; },
175 + // pixels_per_point: function(state) { void(state); return 15; },
176 + // container_class: function(state) { void(state); return 'netdata-container'; }
177 + // },
178 + "google": {
179 + initialize: NETDATA.googleInitialize,
180 + create: NETDATA.googleChartCreate,
181 + update: NETDATA.googleChartUpdate,
182 + resize: null,
183 + setSelection: undefined, //function(state, t) { void(state); return true; },
184 + clearSelection: undefined, //function(state) { void(state); return true; },
185 + toolboxPanAndZoom: null,
186 + initialized: false,
187 + enabled: true,
188 + xssRegexIgnore: new RegExp('^/api/v1/data\.result.rows$'),
189 + format: function (state) {
190 + void(state);
191 + return 'datatable';
192 + },
193 + options: function (state) {
194 + void(state);
195 + return '';
196 + },
197 + legend: function (state) {
198 + void(state);
199 + return null;
200 + },
201 + autoresize: function (state) {
202 + void(state);
203 + return false;
204 + },
205 + max_updates_to_recreate: function (state) {
206 + void(state);
207 + return 300;
208 + },
209 + track_colors: function (state) {
210 + void(state);
211 + return false;
212 + },
213 + pixels_per_point: function (state) {
214 + void(state);
215 + return 4;
216 + },
217 + container_class: function (state) {
218 + void(state);
219 + return 'netdata-container';
220 + }
221 + },
222 + // "raphael": {
223 + // initialize: NETDATA.raphaelInitialize,
224 + // create: NETDATA.raphaelChartCreate,
225 + // update: NETDATA.raphaelChartUpdate,
226 + // resize: null,
227 + // setSelection: undefined, // function(state, t) { void(state); return true; },
228 + // clearSelection: undefined, // function(state) { void(state); return true; },
229 + // toolboxPanAndZoom: null,
230 + // initialized: false,
231 + // enabled: true,
232 + // xssRegexIgnore: new RegExp('^/api/v1/data\.result.data$'),
233 + // format: function(state) { void(state); return 'json'; },
234 + // options: function(state) { void(state); return ''; },
235 + // legend: function(state) { void(state); return null; },
236 + // autoresize: function(state) { void(state); return false; },
237 + // max_updates_to_recreate: function(state) { void(state); return 5000; },
238 + // track_colors: function(state) { void(state); return false; },
239 + // pixels_per_point: function(state) { void(state); return 3; },
240 + // container_class: function(state) { void(state); return 'netdata-container'; }
241 + // },
242 + // "c3": {
243 + // initialize: NETDATA.c3Initialize,
244 + // create: NETDATA.c3ChartCreate,
245 + // update: NETDATA.c3ChartUpdate,
246 + // resize: null,
247 + // setSelection: undefined, // function(state, t) { void(state); return true; },
248 + // clearSelection: undefined, // function(state) { void(state); return true; },
249 + // toolboxPanAndZoom: null,
250 + // initialized: false,
251 + // enabled: true,
252 + // xssRegexIgnore: new RegExp('^/api/v1/data\.result$'),
253 + // format: function(state) { void(state); return 'csvjsonarray'; },
254 + // options: function(state) { void(state); return 'milliseconds'; },
255 + // legend: function(state) { void(state); return null; },
256 + // autoresize: function(state) { void(state); return false; },
257 + // max_updates_to_recreate: function(state) { void(state); return 5000; },
258 + // track_colors: function(state) { void(state); return false; },
259 + // pixels_per_point: function(state) { void(state); return 15; },
260 + // container_class: function(state) { void(state); return 'netdata-container'; }
261 + // },
262 + "d3pie": {
263 + initialize: NETDATA.d3pieInitialize,
264 + create: NETDATA.d3pieChartCreate,
265 + update: NETDATA.d3pieChartUpdate,
266 + resize: null,
267 + setSelection: NETDATA.d3pieSetSelection,
268 + clearSelection: NETDATA.d3pieClearSelection,
269 + toolboxPanAndZoom: null,
270 + initialized: false,
271 + enabled: true,
272 + xssRegexIgnore: new RegExp('^/api/v1/data\.result.data$'),
273 + format: function (state) {
274 + void(state);
275 + return 'json';
276 + },
277 + options: function (state) {
278 + void(state);
279 + return 'objectrows' + '%7C' + 'ms';
280 + },
281 + legend: function (state) {
282 + void(state);
283 + return null;
284 + },
285 + autoresize: function (state) {
286 + void(state);
287 + return false;
288 + },
289 + max_updates_to_recreate: function (state) {
290 + void(state);
291 + return 5000;
292 + },
293 + track_colors: function (state) {
294 + void(state);
295 + return false;
296 + },
297 + pixels_per_point: function (state) {
298 + void(state);
299 + return 15;
300 + },
301 + container_class: function (state) {
302 + void(state);
303 + return 'netdata-container';
304 + }
305 + },
306 + "d3": {
307 + initialize: NETDATA.d3Initialize,
308 + create: NETDATA.d3ChartCreate,
309 + update: NETDATA.d3ChartUpdate,
310 + resize: null,
311 + setSelection: undefined, // function(state, t) { void(state); return true; },
312 + clearSelection: undefined, // function(state) { void(state); return true; },
313 + toolboxPanAndZoom: null,
314 + initialized: false,
315 + enabled: true,
316 + xssRegexIgnore: new RegExp('^/api/v1/data\.result.data$'),
317 + format: function (state) {
318 + void(state);
319 + return 'json';
320 + },
321 + options: function (state) {
322 + void(state);
323 + return '';
324 + },
325 + legend: function (state) {
326 + void(state);
327 + return null;
328 + },
329 + autoresize: function (state) {
330 + void(state);
331 + return false;
332 + },
333 + max_updates_to_recreate: function (state) {
334 + void(state);
335 + return 5000;
336 + },
337 + track_colors: function (state) {
338 + void(state);
339 + return false;
340 + },
341 + pixels_per_point: function (state) {
342 + void(state);
343 + return 3;
344 + },
345 + container_class: function (state) {
346 + void(state);
347 + return 'netdata-container';
348 + }
349 + },
350 + "easypiechart": {
351 + initialize: NETDATA.easypiechartInitialize,
352 + create: NETDATA.easypiechartChartCreate,
353 + update: NETDATA.easypiechartChartUpdate,
354 + resize: null,
355 + setSelection: NETDATA.easypiechartSetSelection,
356 + clearSelection: NETDATA.easypiechartClearSelection,
357 + toolboxPanAndZoom: null,
358 + initialized: false,
359 + enabled: true,
360 + xssRegexIgnore: new RegExp('^/api/v1/data\.result$'),
361 + format: function (state) {
362 + void(state);
363 + return 'array';
364 + },
365 + options: function (state) {
366 + void(state);
367 + return 'absolute';
368 + },
369 + legend: function (state) {
370 + void(state);
371 + return null;
372 + },
373 + autoresize: function (state) {
374 + void(state);
375 + return false;
376 + },
377 + max_updates_to_recreate: function (state) {
378 + void(state);
379 + return 5000;
380 + },
381 + track_colors: function (state) {
382 + void(state);
383 + return true;
384 + },
385 + pixels_per_point: function (state) {
386 + void(state);
387 + return 3;
388 + },
389 + aspect_ratio: 100,
390 + container_class: function (state) {
391 + void(state);
392 + return 'netdata-container-easypiechart';
393 + }
394 + },
395 + "gauge": {
396 + initialize: NETDATA.gaugeInitialize,
397 + create: NETDATA.gaugeChartCreate,
398 + update: NETDATA.gaugeChartUpdate,
399 + resize: null,
400 + setSelection: NETDATA.gaugeSetSelection,
401 + clearSelection: NETDATA.gaugeClearSelection,
402 + toolboxPanAndZoom: null,
403 + initialized: false,
404 + enabled: true,
405 + xssRegexIgnore: new RegExp('^/api/v1/data\.result$'),
406 + format: function (state) {
407 + void(state);
408 + return 'array';
409 + },
410 + options: function (state) {
411 + void(state);
412 + return 'absolute';
413 + },
414 + legend: function (state) {
415 + void(state);
416 + return null;
417 + },
418 + autoresize: function (state) {
419 + void(state);
420 + return false;
421 + },
422 + max_updates_to_recreate: function (state) {
423 + void(state);
424 + return 5000;
425 + },
426 + track_colors: function (state) {
427 + void(state);
428 + return true;
429 + },
430 + pixels_per_point: function (state) {
431 + void(state);
432 + return 3;
433 + },
434 + aspect_ratio: 60,
435 + container_class: function (state) {
436 + void(state);
437 + return 'netdata-container-gauge';
438 + }
439 + }
440 +};
441 +
442 +NETDATA.registerChartLibrary = function (library, url) {
443 + if (NETDATA.options.debug.libraries) {
444 + console.log("registering chart library: " + library);
445 + }
446 +
447 + NETDATA.chartLibraries[library].url = url;
448 + NETDATA.chartLibraries[library].initialized = true;
449 + NETDATA.chartLibraries[library].enabled = true;
450 +};
web/gui/src/dashboard.js/charting/_c3.js new
+114
@@ -0,0 +1,114 @@
1 +
2 +// DEPRECATED: will be removed!
3 +
4 +// c3
5 +
6 +NETDATA.c3Initialize = function(callback) {
7 + if (typeof netdataNoC3 === 'undefined' || !netdataNoC3) {
8 +
9 + // C3 requires D3
10 + if (!NETDATA.chartLibraries.d3.initialized) {
11 + if (NETDATA.chartLibraries.d3.enabled) {
12 + NETDATA.d3Initialize(function() {
13 + NETDATA.c3Initialize(callback);
14 + });
15 + } else {
16 + NETDATA.chartLibraries.c3.enabled = false;
17 + if (typeof callback === "function")
18 + return callback();
19 + }
20 + } else {
21 + NETDATA._loadCSS(NETDATA.c3_css);
22 +
23 + $.ajax({
24 + url: NETDATA.c3_js,
25 + cache: true,
26 + dataType: "script",
27 + xhrFields: { withCredentials: true } // required for the cookie
28 + })
29 + .done(function() {
30 + NETDATA.registerChartLibrary('c3', NETDATA.c3_js);
31 + })
32 + .fail(function() {
33 + NETDATA.chartLibraries.c3.enabled = false;
34 + NETDATA.error(100, NETDATA.c3_js);
35 + })
36 + .always(function() {
37 + if (typeof callback === "function")
38 + return callback();
39 + });
40 + }
41 + } else {
42 + NETDATA.chartLibraries.c3.enabled = false;
43 + if (typeof callback === "function")
44 + return callback();
45 + }
46 +};
47 +
48 +NETDATA.c3ChartUpdate = function(state, data) {
49 + state.c3_instance.destroy();
50 + return NETDATA.c3ChartCreate(state, data);
51 +
52 + //state.c3_instance.load({
53 + // rows: data.result,
54 + // unload: true
55 + //});
56 +
57 + //return true;
58 +};
59 +
60 +NETDATA.c3ChartCreate = function(state, data) {
61 +
62 + state.element_chart.id = 'c3-' + state.uuid;
63 + // console.log('id = ' + state.element_chart.id);
64 +
65 + state.c3_instance = c3.generate({
66 + bindto: '#' + state.element_chart.id,
67 + size: {
68 + width: state.chartWidth(),
69 + height: state.chartHeight()
70 + },
71 + color: {
72 + pattern: state.chartColors()
73 + },
74 + data: {
75 + x: 'time',
76 + rows: data.result,
77 + type: (state.chart.chart_type === 'line')?'spline':'area-spline'
78 + },
79 + axis: {
80 + x: {
81 + type: 'timeseries',
82 + tick: {
83 + format: function(x) {
84 + return NETDATA.dateTime.xAxisTimeString(x);
85 + }
86 + }
87 + }
88 + },
89 + grid: {
90 + x: {
91 + show: true
92 + },
93 + y: {
94 + show: true
95 + }
96 + },
97 + point: {
98 + show: false
99 + },
100 + line: {
101 + connectNull: false
102 + },
103 + transition: {
104 + duration: 0
105 + },
106 + interaction: {
107 + enabled: true
108 + }
109 + });
110 +
111 + // console.log(state.c3_instance);
112 +
113 + return true;
114 +};
web/gui/src/dashboard.js/charting/_morris.js new
+81
@@ -0,0 +1,81 @@
1 +
2 +// DEPRECATED: will be removed!
3 +
4 +// morris
5 +
6 +NETDATA.morrisInitialize = function(callback) {
7 + if (typeof netdataNoMorris === 'undefined' || !netdataNoMorris) {
8 +
9 + // morris requires raphael
10 + if (!NETDATA.chartLibraries.raphael.initialized) {
11 + if (NETDATA.chartLibraries.raphael.enabled) {
12 + NETDATA.raphaelInitialize(function() {
13 + NETDATA.morrisInitialize(callback);
14 + });
15 + } else {
16 + NETDATA.chartLibraries.morris.enabled = false;
17 + if (typeof callback === "function")
18 + return callback();
19 + }
20 + } else {
21 + NETDATA._loadCSS(NETDATA.morris_css);
22 +
23 + $.ajax({
24 + url: NETDATA.morris_js,
25 + cache: true,
26 + dataType: "script",
27 + xhrFields: { withCredentials: true } // required for the cookie
28 + })
29 + .done(function() {
30 + NETDATA.registerChartLibrary('morris', NETDATA.morris_js);
31 + })
32 + .fail(function() {
33 + NETDATA.chartLibraries.morris.enabled = false;
34 + NETDATA.error(100, NETDATA.morris_js);
35 + })
36 + .always(function() {
37 + if (typeof callback === "function")
38 + return callback();
39 + });
40 + }
41 + } else {
42 + NETDATA.chartLibraries.morris.enabled = false;
43 + if (typeof callback === "function")
44 + return callback();
45 + }
46 +};
47 +
48 +NETDATA.morrisChartUpdate = function(state, data) {
49 + state.morris_instance.setData(data.result.data);
50 + return true;
51 +};
52 +
53 +NETDATA.morrisChartCreate = function(state, data) {
54 +
55 + state.morris_options = {
56 + element: state.element_chart.id,
57 + data: data.result.data,
58 + xkey: 'time',
59 + ykeys: data.dimension_names,
60 + labels: data.dimension_names,
61 + lineWidth: 2,
62 + pointSize: 3,
63 + smooth: true,
64 + hideHover: 'auto',
65 + parseTime: true,
66 + continuousLine: false,
67 + behaveLikeLine: false
68 + };
69 +
70 + if (state.chart.chart_type === 'line')
71 + state.morris_instance = new Morris.Line(state.morris_options);
72 +
73 + else if (state.chart.chart_type === 'area') {
74 + state.morris_options.behaveLikeLine = true;
75 + state.morris_instance = new Morris.Area(state.morris_options);
76 + }
77 + else // stacked
78 + state.morris_instance = new Morris.Area(state.morris_options);
79 +
80 + return true;
81 +};
web/gui/src/dashboard.js/charting/_raphael.js new
+48
@@ -0,0 +1,48 @@
1 +
2 +// DEPRECATED: will be removed!
3 +
4 +// raphael
5 +
6 +NETDATA.raphaelInitialize = function(callback) {
7 + if (typeof netdataStopRaphael === 'undefined' || !netdataStopRaphael) {
8 + $.ajax({
9 + url: NETDATA.raphael_js,
10 + cache: true,
11 + dataType: "script",
12 + xhrFields: { withCredentials: true } // required for the cookie
13 + })
14 + .done(function() {
15 + NETDATA.registerChartLibrary('raphael', NETDATA.raphael_js);
16 + })
17 + .fail(function() {
18 + NETDATA.chartLibraries.raphael.enabled = false;
19 + NETDATA.error(100, NETDATA.raphael_js);
20 + })
21 + .always(function() {
22 + if (typeof callback === "function")
23 + return callback();
24 + });
25 + } else {
26 + NETDATA.chartLibraries.raphael.enabled = false;
27 + if (typeof callback === "function")
28 + return callback();
29 + }
30 +};
31 +
32 +NETDATA.raphaelChartUpdate = function(state, data) {
33 + $(state.element_chart).raphael(data.result, {
34 + width: state.chartWidth(),
35 + height: state.chartHeight()
36 + });
37 +
38 + return false;
39 +};
40 +
41 +NETDATA.raphaelChartCreate = function(state, data) {
42 + $(state.element_chart).raphael(data.result, {
43 + width: state.chartWidth(),
44 + height: state.chartHeight()
45 + });
46 +
47 + return false;
48 +};
web/gui/src/dashboard.js/charting/d3.js new
+43
@@ -0,0 +1,43 @@
1 +
2 +// ----------------------------------------------------------------------------------------------------------------
3 +// D3
4 +
5 +NETDATA.d3Initialize = function(callback) {
6 + if (typeof netdataStopD3 === 'undefined' || !netdataStopD3) {
7 + $.ajax({
8 + url: NETDATA.d3_js,
9 + cache: true,
10 + dataType: "script",
11 + xhrFields: { withCredentials: true } // required for the cookie
12 + })
13 + .done(function() {
14 + NETDATA.registerChartLibrary('d3', NETDATA.d3_js);
15 + })
16 + .fail(function() {
17 + NETDATA.chartLibraries.d3.enabled = false;
18 + NETDATA.error(100, NETDATA.d3_js);
19 + })
20 + .always(function() {
21 + if (typeof callback === "function")
22 + return callback();
23 + });
24 + } else {
25 + NETDATA.chartLibraries.d3.enabled = false;
26 + if (typeof callback === "function")
27 + return callback();
28 + }
29 +};
30 +
31 +NETDATA.d3ChartUpdate = function(state, data) {
32 + void(state);
33 + void(data);
34 +
35 + return false;
36 +};
37 +
38 +NETDATA.d3ChartCreate = function(state, data) {
39 + void(state);
40 + void(data);
41 +
42 + return false;
43 +};
web/gui/src/dashboard.js/charting/d3pie.js new
+341
@@ -0,0 +1,341 @@
1 +
2 +// d3pie
3 +
4 +NETDATA.d3pieInitialize = function (callback) {
5 + if (typeof netdataNoD3pie === 'undefined' || !netdataNoD3pie) {
6 +
7 + // d3pie requires D3
8 + if (!NETDATA.chartLibraries.d3.initialized) {
9 + if (NETDATA.chartLibraries.d3.enabled) {
10 + NETDATA.d3Initialize(function () {
11 + NETDATA.d3pieInitialize(callback);
12 + });
13 + } else {
14 + NETDATA.chartLibraries.d3pie.enabled = false;
15 + if (typeof callback === "function") {
16 + return callback();
17 + }
18 + }
19 + } else {
20 + $.ajax({
21 + url: NETDATA.d3pie_js,
22 + cache: true,
23 + dataType: "script",
24 + xhrFields: {withCredentials: true} // required for the cookie
25 + })
26 + .done(function () {
27 + NETDATA.registerChartLibrary('d3pie', NETDATA.d3pie_js);
28 + })
29 + .fail(function () {
30 + NETDATA.chartLibraries.d3pie.enabled = false;
31 + NETDATA.error(100, NETDATA.d3pie_js);
32 + })
33 + .always(function () {
34 + if (typeof callback === "function") {
35 + return callback();
36 + }
37 + });
38 + }
39 + } else {
40 + NETDATA.chartLibraries.d3pie.enabled = false;
41 + if (typeof callback === "function") {
42 + return callback();
43 + }
44 + }
45 +};
46 +
47 +NETDATA.d3pieSetContent = function (state, data, index) {
48 + state.legendFormatValueDecimalsFromMinMax(
49 + data.min,
50 + data.max
51 + );
52 +
53 + let content = [];
54 + let colors = state.chartColors();
55 + let len = data.result.labels.length;
56 + for (let i = 1; i < len; i++) {
57 + let label = data.result.labels[i];
58 + let value = data.result.data[index][label];
59 + let color = colors[i - 1];
60 +
61 + if (value !== null && value > 0) {
62 + content.push({
63 + label: label,
64 + value: value,
65 + color: color
66 + });
67 + }
68 + }
69 +
70 + if (content.length === 0) {
71 + content.push({
72 + label: 'no data',
73 + value: 100,
74 + color: '#666666'
75 + });
76 + }
77 +
78 + state.tmp.d3pie_last_slot = index;
79 + return content;
80 +};
81 +
82 +NETDATA.d3pieDateRange = function (state, data, index) {
83 + let dt = Math.round((data.before - data.after + 1) / data.points);
84 + let dt_str = NETDATA.seconds4human(dt);
85 +
86 + let before = data.result.data[index].time;
87 + let after = before - (dt * 1000);
88 +
89 + let d1 = NETDATA.dateTime.localeDateString(after);
90 + let t1 = NETDATA.dateTime.localeTimeString(after);
91 + let d2 = NETDATA.dateTime.localeDateString(before);
92 + let t2 = NETDATA.dateTime.localeTimeString(before);
93 +
94 + if (d1 === d2) {
95 + return d1 + ' ' + t1 + ' to ' + t2 + ', ' + dt_str;
96 + }
97 +
98 + return d1 + ' ' + t1 + ' to ' + d2 + ' ' + t2 + ', ' + dt_str;
99 +};
100 +
101 +NETDATA.d3pieSetSelection = function (state, t) {
102 + if (state.timeIsVisible(t) !== true) {
103 + return NETDATA.d3pieClearSelection(state, true);
104 + }
105 +
106 + let slot = state.calculateRowForTime(t);
107 + slot = state.data.result.data.length - slot - 1;
108 +
109 + if (slot < 0 || slot >= state.data.result.length) {
110 + return NETDATA.d3pieClearSelection(state, true);
111 + }
112 +
113 + if (state.tmp.d3pie_last_slot === slot) {
114 + // we already show this slot, don't do anything
115 + return true;
116 + }
117 +
118 + if (state.tmp.d3pie_timer === undefined) {
119 + state.tmp.d3pie_timer = NETDATA.timeout.set(function () {
120 + state.tmp.d3pie_timer = undefined;
121 + NETDATA.d3pieChange(state, NETDATA.d3pieSetContent(state, state.data, slot), NETDATA.d3pieDateRange(state, state.data, slot));
122 + }, 0);
123 + }
124 +
125 + return true;
126 +};
127 +
128 +NETDATA.d3pieClearSelection = function (state, force) {
129 + if (typeof state.tmp.d3pie_timer !== 'undefined') {
130 + NETDATA.timeout.clear(state.tmp.d3pie_timer);
131 + state.tmp.d3pie_timer = undefined;
132 + }
133 +
134 + if (state.isAutoRefreshable() && state.data !== null && force !== true) {
135 + NETDATA.d3pieChartUpdate(state, state.data);
136 + } else {
137 + if (state.tmp.d3pie_last_slot !== -1) {
138 + state.tmp.d3pie_last_slot = -1;
139 + NETDATA.d3pieChange(state, [{label: 'no data', value: 1, color: '#666666'}], 'no data available');
140 + }
141 + }
142 +
143 + return true;
144 +};
145 +
146 +NETDATA.d3pieChange = function (state, content, footer) {
147 + if (state.d3pie_forced_subtitle === null) {
148 + //state.d3pie_instance.updateProp("header.subtitle.text", state.units_current);
149 + state.d3pie_instance.options.header.subtitle.text = state.units_current;
150 + }
151 +
152 + if (state.d3pie_forced_footer === null) {
153 + //state.d3pie_instance.updateProp("footer.text", footer);
154 + state.d3pie_instance.options.footer.text = footer;
155 + }
156 +
157 + //state.d3pie_instance.updateProp("data.content", content);
158 + state.d3pie_instance.options.data.content = content;
159 + state.d3pie_instance.destroy();
160 + state.d3pie_instance.recreate();
161 + return true;
162 +};
163 +
164 +NETDATA.d3pieChartUpdate = function (state, data) {
165 + return NETDATA.d3pieChange(state, NETDATA.d3pieSetContent(state, data, 0), NETDATA.d3pieDateRange(state, data, 0));
166 +};
167 +
168 +NETDATA.d3pieChartCreate = function (state, data) {
169 +
170 + state.element_chart.id = 'd3pie-' + state.uuid;
171 + // console.log('id = ' + state.element_chart.id);
172 +
173 + let content = NETDATA.d3pieSetContent(state, data, 0);
174 +
175 + state.d3pie_forced_title = NETDATA.dataAttribute(state.element, 'd3pie-title', null);
176 + state.d3pie_forced_subtitle = NETDATA.dataAttribute(state.element, 'd3pie-subtitle', null);
177 + state.d3pie_forced_footer = NETDATA.dataAttribute(state.element, 'd3pie-footer', null);
178 +
179 + state.d3pie_options = {
180 + header: {
181 + title: {
182 + text: (state.d3pie_forced_title !== null) ? state.d3pie_forced_title : state.title,
183 + color: NETDATA.dataAttribute(state.element, 'd3pie-title-color', NETDATA.themes.current.d3pie.title),
184 + fontSize: NETDATA.dataAttribute(state.element, 'd3pie-title-fontsize', 12),
185 + fontWeight: NETDATA.dataAttribute(state.element, 'd3pie-title-fontweight', "bold"),
186 + font: NETDATA.dataAttribute(state.element, 'd3pie-title-font', "arial")
187 + },
188 + subtitle: {
189 + text: (state.d3pie_forced_subtitle !== null) ? state.d3pie_forced_subtitle : state.units_current,
190 + color: NETDATA.dataAttribute(state.element, 'd3pie-subtitle-color', NETDATA.themes.current.d3pie.subtitle),
191 + fontSize: NETDATA.dataAttribute(state.element, 'd3pie-subtitle-fontsize', 10),
192 + fontWeight: NETDATA.dataAttribute(state.element, 'd3pie-subtitle-fontweight', "normal"),
193 + font: NETDATA.dataAttribute(state.element, 'd3pie-subtitle-font', "arial")
194 + },
195 + titleSubtitlePadding: 1
196 + },
197 + footer: {
198 + text: (state.d3pie_forced_footer !== null) ? state.d3pie_forced_footer : NETDATA.d3pieDateRange(state, data, 0),
199 + color: NETDATA.dataAttribute(state.element, 'd3pie-footer-color', NETDATA.themes.current.d3pie.footer),
200 + fontSize: NETDATA.dataAttribute(state.element, 'd3pie-footer-fontsize', 9),
201 + fontWeight: NETDATA.dataAttribute(state.element, 'd3pie-footer-fontweight', "bold"),
202 + font: NETDATA.dataAttribute(state.element, 'd3pie-footer-font', "arial"),
203 + location: NETDATA.dataAttribute(state.element, 'd3pie-footer-location', "bottom-center") // bottom-left, bottom-center, bottom-right
204 + },
205 + size: {
206 + canvasHeight: state.chartHeight(),
207 + canvasWidth: state.chartWidth(),
208 + pieInnerRadius: NETDATA.dataAttribute(state.element, 'd3pie-pieinnerradius', "45%"),
209 + pieOuterRadius: NETDATA.dataAttribute(state.element, 'd3pie-pieouterradius', "80%")
210 + },
211 + data: {
212 + // none, random, value-asc, value-desc, label-asc, label-desc
213 + sortOrder: NETDATA.dataAttribute(state.element, 'd3pie-sortorder', "value-desc"),
214 + smallSegmentGrouping: {
215 + enabled: NETDATA.dataAttributeBoolean(state.element, "d3pie-smallsegmentgrouping-enabled", false),
216 + value: NETDATA.dataAttribute(state.element, 'd3pie-smallsegmentgrouping-value', 1),
217 + // percentage, value
218 + valueType: NETDATA.dataAttribute(state.element, 'd3pie-smallsegmentgrouping-valuetype', "percentage"),
219 + label: NETDATA.dataAttribute(state.element, 'd3pie-smallsegmentgrouping-label', "other"),
220 + color: NETDATA.dataAttribute(state.element, 'd3pie-smallsegmentgrouping-color', NETDATA.themes.current.d3pie.other)
221 + },
222 +
223 + // REQUIRED! This is where you enter your pie data; it needs to be an array of objects
224 + // of this form: { label: "label", value: 1.5, color: "#000000" } - color is optional
225 + content: content
226 + },
227 + labels: {
228 + outer: {
229 + // label, value, percentage, label-value1, label-value2, label-percentage1, label-percentage2
230 + format: NETDATA.dataAttribute(state.element, 'd3pie-labels-outer-format', "label-value1"),
231 + hideWhenLessThanPercentage: NETDATA.dataAttribute(state.element, 'd3pie-labels-outer-hidewhenlessthanpercentage', null),
232 + pieDistance: NETDATA.dataAttribute(state.element, 'd3pie-labels-outer-piedistance', 15)
233 + },
234 + inner: {
235 + // label, value, percentage, label-value1, label-value2, label-percentage1, label-percentage2
236 + format: NETDATA.dataAttribute(state.element, 'd3pie-labels-inner-format', "percentage"),
237 + hideWhenLessThanPercentage: NETDATA.dataAttribute(state.element, 'd3pie-labels-inner-hidewhenlessthanpercentage', 2)
238 + },
239 + mainLabel: {
240 + color: NETDATA.dataAttribute(state.element, 'd3pie-labels-mainLabel-color', NETDATA.themes.current.d3pie.mainlabel), // or 'segment' for dynamic color
241 + font: NETDATA.dataAttribute(state.element, 'd3pie-labels-mainLabel-font', "arial"),
242 + fontSize: NETDATA.dataAttribute(state.element, 'd3pie-labels-mainLabel-fontsize', 10),
243 + fontWeight: NETDATA.dataAttribute(state.element, 'd3pie-labels-mainLabel-fontweight', "normal")
244 + },
245 + percentage: {
246 + color: NETDATA.dataAttribute(state.element, 'd3pie-labels-percentage-color', NETDATA.themes.current.d3pie.percentage),
247 + font: NETDATA.dataAttribute(state.element, 'd3pie-labels-percentage-font', "arial"),
248 + fontSize: NETDATA.dataAttribute(state.element, 'd3pie-labels-percentage-fontsize', 10),
249 + fontWeight: NETDATA.dataAttribute(state.element, 'd3pie-labels-percentage-fontweight', "bold"),
250 + decimalPlaces: 0
251 + },
252 + value: {
253 + color: NETDATA.dataAttribute(state.element, 'd3pie-labels-value-color', NETDATA.themes.current.d3pie.value),
254 + font: NETDATA.dataAttribute(state.element, 'd3pie-labels-value-font', "arial"),
255 + fontSize: NETDATA.dataAttribute(state.element, 'd3pie-labels-value-fontsize', 10),
256 + fontWeight: NETDATA.dataAttribute(state.element, 'd3pie-labels-value-fontweight', "bold")
257 + },
258 + lines: {
259 + enabled: NETDATA.dataAttributeBoolean(state.element, 'd3pie-labels-lines-enabled', true),
260 + style: NETDATA.dataAttribute(state.element, 'd3pie-labels-lines-style', "curved"),
261 + color: NETDATA.dataAttribute(state.element, 'd3pie-labels-lines-color', "segment") // "segment" or a hex color
262 + },
263 + truncation: {
264 + enabled: NETDATA.dataAttributeBoolean(state.element, 'd3pie-labels-truncation-enabled', false),
265 + truncateLength: NETDATA.dataAttribute(state.element, 'd3pie-labels-truncation-truncatelength', 30)
266 + },
267 + formatter: function (context) {
268 + // console.log(context);
269 + if (context.part === 'value') {
270 + return state.legendFormatValue(context.value);
271 + }
272 + if (context.part === 'percentage') {
273 + return context.label + '%';
274 + }
275 +
276 + return context.label;
277 + }
278 + },
279 + effects: {
280 + load: {
281 + effect: "none", // none / default
282 + speed: 0 // commented in the d3pie code to speed it up
283 + },
284 + pullOutSegmentOnClick: {
285 + effect: "bounce", // none / linear / bounce / elastic / back
286 + speed: 400,
287 + size: 5
288 + },
289 + highlightSegmentOnMouseover: true,
290 + highlightLuminosity: -0.2
291 + },
292 + tooltips: {
293 + enabled: false,
294 + type: "placeholder", // caption|placeholder
295 + string: "",
296 + placeholderParser: null, // function
297 + styles: {
298 + fadeInSpeed: 250,
299 + backgroundColor: NETDATA.themes.current.d3pie.tooltip_bg,
300 + backgroundOpacity: 0.5,
301 + color: NETDATA.themes.current.d3pie.tooltip_fg,
302 + borderRadius: 2,
303 + font: "arial",
304 + fontSize: 12,
305 + padding: 4
306 + }
307 + },
308 + misc: {
309 + colors: {
310 + background: 'transparent', // transparent or color #
311 + // segments: state.chartColors(),
312 + segmentStroke: NETDATA.dataAttribute(state.element, 'd3pie-misc-colors-segmentstroke', NETDATA.themes.current.d3pie.segment_stroke)
313 + },
314 + gradient: {
315 + enabled: NETDATA.dataAttributeBoolean(state.element, 'd3pie-misc-gradient-enabled', false),
316 + percentage: NETDATA.dataAttribute(state.element, 'd3pie-misc-colors-percentage', 95),
317 + color: NETDATA.dataAttribute(state.element, 'd3pie-misc-gradient-color', NETDATA.themes.current.d3pie.gradient_color)
318 + },
319 + canvasPadding: {
320 + top: 5,
321 + right: 5,
322 + bottom: 5,
323 + left: 5
324 + },
325 + pieCenterOffset: {
326 + x: 0,
327 + y: 0
328 + },
329 + cssPrefix: NETDATA.dataAttribute(state.element, 'd3pie-cssprefix', null)
330 + },
331 + callbacks: {
332 + onload: null,
333 + onMouseoverSegment: null,
334 + onMouseoutSegment: null,
335 + onClickSegment: null
336 + }
337 + };
338 +
339 + state.d3pie_instance = new d3pie(state.element_chart, state.d3pie_options);
340 + return true;
341 +};
web/gui/src/dashboard.js/charting/dygraph.js new
+973
@@ -0,0 +1,973 @@
1 +// dygraph
2 +
3 +NETDATA.dygraph = {
4 + smooth: false
5 +};
6 +
7 +NETDATA.dygraphToolboxPanAndZoom = function (state, after, before) {
8 + if (after < state.netdata_first) {
9 + after = state.netdata_first;
10 + }
11 +
12 + if (before > state.netdata_last) {
13 + before = state.netdata_last;
14 + }
15 +
16 + state.setMode('zoom');
17 + NETDATA.globalSelectionSync.stop();
18 + NETDATA.globalSelectionSync.delay();
19 + state.tmp.dygraph_user_action = true;
20 + state.tmp.dygraph_force_zoom = true;
21 + // state.log('toolboxPanAndZoom');
22 + state.updateChartPanOrZoom(after, before);
23 + NETDATA.globalPanAndZoom.setMaster(state, after, before);
24 +};
25 +
26 +NETDATA.dygraphSetSelection = function (state, t) {
27 + if (typeof state.tmp.dygraph_instance !== 'undefined') {
28 + let r = state.calculateRowForTime(t);
29 + if (r !== -1) {
30 + state.tmp.dygraph_instance.setSelection(r);
31 + return true;
32 + } else {
33 + state.tmp.dygraph_instance.clearSelection();
34 + state.legendShowUndefined();
35 + }
36 + }
37 +
38 + return false;
39 +};
40 +
41 +NETDATA.dygraphClearSelection = function (state) {
42 + if (typeof state.tmp.dygraph_instance !== 'undefined') {
43 + state.tmp.dygraph_instance.clearSelection();
44 + }
45 + return true;
46 +};
47 +
48 +NETDATA.dygraphSmoothInitialize = function (callback) {
49 + $.ajax({
50 + url: NETDATA.dygraph_smooth_js,
51 + cache: true,
52 + dataType: "script",
53 + xhrFields: {withCredentials: true} // required for the cookie
54 + })
55 + .done(function () {
56 + NETDATA.dygraph.smooth = true;
57 + smoothPlotter.smoothing = 0.3;
58 + })
59 + .fail(function () {
60 + NETDATA.dygraph.smooth = false;
61 + })
62 + .always(function () {
63 + if (typeof callback === "function") {
64 + return callback();
65 + }
66 + });
67 +};
68 +
69 +NETDATA.dygraphInitialize = function (callback) {
70 + if (typeof netdataNoDygraphs === 'undefined' || !netdataNoDygraphs) {
71 + $.ajax({
72 + url: NETDATA.dygraph_js,
73 + cache: true,
74 + dataType: "script",
75 + xhrFields: {withCredentials: true} // required for the cookie
76 + })
77 + .done(function () {
78 + NETDATA.registerChartLibrary('dygraph', NETDATA.dygraph_js);
79 + })
80 + .fail(function () {
81 + NETDATA.chartLibraries.dygraph.enabled = false;
82 + NETDATA.error(100, NETDATA.dygraph_js);
83 + })
84 + .always(function () {
85 + if (NETDATA.chartLibraries.dygraph.enabled && NETDATA.options.current.smooth_plot) {
86 + NETDATA.dygraphSmoothInitialize(callback);
87 + } else if (typeof callback === "function") {
88 + return callback();
89 + }
90 + });
91 + } else {
92 + NETDATA.chartLibraries.dygraph.enabled = false;
93 + if (typeof callback === "function") {
94 + return callback();
95 + }
96 + }
97 +};
98 +
99 +NETDATA.dygraphChartUpdate = function (state, data) {
100 + let dygraph = state.tmp.dygraph_instance;
101 +
102 + if (typeof dygraph === 'undefined') {
103 + return NETDATA.dygraphChartCreate(state, data);
104 + }
105 +
106 + // when the chart is not visible, and hidden
107 + // if there is a window resize, dygraph detects
108 + // its element size as 0x0.
109 + // this will make it re-appear properly
110 +
111 + if (state.tm.last_unhidden > state.tmp.dygraph_last_rendered) {
112 + dygraph.resize();
113 + }
114 +
115 + let options = {
116 + file: data.result.data,
117 + colors: state.chartColors(),
118 + labels: data.result.labels,
119 + //labelsDivWidth: state.chartWidth() - 70,
120 + includeZero: state.tmp.dygraph_include_zero,
121 + visibility: state.dimensions_visibility.selected2BooleanArray(state.data.dimension_names)
122 + };
123 +
124 + if (state.tmp.dygraph_chart_type === 'stacked') {
125 + if (options.includeZero && state.dimensions_visibility.countSelected() < options.visibility.length) {
126 + options.includeZero = 0;
127 + }
128 + }
129 +
130 + if (!NETDATA.chartLibraries.dygraph.isSparkline(state)) {
131 + options.ylabel = state.units_current; // (state.units_desired === 'auto')?"":state.units_current;
132 + }
133 +
134 + if (state.tmp.dygraph_force_zoom) {
135 + if (NETDATA.options.debug.dygraph || state.debug) {
136 + state.log('dygraphChartUpdate() forced zoom update');
137 + }
138 +
139 + options.dateWindow = (state.requested_padding !== null) ? [state.view_after, state.view_before] : null;
140 + //options.isZoomedIgnoreProgrammaticZoom = true;
141 + state.tmp.dygraph_force_zoom = false;
142 + } else if (state.current.name !== 'auto') {
143 + if (NETDATA.options.debug.dygraph || state.debug) {
144 + state.log('dygraphChartUpdate() loose update');
145 + }
146 + } else {
147 + if (NETDATA.options.debug.dygraph || state.debug) {
148 + state.log('dygraphChartUpdate() strict update');
149 + }
150 +
151 + options.dateWindow = (state.requested_padding !== null) ? [state.view_after, state.view_before] : null;
152 + //options.isZoomedIgnoreProgrammaticZoom = true;
153 + }
154 +
155 + options.valueRange = state.tmp.dygraph_options.valueRange;
156 +
157 + let oldMax = null, oldMin = null;
158 + if (state.tmp.__commonMin !== null) {
159 + state.data.min = state.tmp.dygraph_instance.axes_[0].extremeRange[0];
160 + oldMin = options.valueRange[0] = NETDATA.commonMin.get(state);
161 + }
162 + if (state.tmp.__commonMax !== null) {
163 + state.data.max = state.tmp.dygraph_instance.axes_[0].extremeRange[1];
164 + oldMax = options.valueRange[1] = NETDATA.commonMax.get(state);
165 + }
166 +
167 + if (state.tmp.dygraph_smooth_eligible) {
168 + if ((NETDATA.options.current.smooth_plot && state.tmp.dygraph_options.plotter !== smoothPlotter)
169 + || (NETDATA.options.current.smooth_plot === false && state.tmp.dygraph_options.plotter === smoothPlotter)) {
170 + NETDATA.dygraphChartCreate(state, data);
171 + return;
172 + }
173 + }
174 +
175 + if (netdataSnapshotData !== null && NETDATA.globalPanAndZoom.isActive() && NETDATA.globalPanAndZoom.isMaster(state) === false) {
176 + // pan and zoom on snapshots
177 + options.dateWindow = [NETDATA.globalPanAndZoom.force_after_ms, NETDATA.globalPanAndZoom.force_before_ms];
178 + //options.isZoomedIgnoreProgrammaticZoom = true;
179 + }
180 +
181 + if (NETDATA.chartLibraries.dygraph.isLogScale(state)) {
182 + if (Array.isArray(options.valueRange) && options.valueRange[0] <= 0) {
183 + options.valueRange[0] = null;
184 + }
185 + }
186 +
187 + dygraph.updateOptions(options);
188 +
189 + let redraw = false;
190 + if (oldMin !== null && oldMin > state.tmp.dygraph_instance.axes_[0].extremeRange[0]) {
191 + state.data.min = state.tmp.dygraph_instance.axes_[0].extremeRange[0];
192 + options.valueRange[0] = NETDATA.commonMin.get(state);
193 + redraw = true;
194 + }
195 + if (oldMax !== null && oldMax < state.tmp.dygraph_instance.axes_[0].extremeRange[1]) {
196 + state.data.max = state.tmp.dygraph_instance.axes_[0].extremeRange[1];
197 + options.valueRange[1] = NETDATA.commonMax.get(state);
198 + redraw = true;
199 + }
200 +
201 + if (redraw) {
202 + // state.log('forcing redraw to adapt to common- min/max');
203 + dygraph.updateOptions(options);
204 + }
205 +
206 + state.tmp.dygraph_last_rendered = Date.now();
207 + return true;
208 +};
209 +
210 +NETDATA.dygraphChartCreate = function (state, data) {
211 + if (NETDATA.options.debug.dygraph || state.debug) {
212 + state.log('dygraphChartCreate()');
213 + }
214 +
215 + state.tmp.dygraph_chart_type = NETDATA.dataAttribute(state.element, 'dygraph-type', state.chart.chart_type);
216 + if (state.tmp.dygraph_chart_type === 'stacked' && data.dimensions === 1) {
217 + state.tmp.dygraph_chart_type = 'area';
218 + }
219 + if (state.tmp.dygraph_chart_type === 'stacked' && NETDATA.chartLibraries.dygraph.isLogScale(state)) {
220 + state.tmp.dygraph_chart_type = 'area';
221 + }
222 +
223 + let highlightCircleSize = NETDATA.chartLibraries.dygraph.isSparkline(state) ? 3 : 4;
224 +
225 + let smooth = NETDATA.dygraph.smooth
226 + ? (NETDATA.dataAttributeBoolean(state.element, 'dygraph-smooth', (state.tmp.dygraph_chart_type === 'line' && NETDATA.chartLibraries.dygraph.isSparkline(state) === false)))
227 + : false;
228 +
229 + state.tmp.dygraph_include_zero = NETDATA.dataAttribute(state.element, 'dygraph-includezero', (state.tmp.dygraph_chart_type === 'stacked'));
230 + let drawAxis = NETDATA.dataAttributeBoolean(state.element, 'dygraph-drawaxis', true);
231 +
232 + state.tmp.dygraph_options = {
233 + colors: NETDATA.dataAttribute(state.element, 'dygraph-colors', state.chartColors()),
234 +
235 + // leave a few pixels empty on the right of the chart
236 + rightGap: NETDATA.dataAttribute(state.element, 'dygraph-rightgap', 5),
237 + showRangeSelector: NETDATA.dataAttributeBoolean(state.element, 'dygraph-showrangeselector', false),
238 + showRoller: NETDATA.dataAttributeBoolean(state.element, 'dygraph-showroller', false),
239 + title: NETDATA.dataAttribute(state.element, 'dygraph-title', state.title),
240 + titleHeight: NETDATA.dataAttribute(state.element, 'dygraph-titleheight', 19),
241 + legend: NETDATA.dataAttribute(state.element, 'dygraph-legend', 'always'), // we need this to get selection events
242 + labels: data.result.labels,
243 + labelsDiv: NETDATA.dataAttribute(state.element, 'dygraph-labelsdiv', state.element_legend_childs.hidden),
244 + //labelsDivStyles: NETDATA.dataAttribute(state.element, 'dygraph-labelsdivstyles', { 'fontSize':'1px' }),
245 + //labelsDivWidth: NETDATA.dataAttribute(state.element, 'dygraph-labelsdivwidth', state.chartWidth() - 70),
246 + labelsSeparateLines: NETDATA.dataAttributeBoolean(state.element, 'dygraph-labelsseparatelines', true),
247 + labelsShowZeroValues: NETDATA.chartLibraries.dygraph.isLogScale(state) ? false : NETDATA.dataAttributeBoolean(state.element, 'dygraph-labelsshowzerovalues', true),
248 + labelsKMB: false,
249 + labelsKMG2: false,
250 + showLabelsOnHighlight: NETDATA.dataAttributeBoolean(state.element, 'dygraph-showlabelsonhighlight', true),
251 + hideOverlayOnMouseOut: NETDATA.dataAttributeBoolean(state.element, 'dygraph-hideoverlayonmouseout', true),
252 + includeZero: state.tmp.dygraph_include_zero,
253 + xRangePad: NETDATA.dataAttribute(state.element, 'dygraph-xrangepad', 0),
254 + yRangePad: NETDATA.dataAttribute(state.element, 'dygraph-yrangepad', 1),
255 + valueRange: NETDATA.dataAttribute(state.element, 'dygraph-valuerange', [null, null]),
256 + ylabel: state.units_current, // (state.units_desired === 'auto')?"":state.units_current,
257 + yLabelWidth: NETDATA.dataAttribute(state.element, 'dygraph-ylabelwidth', 12),
258 +
259 + // the function to plot the chart
260 + plotter: null,
261 +
262 + // The width of the lines connecting data points.
263 + // This can be used to increase the contrast or some graphs.
264 + strokeWidth: NETDATA.dataAttribute(state.element, 'dygraph-strokewidth', ((state.tmp.dygraph_chart_type === 'stacked') ? 0.1 : ((smooth === true) ? 1.5 : 0.7))),
265 + strokePattern: NETDATA.dataAttribute(state.element, 'dygraph-strokepattern', undefined),
266 +
267 + // The size of the dot to draw on each point in pixels (see drawPoints).
268 + // A dot is always drawn when a point is "isolated",
269 + // i.e. there is a missing point on either side of it.
270 + // This also controls the size of those dots.
271 + drawPoints: NETDATA.dataAttributeBoolean(state.element, 'dygraph-drawpoints', false),
272 +
273 + // Draw points at the edges of gaps in the data.
274 + // This improves visibility of small data segments or other data irregularities.
275 + drawGapEdgePoints: NETDATA.dataAttributeBoolean(state.element, 'dygraph-drawgapedgepoints', true),
276 + connectSeparatedPoints: NETDATA.chartLibraries.dygraph.isLogScale(state) ? false : NETDATA.dataAttributeBoolean(state.element, 'dygraph-connectseparatedpoints', false),
277 + pointSize: NETDATA.dataAttribute(state.element, 'dygraph-pointsize', 1),
278 +
279 + // enabling this makes the chart with little square lines
280 + stepPlot: NETDATA.dataAttributeBoolean(state.element, 'dygraph-stepplot', false),
281 +
282 + // Draw a border around graph lines to make crossing lines more easily
283 + // distinguishable. Useful for graphs with many lines.
284 + strokeBorderColor: NETDATA.dataAttribute(state.element, 'dygraph-strokebordercolor', NETDATA.themes.current.background),
285 + strokeBorderWidth: NETDATA.dataAttribute(state.element, 'dygraph-strokeborderwidth', (state.tmp.dygraph_chart_type === 'stacked') ? 0.0 : 0.0),
286 + fillGraph: NETDATA.dataAttribute(state.element, 'dygraph-fillgraph', (state.tmp.dygraph_chart_type === 'area' || state.tmp.dygraph_chart_type === 'stacked')),
287 + fillAlpha: NETDATA.dataAttribute(state.element, 'dygraph-fillalpha',
288 + ((state.tmp.dygraph_chart_type === 'stacked')
289 + ? NETDATA.options.current.color_fill_opacity_stacked
290 + : NETDATA.options.current.color_fill_opacity_area)
291 + ),
292 + stackedGraph: NETDATA.dataAttribute(state.element, 'dygraph-stackedgraph', (state.tmp.dygraph_chart_type === 'stacked')),
293 + stackedGraphNaNFill: NETDATA.dataAttribute(state.element, 'dygraph-stackedgraphnanfill', 'none'),
294 + drawAxis: drawAxis,
295 + axisLabelFontSize: NETDATA.dataAttribute(state.element, 'dygraph-axislabelfontsize', 10),
296 + axisLineColor: NETDATA.dataAttribute(state.element, 'dygraph-axislinecolor', NETDATA.themes.current.axis),
297 + axisLineWidth: NETDATA.dataAttribute(state.element, 'dygraph-axislinewidth', 1.0),
298 + drawGrid: NETDATA.dataAttributeBoolean(state.element, 'dygraph-drawgrid', true),
299 + gridLinePattern: NETDATA.dataAttribute(state.element, 'dygraph-gridlinepattern', null),
300 + gridLineWidth: NETDATA.dataAttribute(state.element, 'dygraph-gridlinewidth', 1.0),
301 + gridLineColor: NETDATA.dataAttribute(state.element, 'dygraph-gridlinecolor', NETDATA.themes.current.grid),
302 + maxNumberWidth: NETDATA.dataAttribute(state.element, 'dygraph-maxnumberwidth', 8),
303 + sigFigs: NETDATA.dataAttribute(state.element, 'dygraph-sigfigs', null),
304 + digitsAfterDecimal: NETDATA.dataAttribute(state.element, 'dygraph-digitsafterdecimal', 2),
305 + valueFormatter: NETDATA.dataAttribute(state.element, 'dygraph-valueformatter', undefined),
306 + highlightCircleSize: NETDATA.dataAttribute(state.element, 'dygraph-highlightcirclesize', highlightCircleSize),
307 + highlightSeriesOpts: NETDATA.dataAttribute(state.element, 'dygraph-highlightseriesopts', null), // TOO SLOW: { strokeWidth: 1.5 },
308 + highlightSeriesBackgroundAlpha: NETDATA.dataAttribute(state.element, 'dygraph-highlightseriesbackgroundalpha', null), // TOO SLOW: (state.tmp.dygraph_chart_type === 'stacked')?0.7:0.5,
309 + pointClickCallback: NETDATA.dataAttribute(state.element, 'dygraph-pointclickcallback', undefined),
310 + visibility: state.dimensions_visibility.selected2BooleanArray(state.data.dimension_names),
311 + logscale: NETDATA.chartLibraries.dygraph.isLogScale(state) ? 'y' : undefined,
312 +
313 + axes: {
314 + x: {
315 + pixelsPerLabel: NETDATA.dataAttribute(state.element, 'dygraph-xpixelsperlabel', 50),
316 + ticker: Dygraph.dateTicker,
317 + axisLabelWidth: NETDATA.dataAttribute(state.element, 'dygraph-xaxislabelwidth', 60),
318 + drawAxis: NETDATA.dataAttributeBoolean(state.element, 'dygraph-drawxaxis', drawAxis),
319 + axisLabelFormatter: function (d, gran) {
320 + void(gran);
321 + return NETDATA.dateTime.xAxisTimeString(d);
322 + }
323 + },
324 + y: {
325 + logscale: NETDATA.chartLibraries.dygraph.isLogScale(state) ? true : undefined,
326 + pixelsPerLabel: NETDATA.dataAttribute(state.element, 'dygraph-ypixelsperlabel', 15),
327 + axisLabelWidth: NETDATA.dataAttribute(state.element, 'dygraph-yaxislabelwidth', 50),
328 + drawAxis: NETDATA.dataAttributeBoolean(state.element, 'dygraph-drawyaxis', drawAxis),
329 + axisLabelFormatter: function (y) {
330 +
331 + // unfortunately, we have to call this every single time
332 + state.legendFormatValueDecimalsFromMinMax(
333 + this.axes_[0].extremeRange[0],
334 + this.axes_[0].extremeRange[1]
335 + );
336 +
337 + let old_units = this.user_attrs_.ylabel;
338 + let v = state.legendFormatValue(y);
339 + let new_units = state.units_current;
340 +
341 + if (state.units_desired === 'auto' && typeof old_units !== 'undefined' && new_units !== old_units && !NETDATA.chartLibraries.dygraph.isSparkline(state)) {
342 + // console.log(this);
343 + // state.log('units discrepancy: old = ' + old_units + ', new = ' + new_units);
344 + let len = this.plugins_.length;
345 + while (len--) {
346 + // console.log(this.plugins_[len]);
347 + if (typeof this.plugins_[len].plugin.ylabel_div_ !== 'undefined'
348 + && this.plugins_[len].plugin.ylabel_div_ !== null
349 + && typeof this.plugins_[len].plugin.ylabel_div_.children !== 'undefined'
350 + && this.plugins_[len].plugin.ylabel_div_.children !== null
351 + && typeof this.plugins_[len].plugin.ylabel_div_.children[0].children !== 'undefined'
352 + && this.plugins_[len].plugin.ylabel_div_.children[0].children !== null
353 + ) {
354 + this.plugins_[len].plugin.ylabel_div_.children[0].children[0].innerHTML = new_units;
355 + this.user_attrs_.ylabel = new_units;
356 + break;
357 + }
358 + }
359 +
360 + if (len < 0) {
361 + state.log('units discrepancy, but cannot find dygraphs div to change: old = ' + old_units + ', new = ' + new_units);
362 + }
363 + }
364 +
365 + return v;
366 + }
367 + }
368 + },
369 + legendFormatter: function (data) {
370 + if (state.tmp.dygraph_mouse_down) {
371 + return;
372 + }
373 +
374 + let elements = state.element_legend_childs;
375 +
376 + // if the hidden div is not there
377 + // we are not managing the legend
378 + if (elements.hidden === null) {
379 + return;
380 + }
381 +
382 + if (typeof data.x !== 'undefined') {
383 + state.legendSetDate(data.x);
384 + let i = data.series.length;
385 + while (i--) {
386 + let series = data.series[i];
387 + if (series.isVisible) {
388 + state.legendSetLabelValue(series.label, series.y);
389 + } else {
390 + state.legendSetLabelValue(series.label, null);
391 + }
392 + }
393 + }
394 +
395 + return '';
396 + },
397 + drawCallback: function (dygraph, is_initial) {
398 +
399 + // the user has panned the chart and this is called to re-draw the chart
400 + // 1. refresh this chart by adding data to it
401 + // 2. notify all the other charts about the update they need
402 +
403 + // to prevent an infinite loop (feedback), we use
404 + // state.tmp.dygraph_user_action
405 + // - when true, this is initiated by a user
406 + // - when false, this is feedback
407 +
408 + if (state.current.name !== 'auto' && state.tmp.dygraph_user_action) {
409 + state.tmp.dygraph_user_action = false;
410 +
411 + let x_range = dygraph.xAxisRange();
412 + let after = Math.round(x_range[0]);
413 + let before = Math.round(x_range[1]);
414 +
415 + if (NETDATA.options.debug.dygraph) {
416 + state.log('dygraphDrawCallback(dygraph, ' + is_initial + '): mode ' + state.current.name + ' ' + (after / 1000).toString() + ' - ' + (before / 1000).toString());
417 + //console.log(state);
418 + }
419 +
420 + if (before <= state.netdata_last && after >= state.netdata_first) {
421 + // update only when we are within the data limits
422 + state.updateChartPanOrZoom(after, before);
423 + }
424 + }
425 + },
426 + zoomCallback: function (minDate, maxDate, yRanges) {
427 +
428 + // the user has selected a range on the chart
429 + // 1. refresh this chart by adding data to it
430 + // 2. notify all the other charts about the update they need
431 +
432 + void(yRanges);
433 +
434 + if (NETDATA.options.debug.dygraph) {
435 + state.log('dygraphZoomCallback(): ' + state.current.name);
436 + }
437 +
438 + NETDATA.globalSelectionSync.stop();
439 + NETDATA.globalSelectionSync.delay();
440 + state.setMode('zoom');
441 +
442 + // refresh it to the greatest possible zoom level
443 + state.tmp.dygraph_user_action = true;
444 + state.tmp.dygraph_force_zoom = true;
445 + state.updateChartPanOrZoom(minDate, maxDate);
446 + },
447 + highlightCallback: function (event, x, points, row, seriesName) {
448 + void(seriesName);
449 +
450 + state.pauseChart();
451 +
452 + // there is a bug in dygraph when the chart is zoomed enough
453 + // the time it thinks is selected is wrong
454 + // here we calculate the time t based on the row number selected
455 + // which is ok
456 + // let t = state.data_after + row * state.data_update_every;
457 + // console.log('row = ' + row + ', x = ' + x + ', t = ' + t + ' ' + ((t === x)?'SAME':(Math.abs(x-t)<=state.data_update_every)?'SIMILAR':'DIFFERENT') + ', rows in db: ' + state.data_points + ' visible(x) = ' + state.timeIsVisible(x) + ' visible(t) = ' + state.timeIsVisible(t) + ' r(x) = ' + state.calculateRowForTime(x) + ' r(t) = ' + state.calculateRowForTime(t) + ' range: ' + state.data_after + ' - ' + state.data_before + ' real: ' + state.data.after + ' - ' + state.data.before + ' every: ' + state.data_update_every);
458 +
459 + if (state.tmp.dygraph_mouse_down !== true) {
460 + NETDATA.globalSelectionSync.sync(state, x);
461 + }
462 +
463 + // fix legend zIndex using the internal structures of dygraph legend module
464 + // this works, but it is a hack!
465 + // state.tmp.dygraph_instance.plugins_[0].plugin.legend_div_.style.zIndex = 10000;
466 + },
467 + unhighlightCallback: function (event) {
468 + void(event);
469 +
470 + if (state.tmp.dygraph_mouse_down) {
471 + return;
472 + }
473 +
474 + if (NETDATA.options.debug.dygraph || state.debug) {
475 + state.log('dygraphUnhighlightCallback()');
476 + }
477 +
478 + state.unpauseChart();
479 + NETDATA.globalSelectionSync.stop();
480 + },
481 + underlayCallback: function (canvas, area, g) {
482 +
483 + // the chart is about to be drawn
484 + // this function renders global highlighted time-frame
485 +
486 + if (NETDATA.globalChartUnderlay.isActive()) {
487 + let after = NETDATA.globalChartUnderlay.after;
488 + let before = NETDATA.globalChartUnderlay.before;
489 +
490 + if (after < state.view_after) {
491 + after = state.view_after;
492 + }
493 +
494 + if (before > state.view_before) {
495 + before = state.view_before;
496 + }
497 +
498 + if (after < before) {
499 + let bottom_left = g.toDomCoords(after, -20);
500 + let top_right = g.toDomCoords(before, +20);
501 +
502 + let left = bottom_left[0];
503 + let right = top_right[0];
504 +
505 + canvas.fillStyle = NETDATA.themes.current.highlight;
506 + canvas.fillRect(left, area.y, right - left, area.h);
507 + }
508 + }
509 + },
510 + interactionModel: {
511 + mousedown: function (event, dygraph, context) {
512 + if (NETDATA.options.debug.dygraph || state.debug) {
513 + state.log('interactionModel.mousedown()');
514 + }
515 +
516 + state.tmp.dygraph_user_action = true;
517 +
518 + if (NETDATA.options.debug.dygraph) {
519 + state.log('dygraphMouseDown()');
520 + }
521 +
522 + // Right-click should not initiate anything.
523 + if (event.button && event.button === 2) {
524 + return;
525 + }
526 +
527 + NETDATA.globalSelectionSync.stop();
528 + NETDATA.globalSelectionSync.delay();
529 +
530 + state.tmp.dygraph_mouse_down = true;
531 + context.initializeMouseDown(event, dygraph, context);
532 +
533 + //console.log(event);
534 + if (event.button && event.button === 1) {
535 + if (event.shiftKey) {
536 + //console.log('middle mouse button dragging (PAN)');
537 +
538 + state.setMode('pan');
539 + // NETDATA.globalSelectionSync.delay();
540 + state.tmp.dygraph_highlight_after = null;
541 + Dygraph.startPan(event, dygraph, context);
542 + } else if (event.altKey || event.ctrlKey || event.metaKey) {
543 + //console.log('middle mouse button highlight');
544 +
545 + if (!(event.offsetX && event.offsetY)) {
546 + event.offsetX = event.layerX - event.target.offsetLeft;
547 + event.offsetY = event.layerY - event.target.offsetTop;
548 + }
549 + state.tmp.dygraph_highlight_after = dygraph.toDataXCoord(event.offsetX);
550 + Dygraph.startZoom(event, dygraph, context);
551 + } else {
552 + //console.log('middle mouse button selection for zoom (ZOOM)');
553 +
554 + state.setMode('zoom');
555 + // NETDATA.globalSelectionSync.delay();
556 + state.tmp.dygraph_highlight_after = null;
557 + Dygraph.startZoom(event, dygraph, context);
558 + }
559 + } else {
560 + if (event.shiftKey) {
561 + //console.log('left mouse button selection for zoom (ZOOM)');
562 +
563 + state.setMode('zoom');
564 + // NETDATA.globalSelectionSync.delay();
565 + state.tmp.dygraph_highlight_after = null;
566 + Dygraph.startZoom(event, dygraph, context);
567 + } else if (event.altKey || event.ctrlKey || event.metaKey) {
568 + //console.log('left mouse button highlight');
569 +
570 + if (!(event.offsetX && event.offsetY)) {
571 + event.offsetX = event.layerX - event.target.offsetLeft;
572 + event.offsetY = event.layerY - event.target.offsetTop;
573 + }
574 + state.tmp.dygraph_highlight_after = dygraph.toDataXCoord(event.offsetX);
575 + Dygraph.startZoom(event, dygraph, context);
576 + } else {
577 + //console.log('left mouse button dragging (PAN)');
578 +
579 + state.setMode('pan');
580 + // NETDATA.globalSelectionSync.delay();
581 + state.tmp.dygraph_highlight_after = null;
582 + Dygraph.startPan(event, dygraph, context);
583 + }
584 + }
585 + },
586 + mousemove: function (event, dygraph, context) {
587 + if (NETDATA.options.debug.dygraph || state.debug) {
588 + state.log('interactionModel.mousemove()');
589 + }
590 +
591 + if (state.tmp.dygraph_highlight_after !== null) {
592 + //console.log('highlight selection...');
593 +
594 + NETDATA.globalSelectionSync.stop();
595 + NETDATA.globalSelectionSync.delay();
596 +
597 + state.tmp.dygraph_user_action = true;
598 + Dygraph.moveZoom(event, dygraph, context);
599 + event.preventDefault();
600 + } else if (context.isPanning) {
601 + //console.log('panning...');
602 +
603 + NETDATA.globalSelectionSync.stop();
604 + NETDATA.globalSelectionSync.delay();
605 +
606 + state.tmp.dygraph_user_action = true;
607 + //NETDATA.globalSelectionSync.stop();
608 + //NETDATA.globalSelectionSync.delay();
609 + state.setMode('pan');
610 + context.is2DPan = false;
611 + Dygraph.movePan(event, dygraph, context);
612 + } else if (context.isZooming) {
613 + //console.log('zooming...');
614 +
615 + NETDATA.globalSelectionSync.stop();
616 + NETDATA.globalSelectionSync.delay();
617 +
618 + state.tmp.dygraph_user_action = true;
619 + //NETDATA.globalSelectionSync.stop();
620 + //NETDATA.globalSelectionSync.delay();
621 + state.setMode('zoom');
622 + Dygraph.moveZoom(event, dygraph, context);
623 + }
624 + },
625 + mouseup: function (event, dygraph, context) {
626 + state.tmp.dygraph_mouse_down = false;
627 +
628 + if (NETDATA.options.debug.dygraph || state.debug) {
629 + state.log('interactionModel.mouseup()');
630 + }
631 +
632 + if (state.tmp.dygraph_highlight_after !== null) {
633 + //console.log('done highlight selection');
634 +
635 + NETDATA.globalSelectionSync.stop();
636 + NETDATA.globalSelectionSync.delay();
637 +
638 + if (!(event.offsetX && event.offsetY)) {
639 + event.offsetX = event.layerX - event.target.offsetLeft;
640 + event.offsetY = event.layerY - event.target.offsetTop;
641 + }
642 +
643 + NETDATA.globalChartUnderlay.set(state
644 + , state.tmp.dygraph_highlight_after
645 + , dygraph.toDataXCoord(event.offsetX)
646 + , state.view_after
647 + , state.view_before
648 + );
649 +
650 + state.tmp.dygraph_highlight_after = null;
651 +
652 + context.isZooming = false;
653 + dygraph.clearZoomRect_();
654 + dygraph.drawGraph_(false);
655 +
656 + // refresh all the charts immediately
657 + NETDATA.options.auto_refresher_stop_until = 0;
658 + } else if (context.isPanning) {
659 + //console.log('done panning');
660 +
661 + NETDATA.globalSelectionSync.stop();
662 + NETDATA.globalSelectionSync.delay();
663 +
664 + state.tmp.dygraph_user_action = true;
665 + Dygraph.endPan(event, dygraph, context);
666 +
667 + // refresh all the charts immediately
668 + NETDATA.options.auto_refresher_stop_until = 0;
669 + } else if (context.isZooming) {
670 + //console.log('done zomming');
671 +
672 + NETDATA.globalSelectionSync.stop();
673 + NETDATA.globalSelectionSync.delay();
674 +
675 + state.tmp.dygraph_user_action = true;
676 + Dygraph.endZoom(event, dygraph, context);
677 +
678 + // refresh all the charts immediately
679 + NETDATA.options.auto_refresher_stop_until = 0;
680 + }
681 + },
682 + click: function (event, dygraph, context) {
683 + void(dygraph);
684 + void(context);
685 +
686 + if (NETDATA.options.debug.dygraph || state.debug) {
687 + state.log('interactionModel.click()');
688 + }
689 +
690 + event.preventDefault();
691 + },
692 + dblclick: function (event, dygraph, context) {
693 + void(event);
694 + void(dygraph);
695 + void(context);
696 +
697 + if (NETDATA.options.debug.dygraph || state.debug) {
698 + state.log('interactionModel.dblclick()');
699 + }
700 + NETDATA.resetAllCharts(state);
701 + },
702 + wheel: function (event, dygraph, context) {
703 + void(context);
704 +
705 + if (NETDATA.options.debug.dygraph || state.debug) {
706 + state.log('interactionModel.wheel()');
707 + }
708 +
709 + // Take the offset of a mouse event on the dygraph canvas and
710 + // convert it to a pair of percentages from the bottom left.
711 + // (Not top left, bottom is where the lower value is.)
712 + function offsetToPercentage(g, offsetX, offsetY) {
713 + // This is calculating the pixel offset of the leftmost date.
714 + let xOffset = g.toDomCoords(g.xAxisRange()[0], null)[0];
715 + let yar0 = g.yAxisRange(0);
716 +
717 + // This is calculating the pixel of the highest value. (Top pixel)
718 + let yOffset = g.toDomCoords(null, yar0[1])[1];
719 +
720 + // x y w and h are relative to the corner of the drawing area,
721 + // so that the upper corner of the drawing area is (0, 0).
722 + let x = offsetX - xOffset;
723 + let y = offsetY - yOffset;
724 +
725 + // This is computing the rightmost pixel, effectively defining the
726 + // width.
727 + let w = g.toDomCoords(g.xAxisRange()[1], null)[0] - xOffset;
728 +
729 + // This is computing the lowest pixel, effectively defining the height.
730 + let h = g.toDomCoords(null, yar0[0])[1] - yOffset;
731 +
732 + // Percentage from the left.
733 + let xPct = w === 0 ? 0 : (x / w);
734 + // Percentage from the top.
735 + let yPct = h === 0 ? 0 : (y / h);
736 +
737 + // The (1-) part below changes it from "% distance down from the top"
738 + // to "% distance up from the bottom".
739 + return [xPct, (1 - yPct)];
740 + }
741 +
742 + // Adjusts [x, y] toward each other by zoomInPercentage%
743 + // Split it so the left/bottom axis gets xBias/yBias of that change and
744 + // tight/top gets (1-xBias)/(1-yBias) of that change.
745 + //
746 + // If a bias is missing it splits it down the middle.
747 + function zoomRange(g, zoomInPercentage, xBias, yBias) {
748 + xBias = xBias || 0.5;
749 + yBias = yBias || 0.5;
750 +
751 + function adjustAxis(axis, zoomInPercentage, bias) {
752 + let delta = axis[1] - axis[0];
753 + let increment = delta * zoomInPercentage;
754 + let foo = [increment * bias, increment * (1 - bias)];
755 +
756 + return [axis[0] + foo[0], axis[1] - foo[1]];
757 + }
758 +
759 + let yAxes = g.yAxisRanges();
760 + let newYAxes = [];
761 + for (let i = 0; i < yAxes.length; i++) {
762 + newYAxes[i] = adjustAxis(yAxes[i], zoomInPercentage, yBias);
763 + }
764 +
765 + return adjustAxis(g.xAxisRange(), zoomInPercentage, xBias);
766 + }
767 +
768 + if (event.altKey || event.shiftKey) {
769 + state.tmp.dygraph_user_action = true;
770 +
771 + NETDATA.globalSelectionSync.stop();
772 + NETDATA.globalSelectionSync.delay();
773 +
774 + // http://dygraphs.com/gallery/interaction-api.js
775 + let normal_def;
776 + if (typeof event.wheelDelta === 'number' && !isNaN(event.wheelDelta))
777 + // chrome
778 + {
779 + normal_def = event.wheelDelta / 40;
780 + } else
781 + // firefox
782 + {
783 + normal_def = event.deltaY * -1.2;
784 + }
785 +
786 + let normal = (event.detail) ? event.detail * -1 : normal_def;
787 + let percentage = normal / 50;
788 +
789 + if (!(event.offsetX && event.offsetY)) {
790 + event.offsetX = event.layerX - event.target.offsetLeft;
791 + event.offsetY = event.layerY - event.target.offsetTop;
792 + }
793 +
794 + let percentages = offsetToPercentage(dygraph, event.offsetX, event.offsetY);
795 + let xPct = percentages[0];
796 + let yPct = percentages[1];
797 +
798 + let new_x_range = zoomRange(dygraph, percentage, xPct, yPct);
799 + let after = new_x_range[0];
800 + let before = new_x_range[1];
801 +
802 + let first = state.netdata_first + state.data_update_every;
803 + let last = state.netdata_last + state.data_update_every;
804 +
805 + if (before > last) {
806 + after -= (before - last);
807 + before = last;
808 + }
809 + if (after < first) {
810 + after = first;
811 + }
812 +
813 + state.setMode('zoom');
814 + state.updateChartPanOrZoom(after, before, function () {
815 + dygraph.updateOptions({dateWindow: [after, before]});
816 + });
817 +
818 + event.preventDefault();
819 + }
820 + },
821 + touchstart: function (event, dygraph, context) {
822 + state.tmp.dygraph_mouse_down = true;
823 +
824 + if (NETDATA.options.debug.dygraph || state.debug) {
825 + state.log('interactionModel.touchstart()');
826 + }
827 +
828 + state.tmp.dygraph_user_action = true;
829 + state.setMode('zoom');
830 + state.pauseChart();
831 +
832 + NETDATA.globalSelectionSync.stop();
833 + NETDATA.globalSelectionSync.delay();
834 +
835 + Dygraph.defaultInteractionModel.touchstart(event, dygraph, context);
836 +
837 + // we overwrite the touch directions at the end, to overwrite
838 + // the internal default of dygraph
839 + context.touchDirections = {x: true, y: false};
840 +
841 + state.dygraph_last_touch_start = Date.now();
842 + state.dygraph_last_touch_move = 0;
843 +
844 + if (typeof event.touches[0].pageX === 'number') {
845 + state.dygraph_last_touch_page_x = event.touches[0].pageX;
846 + } else {
847 + state.dygraph_last_touch_page_x = 0;
848 + }
849 + },
850 + touchmove: function (event, dygraph, context) {
851 + if (NETDATA.options.debug.dygraph || state.debug) {
852 + state.log('interactionModel.touchmove()');
853 + }
854 +
855 + NETDATA.globalSelectionSync.stop();
856 + NETDATA.globalSelectionSync.delay();
857 +
858 + state.tmp.dygraph_user_action = true;
859 + Dygraph.defaultInteractionModel.touchmove(event, dygraph, context);
860 +
861 + state.dygraph_last_touch_move = Date.now();
862 + },
863 + touchend: function (event, dygraph, context) {
864 + state.tmp.dygraph_mouse_down = false;
865 +
866 + if (NETDATA.options.debug.dygraph || state.debug) {
867 + state.log('interactionModel.touchend()');
868 + }
869 +
870 + NETDATA.globalSelectionSync.stop();
871 + NETDATA.globalSelectionSync.delay();
872 +
873 + state.tmp.dygraph_user_action = true;
874 + Dygraph.defaultInteractionModel.touchend(event, dygraph, context);
875 +
876 + // if it didn't move, it is a selection
877 + if (state.dygraph_last_touch_move === 0 && state.dygraph_last_touch_page_x !== 0) {
878 + NETDATA.globalSelectionSync.dont_sync_before = 0;
879 + NETDATA.globalSelectionSync.setMaster(state);
880 +
881 + // internal api of dygraph
882 + let pct = (state.dygraph_last_touch_page_x - (dygraph.plotter_.area.x + state.element.getBoundingClientRect().left)) / dygraph.plotter_.area.w;
883 + console.log('pct: ' + pct.toString());
884 +
885 + let t = Math.round(state.view_after + (state.view_before - state.view_after) * pct);
886 + if (NETDATA.dygraphSetSelection(state, t)) {
887 + NETDATA.globalSelectionSync.sync(state, t);
888 + }
889 + }
890 +
891 + // if it was double tap within double click time, reset the charts
892 + let now = Date.now();
893 + if (typeof state.dygraph_last_touch_end !== 'undefined') {
894 + if (state.dygraph_last_touch_move === 0) {
895 + let dt = now - state.dygraph_last_touch_end;
896 + if (dt <= NETDATA.options.current.double_click_speed) {
897 + NETDATA.resetAllCharts(state);
898 + }
899 + }
900 + }
901 +
902 + // remember the timestamp of the last touch end
903 + state.dygraph_last_touch_end = now;
904 +
905 + // refresh all the charts immediately
906 + NETDATA.options.auto_refresher_stop_until = 0;
907 + }
908 + }
909 + };
910 +
911 + if (NETDATA.chartLibraries.dygraph.isLogScale(state)) {
912 + if (Array.isArray(state.tmp.dygraph_options.valueRange) && state.tmp.dygraph_options.valueRange[0] <= 0) {
913 + state.tmp.dygraph_options.valueRange[0] = null;
914 + }
915 + }
916 +
917 + if (NETDATA.chartLibraries.dygraph.isSparkline(state)) {
918 + state.tmp.dygraph_options.drawGrid = false;
919 + state.tmp.dygraph_options.drawAxis = false;
920 + state.tmp.dygraph_options.title = undefined;
921 + state.tmp.dygraph_options.ylabel = undefined;
922 + state.tmp.dygraph_options.yLabelWidth = 0;
923 + //state.tmp.dygraph_options.labelsDivWidth = 120;
924 + //state.tmp.dygraph_options.labelsDivStyles.width = '120px';
925 + state.tmp.dygraph_options.labelsSeparateLines = true;
926 + state.tmp.dygraph_options.rightGap = 0;
927 + state.tmp.dygraph_options.yRangePad = 1;
928 + state.tmp.dygraph_options.axes.x.drawAxis = false;
929 + state.tmp.dygraph_options.axes.y.drawAxis = false;
930 + }
931 +
932 + if (smooth) {
933 + state.tmp.dygraph_smooth_eligible = true;
934 +
935 + if (NETDATA.options.current.smooth_plot) {
936 + state.tmp.dygraph_options.plotter = smoothPlotter;
937 + }
938 + }
939 + else {
940 + state.tmp.dygraph_smooth_eligible = false;
941 + }
942 +
943 + if (netdataSnapshotData !== null && NETDATA.globalPanAndZoom.isActive() && NETDATA.globalPanAndZoom.isMaster(state) === false) {
944 + // pan and zoom on snapshots
945 + state.tmp.dygraph_options.dateWindow = [NETDATA.globalPanAndZoom.force_after_ms, NETDATA.globalPanAndZoom.force_before_ms];
946 + //state.tmp.dygraph_options.isZoomedIgnoreProgrammaticZoom = true;
947 + }
948 +
949 + state.tmp.dygraph_instance = new Dygraph(state.element_chart,
950 + data.result.data, state.tmp.dygraph_options);
951 +
952 + state.tmp.dygraph_force_zoom = false;
953 + state.tmp.dygraph_user_action = false;
954 + state.tmp.dygraph_last_rendered = Date.now();
955 + state.tmp.dygraph_highlight_after = null;
956 +
957 + if (state.tmp.dygraph_options.valueRange[0] === null && state.tmp.dygraph_options.valueRange[1] === null) {
958 + if (typeof state.tmp.dygraph_instance.axes_[0].extremeRange !== 'undefined') {
959 + state.tmp.__commonMin = NETDATA.dataAttribute(state.element, 'common-min', null);
960 + state.tmp.__commonMax = NETDATA.dataAttribute(state.element, 'common-max', null);
961 + } else {
962 + state.log('incompatible version of Dygraph detected');
963 + state.tmp.__commonMin = null;
964 + state.tmp.__commonMax = null;
965 + }
966 + } else {
967 + // if the user gave a valueRange, respect it
968 + state.tmp.__commonMin = null;
969 + state.tmp.__commonMax = null;
970 + }
971 +
972 + return true;
973 +};
web/gui/src/dashboard.js/charting/easy-pie-chart.js new
+281
@@ -0,0 +1,281 @@
1 +// ----------------------------------------------------------------------------------------------------------------
2 +
3 +NETDATA.easypiechartPercentFromValueMinMax = function (state, value, min, max) {
4 + if (typeof value !== 'number') {
5 + value = 0;
6 + }
7 + if (typeof min !== 'number') {
8 + min = 0;
9 + }
10 + if (typeof max !== 'number') {
11 + max = 0;
12 + }
13 +
14 + if (min > max) {
15 + let t = min;
16 + min = max;
17 + max = t;
18 + }
19 +
20 + if (min > value) {
21 + min = value;
22 + }
23 + if (max < value) {
24 + max = value;
25 + }
26 +
27 + state.legendFormatValueDecimalsFromMinMax(min, max);
28 +
29 + if (state.tmp.easyPieChartMin === null && min > 0) {
30 + min = 0;
31 + }
32 + if (state.tmp.easyPieChartMax === null && max < 0) {
33 + max = 0;
34 + }
35 +
36 + let pcent;
37 +
38 + if (min < 0 && max > 0) {
39 + // it is both positive and negative
40 + // zero at the top center of the chart
41 + max = (-min > max) ? -min : max;
42 + pcent = Math.round(value * 100 / max);
43 + } else if (value >= 0 && min >= 0 && max >= 0) {
44 + // clockwise
45 + pcent = Math.round((value - min) * 100 / (max - min));
46 + if (pcent === 0) {
47 + pcent = 0.1;
48 + }
49 + } else {
50 + // counter clockwise
51 + pcent = Math.round((value - max) * 100 / (max - min));
52 + if (pcent === 0) {
53 + pcent = -0.1;
54 + }
55 + }
56 +
57 + return pcent;
58 +};
59 +
60 +// ----------------------------------------------------------------------------------------------------------------
61 +// easy-pie-chart
62 +
63 +NETDATA.easypiechartInitialize = function (callback) {
64 + if (typeof netdataNoEasyPieChart === 'undefined' || !netdataNoEasyPieChart) {
65 + $.ajax({
66 + url: NETDATA.easypiechart_js,
67 + cache: true,
68 + dataType: "script",
69 + xhrFields: {withCredentials: true} // required for the cookie
70 + })
71 + .done(function () {
72 + NETDATA.registerChartLibrary('easypiechart', NETDATA.easypiechart_js);
73 + })
74 + .fail(function () {
75 + NETDATA.chartLibraries.easypiechart.enabled = false;
76 + NETDATA.error(100, NETDATA.easypiechart_js);
77 + })
78 + .always(function () {
79 + if (typeof callback === "function") {
80 + return callback();
81 + }
82 + })
83 + } else {
84 + NETDATA.chartLibraries.easypiechart.enabled = false;
85 + if (typeof callback === "function") {
86 + return callback();
87 + }
88 + }
89 +};
90 +
91 +NETDATA.easypiechartClearSelection = function (state, force) {
92 + if (typeof state.tmp.easyPieChartEvent !== 'undefined' && typeof state.tmp.easyPieChartEvent.timer !== 'undefined') {
93 + NETDATA.timeout.clear(state.tmp.easyPieChartEvent.timer);
94 + state.tmp.easyPieChartEvent.timer = undefined;
95 + }
96 +
97 + if (state.isAutoRefreshable() && state.data !== null && force !== true) {
98 + NETDATA.easypiechartChartUpdate(state, state.data);
99 + }
100 + else {
101 + state.tmp.easyPieChartLabel.innerText = state.legendFormatValue(null);
102 + state.tmp.easyPieChart_instance.update(0);
103 + }
104 + state.tmp.easyPieChart_instance.enableAnimation();
105 +
106 + return true;
107 +};
108 +
109 +NETDATA.easypiechartSetSelection = function (state, t) {
110 + if (state.timeIsVisible(t) !== true) {
111 + return NETDATA.easypiechartClearSelection(state, true);
112 + }
113 +
114 + let slot = state.calculateRowForTime(t);
115 + if (slot < 0 || slot >= state.data.result.length) {
116 + return NETDATA.easypiechartClearSelection(state, true);
117 + }
118 +
119 + if (typeof state.tmp.easyPieChartEvent === 'undefined') {
120 + state.tmp.easyPieChartEvent = {
121 + timer: undefined,
122 + value: 0,
123 + pcent: 0
124 + };
125 + }
126 +
127 + let value = state.data.result[state.data.result.length - 1 - slot];
128 + let min = (state.tmp.easyPieChartMin === null) ? NETDATA.commonMin.get(state) : state.tmp.easyPieChartMin;
129 + let max = (state.tmp.easyPieChartMax === null) ? NETDATA.commonMax.get(state) : state.tmp.easyPieChartMax;
130 + let pcent = NETDATA.easypiechartPercentFromValueMinMax(state, value, min, max);
131 +
132 + state.tmp.easyPieChartEvent.value = value;
133 + state.tmp.easyPieChartEvent.pcent = pcent;
134 + state.tmp.easyPieChartLabel.innerText = state.legendFormatValue(value);
135 +
136 + if (state.tmp.easyPieChartEvent.timer === undefined) {
137 + state.tmp.easyPieChart_instance.disableAnimation();
138 +
139 + state.tmp.easyPieChartEvent.timer = NETDATA.timeout.set(function () {
140 + state.tmp.easyPieChartEvent.timer = undefined;
141 + state.tmp.easyPieChart_instance.update(state.tmp.easyPieChartEvent.pcent);
142 + }, 0);
143 + }
144 +
145 + return true;
146 +};
147 +
148 +NETDATA.easypiechartChartUpdate = function (state, data) {
149 + let value, min, max, pcent;
150 +
151 + if (NETDATA.globalPanAndZoom.isActive() || state.isAutoRefreshable() === false) {
152 + value = null;
153 + pcent = 0;
154 + }
155 + else {
156 + value = data.result[0];
157 + min = (state.tmp.easyPieChartMin === null) ? NETDATA.commonMin.get(state) : state.tmp.easyPieChartMin;
158 + max = (state.tmp.easyPieChartMax === null) ? NETDATA.commonMax.get(state) : state.tmp.easyPieChartMax;
159 + pcent = NETDATA.easypiechartPercentFromValueMinMax(state, value, min, max);
160 + }
161 +
162 + state.tmp.easyPieChartLabel.innerText = state.legendFormatValue(value);
163 + state.tmp.easyPieChart_instance.update(pcent);
164 + return true;
165 +};
166 +
167 +NETDATA.easypiechartChartCreate = function (state, data) {
168 + let chart = $(state.element_chart);
169 +
170 + let value = data.result[0];
171 + let min = NETDATA.dataAttribute(state.element, 'easypiechart-min-value', null);
172 + let max = NETDATA.dataAttribute(state.element, 'easypiechart-max-value', null);
173 +
174 + if (min === null) {
175 + min = NETDATA.commonMin.get(state);
176 + state.tmp.easyPieChartMin = null;
177 + }
178 + else {
179 + state.tmp.easyPieChartMin = min;
180 + }
181 +
182 + if (max === null) {
183 + max = NETDATA.commonMax.get(state);
184 + state.tmp.easyPieChartMax = null;
185 + }
186 + else {
187 + state.tmp.easyPieChartMax = max;
188 + }
189 +
190 + let size = state.chartWidth();
191 + let stroke = Math.floor(size / 22);
192 + if (stroke < 3) {
193 + stroke = 2;
194 + }
195 +
196 + let valuefontsize = Math.floor((size * 2 / 3) / 5);
197 + let valuetop = Math.round((size - valuefontsize - (size / 40)) / 2);
198 + state.tmp.easyPieChartLabel = document.createElement('span');
199 + state.tmp.easyPieChartLabel.className = 'easyPieChartLabel';
200 + state.tmp.easyPieChartLabel.innerText = state.legendFormatValue(value);
201 + state.tmp.easyPieChartLabel.style.fontSize = valuefontsize + 'px';
202 + state.tmp.easyPieChartLabel.style.top = valuetop.toString() + 'px';
203 + state.element_chart.appendChild(state.tmp.easyPieChartLabel);
204 +
205 + let titlefontsize = Math.round(valuefontsize * 1.6 / 3);
206 + let titletop = Math.round(valuetop - (titlefontsize * 2) - (size / 40));
207 + state.tmp.easyPieChartTitle = document.createElement('span');
208 + state.tmp.easyPieChartTitle.className = 'easyPieChartTitle';
209 + state.tmp.easyPieChartTitle.innerText = state.title;
210 + state.tmp.easyPieChartTitle.style.fontSize = titlefontsize + 'px';
211 + state.tmp.easyPieChartTitle.style.lineHeight = titlefontsize + 'px';
212 + state.tmp.easyPieChartTitle.style.top = titletop.toString() + 'px';
213 + state.element_chart.appendChild(state.tmp.easyPieChartTitle);
214 +
215 + let unitfontsize = Math.round(titlefontsize * 0.9);
216 + let unittop = Math.round(valuetop + (valuefontsize + unitfontsize) + (size / 40));
217 + state.tmp.easyPieChartUnits = document.createElement('span');
218 + state.tmp.easyPieChartUnits.className = 'easyPieChartUnits';
219 + state.tmp.easyPieChartUnits.innerText = state.units_current;
220 + state.tmp.easyPieChartUnits.style.fontSize = unitfontsize + 'px';
221 + state.tmp.easyPieChartUnits.style.top = unittop.toString() + 'px';
222 + state.element_chart.appendChild(state.tmp.easyPieChartUnits);
223 +
224 + let barColor = NETDATA.dataAttribute(state.element, 'easypiechart-barcolor', undefined);
225 + if (typeof barColor === 'undefined' || barColor === null) {
226 + barColor = state.chartCustomColors()[0];
227 + } else {
228 + // <div ... data-easypiechart-barcolor="(function(percent){return(percent < 50 ? '#5cb85c' : percent < 85 ? '#f0ad4e' : '#cb3935');})" ...></div>
229 + let tmp = eval(barColor);
230 + if (typeof tmp === 'function') {
231 + barColor = tmp;
232 + }
233 + }
234 +
235 + let pcent = NETDATA.easypiechartPercentFromValueMinMax(state, value, min, max);
236 + chart.data('data-percent', pcent);
237 +
238 + chart.easyPieChart({
239 + barColor: barColor,
240 + trackColor: NETDATA.dataAttribute(state.element, 'easypiechart-trackcolor', NETDATA.themes.current.easypiechart_track),
241 + scaleColor: NETDATA.dataAttribute(state.element, 'easypiechart-scalecolor', NETDATA.themes.current.easypiechart_scale),
242 + scaleLength: NETDATA.dataAttribute(state.element, 'easypiechart-scalelength', 5),
243 + lineCap: NETDATA.dataAttribute(state.element, 'easypiechart-linecap', 'round'),
244 + lineWidth: NETDATA.dataAttribute(state.element, 'easypiechart-linewidth', stroke),
245 + trackWidth: NETDATA.dataAttribute(state.element, 'easypiechart-trackwidth', undefined),
246 + size: NETDATA.dataAttribute(state.element, 'easypiechart-size', size),
247 + rotate: NETDATA.dataAttribute(state.element, 'easypiechart-rotate', 0),
248 + animate: NETDATA.dataAttribute(state.element, 'easypiechart-animate', {duration: 500, enabled: true}),
249 + easing: NETDATA.dataAttribute(state.element, 'easypiechart-easing', undefined)
250 + });
251 +
252 + // when we just re-create the chart
253 + // do not animate the first update
254 + let animate = true;
255 + if (typeof state.tmp.easyPieChart_instance !== 'undefined') {
256 + animate = false;
257 + }
258 +
259 + state.tmp.easyPieChart_instance = chart.data('easyPieChart');
260 + if (animate === false) {
261 + state.tmp.easyPieChart_instance.disableAnimation();
262 + }
263 + state.tmp.easyPieChart_instance.update(pcent);
264 + if (animate === false) {
265 + state.tmp.easyPieChart_instance.enableAnimation();
266 + }
267 +
268 + state.legendSetUnitsString = function (units) {
269 + if (typeof state.tmp.easyPieChartUnits !== 'undefined' && state.tmp.units !== units) {
270 + state.tmp.easyPieChartUnits.innerText = units;
271 + state.tmp.units = units;
272 + }
273 + };
274 + state.legendShowUndefined = function () {
275 + if (typeof state.tmp.easyPieChart_instance !== 'undefined') {
276 + NETDATA.easypiechartClearSelection(state);
277 + }
278 + };
279 +
280 + return true;
281 +};
web/gui/src/dashboard.js/charting/gauge.js new
+406
@@ -0,0 +1,406 @@
1 +// gauge.js
2 +
3 +NETDATA.gaugeInitialize = function (callback) {
4 + if (typeof netdataNoGauge === 'undefined' || !netdataNoGauge) {
5 + $.ajax({
6 + url: NETDATA.gauge_js,
7 + cache: true,
8 + dataType: "script",
9 + xhrFields: {withCredentials: true} // required for the cookie
10 + })
11 + .done(function () {
12 + NETDATA.registerChartLibrary('gauge', NETDATA.gauge_js);
13 + })
14 + .fail(function () {
15 + NETDATA.chartLibraries.gauge.enabled = false;
16 + NETDATA.error(100, NETDATA.gauge_js);
17 + })
18 + .always(function () {
19 + if (typeof callback === "function") {
20 + return callback();
21 + }
22 + })
23 + }
24 + else {
25 + NETDATA.chartLibraries.gauge.enabled = false;
26 + if (typeof callback === "function") {
27 + return callback();
28 + }
29 + }
30 +};
31 +
32 +NETDATA.gaugeAnimation = function (state, status) {
33 + let speed = 32;
34 +
35 + if (typeof status === 'boolean' && status === false) {
36 + speed = 1000000000;
37 + } else if (typeof status === 'number') {
38 + speed = status;
39 + }
40 +
41 + // console.log('gauge speed ' + speed);
42 + state.tmp.gauge_instance.animationSpeed = speed;
43 + state.tmp.___gaugeOld__.speed = speed;
44 +};
45 +
46 +NETDATA.gaugeSet = function (state, value, min, max) {
47 + if (typeof value !== 'number') {
48 + value = 0;
49 + }
50 + if (typeof min !== 'number') {
51 + min = 0;
52 + }
53 + if (typeof max !== 'number') {
54 + max = 0;
55 + }
56 + if (value > max) {
57 + max = value;
58 + }
59 + if (value < min) {
60 + min = value;
61 + }
62 + if (min > max) {
63 + let t = min;
64 + min = max;
65 + max = t;
66 + }
67 + else if (min === max) {
68 + max = min + 1;
69 + }
70 +
71 + state.legendFormatValueDecimalsFromMinMax(min, max);
72 +
73 + // gauge.js has an issue if the needle
74 + // is smaller than min or larger than max
75 + // when we set the new values
76 + // the needle will go crazy
77 +
78 + // to prevent it, we always feed it
79 + // with a percentage, so that the needle
80 + // is always between min and max
81 + let pcent = (value - min) * 100 / (max - min);
82 +
83 + // bug fix for gauge.js 1.3.1
84 + // if the value is the absolute min or max, the chart is broken
85 + if (pcent < 0.001) {
86 + pcent = 0.001;
87 + }
88 + if (pcent > 99.999) {
89 + pcent = 99.999;
90 + }
91 +
92 + state.tmp.gauge_instance.set(pcent);
93 + // console.log('gauge set ' + pcent + ', value ' + value + ', min ' + min + ', max ' + max);
94 +
95 + state.tmp.___gaugeOld__.value = value;
96 + state.tmp.___gaugeOld__.min = min;
97 + state.tmp.___gaugeOld__.max = max;
98 +};
99 +
100 +NETDATA.gaugeSetLabels = function (state, value, min, max) {
101 + if (state.tmp.___gaugeOld__.valueLabel !== value) {
102 + state.tmp.___gaugeOld__.valueLabel = value;
103 + state.tmp.gaugeChartLabel.innerText = state.legendFormatValue(value);
104 + }
105 + if (state.tmp.___gaugeOld__.minLabel !== min) {
106 + state.tmp.___gaugeOld__.minLabel = min;
107 + state.tmp.gaugeChartMin.innerText = state.legendFormatValue(min);
108 + }
109 + if (state.tmp.___gaugeOld__.maxLabel !== max) {
110 + state.tmp.___gaugeOld__.maxLabel = max;
111 + state.tmp.gaugeChartMax.innerText = state.legendFormatValue(max);
112 + }
113 +};
114 +
115 +NETDATA.gaugeClearSelection = function (state, force) {
116 + if (typeof state.tmp.gaugeEvent !== 'undefined' && typeof state.tmp.gaugeEvent.timer !== 'undefined') {
117 + NETDATA.timeout.clear(state.tmp.gaugeEvent.timer);
118 + state.tmp.gaugeEvent.timer = undefined;
119 + }
120 +
121 + if (state.isAutoRefreshable() && state.data !== null && force !== true) {
122 + NETDATA.gaugeChartUpdate(state, state.data);
123 + } else {
124 + NETDATA.gaugeAnimation(state, false);
125 + NETDATA.gaugeSetLabels(state, null, null, null);
126 + NETDATA.gaugeSet(state, null, null, null);
127 + }
128 +
129 + NETDATA.gaugeAnimation(state, true);
130 + return true;
131 +};
132 +
133 +NETDATA.gaugeSetSelection = function (state, t) {
134 + if (state.timeIsVisible(t) !== true) {
135 + return NETDATA.gaugeClearSelection(state, true);
136 + }
137 +
138 + let slot = state.calculateRowForTime(t);
139 + if (slot < 0 || slot >= state.data.result.length) {
140 + return NETDATA.gaugeClearSelection(state, true);
141 + }
142 +
143 + if (typeof state.tmp.gaugeEvent === 'undefined') {
144 + state.tmp.gaugeEvent = {
145 + timer: undefined,
146 + value: 0,
147 + min: 0,
148 + max: 0
149 + };
150 + }
151 +
152 + let value = state.data.result[state.data.result.length - 1 - slot];
153 + let min = (state.tmp.gaugeMin === null) ? NETDATA.commonMin.get(state) : state.tmp.gaugeMin;
154 + let max = (state.tmp.gaugeMax === null) ? NETDATA.commonMax.get(state) : state.tmp.gaugeMax;
155 +
156 + // make sure it is zero based
157 + // but only if it has not been set by the user
158 + if (state.tmp.gaugeMin === null && min > 0) {
159 + min = 0;
160 + }
161 + if (state.tmp.gaugeMax === null && max < 0) {
162 + max = 0;
163 + }
164 +
165 + state.tmp.gaugeEvent.value = value;
166 + state.tmp.gaugeEvent.min = min;
167 + state.tmp.gaugeEvent.max = max;
168 + NETDATA.gaugeSetLabels(state, value, min, max);
169 +
170 + if (state.tmp.gaugeEvent.timer === undefined) {
171 + NETDATA.gaugeAnimation(state, false);
172 +
173 + state.tmp.gaugeEvent.timer = NETDATA.timeout.set(function () {
174 + state.tmp.gaugeEvent.timer = undefined;
175 + NETDATA.gaugeSet(state, state.tmp.gaugeEvent.value, state.tmp.gaugeEvent.min, state.tmp.gaugeEvent.max);
176 + }, 0);
177 + }
178 +
179 + return true;
180 +};
181 +
182 +NETDATA.gaugeChartUpdate = function (state, data) {
183 + let value, min, max;
184 +
185 + if (NETDATA.globalPanAndZoom.isActive() || state.isAutoRefreshable() === false) {
186 + NETDATA.gaugeSetLabels(state, null, null, null);
187 + state.tmp.gauge_instance.set(0);
188 + } else {
189 + value = data.result[0];
190 + min = (state.tmp.gaugeMin === null) ? NETDATA.commonMin.get(state) : state.tmp.gaugeMin;
191 + max = (state.tmp.gaugeMax === null) ? NETDATA.commonMax.get(state) : state.tmp.gaugeMax;
192 + if (value < min) {
193 + min = value;
194 + }
195 + if (value > max) {
196 + max = value;
197 + }
198 +
199 + // make sure it is zero based
200 + // but only if it has not been set by the user
201 + if (state.tmp.gaugeMin === null && min > 0) {
202 + min = 0;
203 + }
204 + if (state.tmp.gaugeMax === null && max < 0) {
205 + max = 0;
206 + }
207 +
208 + NETDATA.gaugeSet(state, value, min, max);
209 + NETDATA.gaugeSetLabels(state, value, min, max);
210 + }
211 +
212 + return true;
213 +};
214 +
215 +NETDATA.gaugeChartCreate = function (state, data) {
216 + // let chart = $(state.element_chart);
217 +
218 + let value = data.result[0];
219 + let min = NETDATA.dataAttribute(state.element, 'gauge-min-value', null);
220 + let max = NETDATA.dataAttribute(state.element, 'gauge-max-value', null);
221 + // let adjust = NETDATA.dataAttribute(state.element, 'gauge-adjust', null);
222 + let pointerColor = NETDATA.dataAttribute(state.element, 'gauge-pointer-color', NETDATA.themes.current.gauge_pointer);
223 + let strokeColor = NETDATA.dataAttribute(state.element, 'gauge-stroke-color', NETDATA.themes.current.gauge_stroke);
224 + let startColor = NETDATA.dataAttribute(state.element, 'gauge-start-color', state.chartCustomColors()[0]);
225 + let stopColor = NETDATA.dataAttribute(state.element, 'gauge-stop-color', void 0);
226 + let generateGradient = NETDATA.dataAttribute(state.element, 'gauge-generate-gradient', false);
227 +
228 + if (min === null) {
229 + min = NETDATA.commonMin.get(state);
230 + state.tmp.gaugeMin = null;
231 + } else {
232 + state.tmp.gaugeMin = min;
233 + }
234 +
235 + if (max === null) {
236 + max = NETDATA.commonMax.get(state);
237 + state.tmp.gaugeMax = null;
238 + } else {
239 + state.tmp.gaugeMax = max;
240 + }
241 +
242 + // make sure it is zero based
243 + // but only if it has not been set by the user
244 + if (state.tmp.gaugeMin === null && min > 0) {
245 + min = 0;
246 + }
247 + if (state.tmp.gaugeMax === null && max < 0) {
248 + max = 0;
249 + }
250 +
251 + let width = state.chartWidth(), height = state.chartHeight(); //, ratio = 1.5;
252 + // console.log('gauge width: ' + width.toString() + ', height: ' + height.toString());
253 + //switch(adjust) {
254 + // case 'width': width = height * ratio; break;
255 + // case 'height':
256 + // default: height = width / ratio; break;
257 + //}
258 + //state.element.style.width = width.toString() + 'px';
259 + //state.element.style.height = height.toString() + 'px';
260 +
261 + let lum_d = 0.05;
262 +
263 + let options = {
264 + lines: 12, // The number of lines to draw
265 + angle: 0.14, // The span of the gauge arc
266 + lineWidth: 0.57, // The line thickness
267 + radiusScale: 1.0, // Relative radius
268 + pointer: {
269 + length: 0.85, // 0.9 The radius of the inner circle
270 + strokeWidth: 0.045, // The rotation offset
271 + color: pointerColor // Fill color
272 + },
273 + limitMax: true, // If false, the max value of the gauge will be updated if value surpass max
274 + limitMin: true, // If true, the min value of the gauge will be fixed unless you set it manually
275 + colorStart: startColor, // Colors
276 + colorStop: stopColor, // just experiment with them
277 + strokeColor: strokeColor, // to see which ones work best for you
278 + generateGradient: (generateGradient === true), // gmosx:
279 + gradientType: 0,
280 + highDpiSupport: true // High resolution support
281 + };
282 +
283 + if (generateGradient.constructor === Array) {
284 + // example options:
285 + // data-gauge-generate-gradient="[0, 50, 100]"
286 + // data-gauge-gradient-percent-color-0="#FFFFFF"
287 + // data-gauge-gradient-percent-color-50="#999900"
288 + // data-gauge-gradient-percent-color-100="#000000"
289 +
290 + options.percentColors = [];
291 + let len = generateGradient.length;
292 + while (len--) {
293 + let pcent = generateGradient[len];
294 + let color = NETDATA.dataAttribute(state.element, 'gauge-gradient-percent-color-' + pcent.toString(), false);
295 + if (color !== false) {
296 + let a = [];
297 + a[0] = pcent / 100;
298 + a[1] = color;
299 + options.percentColors.unshift(a);
300 + }
301 + }
302 + if (options.percentColors.length === 0) {
303 + delete options.percentColors;
304 + }
305 + } else if (generateGradient === false && NETDATA.themes.current.gauge_gradient) {
306 + //noinspection PointlessArithmeticExpressionJS
307 + options.percentColors = [
308 + [0.0, NETDATA.colorLuminance(startColor, (lum_d * 10) - (lum_d * 0))],
309 + [0.1, NETDATA.colorLuminance(startColor, (lum_d * 10) - (lum_d * 1))],
310 + [0.2, NETDATA.colorLuminance(startColor, (lum_d * 10) - (lum_d * 2))],
311 + [0.3, NETDATA.colorLuminance(startColor, (lum_d * 10) - (lum_d * 3))],
312 + [0.4, NETDATA.colorLuminance(startColor, (lum_d * 10) - (lum_d * 4))],
313 + [0.5, NETDATA.colorLuminance(startColor, (lum_d * 10) - (lum_d * 5))],
314 + [0.6, NETDATA.colorLuminance(startColor, (lum_d * 10) - (lum_d * 6))],
315 + [0.7, NETDATA.colorLuminance(startColor, (lum_d * 10) - (lum_d * 7))],
316 + [0.8, NETDATA.colorLuminance(startColor, (lum_d * 10) - (lum_d * 8))],
317 + [0.9, NETDATA.colorLuminance(startColor, (lum_d * 10) - (lum_d * 9))],
318 + [1.0, NETDATA.colorLuminance(startColor, 0.0)]];
319 + }
320 +
321 + state.tmp.gauge_canvas = document.createElement('canvas');
322 + state.tmp.gauge_canvas.id = 'gauge-' + state.uuid + '-canvas';
323 + state.tmp.gauge_canvas.className = 'gaugeChart';
324 + state.tmp.gauge_canvas.width = width;
325 + state.tmp.gauge_canvas.height = height;
326 + state.element_chart.appendChild(state.tmp.gauge_canvas);
327 +
328 + let valuefontsize = Math.floor(height / 5);
329 + let valuetop = Math.round((height - valuefontsize) / 3.2);
330 + state.tmp.gaugeChartLabel = document.createElement('span');
331 + state.tmp.gaugeChartLabel.className = 'gaugeChartLabel';
332 + state.tmp.gaugeChartLabel.style.fontSize = valuefontsize + 'px';
333 + state.tmp.gaugeChartLabel.style.top = valuetop.toString() + 'px';
334 + state.element_chart.appendChild(state.tmp.gaugeChartLabel);
335 +
336 + let titlefontsize = Math.round(valuefontsize / 2.1);
337 + let titletop = 0;
338 + state.tmp.gaugeChartTitle = document.createElement('span');
339 + state.tmp.gaugeChartTitle.className = 'gaugeChartTitle';
340 + state.tmp.gaugeChartTitle.innerText = state.title;
341 + state.tmp.gaugeChartTitle.style.fontSize = titlefontsize + 'px';
342 + state.tmp.gaugeChartTitle.style.lineHeight = titlefontsize + 'px';
343 + state.tmp.gaugeChartTitle.style.top = titletop.toString() + 'px';
344 + state.element_chart.appendChild(state.tmp.gaugeChartTitle);
345 +
346 + let unitfontsize = Math.round(titlefontsize * 0.9);
347 + state.tmp.gaugeChartUnits = document.createElement('span');
348 + state.tmp.gaugeChartUnits.className = 'gaugeChartUnits';
349 + state.tmp.gaugeChartUnits.innerText = state.units_current;
350 + state.tmp.gaugeChartUnits.style.fontSize = unitfontsize + 'px';
351 + state.element_chart.appendChild(state.tmp.gaugeChartUnits);
352 +
353 + state.tmp.gaugeChartMin = document.createElement('span');
354 + state.tmp.gaugeChartMin.className = 'gaugeChartMin';
355 + state.tmp.gaugeChartMin.style.fontSize = Math.round(valuefontsize * 0.75).toString() + 'px';
356 + state.element_chart.appendChild(state.tmp.gaugeChartMin);
357 +
358 + state.tmp.gaugeChartMax = document.createElement('span');
359 + state.tmp.gaugeChartMax.className = 'gaugeChartMax';
360 + state.tmp.gaugeChartMax.style.fontSize = Math.round(valuefontsize * 0.75).toString() + 'px';
361 + state.element_chart.appendChild(state.tmp.gaugeChartMax);
362 +
363 + // when we just re-create the chart
364 + // do not animate the first update
365 + let animate = true;
366 + if (typeof state.tmp.gauge_instance !== 'undefined') {
367 + animate = false;
368 + }
369 +
370 + state.tmp.gauge_instance = new Gauge(state.tmp.gauge_canvas).setOptions(options); // create sexy gauge!
371 +
372 + state.tmp.___gaugeOld__ = {
373 + value: value,
374 + min: min,
375 + max: max,
376 + valueLabel: null,
377 + minLabel: null,
378 + maxLabel: null
379 + };
380 +
381 + // we will always feed a percentage
382 + state.tmp.gauge_instance.minValue = 0;
383 + state.tmp.gauge_instance.maxValue = 100;
384 +
385 + NETDATA.gaugeAnimation(state, animate);
386 + NETDATA.gaugeSet(state, value, min, max);
387 + NETDATA.gaugeSetLabels(state, value, min, max);
388 + NETDATA.gaugeAnimation(state, true);
389 +
390 + state.legendSetUnitsString = function (units) {
391 + if (typeof state.tmp.gaugeChartUnits !== 'undefined' && state.tmp.units !== units) {
392 + state.tmp.gaugeChartUnits.innerText = units;
393 + state.tmp.___gaugeOld__.valueLabel = null;
394 + state.tmp.___gaugeOld__.minLabel = null;
395 + state.tmp.___gaugeOld__.maxLabel = null;
396 + state.tmp.units = units;
397 + }
398 + };
399 + state.legendShowUndefined = function () {
400 + if (typeof state.tmp.gauge_instance !== 'undefined') {
401 + NETDATA.gaugeClearSelection(state);
402 + }
403 + };
404 +
405 + return true;
406 +};
web/gui/src/dashboard.js/charting/google-charts.js new
+129
@@ -0,0 +1,129 @@
1 +// google charts
2 +
3 +NETDATA.googleInitialize = function (callback) {
4 + if (typeof netdataNoGoogleCharts === 'undefined' || !netdataNoGoogleCharts) {
5 + $.ajax({
6 + url: NETDATA.google_js,
7 + cache: true,
8 + dataType: "script",
9 + xhrFields: {withCredentials: true} // required for the cookie
10 + })
11 + .done(function () {
12 + NETDATA.registerChartLibrary('google', NETDATA.google_js);
13 + google.load('visualization', '1.1', {
14 + 'packages': ['corechart', 'controls'],
15 + 'callback': callback
16 + });
17 + })
18 + .fail(function () {
19 + NETDATA.chartLibraries.google.enabled = false;
20 + NETDATA.error(100, NETDATA.google_js);
21 + if (typeof callback === "function") {
22 + return callback();
23 + }
24 + });
25 + } else {
26 + NETDATA.chartLibraries.google.enabled = false;
27 + if (typeof callback === "function") {
28 + return callback();
29 + }
30 + }
31 +};
32 +
33 +NETDATA.googleChartUpdate = function (state, data) {
34 + let datatable = new google.visualization.DataTable(data.result);
35 + state.google_instance.draw(datatable, state.google_options);
36 + return true;
37 +};
38 +
39 +NETDATA.googleChartCreate = function (state, data) {
40 + let datatable = new google.visualization.DataTable(data.result);
41 +
42 + state.google_options = {
43 + colors: state.chartColors(),
44 +
45 + // do not set width, height - the chart resizes itself
46 + //width: state.chartWidth(),
47 + //height: state.chartHeight(),
48 + lineWidth: 1,
49 + title: state.title,
50 + fontSize: 11,
51 + hAxis: {
52 + // title: "Time of Day",
53 + // format:'HH:mm:ss',
54 + viewWindowMode: 'maximized',
55 + slantedText: false,
56 + format: 'HH:mm:ss',
57 + textStyle: {
58 + fontSize: 9
59 + },
60 + gridlines: {
61 + color: '#EEE'
62 + }
63 + },
64 + vAxis: {
65 + title: state.units_current,
66 + viewWindowMode: 'pretty',
67 + minValue: -0.1,
68 + maxValue: 0.1,
69 + direction: 1,
70 + textStyle: {
71 + fontSize: 9
72 + },
73 + gridlines: {
74 + color: '#EEE'
75 + }
76 + },
77 + chartArea: {
78 + width: '65%',
79 + height: '80%'
80 + },
81 + focusTarget: 'category',
82 + annotation: {
83 + '1': {
84 + style: 'line'
85 + }
86 + },
87 + pointsVisible: 0,
88 + titlePosition: 'out',
89 + titleTextStyle: {
90 + fontSize: 11
91 + },
92 + tooltip: {
93 + isHtml: false,
94 + ignoreBounds: true,
95 + textStyle: {
96 + fontSize: 9
97 + }
98 + },
99 + curveType: 'function',
100 + areaOpacity: 0.3,
101 + isStacked: false
102 + };
103 +
104 + switch (state.chart.chart_type) {
105 + case "area":
106 + state.google_options.vAxis.viewWindowMode = 'maximized';
107 + state.google_options.areaOpacity = NETDATA.options.current.color_fill_opacity_area;
108 + state.google_instance = new google.visualization.AreaChart(state.element_chart);
109 + break;
110 +
111 + case "stacked":
112 + state.google_options.isStacked = true;
113 + state.google_options.areaOpacity = NETDATA.options.current.color_fill_opacity_stacked;
114 + state.google_options.vAxis.viewWindowMode = 'maximized';
115 + state.google_options.vAxis.minValue = null;
116 + state.google_options.vAxis.maxValue = null;
117 + state.google_instance = new google.visualization.AreaChart(state.element_chart);
118 + break;
119 +
120 + default:
121 + case "line":
122 + state.google_options.lineWidth = 2;
123 + state.google_instance = new google.visualization.LineChart(state.element_chart);
124 + break;
125 + }
126 +
127 + state.google_instance.draw(datatable, state.google_options);
128 + return true;
129 +};
web/gui/src/dashboard.js/charting/peity.js new
+62
@@ -0,0 +1,62 @@
1 +
2 +// peity
3 +
4 +NETDATA.peityInitialize = function (callback) {
5 + if (typeof netdataNoPeitys === 'undefined' || !netdataNoPeitys) {
6 + $.ajax({
7 + url: NETDATA.peity_js,
8 + cache: true,
9 + dataType: "script",
10 + xhrFields: {withCredentials: true} // required for the cookie
11 + })
12 + .done(function () {
13 + NETDATA.registerChartLibrary('peity', NETDATA.peity_js);
14 + })
15 + .fail(function () {
16 + NETDATA.chartLibraries.peity.enabled = false;
17 + NETDATA.error(100, NETDATA.peity_js);
18 + })
19 + .always(function () {
20 + if (typeof callback === "function") {
21 + return callback();
22 + }
23 + });
24 + } else {
25 + NETDATA.chartLibraries.peity.enabled = false;
26 + if (typeof callback === "function") {
27 + return callback();
28 + }
29 + }
30 +};
31 +
32 +NETDATA.peityChartUpdate = function (state, data) {
33 + state.peity_instance.innerHTML = data.result;
34 +
35 + if (state.peity_options.stroke !== state.chartCustomColors()[0]) {
36 + state.peity_options.stroke = state.chartCustomColors()[0];
37 + if (state.chart.chart_type === 'line') {
38 + state.peity_options.fill = NETDATA.themes.current.background;
39 + } else {
40 + state.peity_options.fill = NETDATA.colorLuminance(state.chartCustomColors()[0], NETDATA.chartDefaults.fill_luminance);
41 + }
42 + }
43 +
44 + $(state.peity_instance).peity('line', state.peity_options);
45 + return true;
46 +};
47 +
48 +NETDATA.peityChartCreate = function (state, data) {
49 + state.peity_instance = document.createElement('div');
50 + state.element_chart.appendChild(state.peity_instance);
51 +
52 + state.peity_options = {
53 + stroke: NETDATA.themes.current.foreground,
54 + strokeWidth: NETDATA.dataAttribute(state.element, 'peity-strokewidth', 1),
55 + width: state.chartWidth(),
56 + height: state.chartHeight(),
57 + fill: NETDATA.themes.current.foreground
58 + };
59 +
60 + NETDATA.peityChartUpdate(state, data);
61 + return true;
62 +};
web/gui/src/dashboard.js/charting/sparkline.js new
+155
@@ -0,0 +1,155 @@
1 +// ----------------------------------------------------------------------------------------------------------------
2 +// sparkline
3 +
4 +NETDATA.sparklineInitialize = function (callback) {
5 + if (typeof netdataNoSparklines === 'undefined' || !netdataNoSparklines) {
6 + $.ajax({
7 + url: NETDATA.sparkline_js,
8 + cache: true,
9 + dataType: "script",
10 + xhrFields: {withCredentials: true} // required for the cookie
11 + })
12 + .done(function () {
13 + NETDATA.registerChartLibrary('sparkline', NETDATA.sparkline_js);
14 + })
15 + .fail(function () {
16 + NETDATA.chartLibraries.sparkline.enabled = false;
17 + NETDATA.error(100, NETDATA.sparkline_js);
18 + })
19 + .always(function () {
20 + if (typeof callback === "function") {
21 + return callback();
22 + }
23 + });
24 + } else {
25 + NETDATA.chartLibraries.sparkline.enabled = false;
26 + if (typeof callback === "function") {
27 + return callback();
28 + }
29 + }
30 +};
31 +
32 +NETDATA.sparklineChartUpdate = function (state, data) {
33 + state.sparkline_options.width = state.chartWidth();
34 + state.sparkline_options.height = state.chartHeight();
35 +
36 + $(state.element_chart).sparkline(data.result, state.sparkline_options);
37 + return true;
38 +};
39 +
40 +NETDATA.sparklineChartCreate = function (state, data) {
41 + let type = NETDATA.dataAttribute(state.element, 'sparkline-type', 'line');
42 + let lineColor = NETDATA.dataAttribute(state.element, 'sparkline-linecolor', state.chartCustomColors()[0]);
43 + let fillColor = NETDATA.dataAttribute(state.element, 'sparkline-fillcolor', ((state.chart.chart_type === 'line') ? NETDATA.themes.current.background : NETDATA.colorLuminance(lineColor, NETDATA.chartDefaults.fill_luminance)));
44 + let chartRangeMin = NETDATA.dataAttribute(state.element, 'sparkline-chartrangemin', undefined);
45 + let chartRangeMax = NETDATA.dataAttribute(state.element, 'sparkline-chartrangemax', undefined);
46 + let composite = NETDATA.dataAttribute(state.element, 'sparkline-composite', undefined);
47 + let enableTagOptions = NETDATA.dataAttribute(state.element, 'sparkline-enabletagoptions', undefined);
48 + let tagOptionPrefix = NETDATA.dataAttribute(state.element, 'sparkline-tagoptionprefix', undefined);
49 + let tagValuesAttribute = NETDATA.dataAttribute(state.element, 'sparkline-tagvaluesattribute', undefined);
50 + let disableHiddenCheck = NETDATA.dataAttribute(state.element, 'sparkline-disablehiddencheck', undefined);
51 + let defaultPixelsPerValue = NETDATA.dataAttribute(state.element, 'sparkline-defaultpixelspervalue', undefined);
52 + let spotColor = NETDATA.dataAttribute(state.element, 'sparkline-spotcolor', undefined);
53 + let minSpotColor = NETDATA.dataAttribute(state.element, 'sparkline-minspotcolor', undefined);
54 + let maxSpotColor = NETDATA.dataAttribute(state.element, 'sparkline-maxspotcolor', undefined);
55 + let spotRadius = NETDATA.dataAttribute(state.element, 'sparkline-spotradius', undefined);
56 + let valueSpots = NETDATA.dataAttribute(state.element, 'sparkline-valuespots', undefined);
57 + let highlightSpotColor = NETDATA.dataAttribute(state.element, 'sparkline-highlightspotcolor', undefined);
58 + let highlightLineColor = NETDATA.dataAttribute(state.element, 'sparkline-highlightlinecolor', undefined);
59 + let lineWidth = NETDATA.dataAttribute(state.element, 'sparkline-linewidth', undefined);
60 + let normalRangeMin = NETDATA.dataAttribute(state.element, 'sparkline-normalrangemin', undefined);
61 + let normalRangeMax = NETDATA.dataAttribute(state.element, 'sparkline-normalrangemax', undefined);
62 + let drawNormalOnTop = NETDATA.dataAttribute(state.element, 'sparkline-drawnormalontop', undefined);
63 + let xvalues = NETDATA.dataAttribute(state.element, 'sparkline-xvalues', undefined);
64 + let chartRangeClip = NETDATA.dataAttribute(state.element, 'sparkline-chartrangeclip', undefined);
65 + let chartRangeMinX = NETDATA.dataAttribute(state.element, 'sparkline-chartrangeminx', undefined);
66 + let chartRangeMaxX = NETDATA.dataAttribute(state.element, 'sparkline-chartrangemaxx', undefined);
67 + let disableInteraction = NETDATA.dataAttributeBoolean(state.element, 'sparkline-disableinteraction', false);
68 + let disableTooltips = NETDATA.dataAttributeBoolean(state.element, 'sparkline-disabletooltips', false);
69 + let disableHighlight = NETDATA.dataAttributeBoolean(state.element, 'sparkline-disablehighlight', false);
70 + let highlightLighten = NETDATA.dataAttribute(state.element, 'sparkline-highlightlighten', 1.4);
71 + let highlightColor = NETDATA.dataAttribute(state.element, 'sparkline-highlightcolor', undefined);
72 + let tooltipContainer = NETDATA.dataAttribute(state.element, 'sparkline-tooltipcontainer', undefined);
73 + let tooltipClassname = NETDATA.dataAttribute(state.element, 'sparkline-tooltipclassname', undefined);
74 + let tooltipFormat = NETDATA.dataAttribute(state.element, 'sparkline-tooltipformat', undefined);
75 + let tooltipPrefix = NETDATA.dataAttribute(state.element, 'sparkline-tooltipprefix', undefined);
76 + let tooltipSuffix = NETDATA.dataAttribute(state.element, 'sparkline-tooltipsuffix', ' ' + state.units_current);
77 + let tooltipSkipNull = NETDATA.dataAttributeBoolean(state.element, 'sparkline-tooltipskipnull', true);
78 + let tooltipValueLookups = NETDATA.dataAttribute(state.element, 'sparkline-tooltipvaluelookups', undefined);
79 + let tooltipFormatFieldlist = NETDATA.dataAttribute(state.element, 'sparkline-tooltipformatfieldlist', undefined);
80 + let tooltipFormatFieldlistKey = NETDATA.dataAttribute(state.element, 'sparkline-tooltipformatfieldlistkey', undefined);
81 + let numberFormatter = NETDATA.dataAttribute(state.element, 'sparkline-numberformatter', function (n) {
82 + return n.toFixed(2);
83 + });
84 + let numberDigitGroupSep = NETDATA.dataAttribute(state.element, 'sparkline-numberdigitgroupsep', undefined);
85 + let numberDecimalMark = NETDATA.dataAttribute(state.element, 'sparkline-numberdecimalmark', undefined);
86 + let numberDigitGroupCount = NETDATA.dataAttribute(state.element, 'sparkline-numberdigitgroupcount', undefined);
87 + let animatedZooms = NETDATA.dataAttributeBoolean(state.element, 'sparkline-animatedzooms', false);
88 +
89 + if (spotColor === 'disable') {
90 + spotColor = '';
91 + }
92 + if (minSpotColor === 'disable') {
93 + minSpotColor = '';
94 + }
95 + if (maxSpotColor === 'disable') {
96 + maxSpotColor = '';
97 + }
98 +
99 + // state.log('sparkline type ' + type + ', lineColor: ' + lineColor + ', fillColor: ' + fillColor);
100 +
101 + state.sparkline_options = {
102 + type: type,
103 + lineColor: lineColor,
104 + fillColor: fillColor,
105 + chartRangeMin: chartRangeMin,
106 + chartRangeMax: chartRangeMax,
107 + composite: composite,
108 + enableTagOptions: enableTagOptions,
109 + tagOptionPrefix: tagOptionPrefix,
110 + tagValuesAttribute: tagValuesAttribute,
111 + disableHiddenCheck: disableHiddenCheck,
112 + defaultPixelsPerValue: defaultPixelsPerValue,
113 + spotColor: spotColor,
114 + minSpotColor: minSpotColor,
115 + maxSpotColor: maxSpotColor,
116 + spotRadius: spotRadius,
117 + valueSpots: valueSpots,
118 + highlightSpotColor: highlightSpotColor,
119 + highlightLineColor: highlightLineColor,
120 + lineWidth: lineWidth,
121 + normalRangeMin: normalRangeMin,
122 + normalRangeMax: normalRangeMax,
123 + drawNormalOnTop: drawNormalOnTop,
124 + xvalues: xvalues,
125 + chartRangeClip: chartRangeClip,
126 + chartRangeMinX: chartRangeMinX,
127 + chartRangeMaxX: chartRangeMaxX,
128 + disableInteraction: disableInteraction,
129 + disableTooltips: disableTooltips,
130 + disableHighlight: disableHighlight,
131 + highlightLighten: highlightLighten,
132 + highlightColor: highlightColor,
133 + tooltipContainer: tooltipContainer,
134 + tooltipClassname: tooltipClassname,
135 + tooltipChartTitle: state.title,
136 + tooltipFormat: tooltipFormat,
137 + tooltipPrefix: tooltipPrefix,
138 + tooltipSuffix: tooltipSuffix,
139 + tooltipSkipNull: tooltipSkipNull,
140 + tooltipValueLookups: tooltipValueLookups,
141 + tooltipFormatFieldlist: tooltipFormatFieldlist,
142 + tooltipFormatFieldlistKey: tooltipFormatFieldlistKey,
143 + numberFormatter: numberFormatter,
144 + numberDigitGroupSep: numberDigitGroupSep,
145 + numberDecimalMark: numberDecimalMark,
146 + numberDigitGroupCount: numberDigitGroupCount,
147 + animatedZooms: animatedZooms,
148 + width: state.chartWidth(),
149 + height: state.chartHeight()
150 + };
151 +
152 + $(state.element_chart).sparkline(data.result, state.sparkline_options);
153 +
154 + return true;
155 +};
web/gui/src/dashboard.js/colors.js new
+34
@@ -0,0 +1,34 @@
1 +NETDATA.colorHex2Rgb = function (hex) {
2 + // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
3 + let shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
4 + hex = hex.replace(shorthandRegex, function (m, r, g, b) {
5 + return r + r + g + g + b + b;
6 + });
7 +
8 + let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
9 + return result ? {
10 + r: parseInt(result[1], 16),
11 + g: parseInt(result[2], 16),
12 + b: parseInt(result[3], 16)
13 + } : null;
14 +};
15 +
16 +NETDATA.colorLuminance = function (hex, lum) {
17 + // validate hex string
18 + hex = String(hex).replace(/[^0-9a-f]/gi, '');
19 + if (hex.length < 6) {
20 + hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
21 + }
22 +
23 + lum = lum || 0;
24 +
25 + // convert to decimal and change luminosity
26 + let rgb = "#";
27 + for (let i = 0; i < 3; i++) {
28 + let c = parseInt(hex.substr(i * 2, 2), 16);
29 + c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16);
30 + rgb += ("00" + c).substr(c.length);
31 + }
32 +
33 + return rgb;
34 +};
web/gui/src/dashboard.js/compatibility.js new
+39
@@ -0,0 +1,39 @@
1 +// ------------------------------------------------------------------------
2 +// compatibility fixes
3 +
4 +// fix IE issue with console
5 +if (!window.console) {
6 + window.console = {
7 + log: function () {
8 + }
9 + };
10 +}
11 +
12 +// if string.endsWith is not defined, define it
13 +if (typeof String.prototype.endsWith !== 'function') {
14 + String.prototype.endsWith = function (s) {
15 + if (s.length > this.length) {
16 + return false;
17 + }
18 + return this.slice(-s.length) === s;
19 + };
20 +}
21 +
22 +// if string.startsWith is not defined, define it
23 +if (typeof String.prototype.startsWith !== 'function') {
24 + String.prototype.startsWith = function (s) {
25 + if (s.length > this.length) {
26 + return false;
27 + }
28 + return this.slice(s.length) === s;
29 + };
30 +}
31 +
32 +NETDATA.name2id = function (s) {
33 + return s
34 + .replace(/ /g, '_')
35 + .replace(/\(/g, '_')
36 + .replace(/\)/g, '_')
37 + .replace(/\./g, '_')
38 + .replace(/\//g, '_');
39 +};
web/gui/src/dashboard.js/epilogue.js.inc new
+1
@@ -0,0 +1 @@
1 +})(window, document, (typeof jQuery === 'function')?jQuery:undefined);
web/gui/src/dashboard.js/error-handling.js new
+52
@@ -0,0 +1,52 @@
1 +// Error Handling
2 +
3 +NETDATA.errorCodes = {
4 + 100: {message: "Cannot load chart library", alert: true},
5 + 101: {message: "Cannot load jQuery", alert: true},
6 + 402: {message: "Chart library not found", alert: false},
7 + 403: {message: "Chart library not enabled/is failed", alert: false},
8 + 404: {message: "Chart not found", alert: false},
9 + 405: {message: "Cannot download charts index from server", alert: true},
10 + 406: {message: "Invalid charts index downloaded from server", alert: true},
11 + 407: {message: "Cannot HELLO netdata server", alert: false},
12 + 408: {message: "Netdata servers sent invalid response to HELLO", alert: false},
13 + 409: {message: "Cannot ACCESS netdata registry", alert: false},
14 + 410: {message: "Netdata registry ACCESS failed", alert: false},
15 + 411: {message: "Netdata registry server send invalid response to DELETE ", alert: false},
16 + 412: {message: "Netdata registry DELETE failed", alert: false},
17 + 413: {message: "Netdata registry server send invalid response to SWITCH ", alert: false},
18 + 414: {message: "Netdata registry SWITCH failed", alert: false},
19 + 415: {message: "Netdata alarms download failed", alert: false},
20 + 416: {message: "Netdata alarms log download failed", alert: false},
21 + 417: {message: "Netdata registry server send invalid response to SEARCH ", alert: false},
22 + 418: {message: "Netdata registry SEARCH failed", alert: false}
23 +};
24 +
25 +NETDATA.errorLast = {
26 + code: 0,
27 + message: "",
28 + datetime: 0
29 +};
30 +
31 +NETDATA.error = function (code, msg) {
32 + NETDATA.errorLast.code = code;
33 + NETDATA.errorLast.message = msg;
34 + NETDATA.errorLast.datetime = Date.now();
35 +
36 + console.log("ERROR " + code + ": " + NETDATA.errorCodes[code].message + ": " + msg);
37 +
38 + let ret = true;
39 + if (typeof netdataErrorCallback === 'function') {
40 + ret = netdataErrorCallback('system', code, msg);
41 + }
42 +
43 + if (ret && NETDATA.errorCodes[code].alert) {
44 + alert("ERROR " + code + ": " + NETDATA.errorCodes[code].message + ": " + msg);
45 + }
46 +};
47 +
48 +NETDATA.errorReset = function () {
49 + NETDATA.errorLast.code = 0;
50 + NETDATA.errorLast.message = "You are doing fine!";
51 + NETDATA.errorLast.datetime = 0;
52 +};
web/gui/src/dashboard.js/localstorage.js new
+173
@@ -0,0 +1,173 @@
1 +
2 +// local storage options
3 +
4 +NETDATA.localStorage = {
5 + default: {},
6 + current: {},
7 + callback: {} // only used for resetting back to defaults
8 +};
9 +
10 +NETDATA.localStorageTested = -1;
11 +NETDATA.localStorageTest = function () {
12 + if (NETDATA.localStorageTested !== -1) {
13 + return NETDATA.localStorageTested;
14 + }
15 +
16 + if (typeof Storage !== "undefined" && typeof localStorage === 'object') {
17 + let test = 'test';
18 + try {
19 + localStorage.setItem(test, test);
20 + localStorage.removeItem(test);
21 + NETDATA.localStorageTested = true;
22 + } catch (e) {
23 + NETDATA.localStorageTested = false;
24 + }
25 + } else {
26 + NETDATA.localStorageTested = false;
27 + }
28 +
29 + return NETDATA.localStorageTested;
30 +};
31 +
32 +NETDATA.localStorageGet = function (key, def, callback) {
33 + let ret = def;
34 +
35 + if (typeof NETDATA.localStorage.default[key.toString()] === 'undefined') {
36 + NETDATA.localStorage.default[key.toString()] = def;
37 + NETDATA.localStorage.callback[key.toString()] = callback;
38 + }
39 +
40 + if (NETDATA.localStorageTest()) {
41 + try {
42 + // console.log('localStorage: loading "' + key.toString() + '"');
43 + ret = localStorage.getItem(key.toString());
44 + // console.log('netdata loaded: ' + key.toString() + ' = ' + ret.toString());
45 + if (ret === null || ret === 'undefined') {
46 + // console.log('localStorage: cannot load it, saving "' + key.toString() + '" with value "' + JSON.stringify(def) + '"');
47 + localStorage.setItem(key.toString(), JSON.stringify(def));
48 + ret = def;
49 + } else {
50 + // console.log('localStorage: got "' + key.toString() + '" with value "' + ret + '"');
51 + ret = JSON.parse(ret);
52 + // console.log('localStorage: loaded "' + key.toString() + '" as value ' + ret + ' of type ' + typeof(ret));
53 + }
54 + } catch (error) {
55 + console.log('localStorage: failed to read "' + key.toString() + '", using default: "' + def.toString() + '"');
56 + ret = def;
57 + }
58 + }
59 +
60 + if (typeof ret === 'undefined' || ret === 'undefined') {
61 + console.log('localStorage: LOADED UNDEFINED "' + key.toString() + '" as value ' + ret + ' of type ' + typeof(ret));
62 + ret = def;
63 + }
64 +
65 + NETDATA.localStorage.current[key.toString()] = ret;
66 + return ret;
67 +};
68 +
69 +NETDATA.localStorageSet = function (key, value, callback) {
70 + if (typeof value === 'undefined' || value === 'undefined') {
71 + console.log('localStorage: ATTEMPT TO SET UNDEFINED "' + key.toString() + '" as value ' + value + ' of type ' + typeof(value));
72 + }
73 +
74 + if (typeof NETDATA.localStorage.default[key.toString()] === 'undefined') {
75 + NETDATA.localStorage.default[key.toString()] = value;
76 + NETDATA.localStorage.current[key.toString()] = value;
77 + NETDATA.localStorage.callback[key.toString()] = callback;
78 + }
79 +
80 + if (NETDATA.localStorageTest()) {
81 + // console.log('localStorage: saving "' + key.toString() + '" with value "' + JSON.stringify(value) + '"');
82 + try {
83 + localStorage.setItem(key.toString(), JSON.stringify(value));
84 + } catch (e) {
85 + console.log('localStorage: failed to save "' + key.toString() + '" with value: "' + value.toString() + '"');
86 + }
87 + }
88 +
89 + NETDATA.localStorage.current[key.toString()] = value;
90 + return value;
91 +};
92 +
93 +NETDATA.localStorageGetRecursive = function (obj, prefix, callback) {
94 + let keys = Object.keys(obj);
95 + let len = keys.length;
96 + while (len--) {
97 + let i = keys[len];
98 +
99 + if (typeof obj[i] === 'object') {
100 + //console.log('object ' + prefix + '.' + i.toString());
101 + NETDATA.localStorageGetRecursive(obj[i], prefix + '.' + i.toString(), callback);
102 + continue;
103 + }
104 +
105 + obj[i] = NETDATA.localStorageGet(prefix + '.' + i.toString(), obj[i], callback);
106 + }
107 +};
108 +
109 +NETDATA.setOption = function (key, value) {
110 + if (key.toString() === 'setOptionCallback') {
111 + if (typeof NETDATA.options.current.setOptionCallback === 'function') {
112 + NETDATA.options.current[key.toString()] = value;
113 + NETDATA.options.current.setOptionCallback();
114 + }
115 + } else if (NETDATA.options.current[key.toString()] !== value) {
116 + let name = 'options.' + key.toString();
117 +
118 + if (typeof NETDATA.localStorage.default[name.toString()] === 'undefined') {
119 + console.log('localStorage: setOption() on unsaved option: "' + name.toString() + '", value: ' + value);
120 + }
121 +
122 + //console.log(NETDATA.localStorage);
123 + //console.log('setOption: setting "' + key.toString() + '" to "' + value + '" of type ' + typeof(value) + ' original type ' + typeof(NETDATA.options.current[key.toString()]));
124 + //console.log(NETDATA.options);
125 + NETDATA.options.current[key.toString()] = NETDATA.localStorageSet(name.toString(), value, null);
126 +
127 + if (typeof NETDATA.options.current.setOptionCallback === 'function') {
128 + NETDATA.options.current.setOptionCallback();
129 + }
130 + }
131 +
132 + return true;
133 +};
134 +
135 +NETDATA.getOption = function (key) {
136 + return NETDATA.options.current[key.toString()];
137 +};
138 +
139 +// read settings from local storage
140 +NETDATA.localStorageGetRecursive(NETDATA.options.current, 'options', null);
141 +
142 +// always start with this option enabled.
143 +NETDATA.setOption('stop_updates_when_focus_is_lost', true);
144 +
145 +NETDATA.resetOptions = function () {
146 + let keys = Object.keys(NETDATA.localStorage.default);
147 + let len = keys.length;
148 +
149 + while (len--) {
150 + let i = keys[len];
151 + let a = i.split('.');
152 +
153 + if (a[0] === 'options') {
154 + if (a[1] === 'setOptionCallback') {
155 + continue;
156 + }
157 + if (typeof NETDATA.localStorage.default[i] === 'undefined') {
158 + continue;
159 + }
160 + if (NETDATA.options.current[i] === NETDATA.localStorage.default[i]) {
161 + continue;
162 + }
163 +
164 + NETDATA.setOption(a[1], NETDATA.localStorage.default[i]);
165 + } else if (a[0] === 'chart_heights') {
166 + if (typeof NETDATA.localStorage.callback[i] === 'function' && typeof NETDATA.localStorage.default[i] !== 'undefined') {
167 + NETDATA.localStorage.callback[i](NETDATA.localStorage.default[i]);
168 + }
169 + }
170 + }
171 +
172 + NETDATA.dateTime.init(NETDATA.options.current.timezone);
173 +};
web/gui/src/dashboard.js/main.js new
+4886
@@ -0,0 +1,4886 @@
1 +// ----------------------------------------------------------------------------------------------------------------
2 +// Detect the netdata server
3 +
4 +// http://stackoverflow.com/questions/984510/what-is-my-script-src-url
5 +// http://stackoverflow.com/questions/6941533/get-protocol-domain-and-port-from-url
6 +NETDATA._scriptSource = function () {
7 + let script = null;
8 +
9 + if (typeof document.currentScript !== 'undefined') {
10 + script = document.currentScript;
11 + } else {
12 + const all_scripts = document.getElementsByTagName('script');
13 + script = all_scripts[all_scripts.length - 1];
14 + }
15 +
16 + if (typeof script.getAttribute.length !== 'undefined') {
17 + script = script.src;
18 + } else {
19 + script = script.getAttribute('src', -1);
20 + }
21 +
22 + return script;
23 +};
24 +
25 +if (typeof netdataServer !== 'undefined') {
26 + NETDATA.serverDefault = netdataServer;
27 +} else {
28 + let s = NETDATA._scriptSource();
29 + if (s) {
30 + NETDATA.serverDefault = s.replace(/\/dashboard.js(\?.*)?$/g, "");
31 + } else {
32 + console.log('WARNING: Cannot detect the URL of the netdata server.');
33 + NETDATA.serverDefault = null;
34 + }
35 +}
36 +
37 +if (NETDATA.serverDefault === null) {
38 + NETDATA.serverDefault = '';
39 +} else if (NETDATA.serverDefault.slice(-1) !== '/') {
40 + NETDATA.serverDefault += '/';
41 +}
42 +
43 +if (typeof netdataServerStatic !== 'undefined' && netdataServerStatic !== null && netdataServerStatic !== '') {
44 + NETDATA.serverStatic = netdataServerStatic;
45 + if (NETDATA.serverStatic.slice(-1) !== '/') {
46 + NETDATA.serverStatic += '/';
47 + }
48 +} else {
49 + NETDATA.serverStatic = NETDATA.serverDefault;
50 +}
51 +
52 +// default URLs for all the external files we need
53 +// make them RELATIVE so that the whole thing can also be
54 +// installed under a web server
55 +NETDATA.jQuery = NETDATA.serverStatic + 'lib/jquery-2.2.4.min.js';
56 +NETDATA.peity_js = NETDATA.serverStatic + 'lib/jquery.peity-3.2.0.min.js';
57 +NETDATA.sparkline_js = NETDATA.serverStatic + 'lib/jquery.sparkline-2.1.2.min.js';
58 +NETDATA.easypiechart_js = NETDATA.serverStatic + 'lib/jquery.easypiechart-97b5824.min.js';
59 +NETDATA.gauge_js = NETDATA.serverStatic + 'lib/gauge-1.3.2.min.js';
60 +NETDATA.dygraph_js = NETDATA.serverStatic + 'lib/dygraph-c91c859.min.js';
61 +NETDATA.dygraph_smooth_js = NETDATA.serverStatic + 'lib/dygraph-smooth-plotter-c91c859.js';
62 +// NETDATA.raphael_js = NETDATA.serverStatic + 'lib/raphael-2.2.4-min.js';
63 +// NETDATA.c3_js = NETDATA.serverStatic + 'lib/c3-0.4.18.min.js';
64 +// NETDATA.c3_css = NETDATA.serverStatic + 'css/c3-0.4.18.min.css';
65 +NETDATA.d3pie_js = NETDATA.serverStatic + 'lib/d3pie-0.2.1-netdata-3.js';
66 +NETDATA.d3_js = NETDATA.serverStatic + 'lib/d3-4.12.2.min.js';
67 +// NETDATA.morris_js = NETDATA.serverStatic + 'lib/morris-0.5.1.min.js';
68 +// NETDATA.morris_css = NETDATA.serverStatic + 'css/morris-0.5.1.css';
69 +NETDATA.google_js = 'https://www.google.com/jsapi';
70 +
71 +NETDATA.themes = {
72 + white: {
73 + bootstrap_css: NETDATA.serverStatic + 'css/bootstrap-3.3.7.css',
74 + dashboard_css: NETDATA.serverStatic + 'dashboard.css?v20180210-1',
75 + background: '#FFFFFF',
76 + foreground: '#000000',
77 + grid: '#F0F0F0',
78 + axis: '#F0F0F0',
79 + highlight: '#F5F5F5',
80 + colors: ['#3366CC', '#DC3912', '#109618', '#FF9900', '#990099', '#DD4477',
81 + '#3B3EAC', '#66AA00', '#0099C6', '#B82E2E', '#AAAA11', '#5574A6',
82 + '#994499', '#22AA99', '#6633CC', '#E67300', '#316395', '#8B0707',
83 + '#329262', '#3B3EAC'],
84 + easypiechart_track: '#f0f0f0',
85 + easypiechart_scale: '#dfe0e0',
86 + gauge_pointer: '#C0C0C0',
87 + gauge_stroke: '#F0F0F0',
88 + gauge_gradient: false,
89 + d3pie: {
90 + title: '#333333',
91 + subtitle: '#666666',
92 + footer: '#888888',
93 + other: '#aaaaaa',
94 + mainlabel: '#333333',
95 + percentage: '#dddddd',
96 + value: '#aaaa22',
97 + tooltip_bg: '#000000',
98 + tooltip_fg: '#efefef',
99 + segment_stroke: "#ffffff",
100 + gradient_color: '#000000'
101 + }
102 + },
103 + slate: {
104 + bootstrap_css: NETDATA.serverStatic + 'css/bootstrap-slate-flat-3.3.7.css?v20161229-1',
105 + dashboard_css: NETDATA.serverStatic + 'dashboard.slate.css?v20180210-1',
106 + background: '#272b30',
107 + foreground: '#C8C8C8',
108 + grid: '#283236',
109 + axis: '#283236',
110 + highlight: '#383838',
111 + /* colors: [ '#55bb33', '#ff2222', '#0099C6', '#faa11b', '#adbce0', '#DDDD00',
112 + '#4178ba', '#f58122', '#a5cc39', '#f58667', '#f5ef89', '#cf93c0',
113 + '#a5d18a', '#b8539d', '#3954a3', '#c8a9cf', '#c7de8a', '#fad20a',
114 + '#a6a479', '#a66da8' ],
115 + */
116 + colors: ['#66AA00', '#FE3912', '#3366CC', '#D66300', '#0099C6', '#DDDD00',
117 + '#5054e6', '#EE9911', '#BB44CC', '#e45757', '#ef0aef', '#CC7700',
118 + '#22AA99', '#109618', '#905bfd', '#f54882', '#4381bf', '#ff3737',
119 + '#329262', '#3B3EFF'],
120 + easypiechart_track: '#373b40',
121 + easypiechart_scale: '#373b40',
122 + gauge_pointer: '#474b50',
123 + gauge_stroke: '#373b40',
124 + gauge_gradient: false,
125 + d3pie: {
126 + title: '#C8C8C8',
127 + subtitle: '#283236',
128 + footer: '#283236',
129 + other: '#283236',
130 + mainlabel: '#C8C8C8',
131 + percentage: '#dddddd',
132 + value: '#cccc44',
133 + tooltip_bg: '#272b30',
134 + tooltip_fg: '#C8C8C8',
135 + segment_stroke: "#283236",
136 + gradient_color: '#000000'
137 + }
138 + }
139 +};
140 +
141 +if (typeof netdataTheme !== 'undefined' && typeof NETDATA.themes[netdataTheme] !== 'undefined') {
142 + NETDATA.themes.current = NETDATA.themes[netdataTheme];
143 +} else {
144 + NETDATA.themes.current = NETDATA.themes.white;
145 +}
146 +
147 +NETDATA.colors = NETDATA.themes.current.colors;
148 +
149 +// these are the colors Google Charts are using
150 +// we have them here to attempt emulate their look and feel on the other chart libraries
151 +// http://there4.io/2012/05/02/google-chart-color-list/
152 +//NETDATA.colors = [ '#3366CC', '#DC3912', '#FF9900', '#109618', '#990099', '#3B3EAC', '#0099C6',
153 +// '#DD4477', '#66AA00', '#B82E2E', '#316395', '#994499', '#22AA99', '#AAAA11',
154 +// '#6633CC', '#E67300', '#8B0707', '#329262', '#5574A6', '#3B3EAC' ];
155 +
156 +// an alternative set
157 +// http://www.mulinblog.com/a-color-palette-optimized-for-data-visualization/
158 +// (blue) (red) (orange) (green) (pink) (brown) (purple) (yellow) (gray)
159 +//NETDATA.colors = [ '#5DA5DA', '#F15854', '#FAA43A', '#60BD68', '#F17CB0', '#B2912F', '#B276B2', '#DECF3F', '#4D4D4D' ];
160 +
161 +// ----------------------------------------------------------------------------------------------------------------
162 +
163 +NETDATA.timeout = {
164 + // by default, these are just wrappers to setTimeout() / clearTimeout()
165 +
166 + step: function (callback) {
167 + return window.setTimeout(callback, 1000 / 60);
168 + },
169 +
170 + set: function (callback, delay) {
171 + return window.setTimeout(callback, delay);
172 + },
173 +
174 + clear: function (id) {
175 + return window.clearTimeout(id);
176 + },
177 +
178 + init: function () {
179 + let custom = true;
180 +
181 + if (window.requestAnimationFrame) {
182 + this.step = function (callback) {
183 + return window.requestAnimationFrame(callback);
184 + };
185 +
186 + this.clear = function (handle) {
187 + return window.cancelAnimationFrame(handle.value);
188 + };
189 + } else if (window.webkitRequestAnimationFrame) {
190 + this.step = function (callback) {
191 + return window.webkitRequestAnimationFrame(callback);
192 + };
193 +
194 + if (window.webkitCancelAnimationFrame) {
195 + this.clear = function (handle) {
196 + return window.webkitCancelAnimationFrame(handle.value);
197 + };
198 + } else if (window.webkitCancelRequestAnimationFrame) {
199 + this.clear = function (handle) {
200 + return window.webkitCancelRequestAnimationFrame(handle.value);
201 + };
202 + }
203 + } else if (window.mozRequestAnimationFrame) {
204 + this.step = function (callback) {
205 + return window.mozRequestAnimationFrame(callback);
206 + };
207 +
208 + this.clear = function (handle) {
209 + return window.mozCancelRequestAnimationFrame(handle.value);
210 + };
211 + } else if (window.oRequestAnimationFrame) {
212 + this.step = function (callback) {
213 + return window.oRequestAnimationFrame(callback);
214 + };
215 +
216 + this.clear = function (handle) {
217 + return window.oCancelRequestAnimationFrame(handle.value);
218 + };
219 + } else if (window.msRequestAnimationFrame) {
220 + this.step = function (callback) {
221 + return window.msRequestAnimationFrame(callback);
222 + };
223 +
224 + this.clear = function (handle) {
225 + return window.msCancelRequestAnimationFrame(handle.value);
226 + };
227 + } else {
228 + custom = false;
229 + }
230 +
231 + if (custom) {
232 + // we have installed custom .step() / .clear() functions
233 + // overwrite the .set() too
234 +
235 + this.set = function (callback, delay) {
236 + let that = this;
237 +
238 + let start = Date.now(),
239 + handle = new Object();
240 +
241 + function loop() {
242 + let current = Date.now(),
243 + delta = current - start;
244 +
245 + if (delta >= delay) {
246 + callback.call();
247 + } else {
248 + handle.value = that.step(loop);
249 + }
250 + }
251 +
252 + handle.value = that.step(loop);
253 + return handle;
254 + };
255 + }
256 + }
257 +};
258 +
259 +NETDATA.timeout.init();
260 +
261 +// ----------------------------------------------------------------------------------------------------------------
262 +
263 +if (NETDATA.options.debug.main_loop) {
264 + console.log('welcome to NETDATA');
265 +}
266 +
267 +NETDATA.onresizeCallback = null;
268 +NETDATA.onresize = function () {
269 + NETDATA.options.last_page_resize = Date.now();
270 + NETDATA.onscroll();
271 +
272 + if (typeof NETDATA.onresizeCallback === 'function') {
273 + NETDATA.onresizeCallback();
274 + }
275 +};
276 +
277 +NETDATA.abort_all_refreshes = function () {
278 + let targets = NETDATA.options.targets;
279 + let len = targets.length;
280 +
281 + while (len--) {
282 + if (targets[len].fetching_data) {
283 + if (typeof targets[len].xhr !== 'undefined') {
284 + targets[len].xhr.abort();
285 + targets[len].running = false;
286 + targets[len].fetching_data = false;
287 + }
288 + }
289 + }
290 +};
291 +
292 +NETDATA.onscroll_start_delay = function () {
293 + NETDATA.options.last_page_scroll = Date.now();
294 +
295 + NETDATA.options.on_scroll_refresher_stop_until =
296 + NETDATA.options.last_page_scroll
297 + + (NETDATA.options.current.async_on_scroll ? 1000 : 0);
298 +};
299 +
300 +NETDATA.onscroll_end_delay = function () {
301 + NETDATA.options.on_scroll_refresher_stop_until =
302 + Date.now()
303 + + (NETDATA.options.current.async_on_scroll ? NETDATA.options.current.onscroll_worker_duration_threshold : 0);
304 +};
305 +
306 +NETDATA.onscroll_updater_timeout_id = undefined;
307 +NETDATA.onscroll_updater = function () {
308 + NETDATA.globalSelectionSync.stop();
309 +
310 + if (NETDATA.options.abort_ajax_on_scroll) {
311 + NETDATA.abort_all_refreshes();
312 + }
313 +
314 + // when the user scrolls he sees that we have
315 + // hidden all the not-visible charts
316 + // using this little function we try to switch
317 + // the charts back to visible quickly
318 +
319 + // if (NETDATA.intersectionObserver.enabled() === false) {
320 + if (!NETDATA.intersectionObserver.enabled()) {
321 + // if (NETDATA.options.current.parallel_refresher === false) {
322 + if (!NETDATA.options.current.parallel_refresher) {
323 + let targets = NETDATA.options.targets;
324 + let len = targets.length;
325 +
326 + while (len--) {
327 + if (!targets[len].running) {
328 + // if (targets[len].running === false) {
329 + targets[len].isVisible();
330 + }
331 + }
332 + }
333 + }
334 +
335 + NETDATA.onscroll_end_delay();
336 +};
337 +
338 +NETDATA.scrollUp = false;
339 +NETDATA.scrollY = window.scrollY;
340 +NETDATA.onscroll = function () {
341 + //console.log('onscroll() begin');
342 +
343 + NETDATA.onscroll_start_delay();
344 + NETDATA.chartRefresherReschedule();
345 +
346 + NETDATA.scrollUp = (window.scrollY > NETDATA.scrollY);
347 + NETDATA.scrollY = window.scrollY;
348 +
349 + if (NETDATA.onscroll_updater_timeout_id) {
350 + NETDATA.timeout.clear(NETDATA.onscroll_updater_timeout_id);
351 + }
352 +
353 + NETDATA.onscroll_updater_timeout_id = NETDATA.timeout.set(NETDATA.onscroll_updater, 0);
354 + //console.log('onscroll() end');
355 +};
356 +
357 +NETDATA.supportsPassiveEvents = function () {
358 + if (NETDATA.options.passive_events === null) {
359 + let supportsPassive = false;
360 + try {
361 + let opts = Object.defineProperty({}, 'passive', {
362 + get: function () {
363 + supportsPassive = true;
364 + }
365 + });
366 + window.addEventListener("test", null, opts);
367 + } catch (e) {
368 + console.log('browser does not support passive events');
369 + }
370 +
371 + NETDATA.options.passive_events = supportsPassive;
372 + }
373 +
374 + // console.log('passive ' + NETDATA.options.passive_events);
375 + return NETDATA.options.passive_events;
376 +};
377 +
378 +window.addEventListener('resize', NETDATA.onresize, NETDATA.supportsPassiveEvents() ? {passive: true} : false);
379 +window.addEventListener('scroll', NETDATA.onscroll, NETDATA.supportsPassiveEvents() ? {passive: true} : false);
380 +// window.onresize = NETDATA.onresize;
381 +// window.onscroll = NETDATA.onscroll;
382 +
383 +// ----------------------------------------------------------------------------------------------------------------
384 +// commonMin & commonMax
385 +
386 +NETDATA.commonMin = {
387 + keys: {},
388 + latest: {},
389 +
390 + globalReset: function () {
391 + this.keys = {};
392 + this.latest = {};
393 + },
394 +
395 + get: function (state) {
396 + if (typeof state.tmp.__commonMin === 'undefined') {
397 + // get the commonMin setting
398 + state.tmp.__commonMin = NETDATA.dataAttribute(state.element, 'common-min', null);
399 + }
400 +
401 + let min = state.data.min;
402 + let name = state.tmp.__commonMin;
403 +
404 + if (name === null) {
405 + // we don't need commonMin
406 + //state.log('no need for commonMin');
407 + return min;
408 + }
409 +
410 + let t = this.keys[name];
411 + if (typeof t === 'undefined') {
412 + // add our commonMin
413 + this.keys[name] = {};
414 + t = this.keys[name];
415 + }
416 +
417 + let uuid = state.uuid;
418 + if (typeof t[uuid] !== 'undefined') {
419 + if (t[uuid] === min) {
420 + //state.log('commonMin ' + state.tmp.__commonMin + ' not changed: ' + this.latest[name]);
421 + return this.latest[name];
422 + } else if (min < this.latest[name]) {
423 + //state.log('commonMin ' + state.tmp.__commonMin + ' increased: ' + min);
424 + t[uuid] = min;
425 + this.latest[name] = min;
426 + return min;
427 + }
428 + }
429 +
430 + // add our min
431 + t[uuid] = min;
432 +
433 + // find the common min
434 + let m = min;
435 + // for (let i in t) {
436 + // if (t.hasOwnProperty(i) && t[i] < m) m = t[i];
437 + // }
438 + for (const ti of Object.values(t)) {
439 + if (ti < m) {
440 + m = ti;
441 + }
442 + }
443 +
444 + //state.log('commonMin ' + state.tmp.__commonMin + ' updated: ' + m);
445 + this.latest[name] = m;
446 + return m;
447 + }
448 +};
449 +
450 +NETDATA.commonMax = {
451 + keys: {},
452 + latest: {},
453 +
454 + globalReset: function () {
455 + this.keys = {};
456 + this.latest = {};
457 + },
458 +
459 + get: function (state) {
460 + if (typeof state.tmp.__commonMax === 'undefined') {
461 + // get the commonMax setting
462 + state.tmp.__commonMax = NETDATA.dataAttribute(state.element, 'common-max', null);
463 + }
464 +
465 + let max = state.data.max;
466 + let name = state.tmp.__commonMax;
467 +
468 + if (name === null) {
469 + // we don't need commonMax
470 + //state.log('no need for commonMax');
471 + return max;
472 + }
473 +
474 + let t = this.keys[name];
475 + if (typeof t === 'undefined') {
476 + // add our commonMax
477 + this.keys[name] = {};
478 + t = this.keys[name];
479 + }
480 +
481 + let uuid = state.uuid;
482 + if (typeof t[uuid] !== 'undefined') {
483 + if (t[uuid] === max) {
484 + //state.log('commonMax ' + state.tmp.__commonMax + ' not changed: ' + this.latest[name]);
485 + return this.latest[name];
486 + } else if (max > this.latest[name]) {
487 + //state.log('commonMax ' + state.tmp.__commonMax + ' increased: ' + max);
488 + t[uuid] = max;
489 + this.latest[name] = max;
490 + return max;
491 + }
492 + }
493 +
494 + // add our max
495 + t[uuid] = max;
496 +
497 + // find the common max
498 + let m = max;
499 + // for (let i in t) {
500 + // if (t.hasOwnProperty(i) && t[i] > m) m = t[i];
501 + // }
502 + for (const ti of Object.values(t)) {
503 + if (ti > m) {
504 + m = ti;
505 + }
506 + }
507 +
508 + //state.log('commonMax ' + state.tmp.__commonMax + ' updated: ' + m);
509 + this.latest[name] = m;
510 + return m;
511 + }
512 +};
513 +
514 +NETDATA.commonColors = {
515 + keys: {},
516 +
517 + globalReset: function () {
518 + this.keys = {};
519 + },
520 +
521 + get: function (state, label) {
522 + let ret = this.refill(state);
523 +
524 + if (typeof ret.assigned[label] === 'undefined') {
525 + ret.assigned[label] = ret.available.shift();
526 + }
527 +
528 + return ret.assigned[label];
529 + },
530 +
531 + refill: function (state) {
532 + let ret, len;
533 +
534 + if (typeof state.tmp.__commonColors === 'undefined') {
535 + ret = this.prepare(state);
536 + } else {
537 + ret = this.keys[state.tmp.__commonColors];
538 + if (typeof ret === 'undefined') {
539 + ret = this.prepare(state);
540 + }
541 + }
542 +
543 + if (ret.available.length === 0) {
544 + if (ret.copy_theme || ret.custom.length === 0) {
545 + // copy the theme colors
546 + len = NETDATA.themes.current.colors.length;
547 + while (len--) {
548 + ret.available.unshift(NETDATA.themes.current.colors[len]);
549 + }
550 + }
551 +
552 + // copy the custom colors
553 + len = ret.custom.length;
554 + while (len--) {
555 + ret.available.unshift(ret.custom[len]);
556 + }
557 + }
558 +
559 + state.colors_assigned = ret.assigned;
560 + state.colors_available = ret.available;
561 + state.colors_custom = ret.custom;
562 +
563 + return ret;
564 + },
565 +
566 + __read_custom_colors: function (state, ret) {
567 + // add the user supplied colors
568 + let c = NETDATA.dataAttribute(state.element, 'colors', undefined);
569 + if (typeof c === 'string' && c.length > 0) {
570 + c = c.split(' ');
571 + let len = c.length;
572 +
573 + if (len > 0 && c[len - 1] === 'ONLY') {
574 + len--;
575 + ret.copy_theme = false;
576 + }
577 +
578 + while (len--) {
579 + ret.custom.unshift(c[len]);
580 + }
581 + }
582 + },
583 +
584 + prepare: function (state) {
585 + let has_custom_colors = false;
586 +
587 + if (typeof state.tmp.__commonColors === 'undefined') {
588 + let defname = state.chart.context;
589 +
590 + // if this chart has data-colors=""
591 + // we should use the chart uuid as the default key (private palette)
592 + // (data-common-colors="NAME" will be used anyways)
593 + let c = NETDATA.dataAttribute(state.element, 'colors', undefined);
594 + if (typeof c === 'string' && c.length > 0) {
595 + defname = state.uuid;
596 + has_custom_colors = true;
597 + }
598 +
599 + // get the commonColors setting
600 + state.tmp.__commonColors = NETDATA.dataAttribute(state.element, 'common-colors', defname);
601 + }
602 +
603 + let name = state.tmp.__commonColors;
604 + let ret = this.keys[name];
605 +
606 + if (typeof ret === 'undefined') {
607 + // add our commonMax
608 + this.keys[name] = {
609 + assigned: {}, // name-value of dimensions and their colors
610 + available: [], // an array of colors available to be used
611 + custom: [], // the array of colors defined by the user
612 + charts: {}, // the charts linked to this
613 + copy_theme: true
614 + };
615 + ret = this.keys[name];
616 + }
617 +
618 + if (typeof ret.charts[state.uuid] === 'undefined') {
619 + ret.charts[state.uuid] = state;
620 +
621 + if (has_custom_colors) {
622 + this.__read_custom_colors(state, ret);
623 + }
624 + }
625 +
626 + return ret;
627 + }
628 +};
629 +
630 +// ----------------------------------------------------------------------------------------------------------------
631 +// Chart Registry
632 +
633 +// When multiple charts need the same chart, we avoid downloading it
634 +// multiple times (and having it in browser memory multiple time)
635 +// by using this registry.
636 +
637 +// Every time we download a chart definition, we save it here with .add()
638 +// Then we try to get it back with .get(). If that fails, we download it.
639 +
640 +NETDATA.fixHost = function (host) {
641 + while (host.slice(-1) === '/') {
642 + host = host.substring(0, host.length - 1);
643 + }
644 +
645 + return host;
646 +};
647 +
648 +NETDATA.chartRegistry = {
649 + charts: {},
650 +
651 + globalReset: function () {
652 + this.charts = {};
653 + },
654 +
655 + add: function (host, id, data) {
656 + if (typeof this.charts[host] === 'undefined') {
657 + this.charts[host] = {};
658 + }
659 +
660 + //console.log('added ' + host + '/' + id);
661 + this.charts[host][id] = data;
662 + },
663 +
664 + get: function (host, id) {
665 + if (typeof this.charts[host] === 'undefined') {
666 + return null;
667 + }
668 +
669 + if (typeof this.charts[host][id] === 'undefined') {
670 + return null;
671 + }
672 +
673 + //console.log('cached ' + host + '/' + id);
674 + return this.charts[host][id];
675 + },
676 +
677 + downloadAll: function (host, callback) {
678 + host = NETDATA.fixHost(host);
679 +
680 + let self = this;
681 +
682 + function got_data(h, data, callback) {
683 + if (data !== null) {
684 + self.charts[h] = data.charts;
685 +
686 + // update the server timezone in our options
687 + if (typeof data.timezone === 'string') {
688 + NETDATA.options.server_timezone = data.timezone;
689 + }
690 + } else {
691 + NETDATA.error(406, h + '/api/v1/charts');
692 + }
693 +
694 + if (typeof callback === 'function') {
695 + callback(data);
696 + }
697 + }
698 +
699 + if (netdataSnapshotData !== null) {
700 + got_data(host, netdataSnapshotData.charts, callback);
701 + } else {
702 + $.ajax({
703 + url: host + '/api/v1/charts',
704 + async: true,
705 + cache: false,
706 + xhrFields: {withCredentials: true} // required for the cookie
707 + })
708 + .done(function (data) {
709 + data = NETDATA.xss.checkOptional('/api/v1/charts', data);
710 + got_data(host, data, callback);
711 + })
712 + .fail(function () {
713 + NETDATA.error(405, host + '/api/v1/charts');
714 +
715 + if (typeof callback === 'function') {
716 + callback(null);
717 + }
718 + });
719 + }
720 + }
721 +};
722 +
723 +// ----------------------------------------------------------------------------------------------------------------
724 +// Global Pan and Zoom on charts
725 +
726 +// Using this structure are synchronize all the charts, so that
727 +// when you pan or zoom one, all others are automatically refreshed
728 +// to the same timespan.
729 +
730 +NETDATA.globalPanAndZoom = {
731 + seq: 0, // timestamp ms
732 + // every time a chart is panned or zoomed
733 + // we set the timestamp here
734 + // then we use it as a sequence number
735 + // to find if other charts are synchronized
736 + // to this time-range
737 +
738 + master: null, // the master chart (state), to which all others
739 + // are synchronized
740 +
741 + force_before_ms: null, // the timespan to sync all other charts
742 + force_after_ms: null,
743 +
744 + callback: null,
745 +
746 + globalReset: function () {
747 + this.clearMaster();
748 + this.seq = 0;
749 + this.master = null;
750 + this.force_after_ms = null;
751 + this.force_before_ms = null;
752 + this.callback = null;
753 + },
754 +
755 + delay: function () {
756 + if (NETDATA.options.debug.globalPanAndZoom) {
757 + console.log('globalPanAndZoom.delay()');
758 + }
759 +
760 + NETDATA.options.auto_refresher_stop_until = Date.now() + NETDATA.options.current.global_pan_sync_time;
761 + },
762 +
763 + // set a new master
764 + setMaster: function (state, after, before) {
765 + this.delay();
766 +
767 + // if (NETDATA.options.current.sync_pan_and_zoom === false)
768 + // return;
769 + if (!NETDATA.options.current.sync_pan_and_zoom) {
770 + return;
771 + }
772 +
773 + if (this.master === null) {
774 + if (NETDATA.options.debug.globalPanAndZoom) {
775 + console.log('globalPanAndZoom.setMaster(' + state.id + ', ' + after + ', ' + before + ') SET MASTER');
776 + }
777 + } else if (this.master !== state) {
778 + if (NETDATA.options.debug.globalPanAndZoom) {
779 + console.log('globalPanAndZoom.setMaster(' + state.id + ', ' + after + ', ' + before + ') CHANGED MASTER');
780 + }
781 +
782 + this.master.resetChart(true, true);
783 + }
784 +
785 + let now = Date.now();
786 + this.master = state;
787 + this.seq = now;
788 + this.force_after_ms = after;
789 + this.force_before_ms = before;
790 +
791 + if (typeof this.callback === 'function') {
792 + this.callback(true, after, before);
793 + }
794 + },
795 +
796 + // clear the master
797 + clearMaster: function () {
798 + // if (NETDATA.options.debug.globalPanAndZoom === true)
799 + // console.log('globalPanAndZoom.clearMaster()');
800 + if (NETDATA.options.debug.globalPanAndZoom) {
801 + console.log('globalPanAndZoom.clearMaster()');
802 + }
803 +
804 + if (this.master !== null) {
805 + let st = this.master;
806 + this.master = null;
807 + st.resetChart();
808 + }
809 +
810 + this.master = null;
811 + this.seq = 0;
812 + this.force_after_ms = null;
813 + this.force_before_ms = null;
814 + NETDATA.options.auto_refresher_stop_until = 0;
815 +
816 + if (typeof this.callback === 'function') {
817 + this.callback(false, 0, 0);
818 + }
819 + },
820 +
821 + // is the given state the master of the global
822 + // pan and zoom sync?
823 + isMaster: function (state) {
824 + return (this.master === state);
825 + },
826 +
827 + // are we currently have a global pan and zoom sync?
828 + isActive: function () {
829 + return (this.master !== null && this.force_before_ms !== null && this.force_after_ms !== null && this.seq !== 0);
830 + },
831 +
832 + // check if a chart, other than the master
833 + // needs to be refreshed, due to the global pan and zoom
834 + shouldBeAutoRefreshed: function (state) {
835 + if (this.master === null || this.seq === 0) {
836 + return false;
837 + }
838 +
839 + //if (state.needsRecreation())
840 + // return true;
841 +
842 + return (state.tm.pan_and_zoom_seq !== this.seq);
843 + }
844 +};
845 +
846 +// ----------------------------------------------------------------------------------------------------------------
847 +// global chart underlay (time-frame highlighting)
848 +
849 +NETDATA.globalChartUnderlay = {
850 + callback: null, // what to call when a highlighted range is setup
851 + after: null, // highlight after this time
852 + before: null, // highlight before this time
853 + view_after: null, // the charts after_ms viewport when the highlight was setup
854 + view_before: null, // the charts before_ms viewport, when the highlight was setup
855 + state: null, // the chart the highlight was setup
856 +
857 + isActive: function () {
858 + return (this.after !== null && this.before !== null);
859 + },
860 +
861 + hasViewport: function () {
862 + return (this.state !== null && this.view_after !== null && this.view_before !== null);
863 + },
864 +
865 + init: function (state, after, before, view_after, view_before) {
866 + this.state = (typeof state !== 'undefined') ? state : null;
867 + this.after = (typeof after !== 'undefined' && after !== null && after > 0) ? after : null;
868 + this.before = (typeof before !== 'undefined' && before !== null && before > 0) ? before : null;
869 + this.view_after = (typeof view_after !== 'undefined' && view_after !== null && view_after > 0) ? view_after : null;
870 + this.view_before = (typeof view_before !== 'undefined' && view_before !== null && view_before > 0) ? view_before : null;
871 + },
872 +
873 + setup: function () {
874 + if (this.isActive()) {
875 + if (this.state === null) {
876 + this.state = NETDATA.options.targets[0];
877 + }
878 +
879 + if (typeof this.callback === 'function') {
880 + this.callback(true, this.after, this.before);
881 + }
882 + } else {
883 + if (typeof this.callback === 'function') {
884 + this.callback(false, 0, 0);
885 + }
886 + }
887 + },
888 +
889 + set: function (state, after, before, view_after, view_before) {
890 + if (after > before) {
891 + let t = after;
892 + after = before;
893 + before = t;
894 + }
895 +
896 + this.init(state, after, before, view_after, view_before);
897 +
898 + // if (this.hasViewport() === true)
899 + // NETDATA.globalPanAndZoom.setMaster(this.state, this.view_after, this.view_before);
900 + if (this.hasViewport()) {
901 + NETDATA.globalPanAndZoom.setMaster(this.state, this.view_after, this.view_before);
902 + }
903 +
904 + this.setup();
905 + },
906 +
907 + clear: function () {
908 + this.after = null;
909 + this.before = null;
910 + this.state = null;
911 + this.view_after = null;
912 + this.view_before = null;
913 +
914 + if (typeof this.callback === 'function') {
915 + this.callback(false, 0, 0);
916 + }
917 + },
918 +
919 + focus: function () {
920 + if (this.isActive() && this.hasViewport()) {
921 + if (this.state === null) {
922 + this.state = NETDATA.options.targets[0];
923 + }
924 +
925 + if (NETDATA.globalPanAndZoom.isMaster(this.state)) {
926 + NETDATA.globalPanAndZoom.clearMaster();
927 + }
928 +
929 + NETDATA.globalPanAndZoom.setMaster(this.state, this.view_after, this.view_before, true);
930 + }
931 + }
932 +};
933 +
934 +// ----------------------------------------------------------------------------------------------------------------
935 +// dimensions selection
936 +
937 +// TODO
938 +// move color assignment to dimensions, here
939 +
940 +let dimensionStatus = function (parent, label, name_div, value_div, color) {
941 + this.enabled = false;
942 + this.parent = parent;
943 + this.label = label;
944 + this.name_div = null;
945 + this.value_div = null;
946 + this.color = NETDATA.themes.current.foreground;
947 + this.selected = (parent.unselected_count === 0);
948 +
949 + this.setOptions(name_div, value_div, color);
950 +};
951 +
952 +dimensionStatus.prototype.invalidate = function () {
953 + this.name_div = null;
954 + this.value_div = null;
955 + this.enabled = false;
956 +};
957 +
958 +dimensionStatus.prototype.setOptions = function (name_div, value_div, color) {
959 + this.color = color;
960 +
961 + if (this.name_div !== name_div) {
962 + this.name_div = name_div;
963 + this.name_div.title = this.label;
964 + this.name_div.style.setProperty('color', this.color, 'important');
965 + // if (this.selected === false)
966 + if (!this.selected) {
967 + this.name_div.className = 'netdata-legend-name not-selected';
968 + } else {
969 + this.name_div.className = 'netdata-legend-name selected';
970 + }
971 + }
972 +
973 + if (this.value_div !== value_div) {
974 + this.value_div = value_div;
975 + this.value_div.title = this.label;
976 + this.value_div.style.setProperty('color', this.color, 'important');
977 + if (this.selected === false) {
978 + this.value_div.className = 'netdata-legend-value not-selected';
979 + } else {
980 + this.value_div.className = 'netdata-legend-value selected';
981 + }
982 + }
983 +
984 + this.enabled = true;
985 + this.setHandler();
986 +};
987 +
988 +dimensionStatus.prototype.setHandler = function () {
989 + // if (this.enabled === false) return;
990 + if (!this.enabled) {
991 + return;
992 + }
993 +
994 + let ds = this;
995 +
996 + // this.name_div.onmousedown = this.value_div.onmousedown = function(e) {
997 + this.name_div.onclick = this.value_div.onclick = function (e) {
998 + e.preventDefault();
999 + if (ds.isSelected()) {
1000 + // this is selected
1001 + if (e.shiftKey || e.ctrlKey) {
1002 + // control or shift key is pressed -> unselect this (except is none will remain selected, in which case select all)
1003 + ds.unselect();
1004 +
1005 + if (ds.parent.countSelected() === 0) {
1006 + ds.parent.selectAll();
1007 + }
1008 + } else {
1009 + // no key is pressed -> select only this (except if it is the only selected already, in which case select all)
1010 + if (ds.parent.countSelected() === 1) {
1011 + ds.parent.selectAll();
1012 + } else {
1013 + ds.parent.selectNone();
1014 + ds.select();
1015 + }
1016 + }
1017 + }
1018 + else {
1019 + // this is not selected
1020 + if (e.shiftKey || e.ctrlKey) {
1021 + // control or shift key is pressed -> select this too
1022 + ds.select();
1023 + } else {
1024 + // no key is pressed -> select only this
1025 + ds.parent.selectNone();
1026 + ds.select();
1027 + }
1028 + }
1029 +
1030 + ds.parent.state.redrawChart();
1031 + }
1032 +};
1033 +
1034 +dimensionStatus.prototype.select = function () {
1035 + // if (this.enabled === false) return;
1036 + if (!this.enabled) {
1037 + return;
1038 + }
1039 +
1040 + this.name_div.className = 'netdata-legend-name selected';
1041 + this.value_div.className = 'netdata-legend-value selected';
1042 + this.selected = true;
1043 +};
1044 +
1045 +dimensionStatus.prototype.unselect = function () {
1046 + // if (this.enabled === false) return;
1047 + if (!this.enabled) {
1048 + return;
1049 + }
1050 +
1051 + this.name_div.className = 'netdata-legend-name not-selected';
1052 + this.value_div.className = 'netdata-legend-value hidden';
1053 + this.selected = false;
1054 +};
1055 +
1056 +dimensionStatus.prototype.isSelected = function () {
1057 + // return(this.enabled === true && this.selected === true);
1058 + return this.enabled && this.selected;
1059 +};
1060 +
1061 +// ----------------------------------------------------------------------------------------------------------------
1062 +
1063 +let dimensionsVisibility = function (state) {
1064 + this.state = state;
1065 + this.len = 0;
1066 + this.dimensions = {};
1067 + this.selected_count = 0;
1068 + this.unselected_count = 0;
1069 +};
1070 +
1071 +dimensionsVisibility.prototype.dimensionAdd = function (label, name_div, value_div, color) {
1072 + if (typeof this.dimensions[label] === 'undefined') {
1073 + this.len++;
1074 + this.dimensions[label] = new dimensionStatus(this, label, name_div, value_div, color);
1075 + } else {
1076 + this.dimensions[label].setOptions(name_div, value_div, color);
1077 + }
1078 +
1079 + return this.dimensions[label];
1080 +};
1081 +
1082 +dimensionsVisibility.prototype.dimensionGet = function (label) {
1083 + return this.dimensions[label];
1084 +};
1085 +
1086 +dimensionsVisibility.prototype.invalidateAll = function () {
1087 + let keys = Object.keys(this.dimensions);
1088 + let len = keys.length;
1089 + while (len--) {
1090 + this.dimensions[keys[len]].invalidate();
1091 + }
1092 +};
1093 +
1094 +dimensionsVisibility.prototype.selectAll = function () {
1095 + let keys = Object.keys(this.dimensions);
1096 + let len = keys.length;
1097 + while (len--) {
1098 + this.dimensions[keys[len]].select();
1099 + }
1100 +};
1101 +
1102 +dimensionsVisibility.prototype.countSelected = function () {
1103 + let selected = 0;
1104 + let keys = Object.keys(this.dimensions);
1105 + let len = keys.length;
1106 + while (len--) {
1107 + if (this.dimensions[keys[len]].isSelected()) {
1108 + selected++;
1109 + }
1110 + }
1111 +
1112 + return selected;
1113 +};
1114 +
1115 +dimensionsVisibility.prototype.selectNone = function () {
1116 + let keys = Object.keys(this.dimensions);
1117 + let len = keys.length;
1118 + while (len--) {
1119 + this.dimensions[keys[len]].unselect();
1120 + }
1121 +};
1122 +
1123 +dimensionsVisibility.prototype.selected2BooleanArray = function (array) {
1124 + let ret = [];
1125 + this.selected_count = 0;
1126 + this.unselected_count = 0;
1127 +
1128 + let len = array.length;
1129 + while (len--) {
1130 + let ds = this.dimensions[array[len]];
1131 + if (typeof ds === 'undefined') {
1132 + // console.log(array[i] + ' is not found');
1133 + ret.unshift(false);
1134 + } else if (ds.isSelected()) {
1135 + ret.unshift(true);
1136 + this.selected_count++;
1137 + } else {
1138 + ret.unshift(false);
1139 + this.unselected_count++;
1140 + }
1141 + }
1142 +
1143 + if (this.selected_count === 0 && this.unselected_count !== 0) {
1144 + this.selectAll();
1145 + return this.selected2BooleanArray(array);
1146 + }
1147 +
1148 + return ret;
1149 +};
1150 +
1151 +// ----------------------------------------------------------------------------------------------------------------
1152 +// date/time conversion
1153 +
1154 +NETDATA.dateTime = {
1155 + using_timezone: false,
1156 +
1157 + // these are the old netdata functions
1158 + // we fallback to these, if the new ones fail
1159 +
1160 + localeDateStringNative: function (d) {
1161 + return d.toLocaleDateString();
1162 + },
1163 +
1164 + localeTimeStringNative: function (d) {
1165 + return d.toLocaleTimeString();
1166 + },
1167 +
1168 + xAxisTimeStringNative: function (d) {
1169 + return NETDATA.zeropad(d.getHours()) + ":"
1170 + + NETDATA.zeropad(d.getMinutes()) + ":"
1171 + + NETDATA.zeropad(d.getSeconds());
1172 + },
1173 +
1174 + // initialize the new date/time conversion
1175 + // functions.
1176 + // if this fails, we fallback to the above
1177 + init: function (timezone) {
1178 + //console.log('init with timezone: ' + timezone);
1179 +
1180 + // detect browser timezone
1181 + try {
1182 + NETDATA.options.browser_timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
1183 + } catch (e) {
1184 + console.log('failed to detect browser timezone: ' + e.toString());
1185 + NETDATA.options.browser_timezone = 'cannot-detect-it';
1186 + }
1187 +
1188 + let ret = false;
1189 +
1190 + try {
1191 + let dateOptions = {
1192 + localeMatcher: 'best fit',
1193 + formatMatcher: 'best fit',
1194 + weekday: 'short',
1195 + year: 'numeric',
1196 + month: 'short',
1197 + day: '2-digit'
1198 + };
1199 +
1200 + let timeOptions = {
1201 + localeMatcher: 'best fit',
1202 + hour12: false,
1203 + formatMatcher: 'best fit',
1204 + hour: '2-digit',
1205 + minute: '2-digit',
1206 + second: '2-digit'
1207 + };
1208 +
1209 + let xAxisOptions = {
1210 + localeMatcher: 'best fit',
1211 + hour12: false,
1212 + formatMatcher: 'best fit',
1213 + hour: '2-digit',
1214 + minute: '2-digit',
1215 + second: '2-digit'
1216 + };
1217 +
1218 + if (typeof timezone === 'string' && timezone !== '' && timezone !== 'default') {
1219 + dateOptions.timeZone = timezone;
1220 + timeOptions.timeZone = timezone;
1221 + timeOptions.timeZoneName = 'short';
1222 + xAxisOptions.timeZone = timezone;
1223 + this.using_timezone = true;
1224 + } else {
1225 + timezone = 'default';
1226 + this.using_timezone = false;
1227 + }
1228 +
1229 + this.dateFormat = new Intl.DateTimeFormat(navigator.language, dateOptions);
1230 + this.timeFormat = new Intl.DateTimeFormat(navigator.language, timeOptions);
1231 + this.xAxisFormat = new Intl.DateTimeFormat(navigator.language, xAxisOptions);
1232 +
1233 + this.localeDateString = function (d) {
1234 + return this.dateFormat.format(d);
1235 + };
1236 +
1237 + this.localeTimeString = function (d) {
1238 + return this.timeFormat.format(d);
1239 + };
1240 +
1241 + this.xAxisTimeString = function (d) {
1242 + return this.xAxisFormat.format(d);
1243 + };
1244 +
1245 + //let d = new Date();
1246 + //let t = this.dateFormat.format(d) + ' ' + this.timeFormat.format(d) + ' ' + this.xAxisFormat.format(d);
1247 +
1248 + ret = true;
1249 + } catch (e) {
1250 + console.log('Cannot setup Date/Time formatting: ' + e.toString());
1251 +
1252 + timezone = 'default';
1253 + this.localeDateString = this.localeDateStringNative;
1254 + this.localeTimeString = this.localeTimeStringNative;
1255 + this.xAxisTimeString = this.xAxisTimeStringNative;
1256 + this.using_timezone = false;
1257 +
1258 + ret = false;
1259 + }
1260 +
1261 + // save it
1262 + //console.log('init setOption timezone: ' + timezone);
1263 + NETDATA.setOption('timezone', timezone);
1264 +
1265 + return ret;
1266 + }
1267 +};
1268 +NETDATA.dateTime.init(NETDATA.options.current.timezone);
1269 +
1270 +// ----------------------------------------------------------------------------------------------------------------
1271 +// global selection sync
1272 +
1273 +NETDATA.globalSelectionSync = {
1274 + state: null,
1275 + dont_sync_before: 0,
1276 + last_t: 0,
1277 + slaves: [],
1278 + timeout_id: undefined,
1279 +
1280 + globalReset: function () {
1281 + this.stop();
1282 + this.state = null;
1283 + this.dont_sync_before = 0;
1284 + this.last_t = 0;
1285 + this.slaves = [];
1286 + this.timeout_id = undefined;
1287 + },
1288 +
1289 + active: function () {
1290 + return (this.state !== null);
1291 + },
1292 +
1293 + // return true if global selection sync can be enabled now
1294 + enabled: function () {
1295 + // console.log('enabled()');
1296 + // can we globally apply selection sync?
1297 + // if (NETDATA.options.current.sync_selection === false) {
1298 + if (!NETDATA.options.current.sync_selection) {
1299 + return false;
1300 + }
1301 +
1302 + return (this.dont_sync_before <= Date.now());
1303 + },
1304 +
1305 + // set the global selection sync master
1306 + setMaster: function (state) {
1307 + // if (this.enabled() === false) {
1308 + if (!this.enabled()) {
1309 + this.stop();
1310 + return;
1311 + }
1312 +
1313 + if (this.state === state) {
1314 + return;
1315 + }
1316 +
1317 + if (this.state !== null) {
1318 + this.stop();
1319 + }
1320 +
1321 + if (NETDATA.options.debug.globalSelectionSync) {
1322 + console.log('globalSelectionSync.setMaster(' + state.id + ')');
1323 + }
1324 +
1325 + state.selected = true;
1326 + this.state = state;
1327 + this.last_t = 0;
1328 +
1329 + // find all slaves
1330 + let targets = NETDATA.intersectionObserver.targets();
1331 + this.slaves = [];
1332 + let len = targets.length;
1333 + while (len--) {
1334 + let st = targets[len];
1335 + if (this.state !== st && st.globalSelectionSyncIsEligible()) {
1336 + this.slaves.push(st);
1337 + }
1338 + }
1339 +
1340 + // this.delay(100);
1341 + },
1342 +
1343 + // stop global selection sync
1344 + stop: function () {
1345 + if (this.state !== null) {
1346 + if (NETDATA.options.debug.globalSelectionSync) {
1347 + console.log('globalSelectionSync.stop()');
1348 + }
1349 +
1350 + let len = this.slaves.length;
1351 + while (len--) {
1352 + this.slaves[len].clearSelection();
1353 + }
1354 +
1355 + this.state.clearSelection();
1356 +
1357 + this.last_t = 0;
1358 + this.slaves = [];
1359 + this.state = null;
1360 + }
1361 + },
1362 +
1363 + // delay global selection sync for some time
1364 + delay: function (ms) {
1365 + if (NETDATA.options.current.sync_selection) {
1366 + // if (NETDATA.options.debug.globalSelectionSync === true) {
1367 + if (NETDATA.options.debug.globalSelectionSync) {
1368 + console.log('globalSelectionSync.delay()');
1369 + }
1370 +
1371 + if (typeof ms === 'number') {
1372 + this.dont_sync_before = Date.now() + ms;
1373 + } else {
1374 + this.dont_sync_before = Date.now() + NETDATA.options.current.sync_selection_delay;
1375 + }
1376 + }
1377 + },
1378 +
1379 + __syncSlaves: function () {
1380 + // if (NETDATA.globalSelectionSync.enabled() === true) {
1381 + if (NETDATA.globalSelectionSync.enabled()) {
1382 + // if (NETDATA.options.debug.globalSelectionSync === true)
1383 + if (NETDATA.options.debug.globalSelectionSync) {
1384 + console.log('globalSelectionSync.__syncSlaves()');
1385 + }
1386 +
1387 + let t = NETDATA.globalSelectionSync.last_t;
1388 + let len = NETDATA.globalSelectionSync.slaves.length;
1389 + while (len--) {
1390 + NETDATA.globalSelectionSync.slaves[len].setSelection(t);
1391 + }
1392 +
1393 + this.timeout_id = undefined;
1394 + }
1395 + },
1396 +
1397 + // sync all the visible charts to the given time
1398 + // this is to be called from the chart libraries
1399 + sync: function (state, t) {
1400 + // if (NETDATA.options.current.sync_selection === true) {
1401 + if (NETDATA.options.current.sync_selection) {
1402 + // if (NETDATA.options.debug.globalSelectionSync === true)
1403 + if (NETDATA.options.debug.globalSelectionSync) {
1404 + console.log('globalSelectionSync.sync(' + state.id + ', ' + t.toString() + ')');
1405 + }
1406 +
1407 + this.setMaster(state);
1408 +
1409 + if (t === this.last_t) {
1410 + return;
1411 + }
1412 +
1413 + this.last_t = t;
1414 +
1415 + if (state.foreign_element_selection !== null) {
1416 + state.foreign_element_selection.innerText = NETDATA.dateTime.localeDateString(t) + ' ' + NETDATA.dateTime.localeTimeString(t);
1417 + }
1418 +
1419 + if (this.timeout_id) {
1420 + NETDATA.timeout.clear(this.timeout_id);
1421 + }
1422 +
1423 + this.timeout_id = NETDATA.timeout.set(this.__syncSlaves, 0);
1424 + }
1425 + }
1426 +};
1427 +
1428 +NETDATA.intersectionObserver = {
1429 + observer: null,
1430 + visible_targets: [],
1431 +
1432 + options: {
1433 + root: null,
1434 + rootMargin: "0px",
1435 + threshold: null
1436 + },
1437 +
1438 + enabled: function () {
1439 + return this.observer !== null;
1440 + },
1441 +
1442 + globalReset: function () {
1443 + if (this.observer !== null) {
1444 + this.visible_targets = [];
1445 + this.observer.disconnect();
1446 + this.init();
1447 + }
1448 + },
1449 +
1450 + targets: function () {
1451 + if (this.enabled() && this.visible_targets.length > 0) {
1452 + return this.visible_targets;
1453 + } else {
1454 + return NETDATA.options.targets;
1455 + }
1456 + },
1457 +
1458 + switchChartVisibility: function () {
1459 + let old = this.__visibilityRatioOld;
1460 +
1461 + if (old !== this.__visibilityRatio) {
1462 + if (old === 0 && this.__visibilityRatio > 0) {
1463 + this.unhideChart();
1464 + } else if (old > 0 && this.__visibilityRatio === 0) {
1465 + this.hideChart();
1466 + }
1467 +
1468 + this.__visibilityRatioOld = this.__visibilityRatio;
1469 + }
1470 + },
1471 +
1472 + handler: function (entries, observer) {
1473 + entries.forEach(function (entry) {
1474 + let state = NETDATA.chartState(entry.target);
1475 +
1476 + let idx;
1477 + if (entry.intersectionRatio > 0) {
1478 + idx = NETDATA.intersectionObserver.visible_targets.indexOf(state);
1479 + if (idx === -1) {
1480 + if (NETDATA.scrollUp) {
1481 + NETDATA.intersectionObserver.visible_targets.push(state);
1482 + } else {
1483 + NETDATA.intersectionObserver.visible_targets.unshift(state);
1484 + }
1485 + }
1486 + else if (state.__visibilityRatio === 0) {
1487 + state.log("was not visible until now, but was already in visible_targets");
1488 + }
1489 + } else {
1490 + idx = NETDATA.intersectionObserver.visible_targets.indexOf(state);
1491 + if (idx !== -1) {
1492 + NETDATA.intersectionObserver.visible_targets.splice(idx, 1);
1493 + } else if (state.__visibilityRatio > 0) {
1494 + state.log("was visible, but not found in visible_targets");
1495 + }
1496 + }
1497 +
1498 + state.__visibilityRatio = entry.intersectionRatio;
1499 +
1500 + if (NETDATA.options.current.async_on_scroll === false) {
1501 + if (window.requestIdleCallback) {
1502 + window.requestIdleCallback(function () {
1503 + NETDATA.intersectionObserver.switchChartVisibility.call(state);
1504 + }, {timeout: 100});
1505 + } else {
1506 + NETDATA.intersectionObserver.switchChartVisibility.call(state);
1507 + }
1508 + }
1509 + });
1510 + },
1511 +
1512 + observe: function (state) {
1513 + if (this.enabled()) {
1514 + state.__visibilityRatioOld = 0;
1515 + state.__visibilityRatio = 0;
1516 + this.observer.observe(state.element);
1517 +
1518 + state.isVisible = function () {
1519 + if (NETDATA.options.current.update_only_visible === false) {
1520 + return true;
1521 + }
1522 +
1523 + NETDATA.intersectionObserver.switchChartVisibility.call(this);
1524 +
1525 + return this.__visibilityRatio > 0;
1526 + }
1527 + }
1528 + },
1529 +
1530 + init: function () {
1531 + if (typeof netdataIntersectionObserver === 'undefined' || netdataIntersectionObserver) {
1532 + try {
1533 + this.observer = new IntersectionObserver(this.handler, this.options);
1534 + } catch (e) {
1535 + console.log("IntersectionObserver is not supported on this browser");
1536 + this.observer = null;
1537 + }
1538 + }
1539 + //else {
1540 + // console.log("IntersectionObserver is disabled");
1541 + //}
1542 + }
1543 +};
1544 +NETDATA.intersectionObserver.init();
1545 +
1546 +// ----------------------------------------------------------------------------------------------------------------
1547 +// Our state object, where all per-chart values are stored
1548 +
1549 +let chartState = function (element) {
1550 + this.element = element;
1551 +
1552 + // IMPORTANT:
1553 + // all private functions should use 'that', instead of 'this'
1554 + let that = this;
1555 +
1556 + // ============================================================================================================
1557 + // ERROR HANDLING
1558 +
1559 + /* error() - private
1560 + * show an error instead of the chart
1561 + */
1562 + let error = function (msg) {
1563 + let ret = true;
1564 +
1565 + if (typeof netdataErrorCallback === 'function') {
1566 + ret = netdataErrorCallback('chart', that.id, msg);
1567 + }
1568 +
1569 + if (ret) {
1570 + that.element.innerHTML = that.id + ': ' + msg;
1571 + that.enabled = false;
1572 + that.current = that.pan;
1573 + }
1574 + };
1575 +
1576 + // console logging
1577 + this.log = function (msg) {
1578 + console.log(this.id + ' (' + this.library_name + ' ' + this.uuid + '): ' + msg);
1579 + };
1580 +
1581 + this.debugLog = function (msg) {
1582 + if (this.debug) {
1583 + this.log(msg);
1584 + }
1585 + };
1586 +
1587 + // ============================================================================================================
1588 + // EARLY INITIALIZATION
1589 +
1590 + // These are variables that should exist even if the chart is never to be rendered.
1591 + // Be careful what you add here - there may be thousands of charts on the page.
1592 +
1593 + // GUID - a unique identifier for the chart
1594 + this.uuid = NETDATA.guid();
1595 +
1596 + // string - the name of chart
1597 + this.id = NETDATA.dataAttribute(this.element, 'netdata', undefined);
1598 + if (typeof this.id === 'undefined') {
1599 + error("netdata elements need data-netdata");
1600 + return;
1601 + }
1602 +
1603 + // string - the key for localStorage settings
1604 + this.settings_id = NETDATA.dataAttribute(this.element, 'id', null);
1605 +
1606 + // the user given dimensions of the element
1607 + this.width = NETDATA.dataAttribute(this.element, 'width', NETDATA.chartDefaults.width);
1608 + this.height = NETDATA.dataAttribute(this.element, 'height', NETDATA.chartDefaults.height);
1609 + this.height_original = this.height;
1610 +
1611 + if (this.settings_id !== null) {
1612 + this.height = NETDATA.localStorageGet('chart_heights.' + this.settings_id, this.height, function (height) {
1613 + // this is the callback that will be called
1614 + // if and when the user resets all localStorage variables
1615 + // to their defaults
1616 +
1617 + resizeChartToHeight(height);
1618 + });
1619 + }
1620 +
1621 + // the chart library requested by the user
1622 + this.library_name = NETDATA.dataAttribute(this.element, 'chart-library', NETDATA.chartDefaults.library);
1623 +
1624 + // check the requested library is available
1625 + // we don't initialize it here - it will be initialized when
1626 + // this chart will be first used
1627 + if (typeof NETDATA.chartLibraries[this.library_name] === 'undefined') {
1628 + NETDATA.error(402, this.library_name);
1629 + error('chart library "' + this.library_name + '" is not found');
1630 + this.enabled = false;
1631 + } else if (NETDATA.chartLibraries[this.library_name].enabled === false) {
1632 + NETDATA.error(403, this.library_name);
1633 + error('chart library "' + this.library_name + '" is not enabled');
1634 + this.enabled = false;
1635 + } else {
1636 + this.library = NETDATA.chartLibraries[this.library_name];
1637 + }
1638 +
1639 + this.auto = {
1640 + name: 'auto',
1641 + autorefresh: true,
1642 + force_update_at: 0, // the timestamp to force the update at
1643 + force_before_ms: null,
1644 + force_after_ms: null
1645 + };
1646 + this.pan = {
1647 + name: 'pan',
1648 + autorefresh: false,
1649 + force_update_at: 0, // the timestamp to force the update at
1650 + force_before_ms: null,
1651 + force_after_ms: null
1652 + };
1653 + this.zoom = {
1654 + name: 'zoom',
1655 + autorefresh: false,
1656 + force_update_at: 0, // the timestamp to force the update at
1657 + force_before_ms: null,
1658 + force_after_ms: null
1659 + };
1660 +
1661 + // this is a pointer to one of the sub-classes below
1662 + // auto, pan, zoom
1663 + this.current = this.auto;
1664 +
1665 + this.running = false; // boolean - true when the chart is being refreshed now
1666 + this.enabled = true; // boolean - is the chart enabled for refresh?
1667 +
1668 + this.force_update_every = null; // number - overwrite the visualization update frequency of the chart
1669 +
1670 + this.tmp = {};
1671 +
1672 + this.foreign_element_before = null;
1673 + this.foreign_element_after = null;
1674 + this.foreign_element_duration = null;
1675 + this.foreign_element_update_every = null;
1676 + this.foreign_element_selection = null;
1677 +
1678 + // ============================================================================================================
1679 + // PRIVATE FUNCTIONS
1680 +
1681 + // reset the runtime status variables to their defaults
1682 + const runtimeInit = function () {
1683 + that.paused = false; // boolean - is the chart paused for any reason?
1684 + that.selected = false; // boolean - is the chart shown a selection?
1685 +
1686 + that.chart_created = false; // boolean - is the library.create() been called?
1687 + that.dom_created = false; // boolean - is the chart DOM been created?
1688 + that.fetching_data = false; // boolean - true while we fetch data via ajax
1689 +
1690 + that.updates_counter = 0; // numeric - the number of refreshes made so far
1691 + that.updates_since_last_unhide = 0; // numeric - the number of refreshes made since the last time the chart was unhidden
1692 + that.updates_since_last_creation = 0; // numeric - the number of refreshes made since the last time the chart was created
1693 +
1694 + that.tm = {
1695 + last_initialized: 0, // milliseconds - the timestamp it was last initialized
1696 + last_dom_created: 0, // milliseconds - the timestamp its DOM was last created
1697 + last_mode_switch: 0, // milliseconds - the timestamp it switched modes
1698 +
1699 + last_info_downloaded: 0, // milliseconds - the timestamp we downloaded the chart
1700 + last_updated: 0, // the timestamp the chart last updated with data
1701 + pan_and_zoom_seq: 0, // the sequence number of the global synchronization
1702 + // between chart.
1703 + // Used with NETDATA.globalPanAndZoom.seq
1704 + last_visible_check: 0, // the time we last checked if it is visible
1705 + last_resized: 0, // the time the chart was resized
1706 + last_hidden: 0, // the time the chart was hidden
1707 + last_unhidden: 0, // the time the chart was unhidden
1708 + last_autorefreshed: 0 // the time the chart was last refreshed
1709 + };
1710 +
1711 + that.data = null; // the last data as downloaded from the netdata server
1712 + that.data_url = 'invalid://'; // string - the last url used to update the chart
1713 + that.data_points = 0; // number - the number of points returned from netdata
1714 + that.data_after = 0; // milliseconds - the first timestamp of the data
1715 + that.data_before = 0; // milliseconds - the last timestamp of the data
1716 + that.data_update_every = 0; // milliseconds - the frequency to update the data
1717 +
1718 + that.tmp = {}; // members that can be destroyed to save memory
1719 + };
1720 +
1721 + // initialize all the variables that are required for the chart to be rendered
1722 + const lateInitialization = function () {
1723 + if (typeof that.host !== 'undefined') {
1724 + return;
1725 + }
1726 +
1727 + // string - the netdata server URL, without any path
1728 + that.host = NETDATA.dataAttribute(that.element, 'host', NETDATA.serverDefault);
1729 +
1730 + // make sure the host does not end with /
1731 + // all netdata API requests use absolute paths
1732 + while (that.host.slice(-1) === '/') {
1733 + that.host = that.host.substring(0, that.host.length - 1);
1734 + }
1735 +
1736 + // string - the grouping method requested by the user
1737 + that.method = NETDATA.dataAttribute(that.element, 'method', NETDATA.chartDefaults.method);
1738 + that.gtime = NETDATA.dataAttribute(that.element, 'gtime', 0);
1739 +
1740 + // the time-range requested by the user
1741 + that.after = NETDATA.dataAttribute(that.element, 'after', NETDATA.chartDefaults.after);
1742 + that.before = NETDATA.dataAttribute(that.element, 'before', NETDATA.chartDefaults.before);
1743 +
1744 + // the pixels per point requested by the user
1745 + that.pixels_per_point = NETDATA.dataAttribute(that.element, 'pixels-per-point', 1);
1746 + that.points = NETDATA.dataAttribute(that.element, 'points', null);
1747 +
1748 + // the forced update_every
1749 + that.force_update_every = NETDATA.dataAttribute(that.element, 'update-every', null);
1750 + if (typeof that.force_update_every !== 'number' || that.force_update_every <= 1) {
1751 + if (that.force_update_every !== null) {
1752 + that.log('ignoring invalid value of property data-update-every');
1753 + }
1754 +
1755 + that.force_update_every = null;
1756 + } else {
1757 + that.force_update_every *= 1000;
1758 + }
1759 +
1760 + // the dimensions requested by the user
1761 + that.dimensions = NETDATA.encodeURIComponent(NETDATA.dataAttribute(that.element, 'dimensions', null));
1762 +
1763 + that.title = NETDATA.dataAttribute(that.element, 'title', null); // the title of the chart
1764 + that.units = NETDATA.dataAttribute(that.element, 'units', null); // the units of the chart dimensions
1765 + that.units_desired = NETDATA.dataAttribute(that.element, 'desired-units', NETDATA.options.current.units); // the units of the chart dimensions
1766 + that.units_current = that.units;
1767 + that.units_common = NETDATA.dataAttribute(that.element, 'common-units', null);
1768 +
1769 + // additional options to pass to netdata
1770 + that.append_options = NETDATA.encodeURIComponent(NETDATA.dataAttribute(that.element, 'append-options', null));
1771 +
1772 + // override options to pass to netdata
1773 + that.override_options = NETDATA.encodeURIComponent(NETDATA.dataAttribute(that.element, 'override-options', null));
1774 +
1775 + that.debug = NETDATA.dataAttributeBoolean(that.element, 'debug', false);
1776 +
1777 + that.value_decimal_detail = -1;
1778 + let d = NETDATA.dataAttribute(that.element, 'decimal-digits', -1);
1779 + if (typeof d === 'number') {
1780 + that.value_decimal_detail = d;
1781 + } else if (typeof d !== 'undefined') {
1782 + that.log('ignoring decimal-digits value: ' + d.toString());
1783 + }
1784 +
1785 + // if we need to report the rendering speed
1786 + // find the element that needs to be updated
1787 + let refresh_dt_element_name = NETDATA.dataAttribute(that.element, 'dt-element-name', null); // string - the element to print refresh_dt_ms
1788 +
1789 + if (refresh_dt_element_name !== null) {
1790 + that.refresh_dt_element = document.getElementById(refresh_dt_element_name) || null;
1791 + }
1792 + else {
1793 + that.refresh_dt_element = null;
1794 + }
1795 +
1796 + that.dimensions_visibility = new dimensionsVisibility(that);
1797 +
1798 + that.netdata_first = 0; // milliseconds - the first timestamp in netdata
1799 + that.netdata_last = 0; // milliseconds - the last timestamp in netdata
1800 + that.requested_after = null; // milliseconds - the timestamp of the request after param
1801 + that.requested_before = null; // milliseconds - the timestamp of the request before param
1802 + that.requested_padding = null;
1803 + that.view_after = 0;
1804 + that.view_before = 0;
1805 +
1806 + that.refresh_dt_ms = 0; // milliseconds - the time the last refresh took
1807 +
1808 + // how many retries we have made to load chart data from the server
1809 + that.retries_on_data_failures = 0;
1810 +
1811 + // color management
1812 + that.colors = null;
1813 + that.colors_assigned = null;
1814 + that.colors_available = null;
1815 + that.colors_custom = null;
1816 +
1817 + that.element_message = null; // the element already created by the user
1818 + that.element_chart = null; // the element with the chart
1819 + that.element_legend = null; // the element with the legend of the chart (if created by us)
1820 + that.element_legend_childs = {
1821 + content: null,
1822 + hidden: null,
1823 + title_date: null,
1824 + title_time: null,
1825 + title_units: null,
1826 + perfect_scroller: null, // the container to apply perfect scroller to
1827 + series: null
1828 + };
1829 +
1830 + that.chart_url = null; // string - the url to download chart info
1831 + that.chart = null; // object - the chart as downloaded from the server
1832 +
1833 + function get_foreign_element_by_id(opt) {
1834 + let id = NETDATA.dataAttribute(that.element, opt, null);
1835 + if (id === null) {
1836 + //that.log('option "' + opt + '" is undefined');
1837 + return null;
1838 + }
1839 +
1840 + let el = document.getElementById(id);
1841 + if (typeof el === 'undefined') {
1842 + that.log('cannot find an element with name "' + id.toString() + '"');
1843 + return null;
1844 + }
1845 +
1846 + return el;
1847 + }
1848 +
1849 + that.foreign_element_before = get_foreign_element_by_id('show-before-at');
1850 + that.foreign_element_after = get_foreign_element_by_id('show-after-at');
1851 + that.foreign_element_duration = get_foreign_element_by_id('show-duration-at');
1852 + that.foreign_element_update_every = get_foreign_element_by_id('show-update-every-at');
1853 + that.foreign_element_selection = get_foreign_element_by_id('show-selection-at');
1854 + };
1855 +
1856 + const destroyDOM = function () {
1857 + if (that.enabled === false) {
1858 + return;
1859 + }
1860 +
1861 + if (that.debug) {
1862 + that.log('destroyDOM()');
1863 + }
1864 +
1865 + // that.element.className = 'netdata-message icon';
1866 + // that.element.innerHTML = '<i class="fas fa-sync"></i> netdata';
1867 + that.element.innerHTML = '';
1868 + that.element_message = null;
1869 + that.element_legend = null;
1870 + that.element_chart = null;
1871 + that.element_legend_childs.series = null;
1872 +
1873 + that.chart_created = false;
1874 + that.dom_created = false;
1875 +
1876 + that.tm.last_resized = 0;
1877 + that.tm.last_dom_created = 0;
1878 + };
1879 +
1880 + let createDOM = function () {
1881 + if (that.enabled === false) {
1882 + return;
1883 + }
1884 + lateInitialization();
1885 +
1886 + destroyDOM();
1887 +
1888 + if (that.debug) {
1889 + that.log('createDOM()');
1890 + }
1891 +
1892 + that.element_message = document.createElement('div');
1893 + that.element_message.className = 'netdata-message icon hidden';
1894 + that.element.appendChild(that.element_message);
1895 +
1896 + that.dom_created = true;
1897 + that.chart_created = false;
1898 +
1899 + that.tm.last_dom_created =
1900 + that.tm.last_resized = Date.now();
1901 +
1902 + showLoading();
1903 + };
1904 +
1905 + const initDOM = function () {
1906 + that.element.className = that.library.container_class(that);
1907 +
1908 + if (typeof(that.width) === 'string') {
1909 + that.element.style.width = that.width;
1910 + } else if (typeof(that.width) === 'number') {
1911 + that.element.style.width = that.width.toString() + 'px';
1912 + }
1913 +
1914 + if (typeof(that.library.aspect_ratio) === 'undefined') {
1915 + if (typeof(that.height) === 'string') {
1916 + that.element.style.height = that.height;
1917 + } else if (typeof(that.height) === 'number') {
1918 + that.element.style.height = that.height.toString() + 'px';
1919 + }
1920 + }
1921 +
1922 + if (NETDATA.chartDefaults.min_width !== null) {
1923 + that.element.style.min_width = NETDATA.chartDefaults.min_width;
1924 + }
1925 + };
1926 +
1927 + const invisibleSearchableText = function () {
1928 + return '<span style="position:absolute; opacity: 0; width: 0px;">' + that.id + '</span>';
1929 + };
1930 +
1931 + /* init() private
1932 + * initialize state variables
1933 + * destroy all (possibly) created state elements
1934 + * create the basic DOM for a chart
1935 + */
1936 + const init = function (opt) {
1937 + if (that.enabled === false) {
1938 + return;
1939 + }
1940 +
1941 + runtimeInit();
1942 + that.element.innerHTML = invisibleSearchableText();
1943 +
1944 + that.tm.last_initialized = Date.now();
1945 + that.setMode('auto');
1946 +
1947 + if (opt !== 'fast') {
1948 + if (that.isVisible(true) || opt === 'force') {
1949 + createDOM();
1950 + }
1951 + }
1952 + };
1953 +
1954 + const maxMessageFontSize = function () {
1955 + let screenHeight = screen.height;
1956 + let el = that.element;
1957 +
1958 + // normally we want a font size, as tall as the element
1959 + let h = el.clientHeight;
1960 +
1961 + // but give it some air, 20% let's say, or 5 pixels min
1962 + let lost = Math.max(h * 0.2, 5);
1963 + h -= lost;
1964 +
1965 + // center the text, vertically
1966 + let paddingTop = (lost - 5) / 2;
1967 +
1968 + // but check the width too
1969 + // it should fit 10 characters in it
1970 + let w = el.clientWidth / 10;
1971 + if (h > w) {
1972 + paddingTop += (h - w) / 2;
1973 + h = w;
1974 + }
1975 +
1976 + // and don't make it too huge
1977 + // 5% of the screen size is good
1978 + if (h > screenHeight / 20) {
1979 + paddingTop += (h - (screenHeight / 20)) / 2;
1980 + h = screenHeight / 20;
1981 + }
1982 +
1983 + // set it
1984 + that.element_message.style.fontSize = h.toString() + 'px';
1985 + that.element_message.style.paddingTop = paddingTop.toString() + 'px';
1986 + };
1987 +
1988 + const showMessageIcon = function (icon) {
1989 + that.element_message.innerHTML = icon;
1990 + maxMessageFontSize();
1991 + $(that.element_message).removeClass('hidden');
1992 + that.tmp.___messageHidden___ = undefined;
1993 + };
1994 +
1995 + const hideMessage = function () {
1996 + if (typeof that.tmp.___messageHidden___ === 'undefined') {
1997 + that.tmp.___messageHidden___ = true;
1998 + $(that.element_message).addClass('hidden');
1999 + }
2000 + };
2001 +
2002 + const showRendering = function () {
2003 + let icon;
2004 + if (that.chart !== null) {
2005 + if (that.chart.chart_type === 'line') {
2006 + icon = NETDATA.icons.lineChart;
2007 + } else {
2008 + icon = NETDATA.icons.areaChart;
2009 + }
2010 + }
2011 + else {
2012 + icon = NETDATA.icons.noChart;
2013 + }
2014 +
2015 + showMessageIcon(icon + ' netdata' + invisibleSearchableText());
2016 + };
2017 +
2018 + const showLoading = function () {
2019 + if (that.chart_created === false) {
2020 + showMessageIcon(NETDATA.icons.loading + ' netdata');
2021 + return true;
2022 + }
2023 + return false;
2024 + };
2025 +
2026 + const isHidden = function () {
2027 + return (typeof that.tmp.___chartIsHidden___ !== 'undefined');
2028 + };
2029 +
2030 + // hide the chart, when it is not visible - called from isVisible()
2031 + this.hideChart = function () {
2032 + // hide it, if it is not already hidden
2033 + // if (isHidden() === true) return;
2034 + if (isHidden()) {
2035 + return;
2036 + }
2037 +
2038 + if (this.chart_created) {
2039 + if (NETDATA.options.current.show_help) {
2040 + if (this.element_legend_childs.toolbox !== null) {
2041 + if (this.debug) {
2042 + this.log('hideChart(): hidding legend popovers');
2043 + }
2044 +
2045 + $(this.element_legend_childs.toolbox_left).popover('hide');
2046 + $(this.element_legend_childs.toolbox_reset).popover('hide');
2047 + $(this.element_legend_childs.toolbox_right).popover('hide');
2048 + $(this.element_legend_childs.toolbox_zoomin).popover('hide');
2049 + $(this.element_legend_childs.toolbox_zoomout).popover('hide');
2050 + }
2051 +
2052 + if (this.element_legend_childs.resize_handler !== null) {
2053 + $(this.element_legend_childs.resize_handler).popover('hide');
2054 + }
2055 +
2056 + if (this.element_legend_childs.content !== null) {
2057 + $(this.element_legend_childs.content).popover('hide');
2058 + }
2059 + }
2060 +
2061 + if (NETDATA.options.current.destroy_on_hide) {
2062 + if (this.debug) {
2063 + this.log('hideChart(): initializing chart');
2064 + }
2065 +
2066 + // we should destroy it
2067 + init('force');
2068 + } else {
2069 + if (this.debug) {
2070 + this.log('hideChart(): hiding chart');
2071 + }
2072 +
2073 + showRendering();
2074 + this.element_chart.style.display = 'none';
2075 + this.element.style.willChange = 'auto';
2076 + if (this.element_legend !== null) {
2077 + this.element_legend.style.display = 'none';
2078 + }
2079 + if (this.element_legend_childs.toolbox !== null) {
2080 + this.element_legend_childs.toolbox.style.display = 'none';
2081 + }
2082 + if (this.element_legend_childs.resize_handler !== null) {
2083 + this.element_legend_childs.resize_handler.style.display = 'none';
2084 + }
2085 +
2086 + this.tm.last_hidden = Date.now();
2087 +
2088 + // de-allocate data
2089 + // This works, but I not sure there are no corner cases somewhere
2090 + // so it is commented - if the user has memory issues he can
2091 + // set Destroy on Hide for all charts
2092 + // this.data = null;
2093 + }
2094 + }
2095 +
2096 + this.tmp.___chartIsHidden___ = true;
2097 + };
2098 +
2099 + // unhide the chart, when it is visible - called from isVisible()
2100 + this.unhideChart = function () {
2101 + if (isHidden() === false) {
2102 + return;
2103 + }
2104 +
2105 + this.tmp.___chartIsHidden___ = undefined;
2106 + this.updates_since_last_unhide = 0;
2107 +
2108 + if (this.chart_created === false) {
2109 + if (this.debug) {
2110 + this.log('unhideChart(): initializing chart');
2111 + }
2112 +
2113 + // we need to re-initialize it, to show our background
2114 + // logo in bootstrap tabs, until the chart loads
2115 + init('force');
2116 + } else {
2117 + if (this.debug) {
2118 + this.log('unhideChart(): unhiding chart');
2119 + }
2120 +
2121 + this.element.style.willChange = 'transform';
2122 + this.tm.last_unhidden = Date.now();
2123 + this.element_chart.style.display = '';
2124 + if (this.element_legend !== null) {
2125 + this.element_legend.style.display = '';
2126 + }
2127 + if (this.element_legend_childs.toolbox !== null) {
2128 + this.element_legend_childs.toolbox.style.display = '';
2129 + }
2130 + if (this.element_legend_childs.resize_handler !== null) {
2131 + this.element_legend_childs.resize_handler.style.display = '';
2132 + }
2133 + resizeChart();
2134 + hideMessage();
2135 + }
2136 +
2137 + if (this.__redraw_on_unhide) {
2138 + if (this.debug) {
2139 + this.log("redrawing chart on unhide");
2140 + }
2141 +
2142 + this.__redraw_on_unhide = undefined;
2143 + this.redrawChart();
2144 + }
2145 + };
2146 +
2147 + const canBeRendered = function (uncached_visibility) {
2148 + if (that.debug) {
2149 + that.log('canBeRendered() called');
2150 + }
2151 +
2152 + // if (NETDATA.options.current.update_only_visible === false)
2153 + if (!NETDATA.options.current.update_only_visible) {
2154 + return true;
2155 + }
2156 +
2157 + let ret = (
2158 + (
2159 + NETDATA.options.page_is_visible ||
2160 + NETDATA.options.current.stop_updates_when_focus_is_lost === false ||
2161 + that.updates_since_last_unhide === 0
2162 + )
2163 + && isHidden() === false && that.isVisible(uncached_visibility)
2164 + );
2165 +
2166 + if (that.debug) {
2167 + that.log('canBeRendered(): ' + ret);
2168 + }
2169 +
2170 + return ret;
2171 + };
2172 +
2173 + // https://github.com/petkaantonov/bluebird/wiki/Optimization-killers
2174 + const callChartLibraryUpdateSafely = function (data) {
2175 + let status;
2176 +
2177 + // we should not do this here
2178 + // if we prevent rendering the chart then:
2179 + // 1. globalSelectionSync will be wrong
2180 + // 2. globalPanAndZoom will be wrong
2181 + //if (canBeRendered(true) === false)
2182 + // return false;
2183 +
2184 + if (NETDATA.options.fake_chart_rendering) {
2185 + return true;
2186 + }
2187 +
2188 + that.updates_counter++;
2189 + that.updates_since_last_unhide++;
2190 + that.updates_since_last_creation++;
2191 +
2192 + if (NETDATA.options.debug.chart_errors) {
2193 + status = that.library.update(that, data);
2194 + } else {
2195 + try {
2196 + status = that.library.update(that, data);
2197 + } catch (err) {
2198 + status = false;
2199 + }
2200 + }
2201 +
2202 + if (!status) {
2203 + error('chart failed to be updated as ' + that.library_name);
2204 + return false;
2205 + }
2206 +
2207 + return true;
2208 + };
2209 +
2210 + // https://github.com/petkaantonov/bluebird/wiki/Optimization-killers
2211 + const callChartLibraryCreateSafely = function (data) {
2212 + let status;
2213 +
2214 + // we should not do this here
2215 + // if we prevent rendering the chart then:
2216 + // 1. globalSelectionSync will be wrong
2217 + // 2. globalPanAndZoom will be wrong
2218 + //if (canBeRendered(true) === false)
2219 + // return false;
2220 +
2221 + if (NETDATA.options.fake_chart_rendering) {
2222 + return true;
2223 + }
2224 +
2225 + that.updates_counter++;
2226 + that.updates_since_last_unhide++;
2227 + that.updates_since_last_creation++;
2228 +
2229 + if (NETDATA.options.debug.chart_errors) {
2230 + status = that.library.create(that, data);
2231 + } else {
2232 + try {
2233 + status = that.library.create(that, data);
2234 + } catch (err) {
2235 + status = false;
2236 + }
2237 + }
2238 +
2239 + if (!status) {
2240 + error('chart failed to be created as ' + that.library_name);
2241 + return false;
2242 + }
2243 +
2244 + that.chart_created = true;
2245 + that.updates_since_last_creation = 0;
2246 + return true;
2247 + };
2248 +
2249 + // ----------------------------------------------------------------------------------------------------------------
2250 + // Chart Resize
2251 +
2252 + // resizeChart() - private
2253 + // to be called just before the chart library to make sure that
2254 + // a properly sized dom is available
2255 + const resizeChart = function () {
2256 + if (that.tm.last_resized < NETDATA.options.last_page_resize) {
2257 + if (!that.chart_created) {
2258 + return;
2259 + }
2260 +
2261 + if (that.needsRecreation()) {
2262 + if (that.debug) {
2263 + that.log('resizeChart(): initializing chart');
2264 + }
2265 +
2266 + init('force');
2267 + } else if (typeof that.library.resize === 'function') {
2268 + if (that.debug) {
2269 + that.log('resizeChart(): resizing chart');
2270 + }
2271 +
2272 + that.library.resize(that);
2273 +
2274 + if (that.element_legend_childs.perfect_scroller !== null) {
2275 + Ps.update(that.element_legend_childs.perfect_scroller);
2276 + }
2277 +
2278 + maxMessageFontSize();
2279 + }
2280 +
2281 + that.tm.last_resized = Date.now();
2282 + }
2283 + };
2284 +
2285 + // this is the actual chart resize algorithm
2286 + // it will:
2287 + // - resize the entire container
2288 + // - update the internal states
2289 + // - resize the chart as the div changes height
2290 + // - update the scrollbar of the legend
2291 + const resizeChartToHeight = function (h) {
2292 + // console.log(h);
2293 + that.element.style.height = h;
2294 +
2295 + if (that.settings_id !== null) {
2296 + NETDATA.localStorageSet('chart_heights.' + that.settings_id, h);
2297 + }
2298 +
2299 + let now = Date.now();
2300 + NETDATA.options.last_page_scroll = now;
2301 + NETDATA.options.auto_refresher_stop_until = now + NETDATA.options.current.stop_updates_while_resizing;
2302 +
2303 + // force a resize
2304 + that.tm.last_resized = 0;
2305 + resizeChart();
2306 + };
2307 +
2308 + this.resizeForPrint = function () {
2309 + if (typeof this.element_legend_childs !== 'undefined' && this.element_legend_childs.perfect_scroller !== null) {
2310 + let current = this.element.clientHeight;
2311 + let optimal = current
2312 + + this.element_legend_childs.perfect_scroller.scrollHeight
2313 + - this.element_legend_childs.perfect_scroller.clientHeight;
2314 +
2315 + if (optimal > current) {
2316 + // this.log('resized');
2317 + this.element.style.height = optimal + 'px';
2318 + this.library.resize(this);
2319 + }
2320 + }
2321 + };
2322 +
2323 + this.resizeHandler = function (e) {
2324 + e.preventDefault();
2325 +
2326 + if (typeof this.event_resize === 'undefined'
2327 + || this.event_resize.chart_original_w === 'undefined'
2328 + || this.event_resize.chart_original_h === 'undefined') {
2329 + this.event_resize = {
2330 + chart_original_w: this.element.clientWidth,
2331 + chart_original_h: this.element.clientHeight,
2332 + last: 0
2333 + };
2334 + }
2335 +
2336 + if (e.type === 'touchstart') {
2337 + this.event_resize.mouse_start_x = e.touches.item(0).pageX;
2338 + this.event_resize.mouse_start_y = e.touches.item(0).pageY;
2339 + } else {
2340 + this.event_resize.mouse_start_x = e.clientX;
2341 + this.event_resize.mouse_start_y = e.clientY;
2342 + }
2343 +
2344 + this.event_resize.chart_start_w = this.element.clientWidth;
2345 + this.event_resize.chart_start_h = this.element.clientHeight;
2346 + this.event_resize.chart_last_w = this.element.clientWidth;
2347 + this.event_resize.chart_last_h = this.element.clientHeight;
2348 +
2349 + let now = Date.now();
2350 + if (now - this.event_resize.last <= NETDATA.options.current.double_click_speed && this.element_legend_childs.perfect_scroller !== null) {
2351 + // double click / double tap event
2352 +
2353 + // console.dir(this.element_legend_childs.content);
2354 + // console.dir(this.element_legend_childs.perfect_scroller);
2355 +
2356 + // the optimal height of the chart
2357 + // showing the entire legend
2358 + let optimal = this.event_resize.chart_last_h
2359 + + this.element_legend_childs.perfect_scroller.scrollHeight
2360 + - this.element_legend_childs.perfect_scroller.clientHeight;
2361 +
2362 + // if we are not optimal, be optimal
2363 + if (this.event_resize.chart_last_h !== optimal) {
2364 + // this.log('resize to optimal, current = ' + this.event_resize.chart_last_h.toString() + 'px, original = ' + this.event_resize.chart_original_h.toString() + 'px, optimal = ' + optimal.toString() + 'px, internal = ' + this.height_original.toString());
2365 + resizeChartToHeight(optimal.toString() + 'px');
2366 + }
2367 +
2368 + // else if the current height is not the original/saved height
2369 + // reset to the original/saved height
2370 + else if (this.event_resize.chart_last_h !== this.event_resize.chart_original_h) {
2371 + // this.log('resize to original, current = ' + this.event_resize.chart_last_h.toString() + 'px, original = ' + this.event_resize.chart_original_h.toString() + 'px, optimal = ' + optimal.toString() + 'px, internal = ' + this.height_original.toString());
2372 + resizeChartToHeight(this.event_resize.chart_original_h.toString() + 'px');
2373 + }
2374 +
2375 + // else if the current height is not the internal default height
2376 + // reset to the internal default height
2377 + else if ((this.event_resize.chart_last_h.toString() + 'px') !== this.height_original) {
2378 + // this.log('resize to internal default, current = ' + this.event_resize.chart_last_h.toString() + 'px, original = ' + this.event_resize.chart_original_h.toString() + 'px, optimal = ' + optimal.toString() + 'px, internal = ' + this.height_original.toString());
2379 + resizeChartToHeight(this.height_original.toString());
2380 + }
2381 +
2382 + // else if the current height is not the firstchild's clientheight
2383 + // resize to it
2384 + else if (typeof this.element_legend_childs.perfect_scroller.firstChild !== 'undefined') {
2385 + let parent_rect = this.element.getBoundingClientRect();
2386 + let content_rect = this.element_legend_childs.perfect_scroller.firstElementChild.getBoundingClientRect();
2387 + let wanted = content_rect.top - parent_rect.top + this.element_legend_childs.perfect_scroller.firstChild.clientHeight + 18; // 15 = toolbox + 3 space
2388 +
2389 + // console.log(parent_rect);
2390 + // console.log(content_rect);
2391 + // console.log(wanted);
2392 +
2393 + // this.log('resize to firstChild, current = ' + this.event_resize.chart_last_h.toString() + 'px, original = ' + this.event_resize.chart_original_h.toString() + 'px, optimal = ' + optimal.toString() + 'px, internal = ' + this.height_original.toString() + 'px, firstChild = ' + wanted.toString() + 'px' );
2394 + if (this.event_resize.chart_last_h !== wanted) {
2395 + resizeChartToHeight(wanted.toString() + 'px');
2396 + }
2397 + }
2398 + } else {
2399 + this.event_resize.last = now;
2400 +
2401 + // process movement event
2402 + document.onmousemove =
2403 + document.ontouchmove =
2404 + this.element_legend_childs.resize_handler.onmousemove =
2405 + this.element_legend_childs.resize_handler.ontouchmove =
2406 + function (e) {
2407 + let y = null;
2408 +
2409 + switch (e.type) {
2410 + case 'mousemove':
2411 + y = e.clientY;
2412 + break;
2413 + case 'touchmove':
2414 + y = e.touches.item(e.touches - 1).pageY;
2415 + break;
2416 + }
2417 +
2418 + if (y !== null) {
2419 + let newH = that.event_resize.chart_start_h + y - that.event_resize.mouse_start_y;
2420 +
2421 + if (newH >= 70 && newH !== that.event_resize.chart_last_h) {
2422 + resizeChartToHeight(newH.toString() + 'px');
2423 + that.event_resize.chart_last_h = newH;
2424 + }
2425 + }
2426 + };
2427 +
2428 + // process end event
2429 + document.onmouseup =
2430 + document.ontouchend =
2431 + this.element_legend_childs.resize_handler.onmouseup =
2432 + this.element_legend_childs.resize_handler.ontouchend =
2433 + function (e) {
2434 + void(e);
2435 +
2436 + // remove all the hooks
2437 + document.onmouseup =
2438 + document.onmousemove =
2439 + document.ontouchmove =
2440 + document.ontouchend =
2441 + that.element_legend_childs.resize_handler.onmousemove =
2442 + that.element_legend_childs.resize_handler.ontouchmove =
2443 + that.element_legend_childs.resize_handler.onmouseout =
2444 + that.element_legend_childs.resize_handler.onmouseup =
2445 + that.element_legend_childs.resize_handler.ontouchend =
2446 + null;
2447 +
2448 + // allow auto-refreshes
2449 + NETDATA.options.auto_refresher_stop_until = 0;
2450 + };
2451 + }
2452 + };
2453 +
2454 + const noDataToShow = function () {
2455 + showMessageIcon(NETDATA.icons.noData + ' empty');
2456 + that.legendUpdateDOM();
2457 + that.tm.last_autorefreshed = Date.now();
2458 + // that.data_update_every = 30 * 1000;
2459 + //that.element_chart.style.display = 'none';
2460 + //if (that.element_legend !== null) that.element_legend.style.display = 'none';
2461 + //that.tmp.___chartIsHidden___ = true;
2462 + };
2463 +
2464 + // ============================================================================================================
2465 + // PUBLIC FUNCTIONS
2466 +
2467 + this.error = function (msg) {
2468 + error(msg);
2469 + };
2470 +
2471 + this.setMode = function (m) {
2472 + if (this.current !== null && this.current.name === m) {
2473 + return;
2474 + }
2475 +
2476 + if (m === 'auto') {
2477 + this.current = this.auto;
2478 + } else if (m === 'pan') {
2479 + this.current = this.pan;
2480 + } else if (m === 'zoom') {
2481 + this.current = this.zoom;
2482 + } else {
2483 + this.current = this.auto;
2484 + }
2485 +
2486 + this.current.force_update_at = 0;
2487 + this.current.force_before_ms = null;
2488 + this.current.force_after_ms = null;
2489 +
2490 + this.tm.last_mode_switch = Date.now();
2491 + };
2492 +
2493 + // ----------------------------------------------------------------------------------------------------------------
2494 + // global selection sync for slaves
2495 +
2496 + // can the chart participate to the global selection sync as a slave?
2497 + this.globalSelectionSyncIsEligible = function () {
2498 + return (
2499 + this.enabled &&
2500 + this.library !== null &&
2501 + typeof this.library.setSelection === 'function' &&
2502 + this.isVisible() &&
2503 + this.chart_created
2504 + );
2505 + };
2506 +
2507 + this.setSelection = function (t) {
2508 + if (typeof this.library.setSelection === 'function') {
2509 + // this.selected = this.library.setSelection(this, t) === true;
2510 + this.selected = this.library.setSelection(this, t);
2511 + } else {
2512 + this.selected = true;
2513 + }
2514 +
2515 + if (this.selected && this.debug) {
2516 + this.log('selection set to ' + t.toString());
2517 + }
2518 +
2519 + if (this.foreign_element_selection !== null) {
2520 + this.foreign_element_selection.innerText = NETDATA.dateTime.localeDateString(t) + ' ' + NETDATA.dateTime.localeTimeString(t);
2521 + }
2522 +
2523 + return this.selected;
2524 + };
2525 +
2526 + this.clearSelection = function () {
2527 + if (this.selected) {
2528 + if (typeof this.library.clearSelection === 'function') {
2529 + this.selected = (this.library.clearSelection(this) !== true);
2530 + } else {
2531 + this.selected = false;
2532 + }
2533 +
2534 + if (this.selected === false && this.debug) {
2535 + this.log('selection cleared');
2536 + }
2537 +
2538 + if (this.foreign_element_selection !== null) {
2539 + this.foreign_element_selection.innerText = '';
2540 + }
2541 +
2542 + this.legendReset();
2543 + }
2544 +
2545 + return this.selected;
2546 + };
2547 +
2548 + // ----------------------------------------------------------------------------------------------------------------
2549 +
2550 + // find if a timestamp (ms) is shown in the current chart
2551 + this.timeIsVisible = function (t) {
2552 + return (t >= this.data_after && t <= this.data_before);
2553 + };
2554 +
2555 + this.calculateRowForTime = function (t) {
2556 + if (this.timeIsVisible(t) === false) {
2557 + return -1;
2558 + }
2559 + return Math.floor((t - this.data_after) / this.data_update_every);
2560 + };
2561 +
2562 + // ----------------------------------------------------------------------------------------------------------------
2563 +
2564 + this.pauseChart = function () {
2565 + if (this.paused === false) {
2566 + if (this.debug) {
2567 + this.log('pauseChart()');
2568 + }
2569 +
2570 + this.paused = true;
2571 + }
2572 + };
2573 +
2574 + this.unpauseChart = function () {
2575 + if (this.paused) {
2576 + if (this.debug) {
2577 + this.log('unpauseChart()');
2578 + }
2579 +
2580 + this.paused = false;
2581 + }
2582 + };
2583 +
2584 + this.resetChart = function (dont_clear_master, dont_update) {
2585 + if (this.debug) {
2586 + this.log('resetChart(' + dont_clear_master + ', ' + dont_update + ') called');
2587 + }
2588 +
2589 + if (typeof dont_clear_master === 'undefined') {
2590 + dont_clear_master = false;
2591 + }
2592 +
2593 + if (typeof dont_update === 'undefined') {
2594 + dont_update = false;
2595 + }
2596 +
2597 + if (dont_clear_master !== true && NETDATA.globalPanAndZoom.isMaster(this)) {
2598 + if (this.debug) {
2599 + this.log('resetChart() diverting to clearMaster().');
2600 + }
2601 + // this will call us back with master === true
2602 + NETDATA.globalPanAndZoom.clearMaster();
2603 + return;
2604 + }
2605 +
2606 + this.clearSelection();
2607 +
2608 + this.tm.pan_and_zoom_seq = 0;
2609 +
2610 + this.setMode('auto');
2611 + this.current.force_update_at = 0;
2612 + this.current.force_before_ms = null;
2613 + this.current.force_after_ms = null;
2614 + this.tm.last_autorefreshed = 0;
2615 + this.paused = false;
2616 + this.selected = false;
2617 + this.enabled = true;
2618 + // this.debug = false;
2619 +
2620 + // do not update the chart here
2621 + // or the chart will flip-flop when it is the master
2622 + // of a selection sync and another chart becomes
2623 + // the new master
2624 +
2625 + if (dont_update !== true && this.isVisible()) {
2626 + this.updateChart();
2627 + }
2628 + };
2629 +
2630 + this.updateChartPanOrZoom = function (after, before, callback) {
2631 + let logme = 'updateChartPanOrZoom(' + after + ', ' + before + '): ';
2632 + let ret = true;
2633 +
2634 + NETDATA.globalPanAndZoom.delay();
2635 + NETDATA.globalSelectionSync.delay();
2636 +
2637 + if (this.debug) {
2638 + this.log(logme);
2639 + }
2640 +
2641 + if (before < after) {
2642 + if (this.debug) {
2643 + this.log(logme + 'flipped parameters, rejecting it.');
2644 + }
2645 + return false;
2646 + }
2647 +
2648 + if (typeof this.fixed_min_duration === 'undefined') {
2649 + this.fixed_min_duration = Math.round((this.chartWidth() / 30) * this.chart.update_every * 1000);
2650 + }
2651 +
2652 + let min_duration = this.fixed_min_duration;
2653 + let current_duration = Math.round(this.view_before - this.view_after);
2654 +
2655 + // round the numbers
2656 + after = Math.round(after);
2657 + before = Math.round(before);
2658 +
2659 + // align them to update_every
2660 + // stretching them further away
2661 + after -= after % this.data_update_every;
2662 + before += this.data_update_every - (before % this.data_update_every);
2663 +
2664 + // the final wanted duration
2665 + let wanted_duration = before - after;
2666 +
2667 + // to allow panning, accept just a point below our minimum
2668 + if ((current_duration - this.data_update_every) < min_duration) {
2669 + min_duration = current_duration - this.data_update_every;
2670 + }
2671 +
2672 + // we do it, but we adjust to minimum size and return false
2673 + // when the wanted size is below the current and the minimum
2674 + // and we zoom
2675 + if (wanted_duration < current_duration && wanted_duration < min_duration) {
2676 + if (this.debug) {
2677 + this.log(logme + 'too small: min_duration: ' + (min_duration / 1000).toString() + ', wanted: ' + (wanted_duration / 1000).toString());
2678 + }
2679 +
2680 + min_duration = this.fixed_min_duration;
2681 +
2682 + let dt = (min_duration - wanted_duration) / 2;
2683 + before += dt;
2684 + after -= dt;
2685 + wanted_duration = before - after;
2686 + ret = false;
2687 + }
2688 +
2689 + let tolerance = this.data_update_every * 2;
2690 + let movement = Math.abs(before - this.view_before);
2691 +
2692 + if (Math.abs(current_duration - wanted_duration) <= tolerance && movement <= tolerance && ret) {
2693 + if (this.debug) {
2694 + this.log(logme + 'REJECTING UPDATE: current/min duration: ' + (current_duration / 1000).toString() + '/' + (this.fixed_min_duration / 1000).toString() + ', wanted duration: ' + (wanted_duration / 1000).toString() + ', duration diff: ' + (Math.round(Math.abs(current_duration - wanted_duration) / 1000)).toString() + ', movement: ' + (movement / 1000).toString() + ', tolerance: ' + (tolerance / 1000).toString() + ', returning: ' + false);
2695 + }
2696 + return false;
2697 + }
2698 +
2699 + if (this.current.name === 'auto') {
2700 + this.log(logme + 'caller called me with mode: ' + this.current.name);
2701 + this.setMode('pan');
2702 + }
2703 +
2704 + if (this.debug) {
2705 + this.log(logme + 'ACCEPTING UPDATE: current/min duration: ' + (current_duration / 1000).toString() + '/' + (this.fixed_min_duration / 1000).toString() + ', wanted duration: ' + (wanted_duration / 1000).toString() + ', duration diff: ' + (Math.round(Math.abs(current_duration - wanted_duration) / 1000)).toString() + ', movement: ' + (movement / 1000).toString() + ', tolerance: ' + (tolerance / 1000).toString() + ', returning: ' + ret);
2706 + }
2707 +
2708 + this.current.force_update_at = Date.now() + NETDATA.options.current.pan_and_zoom_delay;
2709 + this.current.force_after_ms = after;
2710 + this.current.force_before_ms = before;
2711 + NETDATA.globalPanAndZoom.setMaster(this, after, before);
2712 +
2713 + if (ret && typeof callback === 'function') {
2714 + callback();
2715 + }
2716 +
2717 + return ret;
2718 + };
2719 +
2720 + this.updateChartPanOrZoomAsyncTimeOutId = undefined;
2721 + this.updateChartPanOrZoomAsync = function (after, before, callback) {
2722 + NETDATA.globalPanAndZoom.delay();
2723 + NETDATA.globalSelectionSync.delay();
2724 +
2725 + if (NETDATA.globalPanAndZoom.isMaster(this) === false) {
2726 + this.pauseChart();
2727 + NETDATA.globalPanAndZoom.setMaster(this, after, before);
2728 + // NETDATA.globalSelectionSync.stop();
2729 + NETDATA.globalSelectionSync.setMaster(this);
2730 + }
2731 +
2732 + if (this.updateChartPanOrZoomAsyncTimeOutId) {
2733 + NETDATA.timeout.clear(this.updateChartPanOrZoomAsyncTimeOutId);
2734 + }
2735 +
2736 + NETDATA.timeout.set(function () {
2737 + that.updateChartPanOrZoomAsyncTimeOutId = undefined;
2738 + that.updateChartPanOrZoom(after, before, callback);
2739 + }, 0);
2740 + };
2741 +
2742 + let __unitsConversionLastUnits = undefined;
2743 + let __unitsConversionLastUnitsDesired = undefined;
2744 + let __unitsConversionLastMin = undefined;
2745 + let __unitsConversionLastMax = undefined;
2746 + let __unitsConversion = function (value) {
2747 + return value;
2748 + };
2749 + this.unitsConversionSetup = function (min, max) {
2750 + if (this.units !== __unitsConversionLastUnits
2751 + || this.units_desired !== __unitsConversionLastUnitsDesired
2752 + || min !== __unitsConversionLastMin
2753 + || max !== __unitsConversionLastMax) {
2754 +
2755 + __unitsConversionLastUnits = this.units;
2756 + __unitsConversionLastUnitsDesired = this.units_desired;
2757 + __unitsConversionLastMin = min;
2758 + __unitsConversionLastMax = max;
2759 +
2760 + __unitsConversion = NETDATA.unitsConversion.get(this.uuid, min, max, this.units, this.units_desired, this.units_common, function (units) {
2761 + // console.log('switching units from ' + that.units.toString() + ' to ' + units.toString());
2762 + that.units_current = units;
2763 + that.legendSetUnitsString(that.units_current);
2764 + });
2765 + }
2766 + };
2767 +
2768 + let __legendFormatValueChartDecimalsLastMin = undefined;
2769 + let __legendFormatValueChartDecimalsLastMax = undefined;
2770 + let __legendFormatValueChartDecimals = -1;
2771 + let __intlNumberFormat = null;
2772 + this.legendFormatValueDecimalsFromMinMax = function (min, max) {
2773 + if (min === __legendFormatValueChartDecimalsLastMin && max === __legendFormatValueChartDecimalsLastMax) {
2774 + return;
2775 + }
2776 +
2777 + this.unitsConversionSetup(min, max);
2778 + if (__unitsConversion !== null) {
2779 + min = __unitsConversion(min);
2780 + max = __unitsConversion(max);
2781 +
2782 + if (typeof min !== 'number' || typeof max !== 'number') {
2783 + return;
2784 + }
2785 + }
2786 +
2787 + __legendFormatValueChartDecimalsLastMin = min;
2788 + __legendFormatValueChartDecimalsLastMax = max;
2789 +
2790 + let old = __legendFormatValueChartDecimals;
2791 +
2792 + if (this.data !== null && this.data.min === this.data.max)
2793 + // it is a fixed number, let the visualizer decide based on the value
2794 + {
2795 + __legendFormatValueChartDecimals = -1;
2796 + } else if (this.value_decimal_detail !== -1)
2797 + // there is an override
2798 + {
2799 + __legendFormatValueChartDecimals = this.value_decimal_detail;
2800 + } else {
2801 + // ok, let's calculate the proper number of decimal points
2802 + let delta;
2803 +
2804 + if (min === max) {
2805 + delta = Math.abs(min);
2806 + } else {
2807 + delta = Math.abs(max - min);
2808 + }
2809 +
2810 + if (delta > 1000) {
2811 + __legendFormatValueChartDecimals = 0;
2812 + } else if (delta > 10) {
2813 + __legendFormatValueChartDecimals = 1;
2814 + } else if (delta > 1) {
2815 + __legendFormatValueChartDecimals = 2;
2816 + } else if (delta > 0.1) {
2817 + __legendFormatValueChartDecimals = 2;
2818 + } else if (delta > 0.01) {
2819 + __legendFormatValueChartDecimals = 4;
2820 + } else if (delta > 0.001) {
2821 + __legendFormatValueChartDecimals = 5;
2822 + } else if (delta > 0.0001) {
2823 + __legendFormatValueChartDecimals = 6;
2824 + } else {
2825 + __legendFormatValueChartDecimals = 7;
2826 + }
2827 + }
2828 +
2829 + if (__legendFormatValueChartDecimals !== old) {
2830 + if (__legendFormatValueChartDecimals < 0) {
2831 + __intlNumberFormat = null;
2832 + } else {
2833 + __intlNumberFormat = NETDATA.fastNumberFormat.get(
2834 + __legendFormatValueChartDecimals,
2835 + __legendFormatValueChartDecimals
2836 + );
2837 + }
2838 + }
2839 + };
2840 +
2841 + this.legendFormatValue = function (value) {
2842 + if (typeof value !== 'number') {
2843 + return '-';
2844 + }
2845 +
2846 + value = __unitsConversion(value);
2847 +
2848 + if (typeof value !== 'number') {
2849 + return value;
2850 + }
2851 +
2852 + if (__intlNumberFormat !== null) {
2853 + return __intlNumberFormat.format(value);
2854 + }
2855 +
2856 + let dmin, dmax;
2857 + if (this.value_decimal_detail !== -1) {
2858 + dmin = dmax = this.value_decimal_detail;
2859 + } else {
2860 + dmin = 0;
2861 + let abs = (value < 0) ? -value : value;
2862 + if (abs > 1000) {
2863 + dmax = 0;
2864 + } else if (abs > 10) {
2865 + dmax = 1;
2866 + } else if (abs > 1) {
2867 + dmax = 2;
2868 + } else if (abs > 0.1) {
2869 + dmax = 2;
2870 + } else if (abs > 0.01) {
2871 + dmax = 4;
2872 + } else if (abs > 0.001) {
2873 + dmax = 5;
2874 + } else if (abs > 0.0001) {
2875 + dmax = 6;
2876 + } else {
2877 + dmax = 7;
2878 + }
2879 + }
2880 +
2881 + return NETDATA.fastNumberFormat.get(dmin, dmax).format(value);
2882 + };
2883 +
2884 + this.legendSetLabelValue = function (label, value) {
2885 + let series = this.element_legend_childs.series[label];
2886 + if (typeof series === 'undefined') {
2887 + return;
2888 + }
2889 + if (series.value === null && series.user === null) {
2890 + return;
2891 + }
2892 +
2893 + /*
2894 + // this slows down firefox and edge significantly
2895 + // since it requires to use innerHTML(), instead of innerText()
2896 +
2897 + // if the value has not changed, skip DOM update
2898 + //if (series.last === value) return;
2899 +
2900 + let s, r;
2901 + if (typeof value === 'number') {
2902 + let v = Math.abs(value);
2903 + s = r = this.legendFormatValue(value);
2904 +
2905 + if (typeof series.last === 'number') {
2906 + if (v > series.last) s += '<i class="fas fa-angle-up" style="width: 8px; text-align: center; overflow: hidden; vertical-align: middle;"></i>';
2907 + else if (v < series.last) s += '<i class="fas fa-angle-down" style="width: 8px; text-align: center; overflow: hidden; vertical-align: middle;"></i>';
2908 + else s += '<i class="fas fa-angle-left" style="width: 8px; text-align: center; overflow: hidden; vertical-align: middle;"></i>';
2909 + }
2910 + else s += '<i class="fas fa-angle-right" style="width: 8px; text-align: center; overflow: hidden; vertical-align: middle;"></i>';
2911 +
2912 + series.last = v;
2913 + }
2914 + else {
2915 + if (value === null)
2916 + s = r = '';
2917 + else
2918 + s = r = value;
2919 +
2920 + series.last = value;
2921 + }
2922 + */
2923 +
2924 + let s = this.legendFormatValue(value);
2925 +
2926 + // caching: do not update the update to show the same value again
2927 + if (s === series.last_shown_value) {
2928 + return;
2929 + }
2930 + series.last_shown_value = s;
2931 +
2932 + if (series.value !== null) {
2933 + series.value.innerText = s;
2934 + }
2935 + if (series.user !== null) {
2936 + series.user.innerText = s;
2937 + }
2938 + };
2939 +
2940 + this.legendSetDateString = function (date) {
2941 + if (this.element_legend_childs.title_date !== null && date !== this.tmp.__last_shown_legend_date) {
2942 + this.element_legend_childs.title_date.innerText = date;
2943 + this.tmp.__last_shown_legend_date = date;
2944 + }
2945 + };
2946 +
2947 + this.legendSetTimeString = function (time) {
2948 + if (this.element_legend_childs.title_time !== null && time !== this.tmp.__last_shown_legend_time) {
2949 + this.element_legend_childs.title_time.innerText = time;
2950 + this.tmp.__last_shown_legend_time = time;
2951 + }
2952 + };
2953 +
2954 + this.legendSetUnitsString = function (units) {
2955 + if (this.element_legend_childs.title_units !== null && units !== this.tmp.__last_shown_legend_units) {
2956 + this.element_legend_childs.title_units.innerText = units;
2957 + this.tmp.__last_shown_legend_units = units;
2958 + }
2959 + };
2960 +
2961 + this.legendSetDateLast = {
2962 + ms: 0,
2963 + date: undefined,
2964 + time: undefined
2965 + };
2966 +
2967 + this.legendSetDate = function (ms) {
2968 + if (typeof ms !== 'number') {
2969 + this.legendShowUndefined();
2970 + return;
2971 + }
2972 +
2973 + if (this.legendSetDateLast.ms !== ms) {
2974 + let d = new Date(ms);
2975 + this.legendSetDateLast.ms = ms;
2976 + this.legendSetDateLast.date = NETDATA.dateTime.localeDateString(d);
2977 + this.legendSetDateLast.time = NETDATA.dateTime.localeTimeString(d);
2978 + }
2979 +
2980 + this.legendSetDateString(this.legendSetDateLast.date);
2981 + this.legendSetTimeString(this.legendSetDateLast.time);
2982 + this.legendSetUnitsString(this.units_current)
2983 + };
2984 +
2985 + this.legendShowUndefined = function () {
2986 + this.legendSetDateString(this.legendPluginModuleString(false));
2987 + this.legendSetTimeString(this.chart.context.toString());
2988 + // this.legendSetUnitsString(' ');
2989 +
2990 + if (this.data && this.element_legend_childs.series !== null) {
2991 + let labels = this.data.dimension_names;
2992 + let i = labels.length;
2993 + while (i--) {
2994 + let label = labels[i];
2995 +
2996 + if (typeof label === 'undefined' || typeof this.element_legend_childs.series[label] === 'undefined') {
2997 + continue;
2998 + }
2999 + this.legendSetLabelValue(label, null);
3000 + }
3001 + }
3002 + };
3003 +
3004 + this.legendShowLatestValues = function () {
3005 + if (this.chart === null) {
3006 + return;
3007 + }
3008 + if (this.selected) {
3009 + return;
3010 + }
3011 +
3012 + if (this.data === null || this.element_legend_childs.series === null) {
3013 + this.legendShowUndefined();
3014 + return;
3015 + }
3016 +
3017 + let show_undefined = true;
3018 + if (Math.abs(this.netdata_last - this.view_before) <= this.data_update_every) {
3019 + show_undefined = false;
3020 + }
3021 +
3022 + if (show_undefined) {
3023 + this.legendShowUndefined();
3024 + return;
3025 + }
3026 +
3027 + this.legendSetDate(this.view_before);
3028 +
3029 + let labels = this.data.dimension_names;
3030 + let i = labels.length;
3031 + while (i--) {
3032 + let label = labels[i];
3033 +
3034 + if (typeof label === 'undefined') {
3035 + continue;
3036 + }
3037 + if (typeof this.element_legend_childs.series[label] === 'undefined') {
3038 + continue;
3039 + }
3040 +
3041 + this.legendSetLabelValue(label, this.data.view_latest_values[i]);
3042 + }
3043 + };
3044 +
3045 + this.legendReset = function () {
3046 + this.legendShowLatestValues();
3047 + };
3048 +
3049 + // this should be called just ONCE per dimension per chart
3050 + this.__chartDimensionColor = function (label) {
3051 + let c = NETDATA.commonColors.get(this, label);
3052 +
3053 + // it is important to maintain a list of colors
3054 + // for this chart only, since the chart library
3055 + // uses this to assign colors to dimensions in the same
3056 + // order the dimension are given to it
3057 + this.colors.push(c);
3058 +
3059 + return c;
3060 + };
3061 +
3062 + this.chartPrepareColorPalette = function () {
3063 + NETDATA.commonColors.refill(this);
3064 + };
3065 +
3066 + // get the ordered list of chart colors
3067 + // this includes user defined colors
3068 + this.chartCustomColors = function () {
3069 + this.chartPrepareColorPalette();
3070 +
3071 + let colors;
3072 + if (this.colors_custom.length) {
3073 + colors = this.colors_custom;
3074 + } else {
3075 + colors = this.colors;
3076 + }
3077 +
3078 + if (this.debug) {
3079 + this.log("chartCustomColors() returns:");
3080 + this.log(colors);
3081 + }
3082 +
3083 + return colors;
3084 + };
3085 +
3086 + // get the ordered list of chart ASSIGNED colors
3087 + // (this returns only the colors that have been
3088 + // assigned to dimensions, prepended with any
3089 + // custom colors defined)
3090 + this.chartColors = function () {
3091 + this.chartPrepareColorPalette();
3092 +
3093 + if (this.debug) {
3094 + this.log("chartColors() returns:");
3095 + this.log(this.colors);
3096 + }
3097 +
3098 + return this.colors;
3099 + };
3100 +
3101 + this.legendPluginModuleString = function (withContext) {
3102 + let str = ' ';
3103 + let context = '';
3104 +
3105 + if (typeof this.chart !== 'undefined') {
3106 + if (withContext && typeof this.chart.context === 'string') {
3107 + context = this.chart.context;
3108 + }
3109 +
3110 + if (typeof this.chart.plugin === 'string' && this.chart.plugin !== '') {
3111 + str = this.chart.plugin;
3112 +
3113 + if (str.endsWith(".plugin")) {
3114 + str = str.substring(0, str.length - 7);
3115 + }
3116 +
3117 + if (typeof this.chart.module === 'string' && this.chart.module !== '') {
3118 + str += ':' + this.chart.module;
3119 + }
3120 +
3121 + if (withContext && context !== '') {
3122 + str += ', ' + context;
3123 + }
3124 + }
3125 + else if (withContext && context !== '') {
3126 + str = context;
3127 + }
3128 + }
3129 +
3130 + return str;
3131 + };
3132 +
3133 + this.legendResolutionTooltip = function () {
3134 + if (!this.chart) {
3135 + return '';
3136 + }
3137 +
3138 + let collected = this.chart.update_every;
3139 + let viewed = (this.data) ? this.data.view_update_every : collected;
3140 +
3141 + if (collected === viewed) {
3142 + return "resolution " + NETDATA.seconds4human(collected);
3143 + }
3144 +
3145 + return "resolution " + NETDATA.seconds4human(viewed) + ", collected every " + NETDATA.seconds4human(collected);
3146 + };
3147 +
3148 + this.legendUpdateDOM = function () {
3149 + let needed = false, dim, keys, len;
3150 +
3151 + // check that the legend DOM is up to date for the downloaded dimensions
3152 + if (typeof this.element_legend_childs.series !== 'object' || this.element_legend_childs.series === null) {
3153 + // this.log('the legend does not have any series - requesting legend update');
3154 + needed = true;
3155 + } else if (this.data === null) {
3156 + // this.log('the chart does not have any data - requesting legend update');
3157 + needed = true;
3158 + } else if (typeof this.element_legend_childs.series.labels_key === 'undefined') {
3159 + needed = true;
3160 + } else {
3161 + let labels = this.data.dimension_names.toString();
3162 + if (labels !== this.element_legend_childs.series.labels_key) {
3163 + needed = true;
3164 +
3165 + if (this.debug) {
3166 + this.log('NEW LABELS: "' + labels + '" NOT EQUAL OLD LABELS: "' + this.element_legend_childs.series.labels_key + '"');
3167 + }
3168 + }
3169 + }
3170 +
3171 + if (needed === false) {
3172 + // make sure colors available
3173 + this.chartPrepareColorPalette();
3174 +
3175 + // do we have to update the current values?
3176 + // we do this, only when the visible chart is current
3177 + if (Math.abs(this.netdata_last - this.view_before) <= this.data_update_every) {
3178 + if (this.debug) {
3179 + this.log('chart is in latest position... updating values on legend...');
3180 + }
3181 +
3182 + //let labels = this.data.dimension_names;
3183 + //let i = labels.length;
3184 + //while (i--)
3185 + // this.legendSetLabelValue(labels[i], this.data.view_latest_values[i]);
3186 + }
3187 + return;
3188 + }
3189 +
3190 + if (this.colors === null) {
3191 + // this is the first time we update the chart
3192 + // let's assign colors to all dimensions
3193 + if (this.library.track_colors()) {
3194 + this.colors = [];
3195 + keys = Object.keys(this.chart.dimensions);
3196 + len = keys.length;
3197 + for (let i = 0; i < len; i++) {
3198 + NETDATA.commonColors.get(this, this.chart.dimensions[keys[i]].name);
3199 + }
3200 + }
3201 + }
3202 +
3203 + // we will re-generate the colors for the chart
3204 + // based on the dimensions this result has data for
3205 + this.colors = [];
3206 +
3207 + if (this.debug) {
3208 + this.log('updating Legend DOM');
3209 + }
3210 +
3211 + // mark all dimensions as invalid
3212 + this.dimensions_visibility.invalidateAll();
3213 +
3214 + const genLabel = function (state, parent, dim, name, count) {
3215 + let color = state.__chartDimensionColor(name);
3216 +
3217 + let user_element = null;
3218 + let user_id = NETDATA.dataAttribute(state.element, 'show-value-of-' + name.toLowerCase() + '-at', null);
3219 + if (user_id === null) {
3220 + user_id = NETDATA.dataAttribute(state.element, 'show-value-of-' + dim.toLowerCase() + '-at', null);
3221 + }
3222 + if (user_id !== null) {
3223 + user_element = document.getElementById(user_id) || null;
3224 + if (user_element === null) {
3225 + state.log('Cannot find element with id: ' + user_id);
3226 + }
3227 + }
3228 +
3229 + state.element_legend_childs.series[name] = {
3230 + name: document.createElement('span'),
3231 + value: document.createElement('span'),
3232 + user: user_element,
3233 + last: null,
3234 + last_shown_value: null
3235 + };
3236 +
3237 + let label = state.element_legend_childs.series[name];
3238 +
3239 + // create the dimension visibility tracking for this label
3240 + state.dimensions_visibility.dimensionAdd(name, label.name, label.value, color);
3241 +
3242 + let rgb = NETDATA.colorHex2Rgb(color);
3243 + label.name.innerHTML = '<table class="netdata-legend-name-table-'
3244 + + state.chart.chart_type
3245 + + '" style="background-color: '
3246 + + 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + NETDATA.options.current['color_fill_opacity_' + state.chart.chart_type] + ') !important'
3247 + + '"><tr class="netdata-legend-name-tr"><td class="netdata-legend-name-td"></td></tr></table>';
3248 +
3249 + let text = document.createTextNode(' ' + name);
3250 + label.name.appendChild(text);
3251 +
3252 + if (count > 0) {
3253 + parent.appendChild(document.createElement('br'));
3254 + }
3255 +
3256 + parent.appendChild(label.name);
3257 + parent.appendChild(label.value);
3258 + };
3259 +
3260 + let content = document.createElement('div');
3261 +
3262 + if (this.element_chart === null) {
3263 + this.element_chart = document.createElement('div');
3264 + this.element_chart.id = this.library_name + '-' + this.uuid + '-chart';
3265 + this.element.appendChild(this.element_chart);
3266 +
3267 + if (this.hasLegend()) {
3268 + this.element_chart.className = 'netdata-chart-with-legend-right netdata-' + this.library_name + '-chart-with-legend-right';
3269 + } else {
3270 + this.element_chart.className = ' netdata-chart netdata-' + this.library_name + '-chart';
3271 + }
3272 + }
3273 +
3274 + if (this.hasLegend()) {
3275 + if (this.element_legend === null) {
3276 + this.element_legend = document.createElement('div');
3277 + this.element_legend.className = 'netdata-chart-legend netdata-' + this.library_name + '-legend';
3278 + this.element.appendChild(this.element_legend);
3279 + } else {
3280 + this.element_legend.innerHTML = '';
3281 + }
3282 +
3283 + this.element_legend_childs = {
3284 + content: content,
3285 + resize_handler: null,
3286 + toolbox: null,
3287 + toolbox_left: null,
3288 + toolbox_right: null,
3289 + toolbox_reset: null,
3290 + toolbox_zoomin: null,
3291 + toolbox_zoomout: null,
3292 + toolbox_volume: null,
3293 + title_date: document.createElement('span'),
3294 + title_time: document.createElement('span'),
3295 + title_units: document.createElement('span'),
3296 + perfect_scroller: document.createElement('div'),
3297 + series: {}
3298 + };
3299 +
3300 + if (NETDATA.options.current.legend_toolbox && this.library.toolboxPanAndZoom !== null) {
3301 + this.element_legend_childs.toolbox = document.createElement('div');
3302 + this.element_legend_childs.toolbox_left = document.createElement('div');
3303 + this.element_legend_childs.toolbox_right = document.createElement('div');
3304 + this.element_legend_childs.toolbox_reset = document.createElement('div');
3305 + this.element_legend_childs.toolbox_zoomin = document.createElement('div');
3306 + this.element_legend_childs.toolbox_zoomout = document.createElement('div');
3307 + this.element_legend_childs.toolbox_volume = document.createElement('div');
3308 +
3309 + const get_pan_and_zoom_step = function (event) {
3310 + if (event.ctrlKey) {
3311 + return NETDATA.options.current.pan_and_zoom_factor * NETDATA.options.current.pan_and_zoom_factor_multiplier_control;
3312 + } else if (event.shiftKey) {
3313 + return NETDATA.options.current.pan_and_zoom_factor * NETDATA.options.current.pan_and_zoom_factor_multiplier_shift;
3314 + } else if (event.altKey) {
3315 + return NETDATA.options.current.pan_and_zoom_factor * NETDATA.options.current.pan_and_zoom_factor_multiplier_alt;
3316 + } else {
3317 + return NETDATA.options.current.pan_and_zoom_factor;
3318 + }
3319 + };
3320 +
3321 + this.element_legend_childs.toolbox.className += ' netdata-legend-toolbox';
3322 + this.element.appendChild(this.element_legend_childs.toolbox);
3323 +
3324 + this.element_legend_childs.toolbox_left.className += ' netdata-legend-toolbox-button';
3325 + this.element_legend_childs.toolbox_left.innerHTML = NETDATA.icons.left;
3326 + this.element_legend_childs.toolbox.appendChild(this.element_legend_childs.toolbox_left);
3327 + this.element_legend_childs.toolbox_left.onclick = function (e) {
3328 + e.preventDefault();
3329 +
3330 + let step = (that.view_before - that.view_after) * get_pan_and_zoom_step(e);
3331 + let before = that.view_before - step;
3332 + let after = that.view_after - step;
3333 + if (after >= that.netdata_first) {
3334 + that.library.toolboxPanAndZoom(that, after, before);
3335 + }
3336 + };
3337 + if (NETDATA.options.current.show_help) {
3338 + $(this.element_legend_childs.toolbox_left).popover({
3339 + container: "body",
3340 + animation: false,
3341 + html: true,
3342 + trigger: 'hover',
3343 + placement: 'bottom',
3344 + delay: {
3345 + show: NETDATA.options.current.show_help_delay_show_ms,
3346 + hide: NETDATA.options.current.show_help_delay_hide_ms
3347 + },
3348 + title: 'Pan Left',
3349 + content: 'Pan the chart to the left. You can also <b>drag it</b> with your mouse or your finger (on touch devices).<br/><small>Help can be disabled from the settings.</small>'
3350 + });
3351 + }
3352 +
3353 + this.element_legend_childs.toolbox_reset.className += ' netdata-legend-toolbox-button';
3354 + this.element_legend_childs.toolbox_reset.innerHTML = NETDATA.icons.reset;
3355 + this.element_legend_childs.toolbox.appendChild(this.element_legend_childs.toolbox_reset);
3356 + this.element_legend_childs.toolbox_reset.onclick = function (e) {
3357 + e.preventDefault();
3358 + NETDATA.resetAllCharts(that);
3359 + };
3360 + if (NETDATA.options.current.show_help) {
3361 + $(this.element_legend_childs.toolbox_reset).popover({
3362 + container: "body",
3363 + animation: false,
3364 + html: true,
3365 + trigger: 'hover',
3366 + placement: 'bottom',
3367 + delay: {
3368 + show: NETDATA.options.current.show_help_delay_show_ms,
3369 + hide: NETDATA.options.current.show_help_delay_hide_ms
3370 + },
3371 + title: 'Chart Reset',
3372 + content: 'Reset all the charts to their default auto-refreshing state. You can also <b>double click</b> the chart contents with your mouse or your finger (on touch devices).<br/><small>Help can be disabled from the settings.</small>'
3373 + });
3374 + }
3375 +
3376 + this.element_legend_childs.toolbox_right.className += ' netdata-legend-toolbox-button';
3377 + this.element_legend_childs.toolbox_right.innerHTML = NETDATA.icons.right;
3378 + this.element_legend_childs.toolbox.appendChild(this.element_legend_childs.toolbox_right);
3379 + this.element_legend_childs.toolbox_right.onclick = function (e) {
3380 + e.preventDefault();
3381 + let step = (that.view_before - that.view_after) * get_pan_and_zoom_step(e);
3382 + let before = that.view_before + step;
3383 + let after = that.view_after + step;
3384 + if (before <= that.netdata_last) {
3385 + that.library.toolboxPanAndZoom(that, after, before);
3386 + }
3387 + };
3388 + if (NETDATA.options.current.show_help) {
3389 + $(this.element_legend_childs.toolbox_right).popover({
3390 + container: "body",
3391 + animation: false,
3392 + html: true,
3393 + trigger: 'hover',
3394 + placement: 'bottom',
3395 + delay: {
3396 + show: NETDATA.options.current.show_help_delay_show_ms,
3397 + hide: NETDATA.options.current.show_help_delay_hide_ms
3398 + },
3399 + title: 'Pan Right',
3400 + content: 'Pan the chart to the right. You can also <b>drag it</b> with your mouse or your finger (on touch devices).<br/><small>Help, can be disabled from the settings.</small>'
3401 + });
3402 + }
3403 +
3404 + this.element_legend_childs.toolbox_zoomin.className += ' netdata-legend-toolbox-button';
3405 + this.element_legend_childs.toolbox_zoomin.innerHTML = NETDATA.icons.zoomIn;
3406 + this.element_legend_childs.toolbox.appendChild(this.element_legend_childs.toolbox_zoomin);
3407 + this.element_legend_childs.toolbox_zoomin.onclick = function (e) {
3408 + e.preventDefault();
3409 + let dt = ((that.view_before - that.view_after) * (get_pan_and_zoom_step(e) * 0.8) / 2);
3410 + let before = that.view_before - dt;
3411 + let after = that.view_after + dt;
3412 + that.library.toolboxPanAndZoom(that, after, before);
3413 + };
3414 + if (NETDATA.options.current.show_help) {
3415 + $(this.element_legend_childs.toolbox_zoomin).popover({
3416 + container: "body",
3417 + animation: false,
3418 + html: true,
3419 + trigger: 'hover',
3420 + placement: 'bottom',
3421 + delay: {
3422 + show: NETDATA.options.current.show_help_delay_show_ms,
3423 + hide: NETDATA.options.current.show_help_delay_hide_ms
3424 + },
3425 + title: 'Chart Zoom In',
3426 + content: 'Zoom in the chart. You can also press SHIFT and select an area of the chart, or press SHIFT or ALT and use the mouse wheel or 2-finger touchpad scroll to zoom in or out.<br/><small>Help, can be disabled from the settings.</small>'
3427 + });
3428 + }
3429 +
3430 + this.element_legend_childs.toolbox_zoomout.className += ' netdata-legend-toolbox-button';
3431 + this.element_legend_childs.toolbox_zoomout.innerHTML = NETDATA.icons.zoomOut;
3432 + this.element_legend_childs.toolbox.appendChild(this.element_legend_childs.toolbox_zoomout);
3433 + this.element_legend_childs.toolbox_zoomout.onclick = function (e) {
3434 + e.preventDefault();
3435 + let dt = (((that.view_before - that.view_after) / (1.0 - (get_pan_and_zoom_step(e) * 0.8)) - (that.view_before - that.view_after)) / 2);
3436 + let before = that.view_before + dt;
3437 + let after = that.view_after - dt;
3438 +
3439 + that.library.toolboxPanAndZoom(that, after, before);
3440 + };
3441 + if (NETDATA.options.current.show_help) {
3442 + $(this.element_legend_childs.toolbox_zoomout).popover({
3443 + container: "body",
3444 + animation: false,
3445 + html: true,
3446 + trigger: 'hover',
3447 + placement: 'bottom',
3448 + delay: {
3449 + show: NETDATA.options.current.show_help_delay_show_ms,
3450 + hide: NETDATA.options.current.show_help_delay_hide_ms
3451 + },
3452 + title: 'Chart Zoom Out',
3453 + content: 'Zoom out the chart. You can also press SHIFT or ALT and use the mouse wheel, or 2-finger touchpad scroll to zoom in or out.<br/><small>Help, can be disabled from the settings.</small>'
3454 + });
3455 + }
3456 +
3457 + //this.element_legend_childs.toolbox_volume.className += ' netdata-legend-toolbox-button';
3458 + //this.element_legend_childs.toolbox_volume.innerHTML = '<i class="fas fa-sort-amount-down"></i>';
3459 + //this.element_legend_childs.toolbox_volume.title = 'Visible Volume';
3460 + //this.element_legend_childs.toolbox.appendChild(this.element_legend_childs.toolbox_volume);
3461 + //this.element_legend_childs.toolbox_volume.onclick = function(e) {
3462 + //e.preventDefault();
3463 + //alert('clicked toolbox_volume on ' + that.id);
3464 + //}
3465 + }
3466 +
3467 + if (NETDATA.options.current.resize_charts) {
3468 + this.element_legend_childs.resize_handler = document.createElement('div');
3469 +
3470 + this.element_legend_childs.resize_handler.className += " netdata-legend-resize-handler";
3471 + this.element_legend_childs.resize_handler.innerHTML = NETDATA.icons.resize;
3472 + this.element.appendChild(this.element_legend_childs.resize_handler);
3473 + if (NETDATA.options.current.show_help) {
3474 + $(this.element_legend_childs.resize_handler).popover({
3475 + container: "body",
3476 + animation: false,
3477 + html: true,
3478 + trigger: 'hover',
3479 + placement: 'bottom',
3480 + delay: {
3481 + show: NETDATA.options.current.show_help_delay_show_ms,
3482 + hide: NETDATA.options.current.show_help_delay_hide_ms
3483 + },
3484 + title: 'Chart Resize',
3485 + content: 'Drag this point with your mouse or your finger (on touch devices), to resize the chart vertically. You can also <b>double click it</b> or <b>double tap it</b> to reset between 2 states: the default and the one that fits all the values.<br/><small>Help, can be disabled from the settings.</small>'
3486 + });
3487 + }
3488 +
3489 + // mousedown event
3490 + this.element_legend_childs.resize_handler.onmousedown =
3491 + function (e) {
3492 + that.resizeHandler(e);
3493 + };
3494 +
3495 + // touchstart event
3496 + this.element_legend_childs.resize_handler.addEventListener('touchstart', function (e) {
3497 + that.resizeHandler(e);
3498 + }, false);
3499 + }
3500 +
3501 + if (this.chart) {
3502 + this.element_legend_childs.title_date.title = this.legendPluginModuleString(true);
3503 + this.element_legend_childs.title_time.title = this.legendResolutionTooltip();
3504 + }
3505 +
3506 + this.element_legend_childs.title_date.className += " netdata-legend-title-date";
3507 + this.element_legend.appendChild(this.element_legend_childs.title_date);
3508 + this.tmp.__last_shown_legend_date = undefined;
3509 +
3510 + this.element_legend.appendChild(document.createElement('br'));
3511 +
3512 + this.element_legend_childs.title_time.className += " netdata-legend-title-time";
3513 + this.element_legend.appendChild(this.element_legend_childs.title_time);
3514 + this.tmp.__last_shown_legend_time = undefined;
3515 +
3516 + this.element_legend.appendChild(document.createElement('br'));
3517 +
3518 + this.element_legend_childs.title_units.className += " netdata-legend-title-units";
3519 + this.element_legend_childs.title_units.innerText = this.units_current;
3520 + this.element_legend.appendChild(this.element_legend_childs.title_units);
3521 + this.tmp.__last_shown_legend_units = undefined;
3522 +
3523 + this.element_legend.appendChild(document.createElement('br'));
3524 +
3525 + this.element_legend_childs.perfect_scroller.className = 'netdata-legend-series';
3526 + this.element_legend.appendChild(this.element_legend_childs.perfect_scroller);
3527 +
3528 + content.className = 'netdata-legend-series-content';
3529 + this.element_legend_childs.perfect_scroller.appendChild(content);
3530 +
3531 + this.element_legend_childs.content = content;
3532 +
3533 + if (NETDATA.options.current.show_help) {
3534 + $(content).popover({
3535 + container: "body",
3536 + animation: false,
3537 + html: true,
3538 + trigger: 'hover',
3539 + placement: 'bottom',
3540 + title: 'Chart Legend',
3541 + delay: {
3542 + show: NETDATA.options.current.show_help_delay_show_ms,
3543 + hide: NETDATA.options.current.show_help_delay_hide_ms
3544 + },
3545 + content: 'You can click or tap on the values or the labels to select dimensions. By pressing SHIFT or CONTROL, you can enable or disable multiple dimensions.<br/><small>Help, can be disabled from the settings.</small>'
3546 + });
3547 + }
3548 + } else {
3549 + this.element_legend_childs = {
3550 + content: content,
3551 + resize_handler: null,
3552 + toolbox: null,
3553 + toolbox_left: null,
3554 + toolbox_right: null,
3555 + toolbox_reset: null,
3556 + toolbox_zoomin: null,
3557 + toolbox_zoomout: null,
3558 + toolbox_volume: null,
3559 + title_date: null,
3560 + title_time: null,
3561 + title_units: null,
3562 + perfect_scroller: null,
3563 + series: {}
3564 + };
3565 + }
3566 +
3567 + if (this.data) {
3568 + this.element_legend_childs.series.labels_key = this.data.dimension_names.toString();
3569 + if (this.debug) {
3570 + this.log('labels from data: "' + this.element_legend_childs.series.labels_key + '"');
3571 + }
3572 +
3573 + for (let i = 0, len = this.data.dimension_names.length; i < len; i++) {
3574 + genLabel(this, content, this.data.dimension_ids[i], this.data.dimension_names[i], i);
3575 + }
3576 + } else {
3577 + let tmp = [];
3578 + keys = Object.keys(this.chart.dimensions);
3579 + for (let i = 0, len = keys.length; i < len; i++) {
3580 + dim = keys[i];
3581 + tmp.push(this.chart.dimensions[dim].name);
3582 + genLabel(this, content, dim, this.chart.dimensions[dim].name, i);
3583 + }
3584 + this.element_legend_childs.series.labels_key = tmp.toString();
3585 + if (this.debug) {
3586 + this.log('labels from chart: "' + this.element_legend_childs.series.labels_key + '"');
3587 + }
3588 + }
3589 +
3590 + // create a hidden div to be used for hidding
3591 + // the original legend of the chart library
3592 + let el = document.createElement('div');
3593 + if (this.element_legend !== null) {
3594 + this.element_legend.appendChild(el);
3595 + }
3596 + el.style.display = 'none';
3597 +
3598 + this.element_legend_childs.hidden = document.createElement('div');
3599 + el.appendChild(this.element_legend_childs.hidden);
3600 +
3601 + if (this.element_legend_childs.perfect_scroller !== null) {
3602 + Ps.initialize(this.element_legend_childs.perfect_scroller, {
3603 + wheelSpeed: 0.2,
3604 + wheelPropagation: true,
3605 + swipePropagation: true,
3606 + minScrollbarLength: null,
3607 + maxScrollbarLength: null,
3608 + useBothWheelAxes: false,
3609 + suppressScrollX: true,
3610 + suppressScrollY: false,
3611 + scrollXMarginOffset: 0,
3612 + scrollYMarginOffset: 0,
3613 + theme: 'default'
3614 + });
3615 + Ps.update(this.element_legend_childs.perfect_scroller);
3616 + }
3617 +
3618 + this.legendShowLatestValues();
3619 + };
3620 +
3621 + this.hasLegend = function () {
3622 + if (typeof this.tmp.___hasLegendCache___ !== 'undefined') {
3623 + return this.tmp.___hasLegendCache___;
3624 + }
3625 +
3626 + let leg = false;
3627 + if (this.library && this.library.legend(this) === 'right-side') {
3628 + leg = true;
3629 + }
3630 +
3631 + this.tmp.___hasLegendCache___ = leg;
3632 + return leg;
3633 + };
3634 +
3635 + this.legendWidth = function () {
3636 + return (this.hasLegend()) ? 140 : 0;
3637 + };
3638 +
3639 + this.legendHeight = function () {
3640 + return $(this.element).height();
3641 + };
3642 +
3643 + this.chartWidth = function () {
3644 + return $(this.element).width() - this.legendWidth();
3645 + };
3646 +
3647 + this.chartHeight = function () {
3648 + return $(this.element).height();
3649 + };
3650 +
3651 + this.chartPixelsPerPoint = function () {
3652 + // force an options provided detail
3653 + let px = this.pixels_per_point;
3654 +
3655 + if (this.library && px < this.library.pixels_per_point(this)) {
3656 + px = this.library.pixels_per_point(this);
3657 + }
3658 +
3659 + if (px < NETDATA.options.current.pixels_per_point) {
3660 + px = NETDATA.options.current.pixels_per_point;
3661 + }
3662 +
3663 + return px;
3664 + };
3665 +
3666 + this.needsRecreation = function () {
3667 + let ret = (
3668 + this.chart_created &&
3669 + this.library &&
3670 + this.library.autoresize() === false &&
3671 + this.tm.last_resized < NETDATA.options.last_page_resize
3672 + );
3673 +
3674 + if (this.debug) {
3675 + this.log('needsRecreation(): ' + ret.toString() + ', chart_created = ' + this.chart_created.toString());
3676 + }
3677 +
3678 + return ret;
3679 + };
3680 +
3681 + this.chartDataUniqueID = function () {
3682 + return this.id + ',' + this.library_name + ',' + this.dimensions + ',' + this.chartURLOptions();
3683 + };
3684 +
3685 + this.chartURLOptions = function () {
3686 + let ret = '';
3687 +
3688 + if (this.override_options !== null) {
3689 + ret = this.override_options.toString();
3690 + } else {
3691 + ret = this.library.options(this);
3692 + }
3693 +
3694 + if (this.append_options !== null) {
3695 + ret += '%7C' + this.append_options.toString();
3696 + }
3697 +
3698 + ret += '%7C' + 'jsonwrap';
3699 +
3700 + if (NETDATA.options.current.eliminate_zero_dimensions) {
3701 + ret += '%7C' + 'nonzero';
3702 + }
3703 +
3704 + return ret;
3705 + };
3706 +
3707 + this.chartURL = function () {
3708 + let after, before, points_multiplier = 1;
3709 + if (NETDATA.globalPanAndZoom.isActive()) {
3710 + if (this.current.force_before_ms !== null && this.current.force_after_ms !== null) {
3711 + this.tm.pan_and_zoom_seq = 0;
3712 +
3713 + before = Math.round(this.current.force_before_ms / 1000);
3714 + after = Math.round(this.current.force_after_ms / 1000);
3715 + this.view_after = after * 1000;
3716 + this.view_before = before * 1000;
3717 +
3718 + if (NETDATA.options.current.pan_and_zoom_data_padding) {
3719 + this.requested_padding = Math.round((before - after) / 2);
3720 + after -= this.requested_padding;
3721 + before += this.requested_padding;
3722 + this.requested_padding *= 1000;
3723 + points_multiplier = 2;
3724 + }
3725 +
3726 + this.current.force_before_ms = null;
3727 + this.current.force_after_ms = null;
3728 + } else {
3729 + this.tm.pan_and_zoom_seq = NETDATA.globalPanAndZoom.seq;
3730 +
3731 + after = Math.round(NETDATA.globalPanAndZoom.force_after_ms / 1000);
3732 + before = Math.round(NETDATA.globalPanAndZoom.force_before_ms / 1000);
3733 + this.view_after = after * 1000;
3734 + this.view_before = before * 1000;
3735 +
3736 + this.requested_padding = null;
3737 + points_multiplier = 1;
3738 + }
3739 + } else {
3740 + this.tm.pan_and_zoom_seq = 0;
3741 +
3742 + before = this.before;
3743 + after = this.after;
3744 + this.view_after = after * 1000;
3745 + this.view_before = before * 1000;
3746 +
3747 + this.requested_padding = null;
3748 + points_multiplier = 1;
3749 + }
3750 +
3751 + this.requested_after = after * 1000;
3752 + this.requested_before = before * 1000;
3753 +
3754 + let data_points;
3755 + if (NETDATA.options.force_data_points !== 0) {
3756 + data_points = NETDATA.options.force_data_points;
3757 + this.data_points = data_points;
3758 + } else {
3759 + this.data_points = this.points || Math.round(this.chartWidth() / this.chartPixelsPerPoint());
3760 + data_points = this.data_points * points_multiplier;
3761 + }
3762 +
3763 + // build the data URL
3764 + this.data_url = this.host + this.chart.data_url;
3765 + this.data_url += "&format=" + this.library.format();
3766 + this.data_url += "&points=" + (data_points).toString();
3767 + this.data_url += "&group=" + this.method;
3768 + this.data_url += "&gtime=" + this.gtime;
3769 + this.data_url += "&options=" + this.chartURLOptions();
3770 +
3771 + if (after) {
3772 + this.data_url += "&after=" + after.toString();
3773 + }
3774 +
3775 + if (before) {
3776 + this.data_url += "&before=" + before.toString();
3777 + }
3778 +
3779 + if (this.dimensions) {
3780 + this.data_url += "&dimensions=" + this.dimensions;
3781 + }
3782 +
3783 + if (NETDATA.options.debug.chart_data_url || this.debug) {
3784 + this.log('chartURL(): ' + this.data_url + ' WxH:' + this.chartWidth() + 'x' + this.chartHeight() + ' points: ' + data_points.toString() + ' library: ' + this.library_name);
3785 + }
3786 + };
3787 +
3788 + this.redrawChart = function () {
3789 + if (this.data !== null) {
3790 + this.updateChartWithData(this.data);
3791 + }
3792 + };
3793 +
3794 + this.updateChartWithData = function (data) {
3795 + if (this.debug) {
3796 + this.log('updateChartWithData() called.');
3797 + }
3798 +
3799 + // this may force the chart to be re-created
3800 + resizeChart();
3801 +
3802 + this.data = data;
3803 +
3804 + let started = Date.now();
3805 + let view_update_every = data.view_update_every * 1000;
3806 +
3807 + if (this.data_update_every !== view_update_every) {
3808 + if (this.element_legend_childs.title_time) {
3809 + this.element_legend_childs.title_time.title = this.legendResolutionTooltip();
3810 + }
3811 + }
3812 +
3813 + // if the result is JSON, find the latest update-every
3814 + this.data_update_every = view_update_every;
3815 + this.data_after = data.after * 1000;
3816 + this.data_before = data.before * 1000;
3817 + this.netdata_first = data.first_entry * 1000;
3818 + this.netdata_last = data.last_entry * 1000;
3819 + this.data_points = data.points;
3820 +
3821 + data.state = this;
3822 +
3823 + if (NETDATA.options.current.pan_and_zoom_data_padding && this.requested_padding !== null) {
3824 + if (this.view_after < this.data_after) {
3825 + // console.log('adjusting view_after from ' + this.view_after + ' to ' + this.data_after);
3826 + this.view_after = this.data_after;
3827 + }
3828 +
3829 + if (this.view_before > this.data_before) {
3830 + // console.log('adjusting view_before from ' + this.view_before + ' to ' + this.data_before);
3831 + this.view_before = this.data_before;
3832 + }
3833 + } else {
3834 + this.view_after = this.data_after;
3835 + this.view_before = this.data_before;
3836 + }
3837 +
3838 + if (this.debug) {
3839 + this.log('UPDATE No ' + this.updates_counter + ' COMPLETED');
3840 +
3841 + if (this.current.force_after_ms) {
3842 + this.log('STATUS: forced : ' + (this.current.force_after_ms / 1000).toString() + ' - ' + (this.current.force_before_ms / 1000).toString());
3843 + } else {
3844 + this.log('STATUS: forced : unset');
3845 + }
3846 +
3847 + this.log('STATUS: requested : ' + (this.requested_after / 1000).toString() + ' - ' + (this.requested_before / 1000).toString());
3848 + this.log('STATUS: downloaded: ' + (this.data_after / 1000).toString() + ' - ' + (this.data_before / 1000).toString());
3849 + this.log('STATUS: rendered : ' + (this.view_after / 1000).toString() + ' - ' + (this.view_before / 1000).toString());
3850 + this.log('STATUS: points : ' + (this.data_points).toString());
3851 + }
3852 +
3853 + if (this.data_points === 0) {
3854 + noDataToShow();
3855 + return;
3856 + }
3857 +
3858 + if (this.updates_since_last_creation >= this.library.max_updates_to_recreate()) {
3859 + if (this.debug) {
3860 + this.log('max updates of ' + this.updates_since_last_creation.toString() + ' reached. Forcing re-generation.');
3861 + }
3862 +
3863 + init('force');
3864 + return;
3865 + }
3866 +
3867 + // check and update the legend
3868 + this.legendUpdateDOM();
3869 +
3870 + if (this.chart_created && typeof this.library.update === 'function') {
3871 + if (this.debug) {
3872 + this.log('updating chart...');
3873 + }
3874 +
3875 + if (!callChartLibraryUpdateSafely(data)) {
3876 + return;
3877 + }
3878 + } else {
3879 + if (this.debug) {
3880 + this.log('creating chart...');
3881 + }
3882 +
3883 + if (!callChartLibraryCreateSafely(data)) {
3884 + return;
3885 + }
3886 + }
3887 +
3888 + if (this.isVisible()) {
3889 + hideMessage();
3890 + this.legendShowLatestValues();
3891 + } else {
3892 + this.__redraw_on_unhide = true;
3893 +
3894 + if (this.debug) {
3895 + this.log("drawn while not visible");
3896 + }
3897 + }
3898 +
3899 + if (this.selected) {
3900 + NETDATA.globalSelectionSync.stop();
3901 + }
3902 +
3903 + // update the performance counters
3904 + let now = Date.now();
3905 + this.tm.last_updated = now;
3906 +
3907 + // don't update last_autorefreshed if this chart is
3908 + // forced to be updated with global PanAndZoom
3909 + if (NETDATA.globalPanAndZoom.isActive()) {
3910 + this.tm.last_autorefreshed = 0;
3911 + } else {
3912 + if (NETDATA.options.current.parallel_refresher && NETDATA.options.current.concurrent_refreshes && typeof this.force_update_every !== 'number') {
3913 + this.tm.last_autorefreshed = now - (now % this.data_update_every);
3914 + } else {
3915 + this.tm.last_autorefreshed = now;
3916 + }
3917 + }
3918 +
3919 + this.refresh_dt_ms = now - started;
3920 + NETDATA.options.auto_refresher_fast_weight += this.refresh_dt_ms;
3921 +
3922 + if (this.refresh_dt_element !== null) {
3923 + this.refresh_dt_element.innerText = this.refresh_dt_ms.toString();
3924 + }
3925 +
3926 + if (this.foreign_element_before !== null) {
3927 + this.foreign_element_before.innerText = NETDATA.dateTime.localeDateString(this.view_before) + ' ' + NETDATA.dateTime.localeTimeString(this.view_before);
3928 + }
3929 +
3930 + if (this.foreign_element_after !== null) {
3931 + this.foreign_element_after.innerText = NETDATA.dateTime.localeDateString(this.view_after) + ' ' + NETDATA.dateTime.localeTimeString(this.view_after);
3932 + }
3933 +
3934 + if (this.foreign_element_duration !== null) {
3935 + this.foreign_element_duration.innerText = NETDATA.seconds4human(Math.floor((this.view_before - this.view_after) / 1000) + 1);
3936 + }
3937 +
3938 + if (this.foreign_element_update_every !== null) {
3939 + this.foreign_element_update_every.innerText = NETDATA.seconds4human(Math.floor(this.data_update_every / 1000));
3940 + }
3941 + };
3942 +
3943 + this.getSnapshotData = function (key) {
3944 + if (this.debug) {
3945 + this.log('updating from snapshot: ' + key);
3946 + }
3947 +
3948 + if (typeof netdataSnapshotData.data[key] === 'undefined') {
3949 + this.log('snapshot does not include data for key "' + key + '"');
3950 + return null;
3951 + }
3952 +
3953 + if (typeof netdataSnapshotData.data[key] !== 'string') {
3954 + this.log('snapshot data for key "' + key + '" is not string');
3955 + return null;
3956 + }
3957 +
3958 + let uncompressed;
3959 + try {
3960 + uncompressed = netdataSnapshotData.uncompress(netdataSnapshotData.data[key]);
3961 +
3962 + if (uncompressed === null) {
3963 + this.log('uncompressed snapshot data for key ' + key + ' is null');
3964 + return null;
3965 + }
3966 +
3967 + if (typeof uncompressed === 'undefined') {
3968 + this.log('uncompressed snapshot data for key ' + key + ' is undefined');
3969 + return null;
3970 + }
3971 + } catch (e) {
3972 + this.log('decompression of snapshot data for key ' + key + ' failed');
3973 + console.log(e);
3974 + uncompressed = null;
3975 + }
3976 +
3977 + if (typeof uncompressed !== 'string') {
3978 + this.log('uncompressed snapshot data for key ' + key + ' is not string');
3979 + return null;
3980 + }
3981 +
3982 + let data;
3983 + try {
3984 + data = JSON.parse(uncompressed);
3985 + } catch (e) {
3986 + this.log('parsing snapshot data for key ' + key + ' failed');
3987 + console.log(e);
3988 + data = null;
3989 + }
3990 +
3991 + return data;
3992 + };
3993 +
3994 + this.updateChart = function (callback) {
3995 + if (this.debug) {
3996 + this.log('updateChart()');
3997 + }
3998 +
3999 + if (this.fetching_data) {
4000 + if (this.debug) {
4001 + this.log('updateChart(): I am already updating...');
4002 + }
4003 +
4004 + if (typeof callback === 'function') {
4005 + return callback(false, 'already running');
4006 + }
4007 +
4008 + return;
4009 + }
4010 +
4011 + // due to late initialization of charts and libraries
4012 + // we need to check this too
4013 + // if (this.enabled === false) {
4014 + if (!this.enabled) {
4015 + if (this.debug) {
4016 + this.log('updateChart(): I am not enabled');
4017 + }
4018 +
4019 + if (typeof callback === 'function') {
4020 + return callback(false, 'not enabled');
4021 + }
4022 +
4023 + return;
4024 + }
4025 +
4026 + // if (canBeRendered() === false) {
4027 + if (!canBeRendered()) {
4028 + if (this.debug) {
4029 + this.log('updateChart(): cannot be rendered');
4030 + }
4031 +
4032 + if (typeof callback === 'function') {
4033 + return callback(false, 'cannot be rendered');
4034 + }
4035 +
4036 + return;
4037 + }
4038 +
4039 + if (that.dom_created !== true) {
4040 + if (this.debug) {
4041 + this.log('updateChart(): creating DOM');
4042 + }
4043 +
4044 + createDOM();
4045 + }
4046 +
4047 + if (this.chart === null) {
4048 + if (this.debug) {
4049 + this.log('updateChart(): getting chart');
4050 + }
4051 +
4052 + return this.getChart(function () {
4053 + return that.updateChart(callback);
4054 + });
4055 + }
4056 +
4057 + if (this.library.initialized === false) {
4058 + if (this.library.enabled) {
4059 + if (this.debug) {
4060 + this.log('updateChart(): initializing chart library');
4061 + }
4062 +
4063 + return this.library.initialize(function () {
4064 + return that.updateChart(callback);
4065 + });
4066 + } else {
4067 + error('chart library "' + this.library_name + '" is not available.');
4068 +
4069 + if (typeof callback === 'function') {
4070 + return callback(false, 'library not available');
4071 + }
4072 +
4073 + return;
4074 + }
4075 + }
4076 +
4077 + this.clearSelection();
4078 + this.chartURL();
4079 +
4080 + NETDATA.statistics.refreshes_total++;
4081 + NETDATA.statistics.refreshes_active++;
4082 +
4083 + if (NETDATA.statistics.refreshes_active > NETDATA.statistics.refreshes_active_max) {
4084 + NETDATA.statistics.refreshes_active_max = NETDATA.statistics.refreshes_active;
4085 + }
4086 +
4087 + let ok = false;
4088 + this.fetching_data = true;
4089 +
4090 + if (netdataSnapshotData !== null) {
4091 + let key = this.chartDataUniqueID();
4092 + let data = this.getSnapshotData(key);
4093 + if (data !== null) {
4094 + ok = true;
4095 + data = NETDATA.xss.checkData('/api/v1/data', data, this.library.xssRegexIgnore);
4096 + this.updateChartWithData(data);
4097 + } else {
4098 + ok = false;
4099 + error('cannot get data from snapshot for key: "' + key + '"');
4100 + that.tm.last_autorefreshed = Date.now();
4101 + }
4102 +
4103 + NETDATA.statistics.refreshes_active--;
4104 + this.fetching_data = false;
4105 +
4106 + if (typeof callback === 'function') {
4107 + callback(ok, 'snapshot');
4108 + }
4109 +
4110 + return;
4111 + }
4112 +
4113 + if (this.debug) {
4114 + this.log('updating from ' + this.data_url);
4115 + }
4116 +
4117 + this.xhr = $.ajax({
4118 + url: this.data_url,
4119 + cache: false,
4120 + async: true,
4121 + headers: {
4122 + 'Cache-Control': 'no-cache, no-store',
4123 + 'Pragma': 'no-cache'
4124 + },
4125 + xhrFields: {withCredentials: true} // required for the cookie
4126 + })
4127 + .done(function (data) {
4128 + data = NETDATA.xss.checkData('/api/v1/data', data, that.library.xssRegexIgnore);
4129 +
4130 + that.xhr = undefined;
4131 + that.retries_on_data_failures = 0;
4132 + ok = true;
4133 +
4134 + if (that.debug) {
4135 + that.log('data received. updating chart.');
4136 + }
4137 +
4138 + that.updateChartWithData(data);
4139 + })
4140 + .fail(function (msg) {
4141 + that.xhr = undefined;
4142 +
4143 + if (msg.statusText !== 'abort') {
4144 + that.retries_on_data_failures++;
4145 + if (that.retries_on_data_failures > NETDATA.options.current.retries_on_data_failures) {
4146 + // that.log('failed ' + that.retries_on_data_failures.toString() + ' times - giving up');
4147 + that.retries_on_data_failures = 0;
4148 + error('data download failed for url: ' + that.data_url);
4149 + }
4150 + else {
4151 + that.tm.last_autorefreshed = Date.now();
4152 + // that.log('failed ' + that.retries_on_data_failures.toString() + ' times, but I will retry');
4153 + }
4154 + }
4155 + })
4156 + .always(function () {
4157 + that.xhr = undefined;
4158 +
4159 + NETDATA.statistics.refreshes_active--;
4160 + that.fetching_data = false;
4161 +
4162 + if (typeof callback === 'function') {
4163 + return callback(ok, 'download');
4164 + }
4165 + });
4166 + };
4167 +
4168 + const __isVisible = function () {
4169 + let ret = true;
4170 +
4171 + if (NETDATA.options.current.update_only_visible !== false) {
4172 + // tolerance is the number of pixels a chart can be off-screen
4173 + // to consider it as visible and refresh it as if was visible
4174 + let tolerance = 0;
4175 +
4176 + that.tm.last_visible_check = Date.now();
4177 +
4178 + let rect = that.element.getBoundingClientRect();
4179 +
4180 + let screenTop = window.scrollY;
4181 + let screenBottom = screenTop + window.innerHeight;
4182 +
4183 + let chartTop = rect.top + screenTop;
4184 + let chartBottom = chartTop + rect.height;
4185 +
4186 + ret = !(rect.width === 0 || rect.height === 0 || chartBottom + tolerance < screenTop || chartTop - tolerance > screenBottom);
4187 + }
4188 +
4189 + if (that.debug) {
4190 + that.log('__isVisible(): ' + ret);
4191 + }
4192 +
4193 + return ret;
4194 + };
4195 +
4196 + this.isVisible = function (nocache) {
4197 + // this.log('last_visible_check: ' + this.tm.last_visible_check + ', last_page_scroll: ' + NETDATA.options.last_page_scroll);
4198 +
4199 + // caching - we do not evaluate the charts visibility
4200 + // if the page has not been scrolled since the last check
4201 + if ((typeof nocache !== 'undefined' && nocache)
4202 + || typeof this.tmp.___isVisible___ === 'undefined'
4203 + || this.tm.last_visible_check <= NETDATA.options.last_page_scroll) {
4204 + this.tmp.___isVisible___ = __isVisible();
4205 + if (this.tmp.___isVisible___) {
4206 + this.unhideChart();
4207 + } else {
4208 + this.hideChart();
4209 + }
4210 + }
4211 +
4212 + if (this.debug) {
4213 + this.log('isVisible(' + nocache + '): ' + this.tmp.___isVisible___);
4214 + }
4215 +
4216 + return this.tmp.___isVisible___;
4217 + };
4218 +
4219 + this.isAutoRefreshable = function () {
4220 + return (this.current.autorefresh);
4221 + };
4222 +
4223 + this.canBeAutoRefreshed = function () {
4224 + if (this.enabled === false) {
4225 + if (this.debug) {
4226 + this.log('canBeAutoRefreshed() -> not enabled');
4227 + }
4228 +
4229 + return false;
4230 + }
4231 +
4232 + if (this.running) {
4233 + if (this.debug) {
4234 + this.log('canBeAutoRefreshed() -> already running');
4235 + }
4236 +
4237 + return false;
4238 + }
4239 +
4240 + if (this.library === null || this.library.enabled === false) {
4241 + error('charting library "' + this.library_name + '" is not available');
4242 + if (this.debug) {
4243 + this.log('canBeAutoRefreshed() -> chart library ' + this.library_name + ' is not available');
4244 + }
4245 +
4246 + return false;
4247 + }
4248 +
4249 + // if (this.isVisible() === false) {
4250 + if (!this.isVisible()) {
4251 + if (NETDATA.options.debug.visibility || this.debug) {
4252 + this.log('canBeAutoRefreshed() -> not visible');
4253 + }
4254 +
4255 + return false;
4256 + }
4257 +
4258 + let now = Date.now();
4259 +
4260 + if (this.current.force_update_at !== 0 && this.current.force_update_at < now) {
4261 + if (this.debug) {
4262 + this.log('canBeAutoRefreshed() -> timed force update - allowing this update');
4263 + }
4264 +
4265 + this.current.force_update_at = 0;
4266 + return true;
4267 + }
4268 +
4269 + if (this.isAutoRefreshable() === false) {
4270 + if (this.debug) {
4271 + this.log('canBeAutoRefreshed() -> not auto-refreshable');
4272 + }
4273 +
4274 + return false;
4275 + }
4276 +
4277 + // allow the first update, even if the page is not visible
4278 + if (NETDATA.options.page_is_visible === false && this.updates_counter && this.updates_since_last_unhide) {
4279 + if (NETDATA.options.debug.focus || this.debug) {
4280 + this.log('canBeAutoRefreshed() -> not the first update, and page does not have focus');
4281 + }
4282 +
4283 + return false;
4284 + }
4285 +
4286 + if (this.needsRecreation()) {
4287 + if (this.debug) {
4288 + this.log('canBeAutoRefreshed() -> needs re-creation.');
4289 + }
4290 +
4291 + return true;
4292 + }
4293 +
4294 + if (NETDATA.options.auto_refresher_stop_until >= now) {
4295 + if (this.debug) {
4296 + this.log('canBeAutoRefreshed() -> stopped until is in future.');
4297 + }
4298 +
4299 + return false;
4300 + }
4301 +
4302 + // options valid only for autoRefresh()
4303 + if (NETDATA.globalPanAndZoom.isActive()) {
4304 + if (NETDATA.globalPanAndZoom.shouldBeAutoRefreshed(this)) {
4305 + if (this.debug) {
4306 + this.log('canBeAutoRefreshed(): global panning: I need an update.');
4307 + }
4308 +
4309 + return true;
4310 + }
4311 + else {
4312 + if (this.debug) {
4313 + this.log('canBeAutoRefreshed(): global panning: I am already up to date.');
4314 + }
4315 +
4316 + return false;
4317 + }
4318 + }
4319 +
4320 + if (this.selected) {
4321 + if (this.debug) {
4322 + this.log('canBeAutoRefreshed(): I have a selection in place.');
4323 + }
4324 +
4325 + return false;
4326 + }
4327 +
4328 + if (this.paused) {
4329 + if (this.debug) {
4330 + this.log('canBeAutoRefreshed(): I am paused.');
4331 + }
4332 +
4333 + return false;
4334 + }
4335 +
4336 + let data_update_every = this.data_update_every;
4337 + if (typeof this.force_update_every === 'number') {
4338 + data_update_every = this.force_update_every;
4339 + }
4340 +
4341 + if (now - this.tm.last_autorefreshed >= data_update_every) {
4342 + if (this.debug) {
4343 + this.log('canBeAutoRefreshed(): It is time to update me. Now: ' + now.toString() + ', last_autorefreshed: ' + this.tm.last_autorefreshed + ', data_update_every: ' + data_update_every + ', delta: ' + (now - this.tm.last_autorefreshed).toString());
4344 + }
4345 +
4346 + return true;
4347 + }
4348 +
4349 + return false;
4350 + };
4351 +
4352 + this.autoRefresh = function (callback) {
4353 + let state = that;
4354 +
4355 + if (state.canBeAutoRefreshed() && state.running === false) {
4356 + state.running = true;
4357 + state.updateChart(function () {
4358 + state.running = false;
4359 +
4360 + if (typeof callback === 'function') {
4361 + return callback();
4362 + }
4363 + });
4364 + } else {
4365 + if (typeof callback === 'function') {
4366 + return callback();
4367 + }
4368 + }
4369 + };
4370 +
4371 + this.__defaultsFromDownloadedChart = function (chart) {
4372 + this.chart = chart;
4373 + this.chart_url = chart.url;
4374 + this.data_update_every = chart.update_every * 1000;
4375 + this.data_points = Math.round(this.chartWidth() / this.chartPixelsPerPoint());
4376 + this.tm.last_info_downloaded = Date.now();
4377 +
4378 + if (this.title === null) {
4379 + this.title = chart.title;
4380 + }
4381 +
4382 + if (this.units === null) {
4383 + this.units = chart.units;
4384 + this.units_current = this.units;
4385 + }
4386 + };
4387 +
4388 + // fetch the chart description from the netdata server
4389 + this.getChart = function (callback) {
4390 + this.chart = NETDATA.chartRegistry.get(this.host, this.id);
4391 + if (this.chart) {
4392 + this.__defaultsFromDownloadedChart(this.chart);
4393 +
4394 + if (typeof callback === 'function') {
4395 + return callback();
4396 + }
4397 + } else if (netdataSnapshotData !== null) {
4398 + // console.log(this);
4399 + // console.log(NETDATA.chartRegistry);
4400 + NETDATA.error(404, 'host: ' + this.host + ', chart: ' + this.id);
4401 + error('chart not found in snapshot');
4402 +
4403 + if (typeof callback === 'function') {
4404 + return callback();
4405 + }
4406 + } else {
4407 + this.chart_url = "/api/v1/chart?chart=" + this.id;
4408 +
4409 + if (this.debug) {
4410 + this.log('downloading ' + this.chart_url);
4411 + }
4412 +
4413 + $.ajax({
4414 + url: this.host + this.chart_url,
4415 + cache: false,
4416 + async: true,
4417 + xhrFields: {withCredentials: true} // required for the cookie
4418 + })
4419 + .done(function (chart) {
4420 + chart = NETDATA.xss.checkOptional('/api/v1/chart', chart);
4421 +
4422 + chart.url = that.chart_url;
4423 + that.__defaultsFromDownloadedChart(chart);
4424 + NETDATA.chartRegistry.add(that.host, that.id, chart);
4425 + })
4426 + .fail(function () {
4427 + NETDATA.error(404, that.chart_url);
4428 + error('chart not found on url "' + that.chart_url + '"');
4429 + })
4430 + .always(function () {
4431 + if (typeof callback === 'function') {
4432 + return callback();
4433 + }
4434 + });
4435 + }
4436 + };
4437 +
4438 + // ============================================================================================================
4439 + // INITIALIZATION
4440 +
4441 + initDOM();
4442 + init('fast');
4443 +};
4444 +
4445 +NETDATA.resetAllCharts = function (state) {
4446 + // first clear the global selection sync
4447 + // to make sure no chart is in selected state
4448 + NETDATA.globalSelectionSync.stop();
4449 +
4450 + // there are 2 possibilities here
4451 + // a. state is the global Pan and Zoom master
4452 + // b. state is not the global Pan and Zoom master
4453 + let master = true;
4454 + if (NETDATA.globalPanAndZoom.isMaster(state) === false) {
4455 + master = false;
4456 + }
4457 +
4458 + // clear the global Pan and Zoom
4459 + // this will also refresh the master
4460 + // and unblock any charts currently mirroring the master
4461 + NETDATA.globalPanAndZoom.clearMaster();
4462 +
4463 + // if we were not the master, reset our status too
4464 + // this is required because most probably the mouse
4465 + // is over this chart, blocking it from auto-refreshing
4466 + if (master === false && (state.paused || state.selected)) {
4467 + state.resetChart();
4468 + }
4469 +};
4470 +
4471 +// get or create a chart state, given a DOM element
4472 +NETDATA.chartState = function (element) {
4473 + let self = $(element);
4474 +
4475 + let state = self.data('netdata-state-object') || null;
4476 + if (state === null) {
4477 + state = new chartState(element);
4478 + self.data('netdata-state-object', state);
4479 + }
4480 + return state;
4481 +};
4482 +
4483 +// ----------------------------------------------------------------------------------------------------------------
4484 +// Library functions
4485 +
4486 +// Load a script without jquery
4487 +// This is used to load jquery - after it is loaded, we use jquery
4488 +NETDATA._loadjQuery = function (callback) {
4489 + if (typeof jQuery === 'undefined') {
4490 + if (NETDATA.options.debug.main_loop) {
4491 + console.log('loading ' + NETDATA.jQuery);
4492 + }
4493 +
4494 + let script = document.createElement('script');
4495 + script.type = 'text/javascript';
4496 + script.async = true;
4497 + script.src = NETDATA.jQuery;
4498 +
4499 + // script.onabort = onError;
4500 + script.onerror = function () {
4501 + NETDATA.error(101, NETDATA.jQuery);
4502 + };
4503 + if (typeof callback === "function") {
4504 + script.onload = function () {
4505 + $ = jQuery;
4506 + return callback();
4507 + };
4508 + }
4509 +
4510 + let s = document.getElementsByTagName('script')[0];
4511 + s.parentNode.insertBefore(script, s);
4512 + }
4513 + else if (typeof callback === "function") {
4514 + $ = jQuery;
4515 + return callback();
4516 + }
4517 +};
4518 +
4519 +NETDATA._loadCSS = function (filename) {
4520 + // don't use jQuery here
4521 + // styles are loaded before jQuery
4522 + // to eliminate showing an unstyled page to the user
4523 +
4524 + let fileref = document.createElement("link");
4525 + fileref.setAttribute("rel", "stylesheet");
4526 + fileref.setAttribute("type", "text/css");
4527 + fileref.setAttribute("href", filename);
4528 +
4529 + if (typeof fileref !== 'undefined') {
4530 + document.getElementsByTagName("head")[0].appendChild(fileref);
4531 + }
4532 +};
4533 +
4534 +// user function to signal us the DOM has been
4535 +// updated.
4536 +NETDATA.updatedDom = function () {
4537 + NETDATA.options.updated_dom = true;
4538 +};
4539 +
4540 +NETDATA.ready = function (callback) {
4541 + NETDATA.options.pauseCallback = callback;
4542 +};
4543 +
4544 +NETDATA.pause = function (callback) {
4545 + if (typeof callback === 'function') {
4546 + if (NETDATA.options.pause) {
4547 + return callback();
4548 + } else {
4549 + NETDATA.options.pauseCallback = callback;
4550 + }
4551 + }
4552 +};
4553 +
4554 +NETDATA.unpause = function () {
4555 + NETDATA.options.pauseCallback = null;
4556 + NETDATA.options.updated_dom = true;
4557 + NETDATA.options.pause = false;
4558 +};
4559 +
4560 +// ----------------------------------------------------------------------------------------------------------------
4561 +
4562 +// this is purely sequential charts refresher
4563 +// it is meant to be autonomous
4564 +NETDATA.chartRefresherNoParallel = function (index, callback) {
4565 + let targets = NETDATA.intersectionObserver.targets();
4566 +
4567 + if (NETDATA.options.debug.main_loop) {
4568 + console.log('NETDATA.chartRefresherNoParallel(' + index + ')');
4569 + }
4570 +
4571 + if (NETDATA.options.updated_dom) {
4572 + // the dom has been updated
4573 + // get the dom parts again
4574 + NETDATA.parseDom(callback);
4575 + return;
4576 + }
4577 + if (index >= targets.length) {
4578 + if (NETDATA.options.debug.main_loop) {
4579 + console.log('waiting to restart main loop...');
4580 + }
4581 +
4582 + NETDATA.options.auto_refresher_fast_weight = 0;
4583 + callback();
4584 + } else {
4585 + let state = targets[index];
4586 +
4587 + if (NETDATA.options.auto_refresher_fast_weight < NETDATA.options.current.fast_render_timeframe) {
4588 + if (NETDATA.options.debug.main_loop) {
4589 + console.log('fast rendering...');
4590 + }
4591 +
4592 + if (state.isVisible()) {
4593 + NETDATA.timeout.set(function () {
4594 + state.autoRefresh(function () {
4595 + NETDATA.chartRefresherNoParallel(++index, callback);
4596 + });
4597 + }, 0);
4598 + } else {
4599 + NETDATA.chartRefresherNoParallel(++index, callback);
4600 + }
4601 + } else {
4602 + if (NETDATA.options.debug.main_loop) {
4603 + console.log('waiting for next refresh...');
4604 + }
4605 + NETDATA.options.auto_refresher_fast_weight = 0;
4606 +
4607 + NETDATA.timeout.set(function () {
4608 + state.autoRefresh(function () {
4609 + NETDATA.chartRefresherNoParallel(++index, callback);
4610 + });
4611 + }, NETDATA.options.current.idle_between_charts);
4612 + }
4613 + }
4614 +};
4615 +
4616 +NETDATA.chartRefresherWaitTime = function () {
4617 + return NETDATA.options.current.idle_parallel_loops;
4618 +};
4619 +
4620 +// the default refresher
4621 +NETDATA.chartRefresherLastRun = 0;
4622 +NETDATA.chartRefresherRunsAfterParseDom = 0;
4623 +NETDATA.chartRefresherTimeoutId = undefined;
4624 +
4625 +NETDATA.chartRefresherReschedule = function () {
4626 + if (NETDATA.options.current.async_on_scroll) {
4627 + if (NETDATA.chartRefresherTimeoutId) {
4628 + NETDATA.timeout.clear(NETDATA.chartRefresherTimeoutId);
4629 + }
4630 + NETDATA.chartRefresherTimeoutId = NETDATA.timeout.set(NETDATA.chartRefresher, NETDATA.options.current.onscroll_worker_duration_threshold);
4631 + //console.log('chartRefresherReschedule()');
4632 + }
4633 +};
4634 +
4635 +NETDATA.chartRefresher = function () {
4636 + // console.log('chartRefresher() begin ' + (Date.now() - NETDATA.chartRefresherLastRun).toString() + ' ms since last run');
4637 +
4638 + if (NETDATA.options.page_is_visible === false
4639 + && NETDATA.options.current.stop_updates_when_focus_is_lost
4640 + && NETDATA.chartRefresherLastRun > NETDATA.options.last_page_resize
4641 + && NETDATA.chartRefresherLastRun > NETDATA.options.last_page_scroll
4642 + && NETDATA.chartRefresherRunsAfterParseDom > 10
4643 + ) {
4644 + setTimeout(
4645 + NETDATA.chartRefresher,
4646 + NETDATA.options.current.idle_lost_focus
4647 + );
4648 +
4649 + // console.log('chartRefresher() page without focus, will run in ' + NETDATA.options.current.idle_lost_focus.toString() + ' ms, ' + NETDATA.chartRefresherRunsAfterParseDom.toString());
4650 + return;
4651 + }
4652 + NETDATA.chartRefresherRunsAfterParseDom++;
4653 +
4654 + let now = Date.now();
4655 + NETDATA.chartRefresherLastRun = now;
4656 +
4657 + if (now < NETDATA.options.on_scroll_refresher_stop_until) {
4658 + NETDATA.chartRefresherTimeoutId = NETDATA.timeout.set(
4659 + NETDATA.chartRefresher,
4660 + NETDATA.chartRefresherWaitTime()
4661 + );
4662 +
4663 + // console.log('chartRefresher() end1 will run in ' + NETDATA.chartRefresherWaitTime().toString() + ' ms');
4664 + return;
4665 + }
4666 +
4667 + if (now < NETDATA.options.auto_refresher_stop_until) {
4668 + NETDATA.chartRefresherTimeoutId = NETDATA.timeout.set(
4669 + NETDATA.chartRefresher,
4670 + NETDATA.chartRefresherWaitTime()
4671 + );
4672 +
4673 + // console.log('chartRefresher() end2 will run in ' + NETDATA.chartRefresherWaitTime().toString() + ' ms');
4674 + return;
4675 + }
4676 +
4677 + if (NETDATA.options.pause) {
4678 + // console.log('auto-refresher is paused');
4679 + NETDATA.chartRefresherTimeoutId = NETDATA.timeout.set(
4680 + NETDATA.chartRefresher,
4681 + NETDATA.chartRefresherWaitTime()
4682 + );
4683 +
4684 + // console.log('chartRefresher() end3 will run in ' + NETDATA.chartRefresherWaitTime().toString() + ' ms');
4685 + return;
4686 + }
4687 +
4688 + if (typeof NETDATA.options.pauseCallback === 'function') {
4689 + // console.log('auto-refresher is calling pauseCallback');
4690 +
4691 + NETDATA.options.pause = true;
4692 + NETDATA.options.pauseCallback();
4693 + NETDATA.chartRefresher();
4694 +
4695 + // console.log('chartRefresher() end4 (nested)');
4696 + return;
4697 + }
4698 +
4699 + if (NETDATA.options.current.parallel_refresher === false) {
4700 + // console.log('auto-refresher is calling chartRefresherNoParallel(0)');
4701 + NETDATA.chartRefresherNoParallel(0, function () {
4702 + NETDATA.chartRefresherTimeoutId = NETDATA.timeout.set(
4703 + NETDATA.chartRefresher,
4704 + NETDATA.options.current.idle_between_loops
4705 + );
4706 + });
4707 + // console.log('chartRefresher() end5 (no parallel, nested)');
4708 + return;
4709 + }
4710 +
4711 + if (NETDATA.options.updated_dom) {
4712 + // the dom has been updated
4713 + // get the dom parts again
4714 + // console.log('auto-refresher is calling parseDom()');
4715 + NETDATA.parseDom(NETDATA.chartRefresher);
4716 + // console.log('chartRefresher() end6 (parseDom)');
4717 + return;
4718 + }
4719 +
4720 + if (NETDATA.globalSelectionSync.active() === false) {
4721 + let parallel = [];
4722 + let targets = NETDATA.intersectionObserver.targets();
4723 + let len = targets.length;
4724 + let state;
4725 + while (len--) {
4726 + state = targets[len];
4727 + if (state.running || state.isVisible() === false) {
4728 + continue;
4729 + }
4730 +
4731 + if (state.library.initialized === false) {
4732 + if (state.library.enabled) {
4733 + state.library.initialize(NETDATA.chartRefresher);
4734 + //console.log('chartRefresher() end6 (library init)');
4735 + return;
4736 + }
4737 + else {
4738 + state.error('chart library "' + state.library_name + '" is not enabled.');
4739 + }
4740 + }
4741 +
4742 + if (NETDATA.scrollUp) {
4743 + parallel.unshift(state);
4744 + } else {
4745 + parallel.push(state);
4746 + }
4747 + }
4748 +
4749 + len = parallel.length;
4750 + while (len--) {
4751 + state = parallel[len];
4752 + // console.log('auto-refresher executing in parallel for ' + parallel.length.toString() + ' charts');
4753 + // this will execute the jobs in parallel
4754 +
4755 + if (state.running === false) {
4756 + NETDATA.timeout.set(state.autoRefresh, 0);
4757 + }
4758 + }
4759 + //else {
4760 + // console.log('auto-refresher nothing to do');
4761 + //}
4762 + }
4763 +
4764 + // run the next refresh iteration
4765 + NETDATA.chartRefresherTimeoutId = NETDATA.timeout.set(
4766 + NETDATA.chartRefresher,
4767 + NETDATA.chartRefresherWaitTime()
4768 + );
4769 +
4770 + //console.log('chartRefresher() completed in ' + (Date.now() - now).toString() + ' ms');
4771 +};
4772 +
4773 +NETDATA.parseDom = function (callback) {
4774 + //console.log('parseDom()');
4775 +
4776 + NETDATA.options.last_page_scroll = Date.now();
4777 + NETDATA.options.updated_dom = false;
4778 + NETDATA.chartRefresherRunsAfterParseDom = 0;
4779 +
4780 + let targets = $('div[data-netdata]'); //.filter(':visible');
4781 +
4782 + if (NETDATA.options.debug.main_loop) {
4783 + console.log('DOM updated - there are ' + targets.length + ' charts on page.');
4784 + }
4785 +
4786 + NETDATA.intersectionObserver.globalReset();
4787 + NETDATA.options.targets = [];
4788 + let len = targets.length;
4789 + while (len--) {
4790 + // the initialization will take care of sizing
4791 + // and the "loading..." message
4792 + let state = NETDATA.chartState(targets[len]);
4793 + NETDATA.options.targets.push(state);
4794 + NETDATA.intersectionObserver.observe(state);
4795 + }
4796 +
4797 + if (NETDATA.globalChartUnderlay.isActive()) {
4798 + NETDATA.globalChartUnderlay.setup();
4799 + } else {
4800 + NETDATA.globalChartUnderlay.clear();
4801 + }
4802 +
4803 + if (typeof callback === 'function') {
4804 + return callback();
4805 + }
4806 +};
4807 +
4808 +// this is the main function - where everything starts
4809 +NETDATA.started = false;
4810 +NETDATA.start = function () {
4811 + // this should be called only once
4812 +
4813 + if (NETDATA.started) {
4814 + console.log('netdata is already started');
4815 + return;
4816 + }
4817 +
4818 + NETDATA.started = true;
4819 + NETDATA.options.page_is_visible = true;
4820 +
4821 + $(window).blur(function () {
4822 + if (NETDATA.options.current.stop_updates_when_focus_is_lost) {
4823 + NETDATA.options.page_is_visible = false;
4824 + if (NETDATA.options.debug.focus) {
4825 + console.log('Lost Focus!');
4826 + }
4827 + }
4828 + });
4829 +
4830 + $(window).focus(function () {
4831 + if (NETDATA.options.current.stop_updates_when_focus_is_lost) {
4832 + NETDATA.options.page_is_visible = true;
4833 + if (NETDATA.options.debug.focus) {
4834 + console.log('Focus restored!');
4835 + }
4836 + }
4837 + });
4838 +
4839 + if (typeof document.hasFocus === 'function' && !document.hasFocus()) {
4840 + if (NETDATA.options.current.stop_updates_when_focus_is_lost) {
4841 + NETDATA.options.page_is_visible = false;
4842 + if (NETDATA.options.debug.focus) {
4843 + console.log('Document has no focus!');
4844 + }
4845 + }
4846 + }
4847 +
4848 + // bootstrap tab switching
4849 + $('a[data-toggle="tab"]').on('shown.bs.tab', NETDATA.onscroll);
4850 +
4851 + // bootstrap modal switching
4852 + let $modal = $('.modal');
4853 + $modal.on('hidden.bs.modal', NETDATA.onscroll);
4854 + $modal.on('shown.bs.modal', NETDATA.onscroll);
4855 +
4856 + // bootstrap collapse switching
4857 + let $collapse = $('.collapse');
4858 + $collapse.on('hidden.bs.collapse', NETDATA.onscroll);
4859 + $collapse.on('shown.bs.collapse', NETDATA.onscroll);
4860 +
4861 + NETDATA.parseDom(NETDATA.chartRefresher);
4862 +
4863 + // Alarms initialization
4864 + setTimeout(NETDATA.alarms.init, 1000);
4865 +
4866 + // Registry initialization
4867 + setTimeout(NETDATA.registry.init, netdataRegistryAfterMs);
4868 +
4869 + if (typeof netdataCallback === 'function') {
4870 + netdataCallback();
4871 + }
4872 +};
4873 +
4874 +NETDATA.globalReset = function () {
4875 + NETDATA.intersectionObserver.globalReset();
4876 + NETDATA.globalSelectionSync.globalReset();
4877 + NETDATA.globalPanAndZoom.globalReset();
4878 + NETDATA.chartRegistry.globalReset();
4879 + NETDATA.commonMin.globalReset();
4880 + NETDATA.commonMax.globalReset();
4881 + NETDATA.commonColors.globalReset();
4882 + NETDATA.unitsConversion.globalReset();
4883 + NETDATA.options.targets = [];
4884 + NETDATA.parseDom();
4885 + NETDATA.unpause();
4886 +};
web/gui/src/dashboard.js/options.js new
+225
@@ -0,0 +1,225 @@
1 +
2 +NETDATA.icons = {
3 + left: '<i class="fas fa-backward"></i>',
4 + reset: '<i class="fas fa-play"></i>',
5 + right: '<i class="fas fa-forward"></i>',
6 + zoomIn: '<i class="fas fa-plus"></i>',
7 + zoomOut: '<i class="fas fa-minus"></i>',
8 + resize: '<i class="fas fa-sort"></i>',
9 + lineChart: '<i class="fas fa-chart-line"></i>',
10 + areaChart: '<i class="fas fa-chart-area"></i>',
11 + noChart: '<i class="fas fa-chart-area"></i>',
12 + loading: '<i class="fas fa-sync-alt"></i>',
13 + noData: '<i class="fas fa-exclamation-triangle"></i>'
14 +};
15 +
16 +if (typeof netdataIcons === 'object') {
17 + // for (let icon in NETDATA.icons) {
18 + // if (NETDATA.icons.hasOwnProperty(icon) && typeof(netdataIcons[icon]) === 'string')
19 + // NETDATA.icons[icon] = netdataIcons[icon];
20 + // }
21 + for (const icon of Object.keys(NETDATA.icons)) {
22 + if (typeof(netdataIcons[icon]) === 'string') {
23 + NETDATA.icons[icon] = netdataIcons[icon]
24 + }
25 + }
26 +}
27 +
28 +if (typeof netdataSnapshotData === 'undefined') {
29 + netdataSnapshotData = null;
30 +}
31 +
32 +if (typeof netdataShowHelp === 'undefined') {
33 + netdataShowHelp = true;
34 +}
35 +
36 +if (typeof netdataShowAlarms === 'undefined') {
37 + netdataShowAlarms = false;
38 +}
39 +
40 +if (typeof netdataRegistryAfterMs !== 'number' || netdataRegistryAfterMs < 0) {
41 + netdataRegistryAfterMs = 1500;
42 +}
43 +
44 +if (typeof netdataRegistry === 'undefined') {
45 + // backward compatibility
46 + netdataRegistry = (typeof netdataNoRegistry !== 'undefined' && netdataNoRegistry === false);
47 +}
48 +
49 +if (netdataRegistry === false && typeof netdataRegistryCallback === 'function') {
50 + netdataRegistry = true;
51 +}
52 +
53 +// ----------------------------------------------------------------------------------------------------------------
54 +// the defaults for all charts
55 +
56 +// if the user does not specify any of these, the following will be used
57 +
58 +NETDATA.chartDefaults = {
59 + width: '100%', // the chart width - can be null
60 + height: '100%', // the chart height - can be null
61 + min_width: null, // the chart minimum width - can be null
62 + library: 'dygraph', // the graphing library to use
63 + method: 'average', // the grouping method
64 + before: 0, // panning
65 + after: -600, // panning
66 + pixels_per_point: 1, // the detail of the chart
67 + fill_luminance: 0.8 // luminance of colors in solid areas
68 +};
69 +
70 +// ----------------------------------------------------------------------------------------------------------------
71 +// global options
72 +
73 +NETDATA.options = {
74 + pauseCallback: null, // a callback when we are really paused
75 +
76 + pause: false, // when enabled we don't auto-refresh the charts
77 +
78 + targets: [], // an array of all the state objects that are
79 + // currently active (independently of their
80 + // viewport visibility)
81 +
82 + updated_dom: true, // when true, the DOM has been updated with
83 + // new elements we have to check.
84 +
85 + auto_refresher_fast_weight: 0, // this is the current time in ms, spent
86 + // rendering charts continuously.
87 + // used with .current.fast_render_timeframe
88 +
89 + page_is_visible: true, // when true, this page is visible
90 +
91 + auto_refresher_stop_until: 0, // timestamp in ms - used internally, to stop the
92 + // auto-refresher for some time (when a chart is
93 + // performing pan or zoom, we need to stop refreshing
94 + // all other charts, to have the maximum speed for
95 + // rendering the chart that is panned or zoomed).
96 + // Used with .current.global_pan_sync_time
97 +
98 + on_scroll_refresher_stop_until: 0, // timestamp in ms - used to stop evaluating
99 + // charts for some time, after a page scroll
100 +
101 + last_page_resize: Date.now(), // the timestamp of the last resize request
102 +
103 + last_page_scroll: 0, // the timestamp the last time the page was scrolled
104 +
105 + browser_timezone: 'unknown', // timezone detected by javascript
106 + server_timezone: 'unknown', // timezone reported by the server
107 +
108 + force_data_points: 0, // force the number of points to be returned for charts
109 + fake_chart_rendering: false, // when set to true, the dashboard will download data but will not render the charts
110 +
111 + passive_events: null, // true if the browser supports passive events
112 +
113 + // the current profile
114 + // we may have many...
115 + current: {
116 + units: 'auto', // can be 'auto' or 'original'
117 + temperature: 'celsius', // can be 'celsius' or 'fahrenheit'
118 + seconds_as_time: true, // show seconds as DDd:HH:MM:SS ?
119 + timezone: 'default', // the timezone to use, or 'default'
120 + user_set_server_timezone: 'default', // as set by the user on the dashboard
121 +
122 + legend_toolbox: true, // show the legend toolbox on charts
123 + resize_charts: true, // show the resize handler on charts
124 +
125 + pixels_per_point: isSlowDevice() ? 5 : 1, // the minimum pixels per point for all charts
126 + // increase this to speed javascript up
127 + // each chart library has its own limit too
128 + // the max of this and the chart library is used
129 + // the final is calculated every time, so a change
130 + // here will have immediate effect on the next chart
131 + // update
132 +
133 + idle_between_charts: 100, // ms - how much time to wait between chart updates
134 +
135 + fast_render_timeframe: 200, // ms - render continuously until this time of continuous
136 + // rendering has been reached
137 + // this setting is used to make it render e.g. 10
138 + // charts at once, sleep idle_between_charts time
139 + // and continue for another 10 charts.
140 +
141 + idle_between_loops: 500, // ms - if all charts have been updated, wait this
142 + // time before starting again.
143 +
144 + idle_parallel_loops: 100, // ms - the time between parallel refresher updates
145 +
146 + idle_lost_focus: 500, // ms - when the window does not have focus, check
147 + // if focus has been regained, every this time
148 +
149 + global_pan_sync_time: 300, // ms - when you pan or zoom a chart, the background
150 + // auto-refreshing of charts is paused for this amount
151 + // of time
152 +
153 + sync_selection_delay: 400, // ms - when you pan or zoom a chart, wait this amount
154 + // of time before setting up synchronized selections
155 + // on hover.
156 +
157 + sync_selection: true, // enable or disable selection sync
158 +
159 + pan_and_zoom_delay: 50, // when panning or zooming, how ofter to update the chart
160 +
161 + sync_pan_and_zoom: true, // enable or disable pan and zoom sync
162 +
163 + pan_and_zoom_data_padding: true, // fetch more data for the master chart when panning or zooming
164 +
165 + update_only_visible: true, // enable or disable visibility management / used for printing
166 +
167 + parallel_refresher: !isSlowDevice(), // enable parallel refresh of charts
168 +
169 + concurrent_refreshes: true, // when parallel_refresher is enabled, sync also the charts
170 +
171 + destroy_on_hide: isSlowDevice(), // destroy charts when they are not visible
172 +
173 + show_help: netdataShowHelp, // when enabled the charts will show some help
174 + show_help_delay_show_ms: 500,
175 + show_help_delay_hide_ms: 0,
176 +
177 + eliminate_zero_dimensions: true, // do not show dimensions with just zeros
178 +
179 + stop_updates_when_focus_is_lost: true, // boolean - shall we stop auto-refreshes when document does not have user focus
180 + stop_updates_while_resizing: 1000, // ms - time to stop auto-refreshes while resizing the charts
181 +
182 + double_click_speed: 500, // ms - time between clicks / taps to detect double click/tap
183 +
184 + smooth_plot: !isSlowDevice(), // enable smooth plot, where possible
185 +
186 + color_fill_opacity_line: 1.0,
187 + color_fill_opacity_area: 0.2,
188 + color_fill_opacity_stacked: 0.8,
189 +
190 + pan_and_zoom_factor: 0.25, // the increment when panning and zooming with the toolbox
191 + pan_and_zoom_factor_multiplier_control: 2.0,
192 + pan_and_zoom_factor_multiplier_shift: 3.0,
193 + pan_and_zoom_factor_multiplier_alt: 4.0,
194 +
195 + abort_ajax_on_scroll: false, // kill pending ajax page scroll
196 + async_on_scroll: false, // sync/async onscroll handler
197 + onscroll_worker_duration_threshold: 30, // time in ms, for async scroll handler
198 +
199 + retries_on_data_failures: 3, // how many retries to make if we can't fetch chart data from the server
200 +
201 + setOptionCallback: function () {
202 + }
203 + },
204 +
205 + debug: {
206 + show_boxes: false,
207 + main_loop: false,
208 + focus: false,
209 + visibility: false,
210 + chart_data_url: false,
211 + chart_errors: true, // remember to set it to false before merging
212 + chart_timing: false,
213 + chart_calls: false,
214 + libraries: false,
215 + dygraph: false,
216 + globalSelectionSync: false,
217 + globalPanAndZoom: false
218 + }
219 +};
220 +
221 +NETDATA.statistics = {
222 + refreshes_total: 0,
223 + refreshes_active: 0,
224 + refreshes_active_max: 0
225 +};
web/gui/src/dashboard.js/prologue.js.inc new
+83
@@ -0,0 +1,83 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +// DO NOT EDIT: This file is automatically generated from the source files in src/
4 +
5 +// ----------------------------------------------------------------------------
6 +// You can set the following variables before loading this script:
7 +
8 +// 'use strict';
9 +
10 +/*global netdataNoDygraphs *//* boolean, disable dygraph charts
11 + * (default: false) */
12 +/*global netdataNoSparklines *//* boolean, disable sparkline charts
13 + * (default: false) */
14 +/*global netdataNoPeitys *//* boolean, disable peity charts
15 + * (default: false) */
16 +/*global netdataNoGoogleCharts *//* boolean, disable google charts
17 + * (default: false) */
18 +/*global netdataNoMorris *//* boolean, disable morris charts
19 + * (default: false) */
20 +/*global netdataNoEasyPieChart *//* boolean, disable easypiechart charts
21 + * (default: false) */
22 +/*global netdataNoGauge *//* boolean, disable gauge.js charts
23 + * (default: false) */
24 +/*global netdataNoD3 *//* boolean, disable d3 charts
25 + * (default: false) */
26 +/*global netdataNoC3 *//* boolean, disable c3 charts
27 + * (default: false) */
28 +/*global netdataNoD3pie *//* boolean, disable d3pie charts
29 + * (default: false) */
30 +/*global netdataNoBootstrap *//* boolean, disable bootstrap - disables help too
31 + * (default: false) */
32 +/*global netdataNoFontAwesome *//* boolean, disable fontawesome (do not load it)
33 + * (default: false) */
34 +/*global netdataIcons *//* object, overwrite netdata fontawesome icons
35 + * (default: null) */
36 +/*global netdataDontStart *//* boolean, do not start the thread to process the charts
37 + * (default: false) */
38 +/*global netdataErrorCallback *//* function, callback to be called when the dashboard encounters an error
39 + * (default: null) */
40 +/*global netdataRegistry:true *//* boolean, use the netdata registry
41 + * (default: false) */
42 +/*global netdataNoRegistry *//* boolean, included only for compatibility with existing custom dashboard
43 + * (obsolete - do not use this any more) */
44 +/*global netdataRegistryCallback *//* function, callback that will be invoked with one param: the URLs from the registry
45 + * (default: null) */
46 +/*global netdataShowHelp:true *//* boolean, disable charts help
47 + * (default: true) */
48 +/*global netdataShowAlarms:true *//* boolean, enable alarms checks and notifications
49 + * (default: false) */
50 +/*global netdataRegistryAfterMs:true *//* ms, delay registry use at started
51 + * (default: 1500) */
52 +/*global netdataCallback *//* function, callback to be called when netdata is ready to start
53 + * (default: null)
54 + * netdata will be running while this is called
55 + * (call NETDATA.pause to stop it) */
56 +/*global netdataPrepCallback *//* function, callback to be called before netdata does anything else
57 + * (default: null) */
58 +/*global netdataServer *//* string, the URL of the netdata server to use
59 + * (default: the URL the page is hosted at) */
60 +/*global netdataServerStatic *//* string, the URL of the netdata server to use for static files
61 + * (default: netdataServer) */
62 +/*global netdataSnapshotData *//* object, a netdata snapshot loaded
63 + * (default: null) */
64 +/*global netdataAlarmsRecipients *//* array, an array of alarm recipients to show notifications for
65 + * (default: null) */
66 +/*global netdataAlarmsRemember *//* boolen, keep our position in the alarm log at browser local storage
67 + * (default: true) */
68 +/*global netdataAlarmsActiveCallback *//* function, a hook for the alarm logs
69 + * (default: undefined) */
70 +/*global netdataAlarmsNotifCallback *//* function, a hook for alarm notifications
71 + * (default: undefined) */
72 +/*global netdataIntersectionObserver *//* boolean, enable or disable the use of intersection observer
73 + * (default: true) */
74 +/*global netdataCheckXSS *//* boolean, enable or disable checking for XSS issues
75 + * (default: false) */
76 +
77 +// ----------------------------------------------------------------------------
78 +// global namespace
79 +
80 +const NETDATA = window.NETDATA || {};
81 +
82 +(function(window, document, $, undefined) {
83 +
web/gui/src/dashboard.js/registry.js new
+272
@@ -0,0 +1,272 @@
1 +
2 +// Registry of netdata hosts
3 +
4 +NETDATA.registry = {
5 + server: null, // the netdata registry server
6 + person_guid: null, // the unique ID of this browser / user
7 + machine_guid: null, // the unique ID the netdata server that served dashboard.js
8 + hostname: 'unknown', // the hostname of the netdata server that served dashboard.js
9 + machines: null, // the user's other URLs
10 + machines_array: null, // the user's other URLs in an array
11 + person_urls: null,
12 +
13 + parsePersonUrls: function (person_urls) {
14 + // console.log(person_urls);
15 + NETDATA.registry.person_urls = person_urls;
16 +
17 + if (person_urls) {
18 + NETDATA.registry.machines = {};
19 + NETDATA.registry.machines_array = [];
20 +
21 + let apu = person_urls;
22 + let i = apu.length;
23 + while (i--) {
24 + if (typeof NETDATA.registry.machines[apu[i][0]] === 'undefined') {
25 + // console.log('adding: ' + apu[i][4] + ', ' + ((now - apu[i][2]) / 1000).toString());
26 +
27 + let obj = {
28 + guid: apu[i][0],
29 + url: apu[i][1],
30 + last_t: apu[i][2],
31 + accesses: apu[i][3],
32 + name: apu[i][4],
33 + alternate_urls: []
34 + };
35 + obj.alternate_urls.push(apu[i][1]);
36 +
37 + NETDATA.registry.machines[apu[i][0]] = obj;
38 + NETDATA.registry.machines_array.push(obj);
39 + } else {
40 + // console.log('appending: ' + apu[i][4] + ', ' + ((now - apu[i][2]) / 1000).toString());
41 +
42 + let pu = NETDATA.registry.machines[apu[i][0]];
43 + if (pu.last_t < apu[i][2]) {
44 + pu.url = apu[i][1];
45 + pu.last_t = apu[i][2];
46 + pu.name = apu[i][4];
47 + }
48 + pu.accesses += apu[i][3];
49 + pu.alternate_urls.push(apu[i][1]);
50 + }
51 + }
52 + }
53 +
54 + if (typeof netdataRegistryCallback === 'function') {
55 + netdataRegistryCallback(NETDATA.registry.machines_array);
56 + }
57 + },
58 +
59 + init: function () {
60 + if (netdataRegistry !== true) {
61 + return;
62 + }
63 +
64 + NETDATA.registry.hello(NETDATA.serverDefault, function (data) {
65 + if (data) {
66 + NETDATA.registry.server = data.registry;
67 + NETDATA.registry.machine_guid = data.machine_guid;
68 + NETDATA.registry.hostname = data.hostname;
69 +
70 + NETDATA.registry.access(2, function (person_urls) {
71 + NETDATA.registry.parsePersonUrls(person_urls);
72 +
73 + });
74 + }
75 + });
76 + },
77 +
78 + hello: function (host, callback) {
79 + host = NETDATA.fixHost(host);
80 +
81 + // send HELLO to a netdata server:
82 + // 1. verifies the server is reachable
83 + // 2. responds with the registry URL, the machine GUID of this netdata server and its hostname
84 + $.ajax({
85 + url: host + '/api/v1/registry?action=hello',
86 + async: true,
87 + cache: false,
88 + headers: {
89 + 'Cache-Control': 'no-cache, no-store',
90 + 'Pragma': 'no-cache'
91 + },
92 + xhrFields: {withCredentials: true} // required for the cookie
93 + })
94 + .done(function (data) {
95 + data = NETDATA.xss.checkOptional('/api/v1/registry?action=hello', data);
96 +
97 + if (typeof data.status !== 'string' || data.status !== 'ok') {
98 + NETDATA.error(408, host + ' response: ' + JSON.stringify(data));
99 + data = null;
100 + }
101 +
102 + if (typeof callback === 'function') {
103 + return callback(data);
104 + }
105 + })
106 + .fail(function () {
107 + NETDATA.error(407, host);
108 +
109 + if (typeof callback === 'function') {
110 + return callback(null);
111 + }
112 + });
113 + },
114 +
115 + access: function (max_redirects, callback) {
116 + // send ACCESS to a netdata registry:
117 + // 1. it lets it know we are accessing a netdata server (its machine GUID and its URL)
118 + // 2. it responds with a list of netdata servers we know
119 + // the registry identifies us using a cookie it sets the first time we access it
120 + // the registry may respond with a redirect URL to send us to another registry
121 + $.ajax({
122 + url: NETDATA.registry.server + '/api/v1/registry?action=access&machine=' + NETDATA.registry.machine_guid + '&name=' + encodeURIComponent(NETDATA.registry.hostname) + '&url=' + encodeURIComponent(NETDATA.serverDefault), // + '&visible_url=' + encodeURIComponent(document.location),
123 + async: true,
124 + cache: false,
125 + headers: {
126 + 'Cache-Control': 'no-cache, no-store',
127 + 'Pragma': 'no-cache'
128 + },
129 + xhrFields: {withCredentials: true} // required for the cookie
130 + })
131 + .done(function (data) {
132 + data = NETDATA.xss.checkAlways('/api/v1/registry?action=access', data);
133 +
134 + let redirect = null;
135 + if (typeof data.registry === 'string') {
136 + redirect = data.registry;
137 + }
138 +
139 + if (typeof data.status !== 'string' || data.status !== 'ok') {
140 + NETDATA.error(409, NETDATA.registry.server + ' responded with: ' + JSON.stringify(data));
141 + data = null;
142 + }
143 +
144 + if (data === null) {
145 + if (redirect !== null && max_redirects > 0) {
146 + NETDATA.registry.server = redirect;
147 + NETDATA.registry.access(max_redirects - 1, callback);
148 + }
149 + else {
150 + if (typeof callback === 'function') {
151 + return callback(null);
152 + }
153 + }
154 + }
155 + else {
156 + if (typeof data.person_guid === 'string') {
157 + NETDATA.registry.person_guid = data.person_guid;
158 + }
159 +
160 + if (typeof callback === 'function') {
161 + return callback(data.urls);
162 + }
163 + }
164 + })
165 + .fail(function () {
166 + NETDATA.error(410, NETDATA.registry.server);
167 +
168 + if (typeof callback === 'function') {
169 + return callback(null);
170 + }
171 + });
172 + },
173 +
174 + delete: function (delete_url, callback) {
175 + // send DELETE to a netdata registry:
176 + $.ajax({
177 + url: NETDATA.registry.server + '/api/v1/registry?action=delete&machine=' + NETDATA.registry.machine_guid + '&name=' + encodeURIComponent(NETDATA.registry.hostname) + '&url=' + encodeURIComponent(NETDATA.serverDefault) + '&delete_url=' + encodeURIComponent(delete_url),
178 + async: true,
179 + cache: false,
180 + headers: {
181 + 'Cache-Control': 'no-cache, no-store',
182 + 'Pragma': 'no-cache'
183 + },
184 + xhrFields: {withCredentials: true} // required for the cookie
185 + })
186 + .done(function (data) {
187 + data = NETDATA.xss.checkAlways('/api/v1/registry?action=delete', data);
188 +
189 + if (typeof data.status !== 'string' || data.status !== 'ok') {
190 + NETDATA.error(411, NETDATA.registry.server + ' responded with: ' + JSON.stringify(data));
191 + data = null;
192 + }
193 +
194 + if (typeof callback === 'function') {
195 + return callback(data);
196 + }
197 + })
198 + .fail(function () {
199 + NETDATA.error(412, NETDATA.registry.server);
200 +
201 + if (typeof callback === 'function') {
202 + return callback(null);
203 + }
204 + });
205 + },
206 +
207 + search: function (machine_guid, callback) {
208 + // SEARCH for the URLs of a machine:
209 + $.ajax({
210 + url: NETDATA.registry.server + '/api/v1/registry?action=search&machine=' + NETDATA.registry.machine_guid + '&name=' + encodeURIComponent(NETDATA.registry.hostname) + '&url=' + encodeURIComponent(NETDATA.serverDefault) + '&for=' + machine_guid,
211 + async: true,
212 + cache: false,
213 + headers: {
214 + 'Cache-Control': 'no-cache, no-store',
215 + 'Pragma': 'no-cache'
216 + },
217 + xhrFields: {withCredentials: true} // required for the cookie
218 + })
219 + .done(function (data) {
220 + data = NETDATA.xss.checkAlways('/api/v1/registry?action=search', data);
221 +
222 + if (typeof data.status !== 'string' || data.status !== 'ok') {
223 + NETDATA.error(417, NETDATA.registry.server + ' responded with: ' + JSON.stringify(data));
224 + data = null;
225 + }
226 +
227 + if (typeof callback === 'function') {
228 + return callback(data);
229 + }
230 + })
231 + .fail(function () {
232 + NETDATA.error(418, NETDATA.registry.server);
233 +
234 + if (typeof callback === 'function') {
235 + return callback(null);
236 + }
237 + });
238 + },
239 +
240 + switch: function (new_person_guid, callback) {
241 + // impersonate
242 + $.ajax({
243 + url: NETDATA.registry.server + '/api/v1/registry?action=switch&machine=' + NETDATA.registry.machine_guid + '&name=' + encodeURIComponent(NETDATA.registry.hostname) + '&url=' + encodeURIComponent(NETDATA.serverDefault) + '&to=' + new_person_guid,
244 + async: true,
245 + cache: false,
246 + headers: {
247 + 'Cache-Control': 'no-cache, no-store',
248 + 'Pragma': 'no-cache'
249 + },
250 + xhrFields: {withCredentials: true} // required for the cookie
251 + })
252 + .done(function (data) {
253 + data = NETDATA.xss.checkAlways('/api/v1/registry?action=switch', data);
254 +
255 + if (typeof data.status !== 'string' || data.status !== 'ok') {
256 + NETDATA.error(413, NETDATA.registry.server + ' responded with: ' + JSON.stringify(data));
257 + data = null;
258 + }
259 +
260 + if (typeof callback === 'function') {
261 + return callback(data);
262 + }
263 + })
264 + .fail(function () {
265 + NETDATA.error(414, NETDATA.registry.server);
266 +
267 + if (typeof callback === 'function') {
268 + return callback(null);
269 + }
270 + });
271 + }
272 +};
web/gui/src/dashboard.js/units-conversion.js new
+402
@@ -0,0 +1,402 @@
1 +NETDATA.unitsConversion = {
2 + keys: {}, // keys for data-common-units
3 + latest: {}, // latest selected units for data-common-units
4 +
5 + globalReset: function () {
6 + this.keys = {};
7 + this.latest = {};
8 + },
9 +
10 + scalableUnits: {
11 + 'packets/s': {
12 + 'pps': 1,
13 + 'Kpps': 1000,
14 + 'Mpps': 1000000
15 + },
16 + 'pps': {
17 + 'pps': 1,
18 + 'Kpps': 1000,
19 + 'Mpps': 1000000
20 + },
21 + 'kilobits/s': {
22 + 'bits/s': 1 / 1000,
23 + 'kilobits/s': 1,
24 + 'megabits/s': 1000,
25 + 'gigabits/s': 1000000,
26 + 'terabits/s': 1000000000
27 + },
28 + 'kilobytes/s': {
29 + 'bytes/s': 1 / 1024,
30 + 'kilobytes/s': 1,
31 + 'megabytes/s': 1024,
32 + 'gigabytes/s': 1024 * 1024,
33 + 'terabytes/s': 1024 * 1024 * 1024
34 + },
35 + 'KB/s': {
36 + 'B/s': 1 / 1024,
37 + 'KB/s': 1,
38 + 'MB/s': 1024,
39 + 'GB/s': 1024 * 1024,
40 + 'TB/s': 1024 * 1024 * 1024
41 + },
42 + 'KB': {
43 + 'B': 1 / 1024,
44 + 'KB': 1,
45 + 'MB': 1024,
46 + 'GB': 1024 * 1024,
47 + 'TB': 1024 * 1024 * 1024
48 + },
49 + 'MB': {
50 + 'B': 1 / (1024 * 1024),
51 + 'KB': 1 / 1024,
52 + 'MB': 1,
53 + 'GB': 1024,
54 + 'TB': 1024 * 1024,
55 + 'PB': 1024 * 1024 * 1024
56 + },
57 + 'GB': {
58 + 'B': 1 / (1024 * 1024 * 1024),
59 + 'KB': 1 / (1024 * 1024),
60 + 'MB': 1 / 1024,
61 + 'GB': 1,
62 + 'TB': 1024,
63 + 'PB': 1024 * 1024,
64 + 'EB': 1024 * 1024 * 1024
65 + }
66 + /*
67 + 'milliseconds': {
68 + 'seconds': 1000
69 + },
70 + 'seconds': {
71 + 'milliseconds': 0.001,
72 + 'seconds': 1,
73 + 'minutes': 60,
74 + 'hours': 3600,
75 + 'days': 86400
76 + }
77 + */
78 + },
79 +
80 + convertibleUnits: {
81 + 'Celsius': {
82 + 'Fahrenheit': {
83 + check: function (max) {
84 + void(max);
85 + return NETDATA.options.current.temperature === 'fahrenheit';
86 + },
87 + convert: function (value) {
88 + return value * 9 / 5 + 32;
89 + }
90 + }
91 + },
92 + 'celsius': {
93 + 'fahrenheit': {
94 + check: function (max) {
95 + void(max);
96 + return NETDATA.options.current.temperature === 'fahrenheit';
97 + },
98 + convert: function (value) {
99 + return value * 9 / 5 + 32;
100 + }
101 + }
102 + },
103 + 'seconds': {
104 + 'time': {
105 + check: function (max) {
106 + void(max);
107 + return NETDATA.options.current.seconds_as_time;
108 + },
109 + convert: function (seconds) {
110 + return NETDATA.unitsConversion.seconds2time(seconds);
111 + }
112 + }
113 + },
114 + 'milliseconds': {
115 + 'milliseconds': {
116 + check: function (max) {
117 + return NETDATA.options.current.seconds_as_time && max < 1000;
118 + },
119 + convert: function (milliseconds) {
120 + let tms = Math.round(milliseconds * 10);
121 + milliseconds = Math.floor(tms / 10);
122 +
123 + tms -= milliseconds * 10;
124 +
125 + return (milliseconds).toString() + '.' + tms.toString();
126 + }
127 + },
128 + 'seconds': {
129 + check: function (max) {
130 + return NETDATA.options.current.seconds_as_time && max >= 1000 && max < 60000;
131 + },
132 + convert: function (milliseconds) {
133 + milliseconds = Math.round(milliseconds);
134 +
135 + let seconds = Math.floor(milliseconds / 1000);
136 + milliseconds -= seconds * 1000;
137 +
138 + milliseconds = Math.round(milliseconds / 10);
139 +
140 + return seconds.toString() + '.'
141 + + NETDATA.zeropad(milliseconds);
142 + }
143 + },
144 + 'M:SS.ms': {
145 + check: function (max) {
146 + return NETDATA.options.current.seconds_as_time && max >= 60000;
147 + },
148 + convert: function (milliseconds) {
149 + milliseconds = Math.round(milliseconds);
150 +
151 + let minutes = Math.floor(milliseconds / 60000);
152 + milliseconds -= minutes * 60000;
153 +
154 + let seconds = Math.floor(milliseconds / 1000);
155 + milliseconds -= seconds * 1000;
156 +
157 + milliseconds = Math.round(milliseconds / 10);
158 +
159 + return minutes.toString() + ':'
160 + + NETDATA.zeropad(seconds) + '.'
161 + + NETDATA.zeropad(milliseconds);
162 + }
163 + }
164 + }
165 + },
166 +
167 + seconds2time: function (seconds) {
168 + seconds = Math.abs(seconds);
169 +
170 + let days = Math.floor(seconds / 86400);
171 + seconds -= days * 86400;
172 +
173 + let hours = Math.floor(seconds / 3600);
174 + seconds -= hours * 3600;
175 +
176 + let minutes = Math.floor(seconds / 60);
177 + seconds -= minutes * 60;
178 +
179 + seconds = Math.round(seconds);
180 +
181 + let ms_txt = '';
182 + /*
183 + let ms = seconds - Math.floor(seconds);
184 + seconds -= ms;
185 + ms = Math.round(ms * 1000);
186 +
187 + if (ms > 1) {
188 + if (ms < 10)
189 + ms_txt = '.00' + ms.toString();
190 + else if (ms < 100)
191 + ms_txt = '.0' + ms.toString();
192 + else
193 + ms_txt = '.' + ms.toString();
194 + }
195 + */
196 +
197 + return ((days > 0) ? days.toString() + 'd:' : '').toString()
198 + + NETDATA.zeropad(hours) + ':'
199 + + NETDATA.zeropad(minutes) + ':'
200 + + NETDATA.zeropad(seconds)
201 + + ms_txt;
202 + },
203 +
204 + // get a function that converts the units
205 + // + every time units are switched call the callback
206 + get: function (uuid, min, max, units, desired_units, common_units_name, switch_units_callback) {
207 + // validate the parameters
208 + if (typeof units === 'undefined') {
209 + units = 'undefined';
210 + }
211 +
212 + // check if we support units conversion
213 + if (typeof this.scalableUnits[units] === 'undefined' && typeof this.convertibleUnits[units] === 'undefined') {
214 + // we can't convert these units
215 + //console.log('DEBUG: ' + uuid.toString() + ' can\'t convert units: ' + units.toString());
216 + return function (value) {
217 + return value;
218 + };
219 + }
220 +
221 + // check if the caller wants the original units
222 + if (typeof desired_units === 'undefined' || desired_units === null || desired_units === 'original' || desired_units === units) {
223 + //console.log('DEBUG: ' + uuid.toString() + ' original units wanted');
224 + switch_units_callback(units);
225 + return function (value) {
226 + return value;
227 + };
228 + }
229 +
230 + // now we know we can convert the units
231 + // and the caller wants some kind of conversion
232 +
233 + let tunits = null;
234 + let tdivider = 0;
235 +
236 + if (typeof this.scalableUnits[units] !== 'undefined') {
237 + // units that can be scaled
238 + // we decide a divider
239 +
240 + // console.log('NETDATA.unitsConversion.get(' + units.toString() + ', ' + desired_units.toString() + ', function()) decide divider with min = ' + min.toString() + ', max = ' + max.toString());
241 +
242 + if (desired_units === 'auto') {
243 + // the caller wants to auto-scale the units
244 +
245 + // find the absolute maximum value that is rendered on the chart
246 + // based on this we decide the scale
247 + min = Math.abs(min);
248 + max = Math.abs(max);
249 + if (min > max) {
250 + max = min;
251 + }
252 +
253 + // find the smallest scale that provides integers
254 + // for (x in this.scalableUnits[units]) {
255 + // if (this.scalableUnits[units].hasOwnProperty(x)) {
256 + // let m = this.scalableUnits[units][x];
257 + // if (m <= max && m > tdivider) {
258 + // tunits = x;
259 + // tdivider = m;
260 + // }
261 + // }
262 + // }
263 + const sunit = this.scalableUnits[units];
264 + for (const x of Object.keys(sunit)) {
265 + let m = sunit[x];
266 + if (m <= max && m > tdivider) {
267 + tunits = x;
268 + tdivider = m;
269 + }
270 + }
271 +
272 + if (tunits === null || tdivider <= 0) {
273 + // we couldn't find one
274 + //console.log('DEBUG: ' + uuid.toString() + ' cannot find an auto-scaling candidate for units: ' + units.toString() + ' (max: ' + max.toString() + ')');
275 + switch_units_callback(units);
276 + return function (value) {
277 + return value;
278 + };
279 + }
280 +
281 + if (typeof common_units_name === 'string' && typeof uuid === 'string') {
282 + // the caller wants several charts to have the same units
283 + // data-common-units
284 +
285 + let common_units_key = common_units_name + '-' + units;
286 +
287 + // add our divider into the list of keys
288 + let t = this.keys[common_units_key];
289 + if (typeof t === 'undefined') {
290 + this.keys[common_units_key] = {};
291 + t = this.keys[common_units_key];
292 + }
293 + t[uuid] = {
294 + units: tunits,
295 + divider: tdivider
296 + };
297 +
298 + // find the max divider of all charts
299 + let common_units = t[uuid];
300 + for (const x in t) {
301 + if (t.hasOwnProperty(x) && t[x].divider > common_units.divider) {
302 + common_units = t[x];
303 + }
304 + }
305 +
306 + // save our common_max to the latest keys
307 + let latest = this.latest[common_units_key];
308 + if (typeof latest === 'undefined') {
309 + this.latest[common_units_key] = {};
310 + latest = this.latest[common_units_key];
311 + }
312 + latest.units = common_units.units;
313 + latest.divider = common_units.divider;
314 +
315 + tunits = latest.units;
316 + tdivider = latest.divider;
317 +
318 + //console.log('DEBUG: ' + uuid.toString() + ' converted units: ' + units.toString() + ' to units: ' + tunits.toString() + ' with divider ' + tdivider.toString() + ', common-units=' + common_units_name.toString() + ((t[uuid].divider !== tdivider)?' USED COMMON, mine was ' + t[uuid].units:' set common').toString());
319 +
320 + // apply it to this chart
321 + switch_units_callback(tunits);
322 + return function (value) {
323 + if (tdivider !== latest.divider) {
324 + // another chart switched our common units
325 + // we should switch them too
326 + //console.log('DEBUG: ' + uuid + ' switching units due to a common-units change, from ' + tunits.toString() + ' to ' + latest.units.toString());
327 + tunits = latest.units;
328 + tdivider = latest.divider;
329 + switch_units_callback(tunits);
330 + }
331 +
332 + return value / tdivider;
333 + };
334 + } else {
335 + // the caller did not give data-common-units
336 + // this chart auto-scales independently of all others
337 + //console.log('DEBUG: ' + uuid.toString() + ' converted units: ' + units.toString() + ' to units: ' + tunits.toString() + ' with divider ' + tdivider.toString() + ', autonomously');
338 +
339 + switch_units_callback(tunits);
340 + return function (value) {
341 + return value / tdivider;
342 + };
343 + }
344 + } else {
345 + // the caller wants specific units
346 +
347 + if (typeof this.scalableUnits[units][desired_units] !== 'undefined') {
348 + // all good, set the new units
349 + tdivider = this.scalableUnits[units][desired_units];
350 + // console.log('DEBUG: ' + uuid.toString() + ' converted units: ' + units.toString() + ' to units: ' + desired_units.toString() + ' with divider ' + tdivider.toString() + ', by reference');
351 + switch_units_callback(desired_units);
352 + return function (value) {
353 + return value / tdivider;
354 + };
355 + } else {
356 + // oops! switch back to original units
357 + console.log('Units conversion from ' + units.toString() + ' to ' + desired_units.toString() + ' is not supported.');
358 + switch_units_callback(units);
359 + return function (value) {
360 + return value;
361 + };
362 + }
363 + }
364 + } else if (typeof this.convertibleUnits[units] !== 'undefined') {
365 + // units that can be converted
366 + if (desired_units === 'auto') {
367 + for (const x in this.convertibleUnits[units]) {
368 + if (this.convertibleUnits[units].hasOwnProperty(x)) {
369 + if (this.convertibleUnits[units][x].check(max)) {
370 + //console.log('DEBUG: ' + uuid.toString() + ' converting ' + units.toString() + ' to: ' + x.toString());
371 + switch_units_callback(x);
372 + return this.convertibleUnits[units][x].convert;
373 + }
374 + }
375 + }
376 +
377 + // none checked ok
378 + //console.log('DEBUG: ' + uuid.toString() + ' no conversion available for ' + units.toString() + ' to: ' + desired_units.toString());
379 + switch_units_callback(units);
380 + return function (value) {
381 + return value;
382 + };
383 + } else if (typeof this.convertibleUnits[units][desired_units] !== 'undefined') {
384 + switch_units_callback(desired_units);
385 + return this.convertibleUnits[units][desired_units].convert;
386 + } else {
387 + console.log('Units conversion from ' + units.toString() + ' to ' + desired_units.toString() + ' is not supported.');
388 + switch_units_callback(units);
389 + return function (value) {
390 + return value;
391 + };
392 + }
393 + } else {
394 + // hm... did we forget to implement the new type?
395 + console.log(`Unmatched unit conversion method for units ${units.toString()}`);
396 + switch_units_callback(units);
397 + return function (value) {
398 + return value;
399 + };
400 + }
401 + }
402 +};
web/gui/src/dashboard.js/utils.js new
+396
@@ -0,0 +1,396 @@
1 +NETDATA.encodeURIComponent = function (s) {
2 + if (typeof(s) === 'string') {
3 + return encodeURIComponent(s);
4 + }
5 +
6 + return s;
7 +};
8 +
9 +/// A heuristic for detecting slow devices.
10 +let isSlowDeviceResult = undefined;
11 +const isSlowDevice = function () {
12 + if (!isSlowDeviceResult) {
13 + return isSlowDeviceResult;
14 + }
15 +
16 + try {
17 + let ua = navigator.userAgent.toLowerCase();
18 +
19 + let iOS = /ipad|iphone|ipod/.test(ua) && !window.MSStream;
20 + let android = /android/.test(ua) && !window.MSStream;
21 + isSlowDeviceResult = (iOS || android);
22 + } catch (e) {
23 + isSlowDeviceResult = false;
24 + }
25 +
26 + return isSlowDeviceResult;
27 +};
28 +
29 +NETDATA.guid = function () {
30 + function s4() {
31 + return Math.floor((1 + Math.random()) * 0x10000)
32 + .toString(16)
33 + .substring(1);
34 + }
35 +
36 + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
37 +};
38 +
39 +NETDATA.zeropad = function (x) {
40 + if (x > -10 && x < 10) {
41 + return '0' + x.toString();
42 + } else {
43 + return x.toString();
44 + }
45 +};
46 +
47 +NETDATA.seconds4human = function (seconds, options) {
48 + let default_options = {
49 + now: 'now',
50 + space: ' ',
51 + negative_suffix: 'ago',
52 + day: 'day',
53 + days: 'days',
54 + hour: 'hour',
55 + hours: 'hours',
56 + minute: 'min',
57 + minutes: 'mins',
58 + second: 'sec',
59 + seconds: 'secs',
60 + and: 'and'
61 + };
62 +
63 + if (typeof options !== 'object') {
64 + options = default_options;
65 + } else {
66 + for (const x in default_options) {
67 + if (typeof options[x] !== 'string') {
68 + options[x] = default_options[x];
69 + }
70 + }
71 + }
72 +
73 + if (typeof seconds === 'string') {
74 + seconds = parseInt(seconds, 10);
75 + }
76 +
77 + if (seconds === 0) {
78 + return options.now;
79 + }
80 +
81 + let suffix = '';
82 + if (seconds < 0) {
83 + seconds = -seconds;
84 + if (options.negative_suffix !== '') {
85 + suffix = options.space + options.negative_suffix;
86 + }
87 + }
88 +
89 + let days = Math.floor(seconds / 86400);
90 + seconds -= (days * 86400);
91 +
92 + let hours = Math.floor(seconds / 3600);
93 + seconds -= (hours * 3600);
94 +
95 + let minutes = Math.floor(seconds / 60);
96 + seconds -= (minutes * 60);
97 +
98 + let strings = [];
99 +
100 + if (days > 1) {
101 + strings.push(days.toString() + options.space + options.days);
102 + } else if (days === 1) {
103 + strings.push(days.toString() + options.space + options.day);
104 + }
105 +
106 + if (hours > 1) {
107 + strings.push(hours.toString() + options.space + options.hours);
108 + } else if (hours === 1) {
109 + strings.push(hours.toString() + options.space + options.hour);
110 + }
111 +
112 + if (minutes > 1) {
113 + strings.push(minutes.toString() + options.space + options.minutes);
114 + } else if (minutes === 1) {
115 + strings.push(minutes.toString() + options.space + options.minute);
116 + }
117 +
118 + if (seconds > 1) {
119 + strings.push(Math.floor(seconds).toString() + options.space + options.seconds);
120 + } else if (seconds === 1) {
121 + strings.push(Math.floor(seconds).toString() + options.space + options.second);
122 + }
123 +
124 + if (strings.length === 1) {
125 + return strings.pop() + suffix;
126 + }
127 +
128 + let last = strings.pop();
129 + return strings.join(", ") + " " + options.and + " " + last + suffix;
130 +};
131 +
132 +// ----------------------------------------------------------------------------------------------------------------
133 +// element data attributes
134 +
135 +NETDATA.dataAttribute = function (element, attribute, def) {
136 + let key = 'data-' + attribute.toString();
137 + if (element.hasAttribute(key)) {
138 + let data = element.getAttribute(key);
139 +
140 + if (data === 'true') {
141 + return true;
142 + }
143 + if (data === 'false') {
144 + return false;
145 + }
146 + if (data === 'null') {
147 + return null;
148 + }
149 +
150 + // Only convert to a number if it doesn't change the string
151 + if (data === +data + '') {
152 + return +data;
153 + }
154 +
155 + if (/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/.test(data)) {
156 + return JSON.parse(data);
157 + }
158 +
159 + return data;
160 + } else {
161 + return def;
162 + }
163 +};
164 +
165 +NETDATA.dataAttributeBoolean = function (element, attribute, def) {
166 + let value = NETDATA.dataAttribute(element, attribute, def);
167 +
168 + if (value === true || value === false) // gmosx: Love this :)
169 + {
170 + return value;
171 + }
172 +
173 + if (typeof(value) === 'string') {
174 + if (value === 'yes' || value === 'on') {
175 + return true;
176 + }
177 +
178 + if (value === '' || value === 'no' || value === 'off' || value === 'null') {
179 + return false;
180 + }
181 +
182 + return def;
183 + }
184 +
185 + if (typeof(value) === 'number') {
186 + return value !== 0;
187 + }
188 +
189 + return def;
190 +};
191 +
192 +// ----------------------------------------------------------------------------------------------------------------
193 +// fast numbers formatting
194 +
195 +NETDATA.fastNumberFormat = {
196 + formatters_fixed: [],
197 + formatters_zero_based: [],
198 +
199 + // this is the fastest and the preferred
200 + getIntlNumberFormat: function (min, max) {
201 + let key = max;
202 + if (min === max) {
203 + if (typeof this.formatters_fixed[key] === 'undefined') {
204 + this.formatters_fixed[key] = new Intl.NumberFormat(undefined, {
205 + // style: 'decimal',
206 + // minimumIntegerDigits: 1,
207 + // minimumSignificantDigits: 1,
208 + // maximumSignificantDigits: 1,
209 + useGrouping: true,
210 + minimumFractionDigits: min,
211 + maximumFractionDigits: max
212 + });
213 + }
214 +
215 + return this.formatters_fixed[key];
216 + } else if (min === 0) {
217 + if (typeof this.formatters_zero_based[key] === 'undefined') {
218 + this.formatters_zero_based[key] = new Intl.NumberFormat(undefined, {
219 + // style: 'decimal',
220 + // minimumIntegerDigits: 1,
221 + // minimumSignificantDigits: 1,
222 + // maximumSignificantDigits: 1,
223 + useGrouping: true,
224 + minimumFractionDigits: min,
225 + maximumFractionDigits: max
226 + });
227 + }
228 +
229 + return this.formatters_zero_based[key];
230 + } else {
231 + // this is never used
232 + // it is added just for completeness
233 + return new Intl.NumberFormat(undefined, {
234 + // style: 'decimal',
235 + // minimumIntegerDigits: 1,
236 + // minimumSignificantDigits: 1,
237 + // maximumSignificantDigits: 1,
238 + useGrouping: true,
239 + minimumFractionDigits: min,
240 + maximumFractionDigits: max
241 + });
242 + }
243 + },
244 +
245 + // this respects locale
246 + getLocaleString: function (min, max) {
247 + let key = max;
248 + if (min === max) {
249 + if (typeof this.formatters_fixed[key] === 'undefined') {
250 + this.formatters_fixed[key] = {
251 + format: function (value) {
252 + return value.toLocaleString(undefined, {
253 + // style: 'decimal',
254 + // minimumIntegerDigits: 1,
255 + // minimumSignificantDigits: 1,
256 + // maximumSignificantDigits: 1,
257 + useGrouping: true,
258 + minimumFractionDigits: min,
259 + maximumFractionDigits: max
260 + });
261 + }
262 + };
263 + }
264 +
265 + return this.formatters_fixed[key];
266 + } else if (min === 0) {
267 + if (typeof this.formatters_zero_based[key] === 'undefined') {
268 + this.formatters_zero_based[key] = {
269 + format: function (value) {
270 + return value.toLocaleString(undefined, {
271 + // style: 'decimal',
272 + // minimumIntegerDigits: 1,
273 + // minimumSignificantDigits: 1,
274 + // maximumSignificantDigits: 1,
275 + useGrouping: true,
276 + minimumFractionDigits: min,
277 + maximumFractionDigits: max
278 + });
279 + }
280 + };
281 + }
282 +
283 + return this.formatters_zero_based[key];
284 + } else {
285 + return {
286 + format: function (value) {
287 + return value.toLocaleString(undefined, {
288 + // style: 'decimal',
289 + // minimumIntegerDigits: 1,
290 + // minimumSignificantDigits: 1,
291 + // maximumSignificantDigits: 1,
292 + useGrouping: true,
293 + minimumFractionDigits: min,
294 + maximumFractionDigits: max
295 + });
296 + }
297 + };
298 + }
299 + },
300 +
301 + // the fallback
302 + getFixed: function (min, max) {
303 + let key = max;
304 + if (min === max) {
305 + if (typeof this.formatters_fixed[key] === 'undefined') {
306 + this.formatters_fixed[key] = {
307 + format: function (value) {
308 + if (value === 0) {
309 + return "0";
310 + }
311 + return value.toFixed(max);
312 + }
313 + };
314 + }
315 +
316 + return this.formatters_fixed[key];
317 + } else if (min === 0) {
318 + if (typeof this.formatters_zero_based[key] === 'undefined') {
319 + this.formatters_zero_based[key] = {
320 + format: function (value) {
321 + if (value === 0) {
322 + return "0";
323 + }
324 + return value.toFixed(max);
325 + }
326 + };
327 + }
328 +
329 + return this.formatters_zero_based[key];
330 + } else {
331 + return {
332 + format: function (value) {
333 + if (value === 0) {
334 + return "0";
335 + }
336 + return value.toFixed(max);
337 + }
338 + };
339 + }
340 + },
341 +
342 + testIntlNumberFormat: function () {
343 + let value = 1.12345;
344 + let e1 = "1.12", e2 = "1,12";
345 + let s = "";
346 +
347 + try {
348 + let x = new Intl.NumberFormat(undefined, {
349 + useGrouping: true,
350 + minimumFractionDigits: 2,
351 + maximumFractionDigits: 2
352 + });
353 +
354 + s = x.format(value);
355 + } catch (e) {
356 + s = "";
357 + }
358 +
359 + // console.log('NumberFormat: ', s);
360 + return (s === e1 || s === e2);
361 + },
362 +
363 + testLocaleString: function () {
364 + let value = 1.12345;
365 + let e1 = "1.12", e2 = "1,12";
366 + let s = "";
367 +
368 + try {
369 + s = value.toLocaleString(undefined, {
370 + useGrouping: true,
371 + minimumFractionDigits: 2,
372 + maximumFractionDigits: 2
373 + });
374 + } catch (e) {
375 + s = "";
376 + }
377 +
378 + // console.log('localeString: ', s);
379 + return (s === e1 || s === e2);
380 + },
381 +
382 + // on first run we decide which formatter to use
383 + get: function (min, max) {
384 + if (this.testIntlNumberFormat()) {
385 + // console.log('numberformat');
386 + this.get = this.getIntlNumberFormat;
387 + } else if (this.testLocaleString()) {
388 + // console.log('localestring');
389 + this.get = this.getLocaleString;
390 + } else {
391 + // console.log('fixed');
392 + this.get = this.getFixed;
393 + }
394 + return this.get(min, max);
395 + }
396 +};
web/gui/src/dashboard.js/xss.js new
+84
@@ -0,0 +1,84 @@
1 +// ----------------------------------------------------------------------------------------------------------------
2 +// XSS checks
3 +
4 +NETDATA.xss = {
5 + enabled: (typeof netdataCheckXSS === 'undefined') ? false : netdataCheckXSS,
6 + enabled_for_data: (typeof netdataCheckXSS === 'undefined') ? false : netdataCheckXSS,
7 +
8 + string: function (s) {
9 + return s.toString()
10 + .replace(/</g, '&lt;')
11 + .replace(/>/g, '&gt;')
12 + .replace(/"/g, '&quot;')
13 + .replace(/'/g, '&#39;');
14 + },
15 +
16 + object: function (name, obj, ignore_regex) {
17 + if (typeof ignore_regex !== 'undefined' && ignore_regex.test(name)) {
18 + // console.log('XSS: ignoring "' + name + '"');
19 + return obj;
20 + }
21 +
22 + switch (typeof(obj)) {
23 + case 'string':
24 + const ret = this.string(obj);
25 + if (ret !== obj) {
26 + console.log('XSS protection changed string ' + name + ' from "' + obj + '" to "' + ret + '"');
27 + }
28 + return ret;
29 +
30 + case 'object':
31 + if (obj === null) {
32 + return obj;
33 + }
34 +
35 + if (Array.isArray(obj)) {
36 + // console.log('checking array "' + name + '"');
37 +
38 + let len = obj.length;
39 + while (len--) {
40 + obj[len] = this.object(name + '[' + len + ']', obj[len], ignore_regex);
41 + }
42 + } else {
43 + // console.log('checking object "' + name + '"');
44 +
45 + for (const i in obj) {
46 + if (obj.hasOwnProperty(i) === false) {
47 + continue;
48 + }
49 + if (this.string(i) !== i) {
50 + console.log('XSS protection removed invalid object member "' + name + '.' + i + '"');
51 + delete obj[i];
52 + } else {
53 + obj[i] = this.object(name + '.' + i, obj[i], ignore_regex);
54 + }
55 + }
56 + }
57 + return obj;
58 +
59 + default:
60 + return obj;
61 + }
62 + },
63 +
64 + checkOptional: function (name, obj, ignore_regex) {
65 + if (this.enabled) {
66 + //console.log('XSS: checking optional "' + name + '"...');
67 + return this.object(name, obj, ignore_regex);
68 + }
69 + return obj;
70 + },
71 +
72 + checkAlways: function (name, obj, ignore_regex) {
73 + //console.log('XSS: checking always "' + name + '"...');
74 + return this.object(name, obj, ignore_regex);
75 + },
76 +
77 + checkData: function (name, obj, ignore_regex) {
78 + if (this.enabled_for_data) {
79 + //console.log('XSS: checking data "' + name + '"...');
80 + return this.object(name, obj, ignore_regex);
81 + }
82 + return obj;
83 + }
84 +};