From be1c7e50e1e8809ea56f2c9d472eccd8ffd73a97 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 04:57:58 +0200 Subject: Adding upstream version 1.44.3. Signed-off-by: Daniel Baumann --- web/gui/src/dashboard.js/alarms.js | 422 ++ web/gui/src/dashboard.js/boot.js | 142 + web/gui/src/dashboard.js/chart-registry.js | 94 + web/gui/src/dashboard.js/charting.js | 492 +++ web/gui/src/dashboard.js/charting/_c3.js | 114 + web/gui/src/dashboard.js/charting/_morris.js | 81 + web/gui/src/dashboard.js/charting/_raphael.js | 48 + web/gui/src/dashboard.js/charting/d3.js | 43 + web/gui/src/dashboard.js/charting/d3pie.js | 341 ++ web/gui/src/dashboard.js/charting/dygraph.js | 1065 +++++ .../src/dashboard.js/charting/easy-pie-chart.js | 281 ++ web/gui/src/dashboard.js/charting/gauge.js | 406 ++ web/gui/src/dashboard.js/charting/google-charts.js | 132 + web/gui/src/dashboard.js/charting/peity.js | 62 + web/gui/src/dashboard.js/charting/sparkline.js | 155 + web/gui/src/dashboard.js/charting/textonly.js | 18 + web/gui/src/dashboard.js/colors.js | 34 + web/gui/src/dashboard.js/common.js | 249 ++ web/gui/src/dashboard.js/compatibility.js | 31 + web/gui/src/dashboard.js/dependencies.js | 21 + web/gui/src/dashboard.js/epilogue.js.inc | 1 + web/gui/src/dashboard.js/error-handling.js | 52 + web/gui/src/dashboard.js/localstorage.js | 173 + web/gui/src/dashboard.js/main.js | 4279 ++++++++++++++++++++ web/gui/src/dashboard.js/options.js | 225 + web/gui/src/dashboard.js/prologue.js.inc | 84 + web/gui/src/dashboard.js/registry.js | 311 ++ web/gui/src/dashboard.js/server-detection.js | 29 + web/gui/src/dashboard.js/themes.js | 92 + web/gui/src/dashboard.js/timeout.js | 100 + web/gui/src/dashboard.js/units-conversion.js | 528 +++ web/gui/src/dashboard.js/utils.js | 432 ++ web/gui/src/dashboard.js/xss.js | 84 + 33 files changed, 10621 insertions(+) create mode 100644 web/gui/src/dashboard.js/alarms.js create mode 100644 web/gui/src/dashboard.js/boot.js create mode 100644 web/gui/src/dashboard.js/chart-registry.js create mode 100644 web/gui/src/dashboard.js/charting.js create mode 100644 web/gui/src/dashboard.js/charting/_c3.js create mode 100644 web/gui/src/dashboard.js/charting/_morris.js create mode 100644 web/gui/src/dashboard.js/charting/_raphael.js create mode 100644 web/gui/src/dashboard.js/charting/d3.js create mode 100644 web/gui/src/dashboard.js/charting/d3pie.js create mode 100644 web/gui/src/dashboard.js/charting/dygraph.js create mode 100644 web/gui/src/dashboard.js/charting/easy-pie-chart.js create mode 100644 web/gui/src/dashboard.js/charting/gauge.js create mode 100644 web/gui/src/dashboard.js/charting/google-charts.js create mode 100644 web/gui/src/dashboard.js/charting/peity.js create mode 100644 web/gui/src/dashboard.js/charting/sparkline.js create mode 100644 web/gui/src/dashboard.js/charting/textonly.js create mode 100644 web/gui/src/dashboard.js/colors.js create mode 100644 web/gui/src/dashboard.js/common.js create mode 100644 web/gui/src/dashboard.js/compatibility.js create mode 100644 web/gui/src/dashboard.js/dependencies.js create mode 100644 web/gui/src/dashboard.js/epilogue.js.inc create mode 100644 web/gui/src/dashboard.js/error-handling.js create mode 100644 web/gui/src/dashboard.js/localstorage.js create mode 100644 web/gui/src/dashboard.js/main.js create mode 100644 web/gui/src/dashboard.js/options.js create mode 100644 web/gui/src/dashboard.js/prologue.js.inc create mode 100644 web/gui/src/dashboard.js/registry.js create mode 100644 web/gui/src/dashboard.js/server-detection.js create mode 100644 web/gui/src/dashboard.js/themes.js create mode 100644 web/gui/src/dashboard.js/timeout.js create mode 100644 web/gui/src/dashboard.js/units-conversion.js create mode 100644 web/gui/src/dashboard.js/utils.js create mode 100644 web/gui/src/dashboard.js/xss.js (limited to 'web/gui/src') diff --git a/web/gui/src/dashboard.js/alarms.js b/web/gui/src/dashboard.js/alarms.js new file mode 100644 index 00000000..82477671 --- /dev/null +++ b/web/gui/src/dashboard.js/alarms.js @@ -0,0 +1,422 @@ + +// Registry of netdata hosts + +NETDATA.alarms = { + onclick: null, // the callback to handle the click - it will be called with the alarm log entry + chart_div_offset: -50, // give that space above the chart when scrolling to it + chart_div_id_prefix: 'chart_', // the chart DIV IDs have this prefix (they should be NETDATA.name2id(chart.id)) + chart_div_animation_duration: 0,// the duration of the animation while scrolling to a chart + + ms_penalty: 0, // the time penalty of the next alarm + ms_between_notifications: 500, // firefox moves the alarms off-screen (above, outside the top of the screen) + // if alarms are shown faster than: one per 500ms + + update_every: 10000, // the time in ms between alarm checks + + notifications: false, // when true, the browser supports notifications (may not be granted though) + last_notification_id: 0, // the id of the last alarm_log we have raised an alarm for + first_notification_id: 0, // the id of the first alarm_log entry for this session + // this is used to prevent CLEAR notifications for past events + // notifications_shown: [], + + server: null, // the server to connect to for fetching alarms + current: null, // the list of raised alarms - updated in the background + + // a callback function to call every time the list of raised alarms is refreshed + callback: (typeof netdataAlarmsActiveCallback === 'function') ? netdataAlarmsActiveCallback : null, + + // a callback function to call every time a notification is shown + // the return value is used to decide if the notification will be shown + notificationCallback: (typeof netdataAlarmsNotifCallback === 'function') ? netdataAlarmsNotifCallback : null, + + recipients: null, // the list (array) of recipients to show alarms for, or null + + recipientMatches: function (to_string, wanted_array) { + if (typeof wanted_array === 'undefined' || wanted_array === null || Array.isArray(wanted_array) === false) { + return true; + } + + let r = ' ' + to_string.toString() + ' '; + let len = wanted_array.length; + while (len--) { + if (r.indexOf(' ' + wanted_array[len] + ' ') >= 0) { + return true; + } + } + + return false; + }, + + activeForRecipients: function () { + let active = {}; + let data = NETDATA.alarms.current; + + if (typeof data === 'undefined' || data === null) { + return active; + } + + for (let x in data.alarms) { + if (!data.alarms.hasOwnProperty(x)) { + continue; + } + + let alarm = data.alarms[x]; + if ((alarm.status === 'WARNING' || alarm.status === 'CRITICAL') && NETDATA.alarms.recipientMatches(alarm.recipient, NETDATA.alarms.recipients)) { + active[x] = alarm; + } + } + + return active; + }, + + notify: function (entry) { + // console.log('alarm ' + entry.unique_id); + + if (entry.updated) { + // console.log('alarm ' + entry.unique_id + ' has been updated by another alarm'); + return; + } + + let value_string = entry.value_string; + + if (NETDATA.alarms.current !== null) { + // get the current value_string + let t = NETDATA.alarms.current.alarms[entry.chart + '.' + entry.name]; + if (typeof t !== 'undefined' && entry.status === t.status && typeof t.value_string !== 'undefined') { + value_string = t.value_string; + } + } + + let name = entry.name.replace(/_/g, ' '); + let status = entry.status.toLowerCase(); + let title = name + ' = ' + value_string.toString(); + let tag = entry.alarm_id; + let icon = 'images/banner-icon-144x144.png'; + let interaction = false; + let data = entry; + let show = true; + + // console.log('alarm ' + entry.unique_id + ' ' + entry.chart + '.' + entry.name + ' is ' + entry.status); + + switch (entry.status) { + case 'REMOVED': + show = false; + break; + + case 'UNDEFINED': + return; + + case 'UNINITIALIZED': + return; + + case 'CLEAR': + if (entry.unique_id < NETDATA.alarms.first_notification_id) { + // console.log('alarm ' + entry.unique_id + ' is not current'); + return; + } + if (entry.old_status === 'UNINITIALIZED' || entry.old_status === 'UNDEFINED') { + // console.log('alarm' + entry.unique_id + ' switch to CLEAR from ' + entry.old_status); + return; + } + if (entry.no_clear_notification) { + // console.log('alarm' + entry.unique_id + ' is CLEAR but has no_clear_notification flag'); + return; + } + title = name + ' back to normal (' + value_string.toString() + ')'; + icon = 'images/check-mark-2-128-green.png'; + interaction = false; + break; + + case 'WARNING': + if (entry.old_status === 'CRITICAL') { + status = 'demoted to ' + entry.status.toLowerCase(); + } + + icon = 'images/alert-128-orange.png'; + interaction = false; + break; + + case 'CRITICAL': + if (entry.old_status === 'WARNING') { + status = 'escalated to ' + entry.status.toLowerCase(); + } + + icon = 'images/alert-128-red.png'; + interaction = true; + break; + + default: + console.log('invalid alarm status ' + entry.status); + return; + } + + // filter recipients + if (show) { + show = NETDATA.alarms.recipientMatches(entry.recipient, NETDATA.alarms.recipients); + } + + /* + // cleanup old notifications with the same alarm_id as this one + // it does not seem to work on any web browser - so notifications cannot be removed + + let len = NETDATA.alarms.notifications_shown.length; + while (len--) { + let n = NETDATA.alarms.notifications_shown[len]; + if (n.data.alarm_id === entry.alarm_id) { + console.log('removing old alarm ' + n.data.unique_id); + + // close the notification + n.close.bind(n); + + // remove it from the array + NETDATA.alarms.notifications_shown.splice(len, 1); + len = NETDATA.alarms.notifications_shown.length; + } + } + */ + + if (show) { + if (typeof NETDATA.alarms.notificationCallback === 'function') { + show = NETDATA.alarms.notificationCallback(entry); + } + + if (show) { + setTimeout(function () { + // show this notification + // console.log('new notification: ' + title); + let n = new Notification(title, { + body: entry.hostname + ' - ' + entry.chart + ' (' + entry.family + ') - ' + status + ': ' + entry.info, + tag: tag, + requireInteraction: interaction, + icon: NETDATA.serverStatic + icon, + data: data + }); + + n.onclick = function (event) { + event.preventDefault(); + NETDATA.alarms.onclick(event.target.data); + }; + + // console.log(n); + // NETDATA.alarms.notifications_shown.push(n); + // console.log(entry); + }, NETDATA.alarms.ms_penalty); + + NETDATA.alarms.ms_penalty += NETDATA.alarms.ms_between_notifications; + } + } + }, + + scrollToChart: function (chart_id) { + if (typeof chart_id === 'string') { + let offset = $('#' + NETDATA.alarms.chart_div_id_prefix + NETDATA.name2id(chart_id)).offset(); + if (typeof offset !== 'undefined') { + $('html, body').animate({scrollTop: offset.top + NETDATA.alarms.chart_div_offset}, NETDATA.alarms.chart_div_animation_duration); + return true; + } + } + return false; + }, + + scrollToAlarm: function (alarm) { + if (typeof alarm === 'object') { + let ret = NETDATA.alarms.scrollToChart(alarm.chart); + + if (ret && NETDATA.options.page_is_visible === false) { + window.focus(); + } + // 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.'); + } + + }, + + notifyAll: function () { + // console.log('FETCHING ALARM LOG'); + NETDATA.alarms.get_log(NETDATA.alarms.last_notification_id, function (data) { + // console.log('ALARM LOG FETCHED'); + + if (data === null || typeof data !== 'object') { + console.log('invalid alarms log response'); + return; + } + + if (data.length === 0) { + console.log('received empty alarm log'); + return; + } + + // console.log('received alarm log of ' + data.length + ' entries, from ' + data[data.length - 1].unique_id.toString() + ' to ' + data[0].unique_id.toString()); + + data.sort(function (a, b) { + if (a.unique_id > b.unique_id) { + return -1; + } + if (a.unique_id < b.unique_id) { + return 1; + } + return 0; + }); + + NETDATA.alarms.ms_penalty = 0; + + let len = data.length; + while (len--) { + if (data[len].unique_id > NETDATA.alarms.last_notification_id) { + NETDATA.alarms.notify(data[len]); + } + //else + // console.log('ignoring alarm (older) with id ' + data[len].unique_id.toString()); + } + + NETDATA.alarms.last_notification_id = data[0].unique_id; + + if (typeof netdataAlarmsRemember === 'undefined' || netdataAlarmsRemember) { + NETDATA.localStorageSet('last_notification_id', NETDATA.alarms.last_notification_id, null); + } + // console.log('last notification id = ' + NETDATA.alarms.last_notification_id); + }) + }, + + check_notifications: function () { + // returns true if we should fire 1+ notifications + + if (NETDATA.alarms.notifications !== true) { + // console.log('web notifications are not available'); + return false; + } + + if (Notification.permission !== 'granted') { + // console.log('web notifications are not granted'); + return false; + } + + if (typeof NETDATA.alarms.current !== 'undefined' && typeof NETDATA.alarms.current.alarms === 'object') { + // console.log('can do alarms: old id = ' + NETDATA.alarms.last_notification_id + ' new id = ' + NETDATA.alarms.current.latest_alarm_log_unique_id); + + if (NETDATA.alarms.current.latest_alarm_log_unique_id > NETDATA.alarms.last_notification_id) { + // console.log('new alarms detected'); + return true; + } + //else console.log('no new alarms'); + } + // else console.log('cannot process alarms'); + + return false; + }, + + get: function (what, callback) { + $.ajax({ + url: NETDATA.alarms.server + '/api/v1/alarms?' + what.toString(), + async: true, + cache: false, + headers: { + 'Cache-Control': 'no-cache, no-store', + 'Pragma': 'no-cache' + }, + xhrFields: {withCredentials: true} // required for the cookie + }) + .done(function (data) { + data = NETDATA.xss.checkOptional('/api/v1/alarms', data /*, '.*\.(calc|calc_parsed|warn|warn_parsed|crit|crit_parsed)$' */); + + if (NETDATA.alarms.first_notification_id === 0 && typeof data.latest_alarm_log_unique_id === 'number') { + NETDATA.alarms.first_notification_id = data.latest_alarm_log_unique_id; + } + + if (typeof callback === 'function') { + return callback(data); + } + }) + .fail(function () { + NETDATA.error(415, NETDATA.alarms.server); + + if (typeof callback === 'function') { + return callback(null); + } + }); + }, + + update_forever: function () { + if (netdataShowAlarms !== true || netdataSnapshotData !== null) { + return; + } + + NETDATA.alarms.get('active', function (data) { + if (data !== null) { + NETDATA.alarms.current = data; + + if (NETDATA.alarms.check_notifications()) { + NETDATA.alarms.notifyAll(); + } + + if (typeof NETDATA.alarms.callback === 'function') { + NETDATA.alarms.callback(data); + } + + // Health monitoring is disabled on this netdata + if (data.status === false) { + return; + } + } + + setTimeout(NETDATA.alarms.update_forever, NETDATA.alarms.update_every); + }); + }, + + get_log: function (last_id, callback) { + // console.log('fetching all log after ' + last_id.toString()); + $.ajax({ + url: NETDATA.alarms.server + '/api/v1/alarm_log?after=' + last_id.toString(), + async: true, + cache: false, + headers: { + 'Cache-Control': 'no-cache, no-store', + 'Pragma': 'no-cache' + }, + xhrFields: {withCredentials: true} // required for the cookie + }) + .done(function (data) { + data = NETDATA.xss.checkOptional('/api/v1/alarm_log', data); + + if (typeof callback === 'function') { + return callback(data); + } + }) + .fail(function () { + NETDATA.error(416, NETDATA.alarms.server); + + if (typeof callback === 'function') { + return callback(null); + } + }); + }, + + init: function () { + NETDATA.alarms.server = NETDATA.fixHost(NETDATA.serverDefault); + + if (typeof netdataAlarmsRemember === 'undefined' || netdataAlarmsRemember) { + NETDATA.alarms.last_notification_id = + NETDATA.localStorageGet('last_notification_id', NETDATA.alarms.last_notification_id, null); + } + + if (NETDATA.alarms.onclick === null) { + NETDATA.alarms.onclick = NETDATA.alarms.scrollToAlarm; + } + + if (typeof netdataAlarmsRecipients !== 'undefined' && Array.isArray(netdataAlarmsRecipients)) { + NETDATA.alarms.recipients = netdataAlarmsRecipients; + } + + if (netdataShowAlarms) { + NETDATA.alarms.update_forever(); + + if ('Notification' in window) { + // console.log('notifications available'); + NETDATA.alarms.notifications = true; + + if (Notification.permission === 'default') { + Notification.requestPermission(); + } + } + } + } +}; diff --git a/web/gui/src/dashboard.js/boot.js b/web/gui/src/dashboard.js/boot.js new file mode 100644 index 00000000..c448213b --- /dev/null +++ b/web/gui/src/dashboard.js/boot.js @@ -0,0 +1,142 @@ + +// Load required JS libraries and CSS + +NETDATA.requiredJs = [ + { + url: NETDATA.serverStatic + 'lib/bootstrap-3.3.7.min.js', + async: false, + isAlreadyLoaded: function () { + // check if bootstrap is loaded + if (typeof $().emulateTransitionEnd === 'function') { + return true; + } else { + return typeof netdataNoBootstrap !== 'undefined' && netdataNoBootstrap; + } + } + }, + { + url: NETDATA.serverStatic + 'lib/fontawesome-all-5.0.1.min.js', + async: true, + isAlreadyLoaded: function () { + return typeof netdataNoFontAwesome !== 'undefined' && netdataNoFontAwesome; + } + }, + { + url: NETDATA.serverStatic + 'lib/perfect-scrollbar-0.6.15.min.js', + isAlreadyLoaded: function () { + return false; + } + } +]; + +NETDATA.requiredCSS = [ + { + url: NETDATA.themes.current.bootstrap_css, + isAlreadyLoaded: function () { + return typeof netdataNoBootstrap !== 'undefined' && netdataNoBootstrap; + } + }, + { + url: NETDATA.themes.current.dashboard_css, + isAlreadyLoaded: function () { + return false; + } + } +]; + +NETDATA.loadedRequiredJs = 0; +NETDATA.loadRequiredJs = function (index, callback) { + if (index >= NETDATA.requiredJs.length) { + if (typeof callback === 'function') { + return callback(); + } + return; + } + + if (NETDATA.requiredJs[index].isAlreadyLoaded()) { + NETDATA.loadedRequiredJs++; + NETDATA.loadRequiredJs(++index, callback); + return; + } + + if (NETDATA.options.debug.main_loop) { + console.log('loading ' + NETDATA.requiredJs[index].url); + } + + let async = true; + if (typeof NETDATA.requiredJs[index].async !== 'undefined' && NETDATA.requiredJs[index].async === false) { + async = false; + } + + $.ajax({ + url: NETDATA.requiredJs[index].url, + cache: true, + dataType: "script", + xhrFields: {withCredentials: true} // required for the cookie + }) + .done(function () { + if (NETDATA.options.debug.main_loop) { + console.log('loaded ' + NETDATA.requiredJs[index].url); + } + }) + .fail(function () { + alert('Cannot load required JS library: ' + NETDATA.requiredJs[index].url); + }) + .always(function () { + NETDATA.loadedRequiredJs++; + + // if (async === false) + if (!async) { + NETDATA.loadRequiredJs(++index, callback); + } + }); + + // if (async === true) + if (async) { + NETDATA.loadRequiredJs(++index, callback); + } +}; + +NETDATA.loadRequiredCSS = function (index) { + if (index >= NETDATA.requiredCSS.length) { + return; + } + + if (NETDATA.requiredCSS[index].isAlreadyLoaded()) { + NETDATA.loadRequiredCSS(++index); + return; + } + + if (NETDATA.options.debug.main_loop) { + console.log('loading ' + NETDATA.requiredCSS[index].url); + } + + NETDATA._loadCSS(NETDATA.requiredCSS[index].url); + NETDATA.loadRequiredCSS(++index); +}; + +// Boot it! + +if (typeof netdataPrepCallback === 'function') { + netdataPrepCallback(); +} + +NETDATA.errorReset(); +NETDATA.loadRequiredCSS(0); + +NETDATA._loadjQuery(function () { + NETDATA.loadRequiredJs(0, function () { + if (typeof $().emulateTransitionEnd !== 'function') { + // bootstrap is not available + NETDATA.options.current.show_help = false; + } + + if (typeof netdataDontStart === 'undefined' || !netdataDontStart) { + if (NETDATA.options.debug.main_loop) { + console.log('starting chart refresh thread'); + } + + NETDATA.start(); + } + }); +}); diff --git a/web/gui/src/dashboard.js/chart-registry.js b/web/gui/src/dashboard.js/chart-registry.js new file mode 100644 index 00000000..542f4e01 --- /dev/null +++ b/web/gui/src/dashboard.js/chart-registry.js @@ -0,0 +1,94 @@ + +// *** src/dashboard.js/chart-registry.js + +// Chart Registry + +// When multiple charts need the same chart, we avoid downloading it +// multiple times (and having it in browser memory multiple time) +// by using this registry. + +// Every time we download a chart definition, we save it here with .add() +// Then we try to get it back with .get(). If that fails, we download it. + +NETDATA.fixHost = function (host) { + while (host.slice(-1) === '/') { + host = host.substring(0, host.length - 1); + } + + return host; +}; + +NETDATA.chartRegistry = { + charts: {}, + + globalReset: function () { + this.charts = {}; + }, + + add: function (host, id, data) { + if (typeof this.charts[host] === 'undefined') { + this.charts[host] = {}; + } + + //console.log('added ' + host + '/' + id); + this.charts[host][id] = data; + }, + + get: function (host, id) { + if (typeof this.charts[host] === 'undefined') { + return null; + } + + if (typeof this.charts[host][id] === 'undefined') { + return null; + } + + //console.log('cached ' + host + '/' + id); + return this.charts[host][id]; + }, + + downloadAll: function (host, callback) { + host = NETDATA.fixHost(host); + + let self = this; + + function got_data(h, data, callback) { + if (data !== null) { + self.charts[h] = data.charts; + + // update the server timezone in our options + if (typeof data.timezone === 'string') { + NETDATA.options.server_timezone = data.timezone; + } + } else { + NETDATA.error(406, h + '/api/v1/charts'); + } + + if (typeof callback === 'function') { + callback(data); + } + } + + if (netdataSnapshotData !== null) { + got_data(host, netdataSnapshotData.charts, callback); + } else { + $.ajax({ + url: host + '/api/v1/charts', + async: true, + cache: false, + xhrFields: {withCredentials: true} // required for the cookie + }) + .done(function (data) { + data = NETDATA.xss.checkOptional('/api/v1/charts', data); + got_data(host, data, callback); + }) + .fail(function () { + NETDATA.error(405, host + '/api/v1/charts'); + + if (typeof callback === 'function') { + callback(null); + } + }); + } + } +}; diff --git a/web/gui/src/dashboard.js/charting.js b/web/gui/src/dashboard.js/charting.js new file mode 100644 index 00000000..1035ff06 --- /dev/null +++ b/web/gui/src/dashboard.js/charting.js @@ -0,0 +1,492 @@ + +// Charts Libraries Registration + +NETDATA.chartLibraries = { + "dygraph": { + initialize: NETDATA.dygraphInitialize, + create: NETDATA.dygraphChartCreate, + update: NETDATA.dygraphChartUpdate, + resize: function (state) { + if (typeof state.tmp.dygraph_instance !== 'undefined' && typeof state.tmp.dygraph_instance.resize === 'function') { + state.tmp.dygraph_instance.resize(); + } + }, + setSelection: NETDATA.dygraphSetSelection, + clearSelection: NETDATA.dygraphClearSelection, + toolboxPanAndZoom: NETDATA.dygraphToolboxPanAndZoom, + initialized: false, + enabled: true, + xssRegexIgnore: new RegExp('^/api/v1/data\.result.data$'), + format: function (state) { + void(state); + return 'json'; + }, + options: function (state) { + return 'ms' + '%7C' + 'flip' + (this.isLogScale(state) ? ('%7C' + 'abs') : '').toString(); + }, + legend: function (state) { + return (this.isSparkline(state) === false && NETDATA.dataAttributeBoolean(state.element, 'legend', true) === true) ? 'right-side' : null; + }, + autoresize: function (state) { + void(state); + return true; + }, + max_updates_to_recreate: function (state) { + void(state); + return 5000; + }, + track_colors: function (state) { + void(state); + return true; + }, + pixels_per_point: function (state) { + return (this.isSparkline(state) === false) ? 3 : 2; + }, + isSparkline: function (state) { + if (typeof state.tmp.dygraph_sparkline === 'undefined') { + state.tmp.dygraph_sparkline = (this.theme(state) === 'sparkline'); + } + return state.tmp.dygraph_sparkline; + }, + isLogScale: function (state) { + if (typeof state.tmp.dygraph_logscale === 'undefined') { + state.tmp.dygraph_logscale = (this.theme(state) === 'logscale'); + } + return state.tmp.dygraph_logscale; + }, + theme: function (state) { + if (typeof state.tmp.dygraph_theme === 'undefined') { + state.tmp.dygraph_theme = NETDATA.dataAttribute(state.element, 'dygraph-theme', 'default'); + } + return state.tmp.dygraph_theme; + }, + container_class: function (state) { + if (this.legend(state) !== null) { + return 'netdata-container-with-legend'; + } + return 'netdata-container'; + } + }, + "sparkline": { + initialize: NETDATA.sparklineInitialize, + create: NETDATA.sparklineChartCreate, + update: NETDATA.sparklineChartUpdate, + resize: null, + setSelection: undefined, // function(state, t) { void(state); return true; }, + clearSelection: undefined, // function(state) { void(state); return true; }, + toolboxPanAndZoom: null, + initialized: false, + enabled: true, + xssRegexIgnore: new RegExp('^/api/v1/data\.result$'), + format: function (state) { + void(state); + return 'array'; + }, + options: function (state) { + void(state); + return 'flip' + '%7C' + 'abs'; + }, + legend: function (state) { + void(state); + return null; + }, + autoresize: function (state) { + void(state); + return false; + }, + max_updates_to_recreate: function (state) { + void(state); + return 5000; + }, + track_colors: function (state) { + void(state); + return false; + }, + pixels_per_point: function (state) { + void(state); + return 3; + }, + container_class: function (state) { + void(state); + return 'netdata-container'; + } + }, + "peity": { + initialize: NETDATA.peityInitialize, + create: NETDATA.peityChartCreate, + update: NETDATA.peityChartUpdate, + resize: null, + setSelection: undefined, // function(state, t) { void(state); return true; }, + clearSelection: undefined, // function(state) { void(state); return true; }, + toolboxPanAndZoom: null, + initialized: false, + enabled: true, + xssRegexIgnore: new RegExp('^/api/v1/data\.result$'), + format: function (state) { + void(state); + return 'ssvcomma'; + }, + options: function (state) { + void(state); + return 'null2zero' + '%7C' + 'flip' + '%7C' + 'abs'; + }, + legend: function (state) { + void(state); + return null; + }, + autoresize: function (state) { + void(state); + return false; + }, + max_updates_to_recreate: function (state) { + void(state); + return 5000; + }, + track_colors: function (state) { + void(state); + return false; + }, + pixels_per_point: function (state) { + void(state); + return 3; + }, + container_class: function (state) { + void(state); + return 'netdata-container'; + } + }, + // "morris": { + // initialize: NETDATA.morrisInitialize, + // create: NETDATA.morrisChartCreate, + // update: NETDATA.morrisChartUpdate, + // resize: null, + // setSelection: undefined, // function(state, t) { void(state); return true; }, + // clearSelection: undefined, // function(state) { void(state); return true; }, + // toolboxPanAndZoom: null, + // initialized: false, + // enabled: true, + // xssRegexIgnore: new RegExp('^/api/v1/data\.result.data$'), + // format: function(state) { void(state); return 'json'; }, + // options: function(state) { void(state); return 'objectrows' + '%7C' + 'ms'; }, + // legend: function(state) { void(state); return null; }, + // autoresize: function(state) { void(state); return false; }, + // max_updates_to_recreate: function(state) { void(state); return 50; }, + // track_colors: function(state) { void(state); return false; }, + // pixels_per_point: function(state) { void(state); return 15; }, + // container_class: function(state) { void(state); return 'netdata-container'; } + // }, + "google": { + initialize: NETDATA.googleInitialize, + create: NETDATA.googleChartCreate, + update: NETDATA.googleChartUpdate, + resize: null, + setSelection: undefined, //function(state, t) { void(state); return true; }, + clearSelection: undefined, //function(state) { void(state); return true; }, + toolboxPanAndZoom: null, + initialized: false, + enabled: true, + xssRegexIgnore: new RegExp('^/api/v1/data\.result.rows$'), + format: function (state) { + void(state); + return 'datatable'; + }, + options: function (state) { + void(state); + return ''; + }, + legend: function (state) { + void(state); + return null; + }, + autoresize: function (state) { + void(state); + return false; + }, + max_updates_to_recreate: function (state) { + void(state); + return 300; + }, + track_colors: function (state) { + void(state); + return false; + }, + pixels_per_point: function (state) { + void(state); + return 4; + }, + container_class: function (state) { + void(state); + return 'netdata-container'; + } + }, + // "raphael": { + // initialize: NETDATA.raphaelInitialize, + // create: NETDATA.raphaelChartCreate, + // update: NETDATA.raphaelChartUpdate, + // resize: null, + // setSelection: undefined, // function(state, t) { void(state); return true; }, + // clearSelection: undefined, // function(state) { void(state); return true; }, + // toolboxPanAndZoom: null, + // initialized: false, + // enabled: true, + // xssRegexIgnore: new RegExp('^/api/v1/data\.result.data$'), + // format: function(state) { void(state); return 'json'; }, + // options: function(state) { void(state); return ''; }, + // legend: function(state) { void(state); return null; }, + // autoresize: function(state) { void(state); return false; }, + // max_updates_to_recreate: function(state) { void(state); return 5000; }, + // track_colors: function(state) { void(state); return false; }, + // pixels_per_point: function(state) { void(state); return 3; }, + // container_class: function(state) { void(state); return 'netdata-container'; } + // }, + // "c3": { + // initialize: NETDATA.c3Initialize, + // create: NETDATA.c3ChartCreate, + // update: NETDATA.c3ChartUpdate, + // resize: null, + // setSelection: undefined, // function(state, t) { void(state); return true; }, + // clearSelection: undefined, // function(state) { void(state); return true; }, + // toolboxPanAndZoom: null, + // initialized: false, + // enabled: true, + // xssRegexIgnore: new RegExp('^/api/v1/data\.result$'), + // format: function(state) { void(state); return 'csvjsonarray'; }, + // options: function(state) { void(state); return 'milliseconds'; }, + // legend: function(state) { void(state); return null; }, + // autoresize: function(state) { void(state); return false; }, + // max_updates_to_recreate: function(state) { void(state); return 5000; }, + // track_colors: function(state) { void(state); return false; }, + // pixels_per_point: function(state) { void(state); return 15; }, + // container_class: function(state) { void(state); return 'netdata-container'; } + // }, + "d3pie": { + initialize: NETDATA.d3pieInitialize, + create: NETDATA.d3pieChartCreate, + update: NETDATA.d3pieChartUpdate, + resize: null, + setSelection: NETDATA.d3pieSetSelection, + clearSelection: NETDATA.d3pieClearSelection, + toolboxPanAndZoom: null, + initialized: false, + enabled: true, + xssRegexIgnore: new RegExp('^/api/v1/data\.result.data$'), + format: function (state) { + void(state); + return 'json'; + }, + options: function (state) { + void(state); + return 'objectrows' + '%7C' + 'ms'; + }, + legend: function (state) { + void(state); + return null; + }, + autoresize: function (state) { + void(state); + return false; + }, + max_updates_to_recreate: function (state) { + void(state); + return 5000; + }, + track_colors: function (state) { + void(state); + return false; + }, + pixels_per_point: function (state) { + void(state); + return 15; + }, + container_class: function (state) { + void(state); + return 'netdata-container'; + } + }, + "d3": { + initialize: NETDATA.d3Initialize, + create: NETDATA.d3ChartCreate, + update: NETDATA.d3ChartUpdate, + resize: null, + setSelection: undefined, // function(state, t) { void(state); return true; }, + clearSelection: undefined, // function(state) { void(state); return true; }, + toolboxPanAndZoom: null, + initialized: false, + enabled: true, + xssRegexIgnore: new RegExp('^/api/v1/data\.result.data$'), + format: function (state) { + void(state); + return 'json'; + }, + options: function (state) { + void(state); + return ''; + }, + legend: function (state) { + void(state); + return null; + }, + autoresize: function (state) { + void(state); + return false; + }, + max_updates_to_recreate: function (state) { + void(state); + return 5000; + }, + track_colors: function (state) { + void(state); + return false; + }, + pixels_per_point: function (state) { + void(state); + return 3; + }, + container_class: function (state) { + void(state); + return 'netdata-container'; + } + }, + "easypiechart": { + initialize: NETDATA.easypiechartInitialize, + create: NETDATA.easypiechartChartCreate, + update: NETDATA.easypiechartChartUpdate, + resize: null, + setSelection: NETDATA.easypiechartSetSelection, + clearSelection: NETDATA.easypiechartClearSelection, + toolboxPanAndZoom: null, + initialized: false, + enabled: true, + xssRegexIgnore: new RegExp('^/api/v1/data\.result$'), + format: function (state) { + void(state); + return 'array'; + }, + options: function (state) { + void(state); + return 'absolute'; + }, + legend: function (state) { + void(state); + return null; + }, + autoresize: function (state) { + void(state); + return false; + }, + max_updates_to_recreate: function (state) { + void(state); + return 5000; + }, + track_colors: function (state) { + void(state); + return true; + }, + pixels_per_point: function (state) { + void(state); + return 3; + }, + aspect_ratio: 100, + container_class: function (state) { + void(state); + return 'netdata-container-easypiechart'; + } + }, + "gauge": { + initialize: NETDATA.gaugeInitialize, + create: NETDATA.gaugeChartCreate, + update: NETDATA.gaugeChartUpdate, + resize: null, + setSelection: NETDATA.gaugeSetSelection, + clearSelection: NETDATA.gaugeClearSelection, + toolboxPanAndZoom: null, + initialized: false, + enabled: true, + xssRegexIgnore: new RegExp('^/api/v1/data\.result$'), + format: function (state) { + void(state); + return 'array'; + }, + options: function (state) { + void(state); + return 'absolute'; + }, + legend: function (state) { + void(state); + return null; + }, + autoresize: function (state) { + void(state); + return false; + }, + max_updates_to_recreate: function (state) { + void(state); + return 5000; + }, + track_colors: function (state) { + void(state); + return true; + }, + pixels_per_point: function (state) { + void(state); + return 3; + }, + aspect_ratio: 60, + container_class: function (state) { + void(state); + return 'netdata-container-gauge'; + } + }, + "textonly": { + autoresize: function (state) { + void(state); + return false; + }, + container_class: function (state) { + void(state); + return 'netdata-container'; + }, + create: NETDATA.textOnlyCreate, + enabled: true, + format: function (state) { + void(state); + return 'array'; + }, + initialized: true, + initialize: function (callback) { + callback(); + }, + legend: function (state) { + void(state); + return null; + }, + max_updates_to_recreate: function (state) { + void(state); + return 5000; + }, + options: function (state) { + void(state); + return 'absolute'; + }, + pixels_per_point: function (state) { + void(state); + return 3; + }, + track_colors: function (state) { + void(state); + return false; + }, + update: NETDATA.textOnlyUpdate, + xssRegexIgnore: new RegExp('^/api/v1/data\.result$'), + } +}; + +NETDATA.registerChartLibrary = function (library, url) { + if (NETDATA.options.debug.libraries) { + console.log("registering chart library: " + library); + } + + NETDATA.chartLibraries[library].url = url; + NETDATA.chartLibraries[library].initialized = true; + NETDATA.chartLibraries[library].enabled = true; +}; diff --git a/web/gui/src/dashboard.js/charting/_c3.js b/web/gui/src/dashboard.js/charting/_c3.js new file mode 100644 index 00000000..6688bbcc --- /dev/null +++ b/web/gui/src/dashboard.js/charting/_c3.js @@ -0,0 +1,114 @@ + +// DEPRECATED: will be removed! + +// c3 + +NETDATA.c3Initialize = function(callback) { + if (typeof netdataNoC3 === 'undefined' || !netdataNoC3) { + + // C3 requires D3 + if (!NETDATA.chartLibraries.d3.initialized) { + if (NETDATA.chartLibraries.d3.enabled) { + NETDATA.d3Initialize(function() { + NETDATA.c3Initialize(callback); + }); + } else { + NETDATA.chartLibraries.c3.enabled = false; + if (typeof callback === "function") + return callback(); + } + } else { + NETDATA._loadCSS(NETDATA.c3_css); + + $.ajax({ + url: NETDATA.c3_js, + cache: true, + dataType: "script", + xhrFields: { withCredentials: true } // required for the cookie + }) + .done(function() { + NETDATA.registerChartLibrary('c3', NETDATA.c3_js); + }) + .fail(function() { + NETDATA.chartLibraries.c3.enabled = false; + NETDATA.error(100, NETDATA.c3_js); + }) + .always(function() { + if (typeof callback === "function") + return callback(); + }); + } + } else { + NETDATA.chartLibraries.c3.enabled = false; + if (typeof callback === "function") + return callback(); + } +}; + +NETDATA.c3ChartUpdate = function(state, data) { + state.c3_instance.destroy(); + return NETDATA.c3ChartCreate(state, data); + + //state.c3_instance.load({ + // rows: data.result, + // unload: true + //}); + + //return true; +}; + +NETDATA.c3ChartCreate = function(state, data) { + + state.element_chart.id = 'c3-' + state.uuid; + // console.log('id = ' + state.element_chart.id); + + state.c3_instance = c3.generate({ + bindto: '#' + state.element_chart.id, + size: { + width: state.chartWidth(), + height: state.chartHeight() + }, + color: { + pattern: state.chartColors() + }, + data: { + x: 'time', + rows: data.result, + type: (state.chart.chart_type === 'line')?'spline':'area-spline' + }, + axis: { + x: { + type: 'timeseries', + tick: { + format: function(x) { + return NETDATA.dateTime.xAxisTimeString(x); + } + } + } + }, + grid: { + x: { + show: true + }, + y: { + show: true + } + }, + point: { + show: false + }, + line: { + connectNull: false + }, + transition: { + duration: 0 + }, + interaction: { + enabled: true + } + }); + + // console.log(state.c3_instance); + + return true; +}; diff --git a/web/gui/src/dashboard.js/charting/_morris.js b/web/gui/src/dashboard.js/charting/_morris.js new file mode 100644 index 00000000..30789e4e --- /dev/null +++ b/web/gui/src/dashboard.js/charting/_morris.js @@ -0,0 +1,81 @@ + +// DEPRECATED: will be removed! + +// morris + +NETDATA.morrisInitialize = function(callback) { + if (typeof netdataNoMorris === 'undefined' || !netdataNoMorris) { + + // morris requires raphael + if (!NETDATA.chartLibraries.raphael.initialized) { + if (NETDATA.chartLibraries.raphael.enabled) { + NETDATA.raphaelInitialize(function() { + NETDATA.morrisInitialize(callback); + }); + } else { + NETDATA.chartLibraries.morris.enabled = false; + if (typeof callback === "function") + return callback(); + } + } else { + NETDATA._loadCSS(NETDATA.morris_css); + + $.ajax({ + url: NETDATA.morris_js, + cache: true, + dataType: "script", + xhrFields: { withCredentials: true } // required for the cookie + }) + .done(function() { + NETDATA.registerChartLibrary('morris', NETDATA.morris_js); + }) + .fail(function() { + NETDATA.chartLibraries.morris.enabled = false; + NETDATA.error(100, NETDATA.morris_js); + }) + .always(function() { + if (typeof callback === "function") + return callback(); + }); + } + } else { + NETDATA.chartLibraries.morris.enabled = false; + if (typeof callback === "function") + return callback(); + } +}; + +NETDATA.morrisChartUpdate = function(state, data) { + state.morris_instance.setData(data.result.data); + return true; +}; + +NETDATA.morrisChartCreate = function(state, data) { + + state.morris_options = { + element: state.element_chart.id, + data: data.result.data, + xkey: 'time', + ykeys: data.dimension_names, + labels: data.dimension_names, + lineWidth: 2, + pointSize: 3, + smooth: true, + hideHover: 'auto', + parseTime: true, + continuousLine: false, + behaveLikeLine: false + }; + + if (state.chart.chart_type === 'line') + state.morris_instance = new Morris.Line(state.morris_options); + + else if (state.chart.chart_type === 'area') { + state.morris_options.behaveLikeLine = true; + state.morris_instance = new Morris.Area(state.morris_options); + } + else // stacked + state.morris_instance = new Morris.Area(state.morris_options); + + return true; +}; diff --git a/web/gui/src/dashboard.js/charting/_raphael.js b/web/gui/src/dashboard.js/charting/_raphael.js new file mode 100644 index 00000000..2d89a22a --- /dev/null +++ b/web/gui/src/dashboard.js/charting/_raphael.js @@ -0,0 +1,48 @@ + +// DEPRECATED: will be removed! + +// raphael + +NETDATA.raphaelInitialize = function(callback) { + if (typeof netdataStopRaphael === 'undefined' || !netdataStopRaphael) { + $.ajax({ + url: NETDATA.raphael_js, + cache: true, + dataType: "script", + xhrFields: { withCredentials: true } // required for the cookie + }) + .done(function() { + NETDATA.registerChartLibrary('raphael', NETDATA.raphael_js); + }) + .fail(function() { + NETDATA.chartLibraries.raphael.enabled = false; + NETDATA.error(100, NETDATA.raphael_js); + }) + .always(function() { + if (typeof callback === "function") + return callback(); + }); + } else { + NETDATA.chartLibraries.raphael.enabled = false; + if (typeof callback === "function") + return callback(); + } +}; + +NETDATA.raphaelChartUpdate = function(state, data) { + $(state.element_chart).raphael(data.result, { + width: state.chartWidth(), + height: state.chartHeight() + }); + + return false; +}; + +NETDATA.raphaelChartCreate = function(state, data) { + $(state.element_chart).raphael(data.result, { + width: state.chartWidth(), + height: state.chartHeight() + }); + + return false; +}; diff --git a/web/gui/src/dashboard.js/charting/d3.js b/web/gui/src/dashboard.js/charting/d3.js new file mode 100644 index 00000000..6528208c --- /dev/null +++ b/web/gui/src/dashboard.js/charting/d3.js @@ -0,0 +1,43 @@ + +// ---------------------------------------------------------------------------------------------------------------- +// D3 + +NETDATA.d3Initialize = function(callback) { + if (typeof netdataStopD3 === 'undefined' || !netdataStopD3) { + $.ajax({ + url: NETDATA.d3_js, + cache: true, + dataType: "script", + xhrFields: { withCredentials: true } // required for the cookie + }) + .done(function() { + NETDATA.registerChartLibrary('d3', NETDATA.d3_js); + }) + .fail(function() { + NETDATA.chartLibraries.d3.enabled = false; + NETDATA.error(100, NETDATA.d3_js); + }) + .always(function() { + if (typeof callback === "function") + return callback(); + }); + } else { + NETDATA.chartLibraries.d3.enabled = false; + if (typeof callback === "function") + return callback(); + } +}; + +NETDATA.d3ChartUpdate = function(state, data) { + void(state); + void(data); + + return false; +}; + +NETDATA.d3ChartCreate = function(state, data) { + void(state); + void(data); + + return false; +}; diff --git a/web/gui/src/dashboard.js/charting/d3pie.js b/web/gui/src/dashboard.js/charting/d3pie.js new file mode 100644 index 00000000..27cff854 --- /dev/null +++ b/web/gui/src/dashboard.js/charting/d3pie.js @@ -0,0 +1,341 @@ + +// d3pie + +NETDATA.d3pieInitialize = function (callback) { + if (typeof netdataNoD3pie === 'undefined' || !netdataNoD3pie) { + + // d3pie requires D3 + if (!NETDATA.chartLibraries.d3.initialized) { + if (NETDATA.chartLibraries.d3.enabled) { + NETDATA.d3Initialize(function () { + NETDATA.d3pieInitialize(callback); + }); + } else { + NETDATA.chartLibraries.d3pie.enabled = false; + if (typeof callback === "function") { + return callback(); + } + } + } else { + $.ajax({ + url: NETDATA.d3pie_js, + cache: true, + dataType: "script", + xhrFields: {withCredentials: true} // required for the cookie + }) + .done(function () { + NETDATA.registerChartLibrary('d3pie', NETDATA.d3pie_js); + }) + .fail(function () { + NETDATA.chartLibraries.d3pie.enabled = false; + NETDATA.error(100, NETDATA.d3pie_js); + }) + .always(function () { + if (typeof callback === "function") { + return callback(); + } + }); + } + } else { + NETDATA.chartLibraries.d3pie.enabled = false; + if (typeof callback === "function") { + return callback(); + } + } +}; + +NETDATA.d3pieSetContent = function (state, data, index) { + state.legendFormatValueDecimalsFromMinMax( + data.min, + data.max + ); + + let content = []; + let colors = state.chartColors(); + let len = data.result.labels.length; + for (let i = 1; i < len; i++) { + let label = data.result.labels[i]; + let value = data.result.data[index][label]; + let color = colors[i - 1]; + + if (value !== null && value > 0) { + content.push({ + label: label, + value: value, + color: color + }); + } + } + + if (content.length === 0) { + content.push({ + label: 'no data', + value: 100, + color: '#666666' + }); + } + + state.tmp.d3pie_last_slot = index; + return content; +}; + +NETDATA.d3pieDateRange = function (state, data, index) { + let dt = Math.round((data.before - data.after + 1) / data.points); + let dt_str = NETDATA.seconds4human(dt); + + let before = data.result.data[index].time; + let after = before - (dt * 1000); + + let d1 = NETDATA.dateTime.localeDateString(after); + let t1 = NETDATA.dateTime.localeTimeString(after); + let d2 = NETDATA.dateTime.localeDateString(before); + let t2 = NETDATA.dateTime.localeTimeString(before); + + if (d1 === d2) { + return d1 + ' ' + t1 + ' to ' + t2 + ', ' + dt_str; + } + + return d1 + ' ' + t1 + ' to ' + d2 + ' ' + t2 + ', ' + dt_str; +}; + +NETDATA.d3pieSetSelection = function (state, t) { + if (state.timeIsVisible(t) !== true) { + return NETDATA.d3pieClearSelection(state, true); + } + + let slot = state.calculateRowForTime(t); + slot = state.data.result.data.length - slot - 1; + + if (slot < 0 || slot >= state.data.result.length) { + return NETDATA.d3pieClearSelection(state, true); + } + + if (state.tmp.d3pie_last_slot === slot) { + // we already show this slot, don't do anything + return true; + } + + if (state.tmp.d3pie_timer === undefined) { + state.tmp.d3pie_timer = NETDATA.timeout.set(function () { + state.tmp.d3pie_timer = undefined; + NETDATA.d3pieChange(state, NETDATA.d3pieSetContent(state, state.data, slot), NETDATA.d3pieDateRange(state, state.data, slot)); + }, 0); + } + + return true; +}; + +NETDATA.d3pieClearSelection = function (state, force) { + if (typeof state.tmp.d3pie_timer !== 'undefined') { + NETDATA.timeout.clear(state.tmp.d3pie_timer); + state.tmp.d3pie_timer = undefined; + } + + if (state.isAutoRefreshable() && state.data !== null && force !== true) { + NETDATA.d3pieChartUpdate(state, state.data); + } else { + if (state.tmp.d3pie_last_slot !== -1) { + state.tmp.d3pie_last_slot = -1; + NETDATA.d3pieChange(state, [{label: 'no data', value: 1, color: '#666666'}], 'no data available'); + } + } + + return true; +}; + +NETDATA.d3pieChange = function (state, content, footer) { + if (state.d3pie_forced_subtitle === null) { + //state.d3pie_instance.updateProp("header.subtitle.text", state.units_current); + state.d3pie_instance.options.header.subtitle.text = state.units_current; + } + + if (state.d3pie_forced_footer === null) { + //state.d3pie_instance.updateProp("footer.text", footer); + state.d3pie_instance.options.footer.text = footer; + } + + //state.d3pie_instance.updateProp("data.content", content); + state.d3pie_instance.options.data.content = content; + state.d3pie_instance.destroy(); + state.d3pie_instance.recreate(); + return true; +}; + +NETDATA.d3pieChartUpdate = function (state, data) { + return NETDATA.d3pieChange(state, NETDATA.d3pieSetContent(state, data, 0), NETDATA.d3pieDateRange(state, data, 0)); +}; + +NETDATA.d3pieChartCreate = function (state, data) { + + state.element_chart.id = 'd3pie-' + state.uuid; + // console.log('id = ' + state.element_chart.id); + + let content = NETDATA.d3pieSetContent(state, data, 0); + + state.d3pie_forced_title = NETDATA.dataAttribute(state.element, 'd3pie-title', null); + state.d3pie_forced_subtitle = NETDATA.dataAttribute(state.element, 'd3pie-subtitle', null); + state.d3pie_forced_footer = NETDATA.dataAttribute(state.element, 'd3pie-footer', null); + + state.d3pie_options = { + header: { + title: { + text: (state.d3pie_forced_title !== null) ? state.d3pie_forced_title : state.title, + color: NETDATA.dataAttribute(state.element, 'd3pie-title-color', NETDATA.themes.current.d3pie.title), + fontSize: NETDATA.dataAttribute(state.element, 'd3pie-title-fontsize', 12), + fontWeight: NETDATA.dataAttribute(state.element, 'd3pie-title-fontweight', "bold"), + font: NETDATA.dataAttribute(state.element, 'd3pie-title-font', "arial") + }, + subtitle: { + text: (state.d3pie_forced_subtitle !== null) ? state.d3pie_forced_subtitle : state.units_current, + color: NETDATA.dataAttribute(state.element, 'd3pie-subtitle-color', NETDATA.themes.current.d3pie.subtitle), + fontSize: NETDATA.dataAttribute(state.element, 'd3pie-subtitle-fontsize', 10), + fontWeight: NETDATA.dataAttribute(state.element, 'd3pie-subtitle-fontweight', "normal"), + font: NETDATA.dataAttribute(state.element, 'd3pie-subtitle-font', "arial") + }, + titleSubtitlePadding: 1 + }, + footer: { + text: (state.d3pie_forced_footer !== null) ? state.d3pie_forced_footer : NETDATA.d3pieDateRange(state, data, 0), + color: NETDATA.dataAttribute(state.element, 'd3pie-footer-color', NETDATA.themes.current.d3pie.footer), + fontSize: NETDATA.dataAttribute(state.element, 'd3pie-footer-fontsize', 9), + fontWeight: NETDATA.dataAttribute(state.element, 'd3pie-footer-fontweight', "bold"), + font: NETDATA.dataAttribute(state.element, 'd3pie-footer-font', "arial"), + location: NETDATA.dataAttribute(state.element, 'd3pie-footer-location', "bottom-center") // bottom-left, bottom-center, bottom-right + }, + size: { + canvasHeight: state.chartHeight(), + canvasWidth: state.chartWidth(), + pieInnerRadius: NETDATA.dataAttribute(state.element, 'd3pie-pieinnerradius', "45%"), + pieOuterRadius: NETDATA.dataAttribute(state.element, 'd3pie-pieouterradius', "80%") + }, + data: { + // none, random, value-asc, value-desc, label-asc, label-desc + sortOrder: NETDATA.dataAttribute(state.element, 'd3pie-sortorder', "value-desc"), + smallSegmentGrouping: { + enabled: NETDATA.dataAttributeBoolean(state.element, "d3pie-smallsegmentgrouping-enabled", false), + value: NETDATA.dataAttribute(state.element, 'd3pie-smallsegmentgrouping-value', 1), + // percentage, value + valueType: NETDATA.dataAttribute(state.element, 'd3pie-smallsegmentgrouping-valuetype', "percentage"), + label: NETDATA.dataAttribute(state.element, 'd3pie-smallsegmentgrouping-label', "other"), + color: NETDATA.dataAttribute(state.element, 'd3pie-smallsegmentgrouping-color', NETDATA.themes.current.d3pie.other) + }, + + // REQUIRED! This is where you enter your pie data; it needs to be an array of objects + // of this form: { label: "label", value: 1.5, color: "#000000" } - color is optional + content: content + }, + labels: { + outer: { + // label, value, percentage, label-value1, label-value2, label-percentage1, label-percentage2 + format: NETDATA.dataAttribute(state.element, 'd3pie-labels-outer-format', "label-value1"), + hideWhenLessThanPercentage: NETDATA.dataAttribute(state.element, 'd3pie-labels-outer-hidewhenlessthanpercentage', null), + pieDistance: NETDATA.dataAttribute(state.element, 'd3pie-labels-outer-piedistance', 15) + }, + inner: { + // label, value, percentage, label-value1, label-value2, label-percentage1, label-percentage2 + format: NETDATA.dataAttribute(state.element, 'd3pie-labels-inner-format', "percentage"), + hideWhenLessThanPercentage: NETDATA.dataAttribute(state.element, 'd3pie-labels-inner-hidewhenlessthanpercentage', 2) + }, + mainLabel: { + color: NETDATA.dataAttribute(state.element, 'd3pie-labels-mainLabel-color', NETDATA.themes.current.d3pie.mainlabel), // or 'segment' for dynamic color + font: NETDATA.dataAttribute(state.element, 'd3pie-labels-mainLabel-font', "arial"), + fontSize: NETDATA.dataAttribute(state.element, 'd3pie-labels-mainLabel-fontsize', 10), + fontWeight: NETDATA.dataAttribute(state.element, 'd3pie-labels-mainLabel-fontweight', "normal") + }, + percentage: { + color: NETDATA.dataAttribute(state.element, 'd3pie-labels-percentage-color', NETDATA.themes.current.d3pie.percentage), + font: NETDATA.dataAttribute(state.element, 'd3pie-labels-percentage-font', "arial"), + fontSize: NETDATA.dataAttribute(state.element, 'd3pie-labels-percentage-fontsize', 10), + fontWeight: NETDATA.dataAttribute(state.element, 'd3pie-labels-percentage-fontweight', "bold"), + decimalPlaces: 0 + }, + value: { + color: NETDATA.dataAttribute(state.element, 'd3pie-labels-value-color', NETDATA.themes.current.d3pie.value), + font: NETDATA.dataAttribute(state.element, 'd3pie-labels-value-font', "arial"), + fontSize: NETDATA.dataAttribute(state.element, 'd3pie-labels-value-fontsize', 10), + fontWeight: NETDATA.dataAttribute(state.element, 'd3pie-labels-value-fontweight', "bold") + }, + lines: { + enabled: NETDATA.dataAttributeBoolean(state.element, 'd3pie-labels-lines-enabled', true), + style: NETDATA.dataAttribute(state.element, 'd3pie-labels-lines-style', "curved"), + color: NETDATA.dataAttribute(state.element, 'd3pie-labels-lines-color', "segment") // "segment" or a hex color + }, + truncation: { + enabled: NETDATA.dataAttributeBoolean(state.element, 'd3pie-labels-truncation-enabled', false), + truncateLength: NETDATA.dataAttribute(state.element, 'd3pie-labels-truncation-truncatelength', 30) + }, + formatter: function (context) { + // console.log(context); + if (context.part === 'value') { + return state.legendFormatValue(context.value); + } + if (context.part === 'percentage') { + return context.label + '%'; + } + + return context.label; + } + }, + effects: { + load: { + effect: "none", // none / default + speed: 0 // commented in the d3pie code to speed it up + }, + pullOutSegmentOnClick: { + effect: "bounce", // none / linear / bounce / elastic / back + speed: 400, + size: 5 + }, + highlightSegmentOnMouseover: true, + highlightLuminosity: -0.2 + }, + tooltips: { + enabled: false, + type: "placeholder", // caption|placeholder + string: "", + placeholderParser: null, // function + styles: { + fadeInSpeed: 250, + backgroundColor: NETDATA.themes.current.d3pie.tooltip_bg, + backgroundOpacity: 0.5, + color: NETDATA.themes.current.d3pie.tooltip_fg, + borderRadius: 2, + font: "arial", + fontSize: 12, + padding: 4 + } + }, + misc: { + colors: { + background: 'transparent', // transparent or color # + // segments: state.chartColors(), + segmentStroke: NETDATA.dataAttribute(state.element, 'd3pie-misc-colors-segmentstroke', NETDATA.themes.current.d3pie.segment_stroke) + }, + gradient: { + enabled: NETDATA.dataAttributeBoolean(state.element, 'd3pie-misc-gradient-enabled', false), + percentage: NETDATA.dataAttribute(state.element, 'd3pie-misc-colors-percentage', 95), + color: NETDATA.dataAttribute(state.element, 'd3pie-misc-gradient-color', NETDATA.themes.current.d3pie.gradient_color) + }, + canvasPadding: { + top: 5, + right: 5, + bottom: 5, + left: 5 + }, + pieCenterOffset: { + x: 0, + y: 0 + }, + cssPrefix: NETDATA.dataAttribute(state.element, 'd3pie-cssprefix', null) + }, + callbacks: { + onload: null, + onMouseoverSegment: null, + onMouseoutSegment: null, + onClickSegment: null + } + }; + + state.d3pie_instance = new d3pie(state.element_chart, state.d3pie_options); + return true; +}; diff --git a/web/gui/src/dashboard.js/charting/dygraph.js b/web/gui/src/dashboard.js/charting/dygraph.js new file mode 100644 index 00000000..4b44e7c4 --- /dev/null +++ b/web/gui/src/dashboard.js/charting/dygraph.js @@ -0,0 +1,1065 @@ +// dygraph + +// Codacy declarations +/* global smoothPlotter */ +/* global Dygraph */ + +NETDATA.dygraph = { + smooth: false +}; + +NETDATA.dygraphToolboxPanAndZoom = function (state, after, before) { + if (after < state.netdata_first) { + after = state.netdata_first; + } + + if (before > state.netdata_last) { + before = state.netdata_last; + } + + state.setMode('zoom'); + NETDATA.globalSelectionSync.stop(); + NETDATA.globalSelectionSync.delay(); + state.tmp.dygraph_user_action = true; + state.tmp.dygraph_force_zoom = true; + // state.log('toolboxPanAndZoom'); + state.updateChartPanOrZoom(after, before); + NETDATA.globalPanAndZoom.setMaster(state, after, before); +}; + +NETDATA.dygraphSetSelection = function (state, t) { + if (typeof state.tmp.dygraph_instance !== 'undefined') { + let r = state.calculateRowForTime(t); + if (r !== -1) { + state.tmp.dygraph_instance.setSelection(r); + return true; + } else { + state.tmp.dygraph_instance.clearSelection(); + state.legendShowUndefined(); + } + } + + return false; +}; + +NETDATA.dygraphClearSelection = function (state) { + if (typeof state.tmp.dygraph_instance !== 'undefined') { + state.tmp.dygraph_instance.clearSelection(); + } + return true; +}; + +NETDATA.dygraphSmoothInitialize = function (callback) { + $.ajax({ + url: NETDATA.dygraph_smooth_js, + cache: true, + dataType: "script", + xhrFields: {withCredentials: true} // required for the cookie + }) + .done(function () { + NETDATA.dygraph.smooth = true; + smoothPlotter.smoothing = 0.3; + }) + .fail(function () { + NETDATA.dygraph.smooth = false; + }) + .always(function () { + if (typeof callback === "function") { + return callback(); + } + }); +}; + +NETDATA.dygraphInitialize = function (callback) { + if (typeof netdataNoDygraphs === 'undefined' || !netdataNoDygraphs) { + $.ajax({ + url: NETDATA.dygraph_js, + cache: true, + dataType: "script", + xhrFields: {withCredentials: true} // required for the cookie + }) + .done(function () { + NETDATA.registerChartLibrary('dygraph', NETDATA.dygraph_js); + }) + .fail(function () { + NETDATA.chartLibraries.dygraph.enabled = false; + NETDATA.error(100, NETDATA.dygraph_js); + }) + .always(function () { + if (NETDATA.chartLibraries.dygraph.enabled && NETDATA.options.current.smooth_plot) { + NETDATA.dygraphSmoothInitialize(callback); + } else if (typeof callback === "function") { + return callback(); + } + }); + } else { + NETDATA.chartLibraries.dygraph.enabled = false; + if (typeof callback === "function") { + return callback(); + } + } +}; + +NETDATA.dygraphChartUpdate = function (state, data) { + let dygraph = state.tmp.dygraph_instance; + + if (typeof dygraph === 'undefined') { + return NETDATA.dygraphChartCreate(state, data); + } + + // when the chart is not visible, and hidden + // if there is a window resize, dygraph detects + // its element size as 0x0. + // this will make it re-appear properly + + if (state.tm.last_unhidden > state.tmp.dygraph_last_rendered) { + dygraph.resize(); + } + + let options = { + file: data.result.data, + colors: state.chartColors(), + labels: data.result.labels, + //labelsDivWidth: state.chartWidth() - 70, + includeZero: state.tmp.dygraph_include_zero, + visibility: state.dimensions_visibility.selected2BooleanArray(state.data.dimension_names) + }; + + if (state.tmp.dygraph_chart_type === 'stacked') { + if (options.includeZero && state.dimensions_visibility.countSelected() < options.visibility.length) { + options.includeZero = 0; + } + } + + if (!NETDATA.chartLibraries.dygraph.isSparkline(state)) { + options.ylabel = state.units_current; // (state.units_desired === 'auto')?"":state.units_current; + } + + if (state.tmp.dygraph_force_zoom) { + if (NETDATA.options.debug.dygraph || state.debug) { + state.log('dygraphChartUpdate() forced zoom update'); + } + + options.dateWindow = (state.requested_padding !== null) ? [state.view_after, state.view_before] : null; + //options.isZoomedIgnoreProgrammaticZoom = true; + state.tmp.dygraph_force_zoom = false; + } else if (state.current.name !== 'auto') { + if (NETDATA.options.debug.dygraph || state.debug) { + state.log('dygraphChartUpdate() loose update'); + } + } else { + if (NETDATA.options.debug.dygraph || state.debug) { + state.log('dygraphChartUpdate() strict update'); + } + + options.dateWindow = (state.requested_padding !== null) ? [state.view_after, state.view_before] : null; + //options.isZoomedIgnoreProgrammaticZoom = true; + } + + options.valueRange = state.tmp.dygraph_options.valueRange; + + let oldMax = null, oldMin = null; + if (state.tmp.__commonMin !== null) { + state.data.min = state.tmp.dygraph_instance.axes_[0].extremeRange[0]; + oldMin = options.valueRange[0] = NETDATA.commonMin.get(state); + } + if (state.tmp.__commonMax !== null) { + state.data.max = state.tmp.dygraph_instance.axes_[0].extremeRange[1]; + oldMax = options.valueRange[1] = NETDATA.commonMax.get(state); + } + + if (state.tmp.dygraph_smooth_eligible) { + if ((NETDATA.options.current.smooth_plot && state.tmp.dygraph_options.plotter !== smoothPlotter) + || (NETDATA.options.current.smooth_plot === false && state.tmp.dygraph_options.plotter === smoothPlotter)) { + NETDATA.dygraphChartCreate(state, data); + return; + } + } + + if (netdataSnapshotData !== null && NETDATA.globalPanAndZoom.isActive() && NETDATA.globalPanAndZoom.isMaster(state) === false) { + // pan and zoom on snapshots + options.dateWindow = [NETDATA.globalPanAndZoom.force_after_ms, NETDATA.globalPanAndZoom.force_before_ms]; + //options.isZoomedIgnoreProgrammaticZoom = true; + } + + if (NETDATA.chartLibraries.dygraph.isLogScale(state)) { + if (Array.isArray(options.valueRange) && options.valueRange[0] <= 0) { + options.valueRange[0] = null; + } + } + + dygraph.updateOptions(options); + + let redraw = false; + if (oldMin !== null && oldMin > state.tmp.dygraph_instance.axes_[0].extremeRange[0]) { + state.data.min = state.tmp.dygraph_instance.axes_[0].extremeRange[0]; + options.valueRange[0] = NETDATA.commonMin.get(state); + redraw = true; + } + if (oldMax !== null && oldMax < state.tmp.dygraph_instance.axes_[0].extremeRange[1]) { + state.data.max = state.tmp.dygraph_instance.axes_[0].extremeRange[1]; + options.valueRange[1] = NETDATA.commonMax.get(state); + redraw = true; + } + + if (redraw) { + // state.log('forcing redraw to adapt to common- min/max'); + dygraph.updateOptions(options); + } + + state.tmp.dygraph_last_rendered = Date.now(); + return true; +}; + +NETDATA.dygraphChartCreate = function (state, data) { + if (NETDATA.options.debug.dygraph || state.debug) { + state.log('dygraphChartCreate()'); + } + + state.tmp.dygraph_chart_type = NETDATA.dataAttribute(state.element, 'dygraph-type', state.chart.chart_type); + if (state.tmp.dygraph_chart_type === 'stacked' && data.dimensions === 1) { + state.tmp.dygraph_chart_type = 'area'; + } + if (state.tmp.dygraph_chart_type === 'stacked' && NETDATA.chartLibraries.dygraph.isLogScale(state)) { + state.tmp.dygraph_chart_type = 'area'; + } + + let highlightCircleSize = NETDATA.chartLibraries.dygraph.isSparkline(state) ? 3 : 4; + + let smooth = NETDATA.dygraph.smooth + ? (NETDATA.dataAttributeBoolean(state.element, 'dygraph-smooth', (state.tmp.dygraph_chart_type === 'line' && NETDATA.chartLibraries.dygraph.isSparkline(state) === false))) + : false; + + state.tmp.dygraph_include_zero = NETDATA.dataAttribute(state.element, 'dygraph-includezero', (state.tmp.dygraph_chart_type === 'stacked')); + let drawAxis = NETDATA.dataAttributeBoolean(state.element, 'dygraph-drawaxis', true); + + state.tmp.dygraph_options = { + colors: NETDATA.dataAttribute(state.element, 'dygraph-colors', state.chartColors()), + + // leave a few pixels empty on the right of the chart + rightGap: NETDATA.dataAttribute(state.element, 'dygraph-rightgap', 5), + showRangeSelector: NETDATA.dataAttributeBoolean(state.element, 'dygraph-showrangeselector', false), + showRoller: NETDATA.dataAttributeBoolean(state.element, 'dygraph-showroller', false), + title: NETDATA.dataAttribute(state.element, 'dygraph-title', state.title), + titleHeight: NETDATA.dataAttribute(state.element, 'dygraph-titleheight', 19), + legend: NETDATA.dataAttribute(state.element, 'dygraph-legend', 'always'), // we need this to get selection events + labels: data.result.labels, + labelsDiv: NETDATA.dataAttribute(state.element, 'dygraph-labelsdiv', state.element_legend_childs.hidden), + //labelsDivStyles: NETDATA.dataAttribute(state.element, 'dygraph-labelsdivstyles', { 'fontSize':'1px' }), + //labelsDivWidth: NETDATA.dataAttribute(state.element, 'dygraph-labelsdivwidth', state.chartWidth() - 70), + labelsSeparateLines: NETDATA.dataAttributeBoolean(state.element, 'dygraph-labelsseparatelines', true), + labelsShowZeroValues: NETDATA.chartLibraries.dygraph.isLogScale(state) ? false : NETDATA.dataAttributeBoolean(state.element, 'dygraph-labelsshowzerovalues', true), + labelsKMB: false, + labelsKMG2: false, + showLabelsOnHighlight: NETDATA.dataAttributeBoolean(state.element, 'dygraph-showlabelsonhighlight', true), + hideOverlayOnMouseOut: NETDATA.dataAttributeBoolean(state.element, 'dygraph-hideoverlayonmouseout', true), + includeZero: state.tmp.dygraph_include_zero, + xRangePad: NETDATA.dataAttribute(state.element, 'dygraph-xrangepad', 0), + yRangePad: NETDATA.dataAttribute(state.element, 'dygraph-yrangepad', 1), + valueRange: NETDATA.dataAttribute(state.element, 'dygraph-valuerange', [null, null]), + ylabel: state.units_current, // (state.units_desired === 'auto')?"":state.units_current, + yLabelWidth: NETDATA.dataAttribute(state.element, 'dygraph-ylabelwidth', 12), + + // the function to plot the chart + plotter: null, + + // The width of the lines connecting data points. + // This can be used to increase the contrast or some graphs. + strokeWidth: NETDATA.dataAttribute(state.element, 'dygraph-strokewidth', ((state.tmp.dygraph_chart_type === 'stacked') ? 0.1 : ((smooth === true) ? 1.5 : 0.7))), + strokePattern: NETDATA.dataAttribute(state.element, 'dygraph-strokepattern', undefined), + + // The size of the dot to draw on each point in pixels (see drawPoints). + // A dot is always drawn when a point is "isolated", + // i.e. there is a missing point on either side of it. + // This also controls the size of those dots. + drawPoints: NETDATA.dataAttributeBoolean(state.element, 'dygraph-drawpoints', false), + + // Draw points at the edges of gaps in the data. + // This improves visibility of small data segments or other data irregularities. + drawGapEdgePoints: NETDATA.dataAttributeBoolean(state.element, 'dygraph-drawgapedgepoints', true), + connectSeparatedPoints: NETDATA.chartLibraries.dygraph.isLogScale(state) ? false : NETDATA.dataAttributeBoolean(state.element, 'dygraph-connectseparatedpoints', false), + pointSize: NETDATA.dataAttribute(state.element, 'dygraph-pointsize', 1), + + // enabling this makes the chart with little square lines + stepPlot: NETDATA.dataAttributeBoolean(state.element, 'dygraph-stepplot', false), + + // Draw a border around graph lines to make crossing lines more easily + // distinguishable. Useful for graphs with many lines. + strokeBorderColor: NETDATA.dataAttribute(state.element, 'dygraph-strokebordercolor', NETDATA.themes.current.background), + strokeBorderWidth: NETDATA.dataAttribute(state.element, 'dygraph-strokeborderwidth', (state.tmp.dygraph_chart_type === 'stacked') ? 0.0 : 0.0), + fillGraph: NETDATA.dataAttribute(state.element, 'dygraph-fillgraph', (state.tmp.dygraph_chart_type === 'area' || state.tmp.dygraph_chart_type === 'stacked')), + fillAlpha: NETDATA.dataAttribute(state.element, 'dygraph-fillalpha', + ((state.tmp.dygraph_chart_type === 'stacked') + ? NETDATA.options.current.color_fill_opacity_stacked + : NETDATA.options.current.color_fill_opacity_area) + ), + stackedGraph: NETDATA.dataAttribute(state.element, 'dygraph-stackedgraph', (state.tmp.dygraph_chart_type === 'stacked')), + stackedGraphNaNFill: NETDATA.dataAttribute(state.element, 'dygraph-stackedgraphnanfill', 'none'), + drawAxis: drawAxis, + axisLabelFontSize: NETDATA.dataAttribute(state.element, 'dygraph-axislabelfontsize', 10), + axisLineColor: NETDATA.dataAttribute(state.element, 'dygraph-axislinecolor', NETDATA.themes.current.axis), + axisLineWidth: NETDATA.dataAttribute(state.element, 'dygraph-axislinewidth', 1.0), + drawGrid: NETDATA.dataAttributeBoolean(state.element, 'dygraph-drawgrid', true), + gridLinePattern: NETDATA.dataAttribute(state.element, 'dygraph-gridlinepattern', null), + gridLineWidth: NETDATA.dataAttribute(state.element, 'dygraph-gridlinewidth', 1.0), + gridLineColor: NETDATA.dataAttribute(state.element, 'dygraph-gridlinecolor', NETDATA.themes.current.grid), + maxNumberWidth: NETDATA.dataAttribute(state.element, 'dygraph-maxnumberwidth', 8), + sigFigs: NETDATA.dataAttribute(state.element, 'dygraph-sigfigs', null), + digitsAfterDecimal: NETDATA.dataAttribute(state.element, 'dygraph-digitsafterdecimal', 2), + valueFormatter: NETDATA.dataAttribute(state.element, 'dygraph-valueformatter', undefined), + highlightCircleSize: NETDATA.dataAttribute(state.element, 'dygraph-highlightcirclesize', highlightCircleSize), + highlightSeriesOpts: NETDATA.dataAttribute(state.element, 'dygraph-highlightseriesopts', null), // TOO SLOW: { strokeWidth: 1.5 }, + highlightSeriesBackgroundAlpha: NETDATA.dataAttribute(state.element, 'dygraph-highlightseriesbackgroundalpha', null), // TOO SLOW: (state.tmp.dygraph_chart_type === 'stacked')?0.7:0.5, + pointClickCallback: NETDATA.dataAttribute(state.element, 'dygraph-pointclickcallback', undefined), + visibility: state.dimensions_visibility.selected2BooleanArray(state.data.dimension_names), + logscale: NETDATA.chartLibraries.dygraph.isLogScale(state) ? 'y' : undefined, + + // Expects a string in the format ":