/*global Dygraph:false */
"use strict";
-// Range selector status values
-var NOT_CREATED = 0; // Range selector interface has not been created
-var CREATED = 1 << 0; // Range selector interface has been created
-var ADDED_TO_GRAPH = 1 << 1; // Range selector elements have been added to the graph
-
var rangeSelector = function() {
this.isIE_ = /MSIE/.test(navigator.userAgent) && !window.opera;
this.hasTouchInterface_ = typeof(TouchEvent) != 'undefined';
this.isMobileDevice_ = /mobile|android/gi.test(navigator.appVersion);
- this.status_ = NOT_CREATED;
+ this.interfaceCreated_ = false;
};
rangeSelector.prototype.toString = function() {
rangeSelector.prototype.activate = function(dygraph) {
this.dygraph_ = dygraph;
- this.layout_ = dygraph.layout_;
- this.graphDiv_ = dygraph.graphDiv;
this.isUsingExcanvas_ = dygraph.isUsingExcanvas_;
if (this.getOption_('showRangeSelector')) {
this.createInterface_();
// Private methods
//------------------------------------------------------------------
-rangeSelector.prototype.getOption_ = function(name) {
- return this.dygraph_.getOption(name);
+rangeSelector.prototype.getOption_ = function(name, opt_series) {
+ return this.dygraph_.getOption(name, opt_series);
};
rangeSelector.prototype.setDefaultOption_ = function(name, value) {
- return this.dygraph_.attrs_[name] = value;
+ this.dygraph_.attrs_[name] = value;
};
/**
// Range selector and animatedZooms have a bad interaction. See issue 359.
if (this.getOption_('animatedZooms')) {
- this.dygraph_.warn('Animated zooms and range selector are not compatible; disabling animatedZooms.');
+ console.warn('Animated zooms and range selector are not compatible; disabling animatedZooms.');
this.dygraph_.updateOptions({animatedZooms: false}, true);
}
+ this.interfaceCreated_ = true;
this.addToGraph_();
- this.status_ = CREATED;
};
/**
* Adds the range selector to the graph.
*/
rangeSelector.prototype.addToGraph_ = function() {
- var graphDiv = this.graphDiv_;
+ var graphDiv = this.graphDiv_ = this.dygraph_.graphDiv;
graphDiv.appendChild(this.bgcanvas_);
graphDiv.appendChild(this.fgcanvas_);
graphDiv.appendChild(this.leftZoomHandle_);
graphDiv.appendChild(this.rightZoomHandle_);
- this.status_ |= ADDED_TO_GRAPH;
};
/**
graphDiv.removeChild(this.fgcanvas_);
graphDiv.removeChild(this.leftZoomHandle_);
graphDiv.removeChild(this.rightZoomHandle_);
- this.status_ ^= ADDED_TO_GRAPH;
+ this.graphDiv_ = null;
};
/**
* Renders the static portion of the range selector at the predraw stage.
*/
rangeSelector.prototype.renderStaticLayer_ = function() {
- if (!this.updateInterfaceStatus_()) {
+ if (!this.updateVisibility_()) {
return;
}
this.resize_();
* Renders the interactive portion of the range selector after the chart has been drawn.
*/
rangeSelector.prototype.renderInteractiveLayer_ = function() {
- if (!this.updateInterfaceStatus_() || this.isChangingRange_) {
+ if (!this.updateVisibility_() || this.isChangingRange_) {
return;
}
this.placeZoomHandles_();
/**
* @private
- * Check to see if the range selector is enabled/disabled and update interface accordingly.
+ * Check to see if the range selector is enabled/disabled and update visibility accordingly.
*/
-rangeSelector.prototype.updateInterfaceStatus_ = function() {
+rangeSelector.prototype.updateVisibility_ = function() {
var enabled = this.getOption_('showRangeSelector');
if (enabled) {
- if (!(this.status_ & CREATED)) {
+ if (!this.interfaceCreated_) {
this.createInterface_();
- } else if (!(this.status_ & ADDED_TO_GRAPH)) {
+ } else if (!this.graphDiv_ || !this.graphDiv_.parentNode) {
this.addToGraph_();
}
- } else if (this.status_ & ADDED_TO_GRAPH) {
+ } else if (this.graphDiv_) {
this.removeFromGraph_();
var dygraph = this.dygraph_;
setTimeout(function() { dygraph.width_ = 0; dygraph.resize(); }, 1);
* Resizes the range selector.
*/
rangeSelector.prototype.resize_ = function() {
- function setElementRect(canvas, rect) {
+ function setElementRect(canvas, context, rect) {
+ var canvasScale = Dygraph.getContextPixelRatio(context);
+
canvas.style.top = rect.y + 'px';
canvas.style.left = rect.x + 'px';
- canvas.width = rect.w;
- canvas.height = rect.h;
- canvas.style.width = canvas.width + 'px'; // for IE
- canvas.style.height = canvas.height + 'px'; // for IE
+ canvas.width = rect.w * canvasScale;
+ canvas.height = rect.h * canvasScale;
+ canvas.style.width = rect.w + 'px';
+ canvas.style.height = rect.h + 'px';
+
+ if(canvasScale != 1) {
+ context.scale(canvasScale, canvasScale);
+ }
}
- var plotArea = this.layout_.getPlotArea();
- var xAxisLabelHeight = this.getOption_('xAxisHeight') || (this.getOption_('axisLabelFontSize') + 2 * this.getOption_('axisTickSize'));
+ var plotArea = this.dygraph_.layout_.getPlotArea();
+
+ var xAxisLabelHeight = 0;
+ if (this.dygraph_.getOptionForAxis('drawAxis', 'x')) {
+ xAxisLabelHeight = this.getOption_('xAxisHeight') || (this.getOption_('axisLabelFontSize') + 2 * this.getOption_('axisTickSize'));
+ }
this.canvasRect_ = {
x: plotArea.x,
y: plotArea.y + plotArea.h + xAxisLabelHeight + 4,
h: this.getOption_('rangeSelectorHeight')
};
- setElementRect(this.bgcanvas_, this.canvasRect_);
- setElementRect(this.fgcanvas_, this.canvasRect_);
+ setElementRect(this.bgcanvas_, this.bgcanvas_ctx_, this.canvasRect_);
+ setElementRect(this.fgcanvas_, this.fgcanvas_ctx_, this.canvasRect_);
};
/**
*/
rangeSelector.prototype.initInteraction_ = function() {
var self = this;
- var topElem = this.isIE_ ? document : window;
- var xLast = 0;
+ var topElem = document;
+ var clientXLast = 0;
var handle = null;
var isZooming = false;
var isPanning = false;
// functions, defined below. Defining them this way (rather than with
// "function foo() {...}" makes JSHint happy.
var toXDataWindow, onZoomStart, onZoom, onZoomEnd, doZoom, isMouseInPanZone,
- onPanStart, onPan, onPanEnd, doPan, onCanvasMouseMove, applyBrowserZoomLevel;
+ onPanStart, onPan, onPanEnd, doPan, onCanvasHover;
// Touch event functions
var onZoomHandleTouchEvent, onCanvasTouchEvent, addTouchEvents;
return [xDataMin, xDataMax];
};
- applyBrowserZoomLevel = function(delX) {
- var zoom = window.outerWidth/document.documentElement.clientWidth;
- if (!isNaN(zoom)) {
- return delX/zoom;
- } else {
- return delX;
- }
- };
-
onZoomStart = function(e) {
Dygraph.cancelEvent(e);
isZooming = true;
- xLast = e.screenX;
+ clientXLast = e.clientX;
handle = e.target ? e.target : e.srcElement;
- self.dygraph_.addEvent(topElem, 'mousemove', onZoom);
- self.dygraph_.addEvent(topElem, 'mouseup', onZoomEnd);
+ if (e.type === 'mousedown' || e.type === 'dragstart') {
+ // These events are removed manually.
+ Dygraph.addEvent(topElem, 'mousemove', onZoom);
+ Dygraph.addEvent(topElem, 'mouseup', onZoomEnd);
+ }
self.fgcanvas_.style.cursor = 'col-resize';
tarp.cover();
return true;
return false;
}
Dygraph.cancelEvent(e);
- var delX = e.screenX - xLast;
- if (Math.abs(delX) < 4 || e.screenX === 0) {
- // First iPad move event seems to have screenX = 0
+
+ var delX = e.clientX - clientXLast;
+ if (Math.abs(delX) < 4) {
return true;
}
- xLast = e.screenX;
- delX = applyBrowserZoomLevel(delX);
+ clientXLast = e.clientX;
// Move handle.
var zoomHandleStatus = self.getZoomHandleStatus_();
if (!isPanning && isMouseInPanZone(e) && self.getZoomHandleStatus_().isZoomed) {
Dygraph.cancelEvent(e);
isPanning = true;
- xLast = e.screenX;
- self.dygraph_.addEvent(topElem, 'mousemove', onPan);
- self.dygraph_.addEvent(topElem, 'mouseup', onPanEnd);
+ clientXLast = e.clientX;
+ if (e.type === 'mousedown') {
+ // These events are removed manually.
+ Dygraph.addEvent(topElem, 'mousemove', onPan);
+ Dygraph.addEvent(topElem, 'mouseup', onPanEnd);
+ }
return true;
}
return false;
}
Dygraph.cancelEvent(e);
- var delX = e.screenX - xLast;
+ var delX = e.clientX - clientXLast;
if (Math.abs(delX) < 4) {
return true;
}
- xLast = e.screenX;
- delX = applyBrowserZoomLevel(delX);
+ clientXLast = e.clientX;
// Move range view
var zoomHandleStatus = self.getZoomHandleStatus_();
}
};
- onCanvasMouseMove = function(e) {
+ onCanvasHover = function(e) {
if (isZooming || isPanning) {
return;
}
addTouchEvents = function(elem, fn) {
var types = ['touchstart', 'touchend', 'touchmove', 'touchcancel'];
for (var i = 0; i < types.length; i++) {
- self.dygraph_.addEvent(elem, types[i], fn);
+ self.dygraph_.addAndTrackEvent(elem, types[i], fn);
}
};
this.setDefaultOption_('panEdgeFraction', 0.0001);
var dragStartEvent = window.opera ? 'mousedown' : 'dragstart';
- this.dygraph_.addEvent(this.leftZoomHandle_, dragStartEvent, onZoomStart);
- this.dygraph_.addEvent(this.rightZoomHandle_, dragStartEvent, onZoomStart);
+ this.dygraph_.addAndTrackEvent(this.leftZoomHandle_, dragStartEvent, onZoomStart);
+ this.dygraph_.addAndTrackEvent(this.rightZoomHandle_, dragStartEvent, onZoomStart);
if (this.isUsingExcanvas_) {
- this.dygraph_.addEvent(this.iePanOverlay_, 'mousedown', onPanStart);
+ this.dygraph_.addAndTrackEvent(this.iePanOverlay_, 'mousedown', onPanStart);
} else {
- this.dygraph_.addEvent(this.fgcanvas_, 'mousedown', onPanStart);
- this.dygraph_.addEvent(this.fgcanvas_, 'mousemove', onCanvasMouseMove);
+ this.dygraph_.addAndTrackEvent(this.fgcanvas_, 'mousedown', onPanStart);
+ this.dygraph_.addAndTrackEvent(this.fgcanvas_, 'mousemove', onCanvasHover);
}
// Touch events
try {
this.drawMiniPlot_();
} catch(ex) {
- Dygraph.warn(ex);
+ console.warn(ex);
}
var margin = 0.5;
var dataPoint = combinedSeriesData.data[i];
var x = ((dataPoint[0] !== null) ? ((dataPoint[0] - xExtremes[0])*xFact) : NaN);
var y = ((dataPoint[1] !== null) ? (canvasHeight - (dataPoint[1] - combinedSeriesData.yMin)*yFact) : NaN);
+
+ // Skip points that don't change the x-value. Overly fine-grained points
+ // can cause major slowdowns with the ctx.fill() call below.
+ if (!stepPlot && prevX !== null && Math.round(x) == Math.round(prevX)) {
+ continue;
+ }
+
if (isFinite(x) && isFinite(y)) {
if(prevX === null) {
ctx.lineTo(x, canvasHeight);
/**
* @private
- * Computes and returns the combinded series data along with min/max for the mini plot.
- * @return {Object} An object containing combinded series array, ymin, ymax.
+ * Computes and returns the combined series data along with min/max for the mini plot.
+ * The combined series consists of averaged values for all series.
+ * When series have error bars, the error bars are ignored.
+ * @return {Object} An object containing combined series array, ymin, ymax.
*/
rangeSelector.prototype.computeCombinedSeriesAndLimits_ = function() {
- var data = this.dygraph_.rawData_;
+ var g = this.dygraph_;
var logscale = this.getOption_('logscale');
-
- // Create a combined series (average of all series values).
- var combinedSeries = [];
- var sum;
- var count;
- var mutipleValues;
- var i, j, k;
- var xVal, yVal;
-
- // Find out if data has multiple values per datapoint.
- // Go to first data point that actually has values (see http://code.google.com/p/dygraphs/issues/detail?id=246)
- for (i = 0; i < data.length; i++) {
- if (data[i].length > 1 && data[i][1] !== null) {
- mutipleValues = typeof data[i][1] != 'number';
- if (mutipleValues) {
- sum = [];
- count = [];
- for (k = 0; k < data[i][1].length; k++) {
- sum.push(0);
- count.push(0);
- }
- }
- break;
- }
+ var i;
+
+ // Select series to combine. By default, all series are combined.
+ var numColumns = g.numColumns();
+ var labels = g.getLabels();
+ var includeSeries = new Array(numColumns);
+ var anySet = false;
+ for (i = 1; i < numColumns; i++) {
+ var include = this.getOption_('showInRangeSelector', labels[i]);
+ includeSeries[i] = include;
+ if (include !== null) anySet = true; // it's set explicitly for this series
}
-
- for (i = 0; i < data.length; i++) {
- var dataPoint = data[i];
- xVal = dataPoint[0];
-
- if (mutipleValues) {
- for (k = 0; k < sum.length; k++) {
- sum[k] = count[k] = 0;
- }
- } else {
- sum = count = 0;
- }
-
- for (j = 1; j < dataPoint.length; j++) {
- if (this.dygraph_.visibility()[j-1]) {
- var y;
- if (mutipleValues) {
- for (k = 0; k < sum.length; k++) {
- y = dataPoint[j][k];
- if (y === null || isNaN(y)) continue;
- sum[k] += y;
- count[k]++;
- }
- } else {
- y = dataPoint[j];
- if (y === null || isNaN(y)) continue;
- sum += y;
- count++;
- }
- }
- }
-
- if (mutipleValues) {
- for (k = 0; k < sum.length; k++) {
- sum[k] /= count[k];
- }
- yVal = sum.slice(0);
- } else {
- yVal = sum/count;
- }
-
- combinedSeries.push([xVal, yVal]);
+ if (!anySet) {
+ for (i = 0; i < includeSeries.length; i++) includeSeries[i] = true;
}
- // Account for roll period, fractions.
- combinedSeries = this.dygraph_.rollingAverage(combinedSeries, this.dygraph_.rollPeriod_);
+ // Create a combined series (average of selected series values).
+ // TODO(danvk): short-circuit if there's only one series.
+ var rolledSeries = [];
+ var dataHandler = g.dataHandler_;
+ var options = g.attributes_;
+ for (i = 1; i < g.numColumns(); i++) {
+ if (!includeSeries[i]) continue;
+ var series = dataHandler.extractSeries(g.rawData_, i, options);
+ if (g.rollPeriod() > 1) {
+ series = dataHandler.rollingAverage(series, g.rollPeriod(), options);
+ }
+
+ rolledSeries.push(series);
+ }
- if (typeof combinedSeries[0][1] != 'number') {
- for (i = 0; i < combinedSeries.length; i++) {
- yVal = combinedSeries[i][1];
- combinedSeries[i][1] = yVal[0];
- }
+ var combinedSeries = [];
+ for (i = 0; i < rolledSeries[0].length; i++) {
+ var sum = 0;
+ var count = 0;
+ for (var j = 0; j < rolledSeries.length; j++) {
+ var y = rolledSeries[j][i][1];
+ if (y === null || isNaN(y)) continue;
+ count++;
+ sum += y;
+ }
+ combinedSeries.push([rolledSeries[0][i][0], sum / count]);
}
// Compute the y range.
var yMin = Number.MAX_VALUE;
var yMax = -Number.MAX_VALUE;
for (i = 0; i < combinedSeries.length; i++) {
- yVal = combinedSeries[i][1];
+ var yVal = combinedSeries[i][1];
if (yVal !== null && isFinite(yVal) && (!logscale || yVal > 0)) {
yMin = Math.min(yMin, yVal);
yMax = Math.max(yMax, yVal);