// *** src/dashboard.js/main.js // Codacy declarations /* global clipboard */ if (NETDATA.options.debug.main_loop) { console.log('welcome to NETDATA'); } NETDATA.onresizeCallback = null; NETDATA.onresize = function () { NETDATA.options.last_page_resize = Date.now(); NETDATA.onscroll(); if (typeof NETDATA.onresizeCallback === 'function') { NETDATA.onresizeCallback(); } }; NETDATA.abortAllRefreshes = function () { let targets = NETDATA.options.targets; let len = targets.length; while (len--) { if (targets[len].fetching_data) { if (typeof targets[len].xhr !== 'undefined') { targets[len].xhr.abort(); targets[len].running = false; targets[len].fetching_data = false; } } } }; NETDATA.onscrollStartDelay = function () { NETDATA.options.last_page_scroll = Date.now(); NETDATA.options.on_scroll_refresher_stop_until = NETDATA.options.last_page_scroll + (NETDATA.options.current.async_on_scroll ? 1000 : 0); }; NETDATA.onscrollEndDelay = function () { NETDATA.options.on_scroll_refresher_stop_until = Date.now() + (NETDATA.options.current.async_on_scroll ? NETDATA.options.current.onscroll_worker_duration_threshold : 0); }; NETDATA.onscroll_updater_timeout_id = undefined; NETDATA.onscrollUpdater = function () { NETDATA.globalSelectionSync.stop(); if (NETDATA.options.abort_ajax_on_scroll) { NETDATA.abortAllRefreshes(); } // when the user scrolls he sees that we have // hidden all the not-visible charts // using this little function we try to switch // the charts back to visible quickly if (!NETDATA.intersectionObserver.enabled()) { if (!NETDATA.options.current.parallel_refresher) { let targets = NETDATA.options.targets; let len = targets.length; while (len--) { if (!targets[len].running) { targets[len].isVisible(); } } } } NETDATA.onscrollEndDelay(); }; NETDATA.scrollUp = false; NETDATA.scrollY = window.scrollY; NETDATA.onscroll = function () { //console.log('onscroll() begin'); NETDATA.onscrollStartDelay(); NETDATA.chartRefresherReschedule(); NETDATA.scrollUp = (window.scrollY > NETDATA.scrollY); NETDATA.scrollY = window.scrollY; if (NETDATA.onscroll_updater_timeout_id) { NETDATA.timeout.clear(NETDATA.onscroll_updater_timeout_id); } NETDATA.onscroll_updater_timeout_id = NETDATA.timeout.set(NETDATA.onscrollUpdater, 0); //console.log('onscroll() end'); }; NETDATA.supportsPassiveEvents = function () { if (NETDATA.options.passive_events === null) { let supportsPassive = false; try { let opts = Object.defineProperty({}, 'passive', { get: function () { supportsPassive = true; } }); window.addEventListener("test", null, opts); } catch (e) { console.log('browser does not support passive events'); } NETDATA.options.passive_events = supportsPassive; } // console.log('passive ' + NETDATA.options.passive_events); return NETDATA.options.passive_events; }; window.addEventListener('resize', NETDATA.onresize, NETDATA.supportsPassiveEvents() ? {passive: true} : false); window.addEventListener('scroll', NETDATA.onscroll, NETDATA.supportsPassiveEvents() ? {passive: true} : false); // window.onresize = NETDATA.onresize; // window.onscroll = NETDATA.onscroll; // ---------------------------------------------------------------------------------------------------------------- // Global Pan and Zoom on charts // Using this structure are synchronize all the charts, so that // when you pan or zoom one, all others are automatically refreshed // to the same timespan. NETDATA.globalPanAndZoom = { seq: 0, // timestamp ms // every time a chart is panned or zoomed // we set the timestamp here // then we use it as a sequence number // to find if other charts are synchronized // to this time-range master: null, // the master chart (state), to which all others // are synchronized force_before_ms: null, // the timespan to sync all other charts force_after_ms: null, callback: null, globalReset: function () { this.clearMaster(); this.seq = 0; this.master = null; this.force_after_ms = null; this.force_before_ms = null; this.callback = null; }, delay: function () { if (NETDATA.options.debug.globalPanAndZoom) { console.log('globalPanAndZoom.delay()'); } NETDATA.options.auto_refresher_stop_until = Date.now() + NETDATA.options.current.global_pan_sync_time; }, // set a new master setMaster: function (state, after, before) { this.delay(); if (!NETDATA.options.current.sync_pan_and_zoom) { return; } if (this.master === null) { if (NETDATA.options.debug.globalPanAndZoom) { console.log('globalPanAndZoom.setMaster(' + state.id + ', ' + after + ', ' + before + ') SET MASTER'); } } else if (this.master !== state) { if (NETDATA.options.debug.globalPanAndZoom) { console.log('globalPanAndZoom.setMaster(' + state.id + ', ' + after + ', ' + before + ') CHANGED MASTER'); } this.master.resetChart(true, true); } let now = Date.now(); this.master = state; this.seq = now; this.force_after_ms = after; this.force_before_ms = before; if (typeof this.callback === 'function') { this.callback(true, after, before); } }, // clear the master clearMaster: function () { // if (NETDATA.options.debug.globalPanAndZoom === true) // console.log('globalPanAndZoom.clearMaster()'); if (NETDATA.options.debug.globalPanAndZoom) { console.log('globalPanAndZoom.clearMaster()'); } if (this.master !== null) { let st = this.master; this.master = null; st.resetChart(); } this.master = null; this.seq = 0; this.force_after_ms = null; this.force_before_ms = null; NETDATA.options.auto_refresher_stop_until = 0; if (typeof this.callback === 'function') { this.callback(false, 0, 0); } }, // is the given state the master of the global // pan and zoom sync? isMaster: function (state) { return (this.master === state); }, // are we currently have a global pan and zoom sync? isActive: function () { return (this.master !== null && this.force_before_ms !== null && this.force_after_ms !== null && this.seq !== 0); }, // check if a chart, other than the master // needs to be refreshed, due to the global pan and zoom shouldBeAutoRefreshed: function (state) { if (this.master === null || this.seq === 0) { return false; } //if (state.needsRecreation()) // return true; return (state.tm.pan_and_zoom_seq !== this.seq); } }; // ---------------------------------------------------------------------------------------------------------------- // global chart underlay (time-frame highlighting) NETDATA.globalChartUnderlay = { callback: null, // what to call when a highlighted range is setup after: null, // highlight after this time before: null, // highlight before this time view_after: null, // the charts after_ms viewport when the highlight was setup view_before: null, // the charts before_ms viewport, when the highlight was setup state: null, // the chart the highlight was setup isActive: function () { return (this.after !== null && this.before !== null); }, hasViewport: function () { return (this.state !== null && this.view_after !== null && this.view_before !== null); }, init: function (state, after, before, view_after, view_before) { this.state = (typeof state !== 'undefined') ? state : null; this.after = (typeof after !== 'undefined' && after !== null && after > 0) ? after : null; this.before = (typeof before !== 'undefined' && before !== null && before > 0) ? before : null; this.view_after = (typeof view_after !== 'undefined' && view_after !== null && view_after > 0) ? view_after : null; this.view_before = (typeof view_before !== 'undefined' && view_before !== null && view_before > 0) ? view_before : null; }, setup: function () { if (this.isActive()) { if (this.state === null) { this.state = NETDATA.options.targets[0]; } if (typeof this.callback === 'function') { this.callback(true, this.after, this.before); } } else { if (typeof this.callback === 'function') { this.callback(false, 0, 0); } } }, set: function (state, after, before, view_after, view_before) { if (after > before) { let t = after; after = before; before = t; } this.init(state, after, before, view_after, view_before); // if (this.hasViewport() === true) // NETDATA.globalPanAndZoom.setMaster(this.state, this.view_after, this.view_before); if (this.hasViewport()) { NETDATA.globalPanAndZoom.setMaster(this.state, this.view_after, this.view_before); } this.setup(); }, clear: function () { this.after = null; this.before = null; this.state = null; this.view_after = null; this.view_before = null; if (typeof this.callback === 'function') { this.callback(false, 0, 0); } }, focus: function () { if (this.isActive() && this.hasViewport()) { if (this.state === null) { this.state = NETDATA.options.targets[0]; } if (NETDATA.globalPanAndZoom.isMaster(this.state)) { NETDATA.globalPanAndZoom.clearMaster(); } NETDATA.globalPanAndZoom.setMaster(this.state, this.view_after, this.view_before, true); } } }; // ---------------------------------------------------------------------------------------------------------------- // dimensions selection // TODO // move color assignment to dimensions, here let dimensionStatus = function (parent, label, name_div, value_div, color) { this.enabled = false; this.parent = parent; this.label = label; this.name_div = null; this.value_div = null; this.color = NETDATA.themes.current.foreground; this.selected = (parent.unselected_count === 0); this.setOptions(name_div, value_div, color); }; dimensionStatus.prototype.invalidate = function () { this.name_div = null; this.value_div = null; this.enabled = false; }; dimensionStatus.prototype.setOptions = function (name_div, value_div, color) { this.color = color; if (this.name_div !== name_div) { this.name_div = name_div; this.name_div.title = this.label; this.name_div.style.setProperty('color', this.color, 'important'); if (!this.selected) { this.name_div.className = 'netdata-legend-name not-selected'; } else { this.name_div.className = 'netdata-legend-name selected'; } } if (this.value_div !== value_div) { this.value_div = value_div; this.value_div.title = this.label; this.value_div.style.setProperty('color', this.color, 'important'); if (!this.selected) { this.value_div.className = 'netdata-legend-value not-selected'; } else { this.value_div.className = 'netdata-legend-value selected'; } } this.enabled = true; this.setHandler(); }; dimensionStatus.prototype.setHandler = function () { if (!this.enabled) { return; } let ds = this; // this.name_div.onmousedown = this.value_div.onmousedown = function(e) { this.name_div.onclick = this.value_div.onclick = function (e) { e.preventDefault(); if (ds.isSelected()) { // this is selected if (e.shiftKey || e.ctrlKey) { // control or shift key is pressed -> unselect this (except is none will remain selected, in which case select all) ds.unselect(); if (ds.parent.countSelected() === 0) { ds.parent.selectAll(); } } else { // no key is pressed -> select only this (except if it is the only selected already, in which case select all) if (ds.parent.countSelected() === 1) { ds.parent.selectAll(); } else { ds.parent.selectNone(); ds.select(); } } } else { // this is not selected if (e.shiftKey || e.ctrlKey) { // control or shift key is pressed -> select this too ds.select(); } else { // no key is pressed -> select only this ds.parent.selectNone(); ds.select(); } } ds.parent.state.redrawChart(); } }; dimensionStatus.prototype.select = function () { if (!this.enabled) { return; } this.name_div.className = 'netdata-legend-name selected'; this.value_div.className = 'netdata-legend-value selected'; this.selected = true; }; dimensionStatus.prototype.unselect = function () { if (!this.enabled) { return; } this.name_div.className = 'netdata-legend-name not-selected'; this.value_div.className = 'netdata-legend-value hidden'; this.selected = false; }; dimensionStatus.prototype.isSelected = function () { // return(this.enabled === true && this.selected === true); return this.enabled && this.selected; }; // ---------------------------------------------------------------------------------------------------------------- let dimensionsVisibility = function (state) { this.state = state; this.len = 0; this.dimensions = {}; this.selected_count = 0; this.unselected_count = 0; }; dimensionsVisibility.prototype.dimensionAdd = function (label, name_div, value_div, color) { if (typeof this.dimensions[label] === 'undefined') { this.len++; this.dimensions[label] = new dimensionStatus(this, label, name_div, value_div, color); } else { this.dimensions[label].setOptions(name_div, value_div, color); } return this.dimensions[label]; }; dimensionsVisibility.prototype.dimensionGet = function (label) { return this.dimensions[label]; }; dimensionsVisibility.prototype.invalidateAll = function () { let keys = Object.keys(this.dimensions); let len = keys.length; while (len--) { this.dimensions[keys[len]].invalidate(); } }; dimensionsVisibility.prototype.selectAll = function () { let keys = Object.keys(this.dimensions); let len = keys.length; while (len--) { this.dimensions[keys[len]].select(); } }; dimensionsVisibility.prototype.countSelected = function () { let selected = 0; let keys = Object.keys(this.dimensions); let len = keys.length; while (len--) { if (this.dimensions[keys[len]].isSelected()) { selected++; } } return selected; }; dimensionsVisibility.prototype.selectNone = function () { let keys = Object.keys(this.dimensions); let len = keys.length; while (len--) { this.dimensions[keys[len]].unselect(); } }; dimensionsVisibility.prototype.selected2BooleanArray = function (array) { let ret = []; this.selected_count = 0; this.unselected_count = 0; let len = array.length; while (len--) { let ds = this.dimensions[array[len]]; if (typeof ds === 'undefined') { // console.log(array[i] + ' is not found'); ret.unshift(false); } else if (ds.isSelected()) { ret.unshift(true); this.selected_count++; } else { ret.unshift(false); this.unselected_count++; } } if (this.selected_count === 0 && this.unselected_count !== 0) { this.selectAll(); return this.selected2BooleanArray(array); } return ret; }; // ---------------------------------------------------------------------------------------------------------------- // date/time conversion NETDATA.dateTime = { using_timezone: false, // these are the old netdata functions // we fallback to these, if the new ones fail localeDateStringNative: function (d) { return d.toLocaleDateString(); }, localeTimeStringNative: function (d) { return d.toLocaleTimeString(); }, xAxisTimeStringNative: function (d) { return NETDATA.zeropad(d.getHours()) + ":" + NETDATA.zeropad(d.getMinutes()) + ":" + NETDATA.zeropad(d.getSeconds()); }, // initialize the new date/time conversion // functions. // if this fails, we fallback to the above init: function (timezone) { //console.log('init with timezone: ' + timezone); // detect browser timezone try { NETDATA.options.browser_timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; } catch (e) { console.log('failed to detect browser timezone: ' + e.toString()); NETDATA.options.browser_timezone = 'cannot-detect-it'; } let ret = false; try { let dateOptions = { localeMatcher: 'best fit', formatMatcher: 'best fit', weekday: 'short', year: 'numeric', month: 'short', day: '2-digit' }; let timeOptions = { localeMatcher: 'best fit', hour12: false, formatMatcher: 'best fit', hour: '2-digit', minute: '2-digit', second: '2-digit' }; let xAxisOptions = { localeMatcher: 'best fit', hour12: false, formatMatcher: 'best fit', hour: '2-digit', minute: '2-digit', second: '2-digit' }; if (typeof timezone === 'string' && timezone !== '' && timezone !== 'default') { dateOptions.timeZone = timezone; timeOptions.timeZone = timezone; timeOptions.timeZoneName = 'short'; xAxisOptions.timeZone = timezone; this.using_timezone = true; } else { timezone = 'default'; this.using_timezone = false; } this.dateFormat = new Intl.DateTimeFormat(navigator.language, dateOptions); this.timeFormat = new Intl.DateTimeFormat(navigator.language, timeOptions); this.xAxisFormat = new Intl.DateTimeFormat(navigator.language, xAxisOptions); this.localeDateString = function (d) { return this.dateFormat.format(d); }; this.localeTimeString = function (d) { return this.timeFormat.format(d); }; this.xAxisTimeString = function (d) { return this.xAxisFormat.format(d); }; //let d = new Date(); //let t = this.dateFormat.format(d) + ' ' + this.timeFormat.format(d) + ' ' + this.xAxisFormat.format(d); ret = true; } catch (e) { console.log('Cannot setup Date/Time formatting: ' + e.toString()); timezone = 'default'; this.localeDateString = this.localeDateStringNative; this.localeTimeString = this.localeTimeStringNative; this.xAxisTimeString = this.xAxisTimeStringNative; this.using_timezone = false; ret = false; } // save it //console.log('init setOption timezone: ' + timezone); NETDATA.setOption('timezone', timezone); return ret; } }; NETDATA.dateTime.init(NETDATA.options.current.timezone); // ---------------------------------------------------------------------------------------------------------------- // global selection sync NETDATA.globalSelectionSync = { state: null, dontSyncBefore: 0, last_t: 0, slaves: [], timeoutId: undefined, globalReset: function () { this.stop(); this.state = null; this.dontSyncBefore = 0; this.last_t = 0; this.slaves = []; this.timeoutId = undefined; }, active: function () { return (this.state !== null); }, // return true if global selection sync can be enabled now enabled: function () { // console.log('enabled()'); // can we globally apply selection sync? if (!NETDATA.options.current.sync_selection) { return false; } return (this.dontSyncBefore <= Date.now()); }, // set the global selection sync master setMaster: function (state) { if (!this.enabled()) { this.stop(); return; } if (this.state === state) { return; } if (this.state !== null) { this.stop(); } if (NETDATA.options.debug.globalSelectionSync) { console.log('globalSelectionSync.setMaster(' + state.id + ')'); } state.selected = true; this.state = state; this.last_t = 0; // find all slaves let targets = NETDATA.intersectionObserver.targets(); this.slaves = []; let len = targets.length; while (len--) { let st = targets[len]; if (this.state !== st && st.globalSelectionSyncIsEligible()) { this.slaves.push(st); } } // this.delay(100); }, // stop global selection sync stop: function () { if (this.state !== null) { if (NETDATA.options.debug.globalSelectionSync) { console.log('globalSelectionSync.stop()'); } let len = this.slaves.length; while (len--) { this.slaves[len].clearSelection(); } this.state.clearSelection(); this.last_t = 0; this.slaves = []; this.state = null; } }, // delay global selection sync for some time delay: function (ms) { if (NETDATA.options.current.sync_selection) { // if (NETDATA.options.debug.globalSelectionSync === true) { if (NETDATA.options.debug.globalSelectionSync) { console.log('globalSelectionSync.delay()'); } if (typeof ms === 'number') { this.dontSyncBefore = Date.now() + ms; } else { this.dontSyncBefore = Date.now() + NETDATA.options.current.sync_selection_delay; } } }, __syncSlaves: function () { // if (NETDATA.globalSelectionSync.enabled() === true) { if (NETDATA.globalSelectionSync.enabled()) { // if (NETDATA.options.debug.globalSelectionSync === true) if (NETDATA.options.debug.globalSelectionSync) { console.log('globalSelectionSync.__syncSlaves()'); } let t = NETDATA.globalSelectionSync.last_t; let len = NETDATA.globalSelectionSync.slaves.length; while (len--) { NETDATA.globalSelectionSync.slaves[len].setSelection(t); } this.timeoutId = undefined; } }, // sync all the visible charts to the given time // this is to be called from the chart libraries sync: function (state, t) { // if (NETDATA.options.current.sync_selection === true) { if (NETDATA.options.current.sync_selection) { // if (NETDATA.options.debug.globalSelectionSync === true) if (NETDATA.options.debug.globalSelectionSync) { console.log('globalSelectionSync.sync(' + state.id + ', ' + t.toString() + ')'); } this.setMaster(state); if (t === this.last_t) { return; } this.last_t = t; if (state.foreignElementSelection !== null) { state.foreignElementSelection.innerText = NETDATA.dateTime.localeDateString(t) + ' ' + NETDATA.dateTime.localeTimeString(t); } if (this.timeoutId) { NETDATA.timeout.clear(this.timeoutId); } this.timeoutId = NETDATA.timeout.set(this.__syncSlaves, 0); } } }; NETDATA.intersectionObserver = { observer: null, visible_targets: [], options: { root: null, rootMargin: "0px", threshold: null }, enabled: function () { return this.observer !== null; }, globalReset: function () { if (this.observer !== null) { this.visible_targets = []; this.observer.disconnect(); this.init(); } }, targets: function () { if (this.enabled() && this.visible_targets.length > 0) { return this.visible_targets; } else { return NETDATA.options.targets; } }, switchChartVisibility: function () { let old = this.__visibilityRatioOld; if (old !== this.__visibilityRatio) { if (old === 0 && this.__visibilityRatio > 0) { this.unhideChart(); } else if (old > 0 && this.__visibilityRatio === 0) { this.hideChart(); } this.__visibilityRatioOld = this.__visibilityRatio; } }, handler: function (entries, observer) { entries.forEach(function (entry) { let state = NETDATA.chartState(entry.target); let idx; if (entry.intersectionRatio > 0) { idx = NETDATA.intersectionObserver.visible_targets.indexOf(state); if (idx === -1) { if (NETDATA.scrollUp) { NETDATA.intersectionObserver.visible_targets.push(state); } else { NETDATA.intersectionObserver.visible_targets.unshift(state); } } else if (state.__visibilityRatio === 0) { state.log("was not visible until now, but was already in visible_targets"); } } else { idx = NETDATA.intersectionObserver.visible_targets.indexOf(state); if (idx !== -1) { NETDATA.intersectionObserver.visible_targets.splice(idx, 1); } else if (state.__visibilityRatio > 0) { state.log("was visible, but not found in visible_targets"); } } state.__visibilityRatio = entry.intersectionRatio; if (!NETDATA.options.current.async_on_scroll) { if (window.requestIdleCallback) { window.requestIdleCallback(function () { NETDATA.intersectionObserver.switchChartVisibility.call(state); }, {timeout: 100}); } else { NETDATA.intersectionObserver.switchChartVisibility.call(state); } } }); }, observe: function (state) { if (this.enabled()) { state.__visibilityRatioOld = 0; state.__visibilityRatio = 0; this.observer.observe(state.element); state.isVisible = function () { if (!NETDATA.options.current.update_only_visible) { return true; } NETDATA.intersectionObserver.switchChartVisibility.call(this); return this.__visibilityRatio > 0; } } }, init: function () { if (typeof netdataIntersectionObserver === 'undefined' || netdataIntersectionObserver) { try { this.observer = new IntersectionObserver(this.handler, this.options); } catch (e) { console.log("IntersectionObserver is not supported on this browser"); this.observer = null; } } //else { // console.log("IntersectionObserver is disabled"); //} } }; NETDATA.intersectionObserver.init(); // ---------------------------------------------------------------------------------------------------------------- // Our state object, where all per-chart values are stored let chartState = function (element) { this.element = element; // IMPORTANT: // all private functions should use 'that', instead of 'this' // Alternatively, you can use arrow functions (related issue #4514) let that = this; // ============================================================================================================ // ERROR HANDLING /* error() - private * show an error instead of the chart */ let error = (msg) => { let ret = true; if (typeof netdataErrorCallback === 'function') { ret = netdataErrorCallback('chart', this.id, msg); } if (ret) { this.element.innerHTML = this.id + ': ' + msg; this.enabled = false; this.current = this.pan; } }; // console logging this.log = function (msg) { console.log(this.id + ' (' + this.library_name + ' ' + this.uuid + '): ' + msg); }; this.debugLog = function (msg) { if (this.debug) { this.log(msg); } }; // ============================================================================================================ // EARLY INITIALIZATION // These are variables that should exist even if the chart is never to be rendered. // Be careful what you add here - there may be thousands of charts on the page. // GUID - a unique identifier for the chart this.uuid = NETDATA.guid(); // string - the name of chart this.id = NETDATA.dataAttribute(this.element, 'netdata', undefined); if (typeof this.id === 'undefined') { error("netdata elements need data-netdata"); return; } // string - the key for localStorage settings this.settings_id = NETDATA.dataAttribute(this.element, 'id', null); // the user given dimensions of the element this.width = NETDATA.dataAttribute(this.element, 'width', NETDATA.chartDefaults.width); this.height = NETDATA.dataAttribute(this.element, 'height', NETDATA.chartDefaults.height); this.height_original = this.height; if (this.settings_id !== null) { this.height = NETDATA.localStorageGet('chart_heights.' + this.settings_id, this.height, function (height) { // this is the callback that will be called // if and when the user resets all localStorage variables // to their defaults resizeChartToHeight(height); }); } // the chart library requested by the user this.library_name = NETDATA.dataAttribute(this.element, 'chart-library', NETDATA.chartDefaults.library); // check the requested library is available // we don't initialize it here - it will be initialized when // this chart will be first used if (typeof NETDATA.chartLibraries[this.library_name] === 'undefined') { NETDATA.error(402, this.library_name); error('chart library "' + this.library_name + '" is not found'); this.enabled = false; } else if (!NETDATA.chartLibraries[this.library_name].enabled) { NETDATA.error(403, this.library_name); error('chart library "' + this.library_name + '" is not enabled'); this.enabled = false; } else { this.library = NETDATA.chartLibraries[this.library_name]; } this.auto = { name: 'auto', autorefresh: true, force_update_at: 0, // the timestamp to force the update at force_before_ms: null, force_after_ms: null }; this.pan = { name: 'pan', autorefresh: false, force_update_at: 0, // the timestamp to force the update at force_before_ms: null, force_after_ms: null }; this.zoom = { name: 'zoom', autorefresh: false, force_update_at: 0, // the timestamp to force the update at force_before_ms: null, force_after_ms: null }; // this is a pointer to one of the sub-classes below // auto, pan, zoom this.current = this.auto; this.running = false; // boolean - true when the chart is being refreshed now this.enabled = true; // boolean - is the chart enabled for refresh? this.force_update_every = null; // number - overwrite the visualization update frequency of the chart this.tmp = {}; this.foreignElementBefore = null; this.foreignElementAfter = null; this.foreignElementDuration = null; this.foreignElementUpdateEvery = null; this.foreignElementSelection = null; // ============================================================================================================ // PRIVATE FUNCTIONS // reset the runtime status variables to their defaults const runtimeInit = () => { this.paused = false; // boolean - is the chart paused for any reason? this.selected = false; // boolean - is the chart shown a selection? this.chart_created = false; // boolean - is the library.create() been called? this.dom_created = false; // boolean - is the chart DOM been created? this.fetching_data = false; // boolean - true while we fetch data via ajax this.updates_counter = 0; // numeric - the number of refreshes made so far this.updates_since_last_unhide = 0; // numeric - the number of refreshes made since the last time the chart was unhidden this.updates_since_last_creation = 0; // numeric - the number of refreshes made since the last time the chart was created this.tm = { last_initialized: 0, // milliseconds - the timestamp it was last initialized last_dom_created: 0, // milliseconds - the timestamp its DOM was last created last_mode_switch: 0, // milliseconds - the timestamp it switched modes last_info_downloaded: 0, // milliseconds - the timestamp we downloaded the chart last_updated: 0, // the timestamp the chart last updated with data pan_and_zoom_seq: 0, // the sequence number of the global synchronization // between chart. // Used with NETDATA.globalPanAndZoom.seq last_visible_check: 0, // the time we last checked if it is visible last_resized: 0, // the time the chart was resized last_hidden: 0, // the time the chart was hidden last_unhidden: 0, // the time the chart was unhidden last_autorefreshed: 0 // the time the chart was last refreshed }; this.data = null; // the last data as downloaded from the netdata server this.data_url = 'invalid://'; // string - the last url used to update the chart this.data_points = 0; // number - the number of points returned from netdata this.data_after = 0; // milliseconds - the first timestamp of the data this.data_before = 0; // milliseconds - the last timestamp of the data this.data_update_every = 0; // milliseconds - the frequency to update the data this.tmp = {}; // members that can be destroyed to save memory }; // initialize all the variables that are required for the chart to be rendered const lateInitialization = () => { if (typeof this.host !== 'undefined') { return; } // string - the netdata server URL, without any path this.host = NETDATA.dataAttribute(this.element, 'host', NETDATA.serverDefault); // make sure the host does not end with / // all netdata API requests use absolute paths while (this.host.slice(-1) === '/') { this.host = this.host.substring(0, this.host.length - 1); } // string - the grouping method requested by the user this.method = NETDATA.dataAttribute(this.element, 'method', NETDATA.chartDefaults.method); this.gtime = NETDATA.dataAttribute(this.element, 'gtime', 0); // the time-range requested by the user this.after = NETDATA.dataAttribute(this.element, 'after', NETDATA.chartDefaults.after); this.before = NETDATA.dataAttribute(this.element, 'before', NETDATA.chartDefaults.before); // the pixels per point requested by the user this.pixels_per_point = NETDATA.dataAttribute(this.element, 'pixels-per-point', 1); this.points = NETDATA.dataAttribute(this.element, 'points', null); // the forced update_every this.force_update_every = NETDATA.dataAttribute(this.element, 'update-every', null); if (typeof this.force_update_every !== 'number' || this.force_update_every <= 1) { if (this.force_update_every !== null) { this.log('ignoring invalid value of property data-update-every'); } this.force_update_every = null; } else { this.force_update_every *= 1000; } // the dimensions requested by the user this.dimensions = NETDATA.encodeURIComponent(NETDATA.dataAttribute(this.element, 'dimensions', null)); this.title = NETDATA.dataAttribute(this.element, 'title', null); // the title of the chart this.units = NETDATA.dataAttribute(this.element, 'units', null); // the units of the chart dimensions this.units_desired = NETDATA.dataAttribute(this.element, 'desired-units', NETDATA.options.current.units); // the units of the chart dimensions this.units_current = this.units; this.units_common = NETDATA.dataAttribute(this.element, 'common-units', null); // additional options to pass to netdata this.append_options = NETDATA.encodeURIComponent(NETDATA.dataAttribute(this.element, 'append-options', null)); // override options to pass to netdata this.override_options = NETDATA.encodeURIComponent(NETDATA.dataAttribute(this.element, 'override-options', null)); this.debug = NETDATA.dataAttributeBoolean(this.element, 'debug', false); this.value_decimal_detail = -1; let d = NETDATA.dataAttribute(this.element, 'decimal-digits', -1); if (typeof d === 'number') { this.value_decimal_detail = d; } else if (typeof d !== 'undefined') { this.log('ignoring decimal-digits value: ' + d.toString()); } // if we need to report the rendering speed // find the element that needs to be updated let refresh_dt_element_name = NETDATA.dataAttribute(this.element, 'dt-element-name', null); // string - the element to print refresh_dt_ms if (refresh_dt_element_name !== null) { this.refresh_dt_element = document.getElementById(refresh_dt_element_name) || null; } else { this.refresh_dt_element = null; } this.dimensions_visibility = new dimensionsVisibility(that); this.netdata_first = 0; // milliseconds - the first timestamp in netdata this.netdata_last = 0; // milliseconds - the last timestamp in netdata this.requested_after = null; // milliseconds - the timestamp of the request after param this.requested_before = null; // milliseconds - the timestamp of the request before param this.requested_padding = null; this.view_after = 0; this.view_before = 0; this.refresh_dt_ms = 0; // milliseconds - the time the last refresh took // how many retries we have made to load chart data from the server this.retries_on_data_failures = 0; // color management this.colors = null; this.colors_assigned = null; this.colors_available = null; this.colors_custom = null; this.element_message = null; // the element already created by the user this.element_chart = null; // the element with the chart this.element_legend = null; // the element with the legend of the chart (if created by us) this.element_legend_childs = { content: null, hidden: null, title_date: null, title_time: null, title_units: null, perfect_scroller: null, // the container to apply perfect scroller to series: null }; this.chart_url = null; // string - the url to download chart info this.chart = null; // object - the chart as downloaded from the server const getForeignElementById = (opt) => { let id = NETDATA.dataAttribute(this.element, opt, null); if (id === null) { //this.log('option "' + opt + '" is undefined'); return null; } let el = document.getElementById(id); if (typeof el === 'undefined') { this.log('cannot find an element with name "' + id.toString() + '"'); return null; } return el; }; this.foreignElementBefore = getForeignElementById('show-before-at'); this.foreignElementAfter = getForeignElementById('show-after-at'); this.foreignElementDuration = getForeignElementById('show-duration-at'); this.foreignElementUpdateEvery = getForeignElementById('show-update-every-at'); this.foreignElementSelection = getForeignElementById('show-selection-at'); }; const destroyDOM = () => { if (!this.enabled) { return; } if (this.debug) { this.log('destroyDOM()'); } // this.element.className = 'netdata-message icon'; // this.element.innerHTML = ' netdata'; this.element.innerHTML = ''; this.element_message = null; this.element_legend = null; this.element_chart = null; this.element_legend_childs.series = null; this.chart_created = false; this.dom_created = false; this.tm.last_resized = 0; this.tm.last_dom_created = 0; }; let createDOM = () => { if (!this.enabled) { return; } lateInitialization(); destroyDOM(); if (this.debug) { this.log('createDOM()'); } this.element_message = document.createElement('div'); this.element_message.className = 'netdata-message icon hidden'; this.element.appendChild(this.element_message); this.dom_created = true; this.chart_created = false; this.tm.last_dom_created = this.tm.last_resized = Date.now(); showLoading(); }; const initDOM = () => { this.element.className = this.library.container_class(that); if (typeof(this.width) === 'string') { this.element.style.width = this.width; } else if (typeof(this.width) === 'number') { this.element.style.width = this.width.toString() + 'px'; } if (typeof(this.library.aspect_ratio) === 'undefined') { if (typeof(this.height) === 'string') { this.element.style.height = this.height; } else if (typeof(this.height) === 'number') { this.element.style.height = this.height.toString() + 'px'; } } if (NETDATA.chartDefaults.min_width !== null) { this.element.style.min_width = NETDATA.chartDefaults.min_width; } }; const invisibleSearchableText = () => { return '' + this.id + ''; }; /* init() private * initialize state variables * destroy all (possibly) created state elements * create the basic DOM for a chart */ const init = (opt) => { if (!this.enabled) { return; } runtimeInit(); this.element.innerHTML = invisibleSearchableText(); this.tm.last_initialized = Date.now(); this.setMode('auto'); if (opt !== 'fast') { if (this.isVisible(true) || opt === 'force') { createDOM(); } } }; const maxMessageFontSize = () => { let screenHeight = screen.height; let el = this.element; // normally we want a font size, as tall as the element let h = el.clientHeight; // but give it some air, 20% let's say, or 5 pixels min let lost = Math.max(h * 0.2, 5); h -= lost; // center the text, vertically let paddingTop = (lost - 5) / 2; // but check the width too // it should fit 10 characters in it let w = el.clientWidth / 10; if (h > w) { paddingTop += (h - w) / 2; h = w; } // and don't make it too huge // 5% of the screen size is good if (h > screenHeight / 20) { paddingTop += (h - (screenHeight / 20)) / 2; h = screenHeight / 20; } // set it this.element_message.style.fontSize = h.toString() + 'px'; this.element_message.style.paddingTop = paddingTop.toString() + 'px'; }; const showMessageIcon = (icon) => { this.element_message.innerHTML = icon; maxMessageFontSize(); $(this.element_message).removeClass('hidden'); this.tmp.___messageHidden___ = undefined; }; const hideMessage = () => { if (typeof this.tmp.___messageHidden___ === 'undefined') { this.tmp.___messageHidden___ = true; $(this.element_message).addClass('hidden'); } }; const showRendering = () => { let icon; if (this.chart !== null) { if (this.chart.chart_type === 'line') { icon = NETDATA.icons.lineChart; } else { icon = NETDATA.icons.areaChart; } } else { icon = NETDATA.icons.noChart; } showMessageIcon(icon + ' netdata' + invisibleSearchableText()); }; const showLoading = () => { if (!this.chart_created) { showMessageIcon(NETDATA.icons.loading + ' netdata'); return true; } return false; }; const isHidden = () => { return (typeof this.tmp.___chartIsHidden___ !== 'undefined'); }; // hide the chart, when it is not visible - called from isVisible() this.hideChart = function () { // hide it, if it is not already hidden if (isHidden()) { return; } if (this.chart_created) { if (NETDATA.options.current.show_help) { if (this.element_legend_childs.toolbox !== null) { if (this.debug) { this.log('hideChart(): hidding legend popovers'); } $(this.element_legend_childs.toolbox_left).popover('hide'); $(this.element_legend_childs.toolbox_reset).popover('hide'); $(this.element_legend_childs.toolbox_right).popover('hide'); $(this.element_legend_childs.toolbox_zoomin).popover('hide'); $(this.element_legend_childs.toolbox_zoomout).popover('hide'); } if (this.element_legend_childs.resize_handler !== null) { $(this.element_legend_childs.resize_handler).popover('hide'); } if (this.element_legend_childs.content !== null) { $(this.element_legend_childs.content).popover('hide'); } } if (NETDATA.options.current.destroy_on_hide) { if (this.debug) { this.log('hideChart(): initializing chart'); } // we should destroy it init('force'); } else { if (this.debug) { this.log('hideChart(): hiding chart'); } showRendering(); this.element_chart.style.display = 'none'; this.element.style.willChange = 'auto'; if (this.element_legend !== null) { this.element_legend.style.display = 'none'; } if (this.element_legend_childs.toolbox !== null) { this.element_legend_childs.toolbox.style.display = 'none'; } if (this.element_legend_childs.resize_handler !== null) { this.element_legend_childs.resize_handler.style.display = 'none'; } this.tm.last_hidden = Date.now(); // de-allocate data // This works, but I not sure there are no corner cases somewhere // so it is commented - if the user has memory issues he can // set Destroy on Hide for all charts // this.data = null; } } this.tmp.___chartIsHidden___ = true; }; // unhide the chart, when it is visible - called from isVisible() this.unhideChart = function () { if (!isHidden()) { return; } this.tmp.___chartIsHidden___ = undefined; this.updates_since_last_unhide = 0; if (!this.chart_created) { if (this.debug) { this.log('unhideChart(): initializing chart'); } // we need to re-initialize it, to show our background // logo in bootstrap tabs, until the chart loads init('force'); } else { if (this.debug) { this.log('unhideChart(): unhiding chart'); } this.element.style.willChange = 'transform'; this.tm.last_unhidden = Date.now(); this.element_chart.style.display = ''; if (this.element_legend !== null) { this.element_legend.style.display = ''; } if (this.element_legend_childs.toolbox !== null) { this.element_legend_childs.toolbox.style.display = ''; } if (this.element_legend_childs.resize_handler !== null) { this.element_legend_childs.resize_handler.style.display = ''; } resizeChart(); hideMessage(); } if (this.__redraw_on_unhide) { if (this.debug) { this.log("redrawing chart on unhide"); } this.__redraw_on_unhide = undefined; this.redrawChart(); } }; const canBeRendered = (uncached_visibility) => { if (this.debug) { this.log('canBeRendered() called'); } if (!NETDATA.options.current.update_only_visible) { return true; } let ret = ( ( NETDATA.options.page_is_visible || NETDATA.options.current.stop_updates_when_focus_is_lost === false || this.updates_since_last_unhide === 0 ) && isHidden() === false && this.isVisible(uncached_visibility) ); if (this.debug) { this.log('canBeRendered(): ' + ret); } return ret; }; // https://github.com/petkaantonov/bluebird/wiki/Optimization-killers const callChartLibraryUpdateSafely = (data) => { let status; // we should not do this here // if we prevent rendering the chart then: // 1. globalSelectionSync will be wrong // 2. globalPanAndZoom will be wrong //if (canBeRendered(true) === false) // return false; if (NETDATA.options.fake_chart_rendering) { return true; } this.updates_counter++; this.updates_since_last_unhide++; this.updates_since_last_creation++; if (NETDATA.options.debug.chart_errors) { status = this.library.update(that, data); } else { try { status = this.library.update(that, data); } catch (err) { status = false; } } if (!status) { error('chart failed to be updated as ' + this.library_name); return false; } return true; }; // https://github.com/petkaantonov/bluebird/wiki/Optimization-killers const callChartLibraryCreateSafely = (data) => { let status; // we should not do this here // if we prevent rendering the chart then: // 1. globalSelectionSync will be wrong // 2. globalPanAndZoom will be wrong //if (canBeRendered(true) === false) // return false; if (NETDATA.options.fake_chart_rendering) { return true; } this.updates_counter++; this.updates_since_last_unhide++; this.updates_since_last_creation++; if (NETDATA.options.debug.chart_errors) { status = this.library.create(that, data); } else { try { status = this.library.create(that, data); } catch (err) { status = false; } } if (!status) { error('chart failed to be created as ' + this.library_name); return false; } this.chart_created = true; this.updates_since_last_creation = 0; return true; }; // ---------------------------------------------------------------------------------------------------------------- // Chart Resize // resizeChart() - private // to be called just before the chart library to make sure that // a properly sized dom is available const resizeChart = () => { if (this.tm.last_resized < NETDATA.options.last_page_resize) { if (!this.chart_created) { return; } if (this.needsRecreation()) { if (this.debug) { this.log('resizeChart(): initializing chart'); } init('force'); } else if (typeof this.library.resize === 'function') { if (this.debug) { this.log('resizeChart(): resizing chart'); } this.library.resize(that); if (this.element_legend_childs.perfect_scroller !== null) { Ps.update(this.element_legend_childs.perfect_scroller); } maxMessageFontSize(); } this.tm.last_resized = Date.now(); } }; // this is the actual chart resize algorithm // it will: // - resize the entire container // - update the internal states // - resize the chart as the div changes height // - update the scrollbar of the legend const resizeChartToHeight = (h) => { // console.log(h); this.element.style.height = h; if (this.settings_id !== null) { NETDATA.localStorageSet('chart_heights.' + this.settings_id, h); } let now = Date.now(); NETDATA.options.last_page_scroll = now; NETDATA.options.auto_refresher_stop_until = now + NETDATA.options.current.stop_updates_while_resizing; // force a resize this.tm.last_resized = 0; resizeChart(); }; this.resizeForPrint = function () { if (typeof this.element_legend_childs !== 'undefined' && this.element_legend_childs.perfect_scroller !== null) { let current = this.element.clientHeight; let optimal = current + this.element_legend_childs.perfect_scroller.scrollHeight - this.element_legend_childs.perfect_scroller.clientHeight; if (optimal > current) { // this.log('resized'); this.element.style.height = optimal + 'px'; this.library.resize(this); } } }; this.resizeHandler = function (e) { e.preventDefault(); if (typeof this.event_resize === 'undefined' || this.event_resize.chart_original_w === 'undefined' || this.event_resize.chart_original_h === 'undefined') { this.event_resize = { chart_original_w: this.element.clientWidth, chart_original_h: this.element.clientHeight, last: 0 }; } if (e.type === 'touchstart') { this.event_resize.mouse_start_x = e.touches.item(0).pageX; this.event_resize.mouse_start_y = e.touches.item(0).pageY; } else { this.event_resize.mouse_start_x = e.clientX; this.event_resize.mouse_start_y = e.clientY; } this.event_resize.chart_start_w = this.element.clientWidth; this.event_resize.chart_start_h = this.element.clientHeight; this.event_resize.chart_last_w = this.element.clientWidth; this.event_resize.chart_last_h = this.element.clientHeight; let now = Date.now(); if (now - this.event_resize.last <= NETDATA.options.current.double_click_speed && this.element_legend_childs.perfect_scroller !== null) { // double click / double tap event // console.dir(this.element_legend_childs.content); // console.dir(this.element_legend_childs.perfect_scroller); // the optimal height of the chart // showing the entire legend let optimal = this.event_resize.chart_last_h + this.element_legend_childs.perfect_scroller.scrollHeight - this.element_legend_childs.perfect_scroller.clientHeight; // if we are not optimal, be optimal if (this.event_resize.chart_last_h !== optimal) { // 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()); resizeChartToHeight(optimal.toString() + 'px'); } // else if the current height is not the original/saved height // reset to the original/saved height else if (this.event_resize.chart_last_h !== this.event_resize.chart_original_h) { // 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()); resizeChartToHeight(this.event_resize.chart_original_h.toString() + 'px'); } // else if the current height is not the internal default height // reset to the internal default height else if ((this.event_resize.chart_last_h.toString() + 'px') !== this.height_original) { // 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()); resizeChartToHeight(this.height_original.toString()); } // else if the current height is not the firstchild's clientheight // resize to it else if (typeof this.element_legend_childs.perfect_scroller.firstChild !== 'undefined') { let parent_rect = this.element.getBoundingClientRect(); let content_rect = this.element_legend_childs.perfect_scroller.firstElementChild.getBoundingClientRect(); let wanted = content_rect.top - parent_rect.top + this.element_legend_childs.perfect_scroller.firstChild.clientHeight + 18; // 15 = toolbox + 3 space // console.log(parent_rect); // console.log(content_rect); // console.log(wanted); // 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' ); if (this.event_resize.chart_last_h !== wanted) { resizeChartToHeight(wanted.toString() + 'px'); } } } else { this.event_resize.last = now; // process movement event document.onmousemove = document.ontouchmove = this.element_legend_childs.resize_handler.onmousemove = this.element_legend_childs.resize_handler.ontouchmove = function (e) { let y = null; switch (e.type) { case 'mousemove': y = e.clientY; break; case 'touchmove': y = e.touches.item(e.touches - 1).pageY; break; } if (y !== null) { let newH = that.event_resize.chart_start_h + y - that.event_resize.mouse_start_y; if (newH >= 70 && newH !== that.event_resize.chart_last_h) { resizeChartToHeight(newH.toString() + 'px'); that.event_resize.chart_last_h = newH; } } }; // process end event document.onmouseup = document.ontouchend = this.element_legend_childs.resize_handler.onmouseup = this.element_legend_childs.resize_handler.ontouchend = function (e) { void(e); // remove all the hooks document.onmouseup = document.onmousemove = document.ontouchmove = document.ontouchend = that.element_legend_childs.resize_handler.onmousemove = that.element_legend_childs.resize_handler.ontouchmove = that.element_legend_childs.resize_handler.onmouseout = that.element_legend_childs.resize_handler.onmouseup = that.element_legend_childs.resize_handler.ontouchend = null; // allow auto-refreshes NETDATA.options.auto_refresher_stop_until = 0; }; } }; const noDataToShow = () => { showMessageIcon(NETDATA.icons.noData + ' empty'); this.legendUpdateDOM(); this.tm.last_autorefreshed = Date.now(); // this.data_update_every = 30 * 1000; //this.element_chart.style.display = 'none'; //if (this.element_legend !== null) this.element_legend.style.display = 'none'; //this.tmp.___chartIsHidden___ = true; }; // ============================================================================================================ // PUBLIC FUNCTIONS this.error = function (msg) { error(msg); }; this.setMode = function (m) { if (this.current !== null && this.current.name === m) { return; } if (m === 'auto') { this.current = this.auto; } else if (m === 'pan') { this.current = this.pan; } else if (m === 'zoom') { this.current = this.zoom; } else { this.current = this.auto; } this.current.force_update_at = 0; this.current.force_before_ms = null; this.current.force_after_ms = null; this.tm.last_mode_switch = Date.now(); }; // ---------------------------------------------------------------------------------------------------------------- // global selection sync for slaves // can the chart participate to the global selection sync as a slave? this.globalSelectionSyncIsEligible = function () { return ( this.enabled && this.library !== null && typeof this.library.setSelection === 'function' && this.isVisible() && this.chart_created ); }; this.setSelection = function (t) { if (typeof this.library.setSelection === 'function') { // this.selected = this.library.setSelection(this, t) === true; this.selected = this.library.setSelection(this, t); } else { this.selected = true; } if (this.selected && this.debug) { this.log('selection set to ' + t.toString()); } if (this.foreignElementSelection !== null) { this.foreignElementSelection.innerText = NETDATA.dateTime.localeDateString(t) + ' ' + NETDATA.dateTime.localeTimeString(t); } return this.selected; }; this.clearSelection = function () { if (this.selected) { if (typeof this.library.clearSelection === 'function') { this.selected = (this.library.clearSelection(this) !== true); } else { this.selected = false; } if (this.selected === false && this.debug) { this.log('selection cleared'); } if (this.foreignElementSelection !== null) { this.foreignElementSelection.innerText = ''; } this.legendReset(); } return this.selected; }; // ---------------------------------------------------------------------------------------------------------------- // find if a timestamp (ms) is shown in the current chart this.timeIsVisible = function (t) { return (t >= this.data_after && t <= this.data_before); }; this.calculateRowForTime = function (t) { if (!this.timeIsVisible(t)) { return -1; } return Math.floor((t - this.data_after) / this.data_update_every); }; // ---------------------------------------------------------------------------------------------------------------- this.pauseChart = function () { if (!this.paused) { if (this.debug) { this.log('pauseChart()'); } this.paused = true; } }; this.unpauseChart = function () { if (this.paused) { if (this.debug) { this.log('unpauseChart()'); } this.paused = false; } }; this.resetChart = function (dontClearMaster, dontUpdate) { if (this.debug) { this.log('resetChart(' + dontClearMaster + ', ' + dontUpdate + ') called'); } if (typeof dontClearMaster === 'undefined') { dontClearMaster = false; } if (typeof dontUpdate === 'undefined') { dontUpdate = false; } if (dontClearMaster !== true && NETDATA.globalPanAndZoom.isMaster(this)) { if (this.debug) { this.log('resetChart() diverting to clearMaster().'); } // this will call us back with master === true NETDATA.globalPanAndZoom.clearMaster(); return; } this.clearSelection(); this.tm.pan_and_zoom_seq = 0; this.setMode('auto'); this.current.force_update_at = 0; this.current.force_before_ms = null; this.current.force_after_ms = null; this.tm.last_autorefreshed = 0; this.paused = false; this.selected = false; this.enabled = true; // this.debug = false; // do not update the chart here // or the chart will flip-flop when it is the master // of a selection sync and another chart becomes // the new master if (dontUpdate !== true && this.isVisible()) { this.updateChart(); } }; this.updateChartPanOrZoom = function (after, before, callback) { let logme = 'updateChartPanOrZoom(' + after + ', ' + before + '): '; let ret = true; NETDATA.globalPanAndZoom.delay(); NETDATA.globalSelectionSync.delay(); if (this.debug) { this.log(logme); } if (before < after) { if (this.debug) { this.log(logme + 'flipped parameters, rejecting it.'); } return false; } if (typeof this.fixed_min_duration === 'undefined') { this.fixed_min_duration = Math.round((this.chartWidth() / 30) * this.chart.update_every * 1000); } let min_duration = this.fixed_min_duration; let current_duration = Math.round(this.view_before - this.view_after); // round the numbers after = Math.round(after); before = Math.round(before); // align them to update_every // stretching them further away after -= after % this.data_update_every; before += this.data_update_every - (before % this.data_update_every); // the final wanted duration let wanted_duration = before - after; // to allow panning, accept just a point below our minimum if ((current_duration - this.data_update_every) < min_duration) { min_duration = current_duration - this.data_update_every; } // we do it, but we adjust to minimum size and return false // when the wanted size is below the current and the minimum // and we zoom if (wanted_duration < current_duration && wanted_duration < min_duration) { if (this.debug) { this.log(logme + 'too small: min_duration: ' + (min_duration / 1000).toString() + ', wanted: ' + (wanted_duration / 1000).toString()); } min_duration = this.fixed_min_duration; let dt = (min_duration - wanted_duration) / 2; before += dt; after -= dt; wanted_duration = before - after; ret = false; } let tolerance = this.data_update_every * 2; let movement = Math.abs(before - this.view_before); if (Math.abs(current_duration - wanted_duration) <= tolerance && movement <= tolerance && ret) { if (this.debug) { 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); } return false; } if (this.current.name === 'auto') { this.log(logme + 'caller called me with mode: ' + this.current.name); this.setMode('pan'); } if (this.debug) { 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); } this.current.force_update_at = Date.now() + NETDATA.options.current.pan_and_zoom_delay; this.current.force_after_ms = after; this.current.force_before_ms = before; NETDATA.globalPanAndZoom.setMaster(this, after, before); if (ret && typeof callback === 'function') { callback(); } return ret; }; this.updateChartPanOrZoomAsyncTimeOutId = undefined; this.updateChartPanOrZoomAsync = function (after, before, callback) { NETDATA.globalPanAndZoom.delay(); NETDATA.globalSelectionSync.delay(); if (!NETDATA.globalPanAndZoom.isMaster(this)) { this.pauseChart(); NETDATA.globalPanAndZoom.setMaster(this, after, before); // NETDATA.globalSelectionSync.stop(); NETDATA.globalSelectionSync.setMaster(this); } if (this.updateChartPanOrZoomAsyncTimeOutId) { NETDATA.timeout.clear(this.updateChartPanOrZoomAsyncTimeOutId); } NETDATA.timeout.set(function () { that.updateChartPanOrZoomAsyncTimeOutId = undefined; that.updateChartPanOrZoom(after, before, callback); }, 0); }; let _unitsConversionLastUnits = undefined; let _unitsConversionLastUnitsDesired = undefined; let _unitsConversionLastMin = undefined; let _unitsConversionLastMax = undefined; let _unitsConversion = function (value) { return value; }; this.unitsConversionSetup = function (min, max) { if (this.units !== _unitsConversionLastUnits || this.units_desired !== _unitsConversionLastUnitsDesired || min !== _unitsConversionLastMin || max !== _unitsConversionLastMax) { _unitsConversionLastUnits = this.units; _unitsConversionLastUnitsDesired = this.units_desired; _unitsConversionLastMin = min; _unitsConversionLastMax = max; _unitsConversion = NETDATA.unitsConversion.get(this.uuid, min, max, this.units, this.units_desired, this.units_common, function (units) { // console.log('switching units from ' + that.units.toString() + ' to ' + units.toString()); that.units_current = units; that.legendSetUnitsString(that.units_current); }); } }; let _legendFormatValueChartDecimalsLastMin = undefined; let _legendFormatValueChartDecimalsLastMax = undefined; let _legendFormatValueChartDecimals = -1; let _intlNumberFormat = null; this.legendFormatValueDecimalsFromMinMax = function (min, max) { if (min === _legendFormatValueChartDecimalsLastMin && max === _legendFormatValueChartDecimalsLastMax) { return; } this.unitsConversionSetup(min, max); if (_unitsConversion !== null) { min = _unitsConversion(min); max = _unitsConversion(max); if (typeof min !== 'number' || typeof max !== 'number') { return; } } _legendFormatValueChartDecimalsLastMin = min; _legendFormatValueChartDecimalsLastMax = max; let old = _legendFormatValueChartDecimals; if (this.data !== null && this.data.min === this.data.max) // it is a fixed number, let the visualizer decide based on the value { _legendFormatValueChartDecimals = -1; } else if (this.value_decimal_detail !== -1) // there is an override { _legendFormatValueChartDecimals = this.value_decimal_detail; } else { // ok, let's calculate the proper number of decimal points let delta; if (min === max) { delta = Math.abs(min); } else { delta = Math.abs(max - min); } if (delta > 1000) { _legendFormatValueChartDecimals = 0; } else if (delta > 10) { _legendFormatValueChartDecimals = 1; } else if (delta > 1) { _legendFormatValueChartDecimals = 2; } else if (delta > 0.1) { _legendFormatValueChartDecimals = 2; } else if (delta > 0.01) { _legendFormatValueChartDecimals = 4; } else if (delta > 0.001) { _legendFormatValueChartDecimals = 5; } else if (delta > 0.0001) { _legendFormatValueChartDecimals = 6; } else { _legendFormatValueChartDecimals = 7; } } if (_legendFormatValueChartDecimals !== old) { if (_legendFormatValueChartDecimals < 0) { _intlNumberFormat = null; } else { _intlNumberFormat = NETDATA.fastNumberFormat.get( _legendFormatValueChartDecimals, _legendFormatValueChartDecimals ); } } }; this.legendFormatValue = function (value) { if (typeof value !== 'number') { return '-'; } value = _unitsConversion(value); if (typeof value !== 'number') { return value; } if (_intlNumberFormat !== null) { return _intlNumberFormat.format(value); } let dmin, dmax; if (this.value_decimal_detail !== -1) { dmin = dmax = this.value_decimal_detail; } else { dmin = 0; let abs = (value < 0) ? -value : value; if (abs > 1000) { dmax = 0; } else if (abs > 10) { dmax = 1; } else if (abs > 1) { dmax = 2; } else if (abs > 0.1) { dmax = 2; } else if (abs > 0.01) { dmax = 4; } else if (abs > 0.001) { dmax = 5; } else if (abs > 0.0001) { dmax = 6; } else { dmax = 7; } } return NETDATA.fastNumberFormat.get(dmin, dmax).format(value); }; this.legendSetLabelValue = function (label, value) { let series = this.element_legend_childs.series[label]; if (typeof series === 'undefined') { return; } if (series.value === null && series.user === null) { return; } /* // this slows down firefox and edge significantly // since it requires to use innerHTML(), instead of innerText() // if the value has not changed, skip DOM update //if (series.last === value) return; let s, r; if (typeof value === 'number') { let v = Math.abs(value); s = r = this.legendFormatValue(value); if (typeof series.last === 'number') { if (v > series.last) s += ''; else if (v < series.last) s += ''; else s += ''; } else s += ''; series.last = v; } else { if (value === null) s = r = ''; else s = r = value; series.last = value; } */ let s = this.legendFormatValue(value); // caching: do not update the update to show the same value again if (s === series.last_shown_value) { return; } series.last_shown_value = s; if (series.value !== null) { series.value.innerText = s; } if (series.user !== null) { series.user.innerText = s; } }; this.legendSetDateString = function (date) { if (this.element_legend_childs.title_date !== null && date !== this.tmp.__last_shown_legend_date) { this.element_legend_childs.title_date.innerText = date; this.tmp.__last_shown_legend_date = date; } }; this.legendSetTimeString = function (time) { if (this.element_legend_childs.title_time !== null && time !== this.tmp.__last_shown_legend_time) { this.element_legend_childs.title_time.innerText = time; this.tmp.__last_shown_legend_time = time; } }; this.legendSetUnitsString = function (units) { if (this.element_legend_childs.title_units !== null && units !== this.tmp.__last_shown_legend_units) { this.element_legend_childs.title_units.innerText = units; this.tmp.__last_shown_legend_units = units; } }; this.legendSetDateLast = { ms: 0, date: undefined, time: undefined }; this.legendSetDate = function (ms) { if (typeof ms !== 'number') { this.legendShowUndefined(); return; } if (this.legendSetDateLast.ms !== ms) { let d = new Date(ms); this.legendSetDateLast.ms = ms; this.legendSetDateLast.date = NETDATA.dateTime.localeDateString(d); this.legendSetDateLast.time = NETDATA.dateTime.localeTimeString(d); } this.legendSetDateString(this.legendSetDateLast.date); this.legendSetTimeString(this.legendSetDateLast.time); this.legendSetUnitsString(this.units_current) }; this.legendShowUndefined = function () { this.legendSetDateString(this.legendPluginModuleString(false)); this.legendSetTimeString(this.chart.context.toString()); // this.legendSetUnitsString(' '); if (this.data && this.element_legend_childs.series !== null) { let labels = this.data.dimension_names; let i = labels.length; while (i--) { let label = labels[i]; if (typeof label === 'undefined' || typeof this.element_legend_childs.series[label] === 'undefined') { continue; } this.legendSetLabelValue(label, null); } } }; this.legendShowLatestValues = function () { if (this.chart === null) { return; } if (this.selected) { return; } if (this.data === null || this.element_legend_childs.series === null) { this.legendShowUndefined(); return; } let show_undefined = true; if (Math.abs(this.netdata_last - this.view_before) <= this.data_update_every) { show_undefined = false; } if (show_undefined) { this.legendShowUndefined(); return; } this.legendSetDate(this.view_before); let labels = this.data.dimension_names; let i = labels.length; while (i--) { let label = labels[i]; if (typeof label === 'undefined') { continue; } if (typeof this.element_legend_childs.series[label] === 'undefined') { continue; } this.legendSetLabelValue(label, this.data.view_latest_values[i]); } }; this.legendReset = function () { this.legendShowLatestValues(); }; // this should be called just ONCE per dimension per chart this.__chartDimensionColor = function (label) { let c = NETDATA.commonColors.get(this, label); // it is important to maintain a list of colors // for this chart only, since the chart library // uses this to assign colors to dimensions in the same // order the dimension are given to it this.colors.push(c); return c; }; this.chartPrepareColorPalette = function () { NETDATA.commonColors.refill(this); }; // get the ordered list of chart colors // this includes user defined colors this.chartCustomColors = function () { this.chartPrepareColorPalette(); let colors; if (this.colors_custom.length) { colors = this.colors_custom; } else { colors = this.colors; } if (this.debug) { this.log("chartCustomColors() returns:"); this.log(colors); } return colors; }; // get the ordered list of chart ASSIGNED colors // (this returns only the colors that have been // assigned to dimensions, prepended with any // custom colors defined) this.chartColors = function () { this.chartPrepareColorPalette(); if (this.debug) { this.log("chartColors() returns:"); this.log(this.colors); } return this.colors; }; this.legendPluginModuleString = function (withContext) { let str = ' '; let context = ''; if (typeof this.chart !== 'undefined') { if (withContext && typeof this.chart.context === 'string') { context = this.chart.context; } if (typeof this.chart.plugin === 'string' && this.chart.plugin !== '') { str = this.chart.plugin; if (str.endsWith(".plugin")) { str = str.substring(0, str.length - 7); } if (typeof this.chart.module === 'string' && this.chart.module !== '') { str += ':' + this.chart.module; } if (withContext && context !== '') { str += ', ' + context; } } else if (withContext && context !== '') { str = context; } } return str; }; this.legendResolutionTooltip = function () { if (!this.chart) { return ''; } let collected = this.chart.update_every; let viewed = (this.data) ? this.data.view_update_every : collected; if (collected === viewed) { return "resolution " + NETDATA.seconds4human(collected); } return "resolution " + NETDATA.seconds4human(viewed) + ", collected every " + NETDATA.seconds4human(collected); }; this.legendUpdateDOM = function () { let needed = false, dim, keys, len; // check that the legend DOM is up to date for the downloaded dimensions if (typeof this.element_legend_childs.series !== 'object' || this.element_legend_childs.series === null) { // this.log('the legend does not have any series - requesting legend update'); needed = true; } else if (this.data === null) { // this.log('the chart does not have any data - requesting legend update'); needed = true; } else if (typeof this.element_legend_childs.series.labels_key === 'undefined') { needed = true; } else { let labels = this.data.dimension_names.toString(); if (labels !== this.element_legend_childs.series.labels_key) { needed = true; if (this.debug) { this.log('NEW LABELS: "' + labels + '" NOT EQUAL OLD LABELS: "' + this.element_legend_childs.series.labels_key + '"'); } } } if (!needed) { // make sure colors available this.chartPrepareColorPalette(); // do we have to update the current values? // we do this, only when the visible chart is current if (Math.abs(this.netdata_last - this.view_before) <= this.data_update_every) { if (this.debug) { this.log('chart is in latest position... updating values on legend...'); } //let labels = this.data.dimension_names; //let i = labels.length; //while (i--) // this.legendSetLabelValue(labels[i], this.data.view_latest_values[i]); } return; } if (this.colors === null) { // this is the first time we update the chart // let's assign colors to all dimensions if (this.library.track_colors()) { this.colors = []; keys = Object.keys(this.chart.dimensions); len = keys.length; for (let i = 0; i < len; i++) { NETDATA.commonColors.get(this, this.chart.dimensions[keys[i]].name); } } } // we will re-generate the colors for the chart // based on the dimensions this result has data for this.colors = []; if (this.debug) { this.log('updating Legend DOM'); } // mark all dimensions as invalid this.dimensions_visibility.invalidateAll(); const genLabel = function (state, parent, dim, name, count) { let color = state.__chartDimensionColor(name); let user_element = null; let user_id = NETDATA.dataAttribute(state.element, 'show-value-of-' + name.toLowerCase() + '-at', null); if (user_id === null) { user_id = NETDATA.dataAttribute(state.element, 'show-value-of-' + dim.toLowerCase() + '-at', null); } if (user_id !== null) { user_element = document.getElementById(user_id) || null; if (user_element === null) { state.log('Cannot find element with id: ' + user_id); } } state.element_legend_childs.series[name] = { name: document.createElement('span'), value: document.createElement('span'), user: user_element, last: null, last_shown_value: null }; let label = state.element_legend_childs.series[name]; // create the dimension visibility tracking for this label state.dimensions_visibility.dimensionAdd(name, label.name, label.value, color); let rgb = NETDATA.colorHex2Rgb(color); label.name.innerHTML = '