]> arthur.barton.de Git - netdata.git/commitdiff
re-worked pan and zoom to allow data padding
authorCosta Tsaousis (ktsaou) <costa@tsaousis.gr>
Thu, 14 Jan 2016 22:43:47 +0000 (00:43 +0200)
committerCosta Tsaousis (ktsaou) <costa@tsaousis.gr>
Thu, 14 Jan 2016 22:43:47 +0000 (00:43 +0200)
web/dashboard.js

index 1aef72a154f747db2c8c9116b2653cd23637a738..aa114a37ba78774af556120501b72380254b12c4 100755 (executable)
                                                                                // rendering the chart that is panned or zoomed).
                                                                                // Used with .current.global_pan_sync_time
 
-               last_resized: 0,                                // the timestamp of the last resize request
+               last_resized: new Date().getTime(), // the timestamp of the last resize request
 
                crossDomainAjax: false,                 // enable this to request crossDomain AJAX
 
 
                        sync_pan_and_zoom: true,        // enable or disable pan and zoom sync
 
+                       pan_and_zoom_data_padding: true, // fetch more data for the master chart when panning or zooming
+
                        update_only_visible: true,      // enable or disable visibility management
 
                        parallel_refresher: true,       // enable parallel refresh of charts
                        if(this.master === null || this.seq === 0)
                                return false;
 
-                       if(state.needsRecreation())
-                               return true;
+                       //if(state.needsRecreation())
+                       //      return true;
 
                        if(state.tm.pan_and_zoom_seq === this.seq)
                                return false;
                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.auto = {
                        name: 'auto',
                        }
                        else {
                                var w = that.element.offsetWidth;
-                               if(w === null || w === 0)
+                               if(w === null || w === 0) {
+                                       // the div is hidden
+                                       // this is resize the chart when next viewed
                                        that.tm.last_resized = 0;
+                               }
                                else
                                        $(that.element).css('height', (that.element.offsetWidth * that.library.aspect_ratio / 100).toString() + 'px');
                        }
                        that.paused = false;
                        that.selected = false;
 
-                       that.chart_created = false;             // boolean - is the library.create() been called?
-                       that.updates_counter = 0;               // numeric - the number of refreshes made so far
-                       that.updates_since_last_creation = 0;
+                       that.chart_created = false;                     // boolean - is the library.create() been called?
+                       that.updates_counter = 0;                       // numeric - the number of refreshes made so far
+                       that.updates_since_last_unhide = 0;     // numeric - the number of refreshes made since the last time the chart was unhidden
+                       that.updates_since_last_creation = 0; // numeric - the number of refreshes made since the last time the chart was created
 
                        that.tm = {
                                last_initialized: 0,            // milliseconds - the timestamp it was last initialized
                                        showRendering();
                                        that.element_chart.style.display = 'none';
                                        if(that.element_legend !== null) that.element_legend.style.display = 'none';
+                                       that.tm.last_hidden = new Date().getTime();
                                }
                        }
                        
                        if(isHidden() === false) return;
 
                        that.___chartIsHidden___ = undefined;
+                       that.updates_since_last_unhide = 0;
 
                        if(that.chart_created === false) {
                                // we need to re-initialize it, to show our background
                                init();
                        }
                        else {
+                               that.tm.last_unhidden = new Date().getTime();
                                that.element_chart.style.display = '';
                                if(that.element_legend !== null) that.element_legend.style.display = '';
                                resizeChart();
                                return false;
                        }
 
-                       that.updates_since_last_creation++;
                        return true;
                }
 
 
                        var now = new Date().getTime();
                        NETDATA.options.last_page_scroll = now;
-                       NETDATA.options.last_resized = now;
                        NETDATA.options.auto_refresher_stop_until = now + NETDATA.options.current.stop_updates_while_resizing;
 
                        // force a resize
                        showMessageIcon('<i class="fa fa-warning"></i> empty');
                        that.legendUpdateDOM();
                        that.tm.last_autorefreshed = new Date().getTime();
-                       that.data_update_every = 30 * 1000;
+                       // that.data_update_every = 30 * 1000;
+                       //that.element_chart.style.display = 'none';
+                       //if(that.element_legend !== null) that.element_legend.style.display = 'none';
+                       //that.___chartIsHidden___ = true;
                }
 
                // ============================================================================================================
                }
 
                this.updateChartPanOrZoom = function(after, before) {
+                       var logme = 'updateChartPanOrZoom(' + after + ', ' + before + '): ';
+                       var ret = true;
+
                        if(this.debug === true)
-                               this.log('updateChartPanOrZoom() called.');
+                               this.log(logme);
 
-                       if(before < after) return false;
+                       if(before < after) {
+                               this.log(logme + 'flipped parameters, rejecting it.');
+                               return false;
+                       }
 
-                       var min_duration = Math.round((this.chartWidth() / 30 * this.chart.update_every * 1000));
+                       if(typeof this.fixed_min_duration === 'undefined')
+                               this.fixed_min_duration = Math.round((this.chartWidth() / 30) * this.chart.update_every * 1000);
+                       
+                       var min_duration = this.fixed_min_duration;
+                       var current_duration = Math.round(this.view_before - this.view_after);
 
-                       if(this.debug === true)
-                               this.log('requested duration of ' + ((before - after) / 1000).toString() + ' (' + after + ' - ' + before + '), minimum ' + min_duration / 1000);
+                       // round the numbers
+                       after = Math.round(after);
+                       before = Math.round(before);
 
-                       if((before - after) < min_duration) return false;
+                       // 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);
 
-                       var current_duration = this.data_before - this.data_after;
+                       // the final wanted duration
                        var wanted_duration = before - after;
-                       var tolerance = this.data_update_every * 2;
-                       var movement = Math.abs(before - this.data_before);
+                       
+                       // 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(this.current === this.zoom && wanted_duration < current_duration && wanted_duration < min_duration) {
+                               if(this.debug === true)
+                                       this.log(logme + 'too small: min_duration: ' + (min_duration / 1000).toString() + ', wanted: ' + (wanted_duration / 1000).toString());
 
-                       if(this.debug === true)
-                               this.log('current duration: ' + current_duration / 1000 + ', wanted duration: ' + wanted_duration / 1000 + ', movement: ' + movement / 1000 + ', tolerance: ' + tolerance / 1000);
+                               min_duration = this.fixed_min_duration;
+
+                               var dt = (min_duration - wanted_duration) / 2;
+                               before += dt;
+                               after -= dt;
+                               ret = false;
+                       }
+
+                       var tolerance = this.data_update_every * 2;
+                       var movement = Math.abs(before - this.view_before);
 
                        if(Math.abs(current_duration - wanted_duration) <= tolerance && movement <= tolerance) {
                                if(this.debug === true)
-                                       this.log('IGNORED');
-
+                                       this.log(logme + 'REJECTING UPDATE: current duration: ' + (current_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 === true)
-                                       this.log('updateChartPanOrZoom(): caller did not set proper mode');
                        }
 
+                       if(this.debug === true)
+                               this.log(logme + 'ACCEPTING UPDATE: current duration: ' + (current_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 = new Date().getTime() + 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);
-                       return true;
+                       return ret;
                }
 
                this.legendFormatValue = function(value) {
                        if(typeof value !== 'number') return value;
 
                        var abs = Math.abs(value);
-                       if(abs >= 100) return Math.round(value).toLocaleString();
-                       if(abs >= 1) return (Math.round(value * 100) / 100).toLocaleString();
-                       if(abs >= 0.1) return (Math.round(value * 1000) / 1000).toLocaleString();
+                       if(abs >= 1000) return (Math.round(value)).toLocaleString();
+                       if(abs >= 100 ) return (Math.round(value * 10) / 10).toLocaleString();
+                       if(abs >= 1   ) return (Math.round(value * 100) / 100).toLocaleString();
+                       if(abs >= 0.1 ) return (Math.round(value * 1000) / 1000).toLocaleString();
                        return (Math.round(value * 10000) / 10000).toLocaleString();
                }
 
                }
 
                this.chartURL = function() {
-                       var before;
-                       var after;
-                       if(NETDATA.globalPanAndZoom.isActive()) {
+                       var after, before, points_multiplier = 1;
+                       if(NETDATA.globalPanAndZoom.isActive() && NETDATA.globalPanAndZoom.isMaster(this) === false) {
+                               this.tm.pan_and_zoom_seq = NETDATA.globalPanAndZoom.seq;
+
                                after = Math.round(NETDATA.globalPanAndZoom.force_after_ms / 1000);
                                before = Math.round(NETDATA.globalPanAndZoom.force_before_ms / 1000);
-                               this.tm.pan_and_zoom_seq = NETDATA.globalPanAndZoom.seq;
+                               this.view_after = after * 1000;
+                               this.view_before = before * 1000;
+
+                               this.requested_padding = null;
+                               points_multiplier = 1;
+                       }
+                       else if(this.current.force_before_ms !== null && this.current.force_after_ms !== null) {
+                               this.tm.pan_and_zoom_seq = 0;
+
+                               before = Math.round(this.current.force_before_ms / 1000);
+                               after  = Math.round(this.current.force_after_ms / 1000);
+                               this.view_after = after * 1000;
+                               this.view_before = before * 1000;
+
+                               if(NETDATA.options.current.pan_and_zoom_data_padding === true) {
+                                       this.requested_padding = Math.round((before - after) / 2);
+                                       after -= this.requested_padding;
+                                       before += this.requested_padding;
+                                       this.requested_padding *= 1000;
+                                       points_multiplier = 2;
+                               }
+
+                               this.current.force_before_ms = null;
+                               this.current.force_after_ms = null;
                        }
                        else {
-                               before = this.current.force_before_ms !== null ? Math.round(this.current.force_before_ms / 1000) : this.before;
-                               after  = this.current.force_after_ms  !== null ? Math.round(this.current.force_after_ms / 1000) : this.after;
                                this.tm.pan_and_zoom_seq = 0;
+
+                               before = this.before;
+                               after  = this.after;
+                               this.view_after = after * 1000;
+                               this.view_before = before * 1000;
+
+                               this.requested_padding = null;
+                               points_multiplier = 1;
                        }
 
                        this.requested_after = after * 1000;
                        // build the data URL
                        this.data_url = this.chart.data_url;
                        this.data_url += "&format="  + this.library.format();
-                       this.data_url += "&points="  + this.data_points.toString();
+                       this.data_url += "&points="  + (this.data_points * points_multiplier).toString();
                        this.data_url += "&group="   + this.method;
                        this.data_url += "&options=" + this.library.options(this);
                        this.data_url += '|jsonwrap';
 
                        this.data = data;
                        this.updates_counter++;
+                       this.updates_since_last_unhide++;
+                       this.updates_since_last_creation++;
 
                        var started = new Date().getTime();
 
                        // if the result is JSON, find the latest update-every
-                       if(typeof data === 'object') {
-                               if(typeof data.view_update_every !== 'undefined')
-                                       this.data_update_every = data.view_update_every * 1000;
-
-                               if(typeof data.after !== 'undefined')
-                                       this.data_after = data.after * 1000;
-
-                               if(typeof data.before !== 'undefined')
-                                       this.data_before = data.before * 1000;
-
-                               if(typeof data.first_entry !== 'undefined')
-                                       this.netdata_first = data.first_entry * 1000;
-
-                               if(typeof data.last_entry !== 'undefined')
-                                       this.netdata_last = data.last_entry * 1000;
-
-                               if(typeof data.points !== 'undefined')
-                                       this.data_points = data.points;
-
-                               data.state = this;
+                       this.data_update_every = data.view_update_every * 1000;
+                       this.data_after = data.after * 1000;
+                       this.data_before = data.before * 1000;
+                       this.netdata_first = data.first_entry * 1000;
+                       this.netdata_last = data.last_entry * 1000;
+                       this.data_points = data.points;
+                       data.state = this;
+
+                       if(NETDATA.options.current.pan_and_zoom_data_padding === true && this.requested_padding !== null) {
+                               if(this.view_after < this.data_after) {
+                                       console.log('adusting view_after from ' + this.view_after + ' to ' + this.data_after);
+                                       this.view_after = this.data_after;
+                               }
+                               
+                               if(this.view_before > this.data_before) {
+                                       console.log('adusting view_before from ' + this.view_before + ' to ' + this.data_before);
+                                       this.view_before = this.data_before;
+                               }
+                       }
+                       else {
+                               this.view_after = this.data_after;
+                               this.view_before = this.data_before;
                        }
 
                        if(this.debug === true) {
                                this.log('UPDATE No ' + this.updates_counter + ' COMPLETED');
 
                                if(this.current.force_after_ms)
-                                       this.log('STATUS: forced   : ' + (this.current.force_after_ms / 1000).toString() + ' - ' + (this.current.force_before_ms / 1000).toString());
+                                       this.log('STATUS: forced    : ' + (this.current.force_after_ms / 1000).toString() + ' - ' + (this.current.force_before_ms / 1000).toString());
                                else
-                                       this.log('STATUS: forced: unset');
+                                       this.log('STATUS: forced    : unset');
 
-                               this.log('STATUS: requested: ' + (this.requested_after / 1000).toString() + ' - ' + (this.requested_before / 1000).toString());
-                               this.log('STATUS: rendered : ' + (this.data_after / 1000).toString() + ' - ' + (this.data_before / 1000).toString());
-                               this.log('STATUS: points   : ' + (this.data_points).toString());
+                               this.log('STATUS: requested : ' + (this.requested_after / 1000).toString() + ' - ' + (this.requested_before / 1000).toString());
+                               this.log('STATUS: downloaded: ' + (this.data_after / 1000).toString() + ' - ' + (this.data_before / 1000).toString());
+                               this.log('STATUS: rendered  : ' + (this.view_after / 1000).toString() + ' - ' + (this.view_before / 1000).toString());
+                               this.log('STATUS: points    : ' + (this.data_points).toString());
                        }
 
                        if(this.data_points === 0) {
 
                        // caching - we do not evaluate the charts visibility
                        // if the page has not been scrolled since the last check
-                       if(nocache === false && this.tm.last_visible_check > NETDATA.options.last_page_scroll) {
-                               if(this.debug === true)
-                                       this.log('isVisible: ' + this.___isVisible___);
-
+                       if(nocache === false && this.tm.last_visible_check > NETDATA.options.last_page_scroll)
                                return this.___isVisible___;
-                       }
 
                        this.tm.last_visible_check = new Date().getTime();
 
                        if(x.width === 0 || x.height === 0) {
                                hideChart();
                                this.___isVisible___ = false;
-
-                               if(this.debug === true)
-                                       this.log('isVisible (width:' + x.width + ', height:' + x.height + ', display:"' + this.element.style.display + '"): ' + this.___isVisible___);
-
                                return this.___isVisible___;
                        }
 
 
                                hideChart();
                                this.___isVisible___ = false;
-
-                               if(this.debug === true)
-                                       this.log('isVisible: ' + this.___isVisible___);
-
                                return this.___isVisible___;
                        }
                        else {
 
                                unhideChart();
                                this.___isVisible___ = true;
-
-                               if(this.debug === true)
-                                       this.log('isVisible: ' + this.___isVisible___);
-
                                return this.___isVisible___;
                        }
                }
 
                        if(this.isAutoRefreshed() === true) {
                                // allow the first update, even if the page is not visible
-                               if(this.updates_counter && NETDATA.options.page_is_visible === false) {
+                               if(this.updates_counter && this.updates_since_last_unhide && NETDATA.options.page_is_visible === false) {
                                        if(NETDATA.options.debug.focus === true || this.debug === true)
                                                this.log('canBeAutoRefreshed(): page does not have focus');
 
                var dygraph = state.dygraph_instance;
 
                if(typeof dygraph === 'undefined')
-                       NETDATA.dygraphChartCreate(state, data);
+                       return NETDATA.dygraphChartCreate(state, data);
 
                // when the chart is not visible, and hidden
                // if there is a window resize, dygraph detects
                                visibility: state.dimensions_visibility.selected2BooleanArray(state.data.dimension_names)
                }
 
-               if(state.current.name === 'pan') {
+               if(state.current.name !== 'auto') {
                        if(NETDATA.options.debug.dygraph === true || state.debug === true)
                                state.log('dygraphChartUpdate() loose update');
                }
                                // here we calculate the time t based on the row number selected
                                // which is ok
                                var t = state.data_after + row * state.data_update_every;
-                               // console.log('row = ' + row + ', x = ' + x + ', t = ' + t + ' ' + ((t === x)?'SAME':'DIFFERENT') + ', rows in db: ' + state.data_points + ' visible(x) = ' + state.timeIsVisible(x) + ' visible(t) = ' + state.timeIsVisible(t) + ' r(x) = ' + state.calculateRowForTime(x) + ' r(t) = ' + state.calculateRowForTime(t) + ' range: ' + state.data_after + ' - ' + state.data_before + ' real: ' + state.data.after + ' - ' + state.data.before + ' every: ' + state.data_update_every);
+                               // console.log('row = ' + row + ', x = ' + x + ', t = ' + t + ' ' + ((t === x)?'SAME':(Math.abs(x-t)<=state.data_update_every)?'SIMILAR':'DIFFERENT') + ', rows in db: ' + state.data_points + ' visible(x) = ' + state.timeIsVisible(x) + ' visible(t) = ' + state.timeIsVisible(t) + ' r(x) = ' + state.calculateRowForTime(x) + ' r(t) = ' + state.calculateRowForTime(t) + ' range: ' + state.data_after + ' - ' + state.data_before + ' real: ' + state.data.after + ' - ' + state.data.before + ' every: ' + state.data_update_every);
 
-                               state.globalSelectionSync(t);
+                               state.globalSelectionSync(x);
 
                                // fix legend zIndex using the internal structures of dygraph legend module
                                // this works, but it is a hack!