},
labelsSeparateLines: false,
labelsKMB: false,
+ labelsKMG2: false,
strokeWidth: 1.0,
// Create the containing DIV and other interactive elements
this.createInterface_();
- // Create the PlotKit grapher
- // TODO(danvk): why does the Layout need its own set of options?
- this.layoutOptions_ = { 'xOriginIsZero': false };
- Dygraph.update(this.layoutOptions_, this.attrs_);
- Dygraph.update(this.layoutOptions_, this.user_attrs_);
- Dygraph.update(this.layoutOptions_, {
- 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
-
- this.layout_ = new DygraphLayout(this, this.layoutOptions_);
-
- // TODO(danvk): why does the Renderer need its own set of options?
- this.renderOptions_ = { colorScheme: this.colors_,
- strokeColor: null,
- axisLineWidth: Dygraph.AXIS_LINE_WIDTH };
- Dygraph.update(this.renderOptions_, this.attrs_);
- Dygraph.update(this.renderOptions_, this.user_attrs_);
- this.plotter_ = new DygraphCanvasRenderer(this,
- this.hidden_, this.layout_,
- this.renderOptions_);
-
- this.createStatusMessage_();
- this.createRollInterface_();
- this.createDragInterface_();
-
this.start_();
};
/**
* Generates interface elements for the Dygraph: a containing div, a div to
* display the current point, and a textbox to adjust the rolling average
- * period.
+ * period. Also creates the Renderer/Layout elements.
* @private
*/
Dygraph.prototype.createInterface_ = function() {
Dygraph.addEvent(this.hidden_, 'mouseout', function(e) {
dygraph.mouseOut_(e);
});
+
+ // Create the grapher
+ // TODO(danvk): why does the Layout need its own set of options?
+ this.layoutOptions_ = { 'xOriginIsZero': false };
+ Dygraph.update(this.layoutOptions_, this.attrs_);
+ Dygraph.update(this.layoutOptions_, this.user_attrs_);
+ Dygraph.update(this.layoutOptions_, {
+ 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
+
+ this.layout_ = new DygraphLayout(this, this.layoutOptions_);
+
+ // TODO(danvk): why does the Renderer need its own set of options?
+ this.renderOptions_ = { colorScheme: this.colors_,
+ strokeColor: null,
+ axisLineWidth: Dygraph.AXIS_LINE_WIDTH };
+ Dygraph.update(this.renderOptions_, this.attrs_);
+ Dygraph.update(this.renderOptions_, this.user_attrs_);
+ this.plotter_ = new DygraphCanvasRenderer(this,
+ this.hidden_, this.layout_,
+ this.renderOptions_);
+
+ this.createStatusMessage_();
+ this.createRollInterface_();
+ this.createDragInterface_();
}
/**
Dygraph.update(messagestyle, this.attr_('labelsDivStyles'));
var div = document.createElement("div");
for (var name in messagestyle) {
- div.style[name] = messagestyle[name];
+ if (messagestyle.hasOwnProperty(name)) {
+ div.style[name] = messagestyle[name];
+ }
}
this.graphDiv.appendChild(div);
this.attrs_.labelsDiv = div;
roller.size = "2";
roller.value = this.rollPeriod_;
for (var name in textAttr) {
- roller.style[name] = textAttr[name];
+ if (textAttr.hasOwnProperty(name)) {
+ roller.style[name] = textAttr[name];
+ }
}
var pa = this.graphDiv;
var self = this;
// Tracks whether the mouse is down right now
- var mouseDown = false;
+ var isZooming = false;
+ var isPanning = false;
var dragStartX = null;
var dragStartY = null;
var dragEndX = null;
var dragEndY = null;
var prevEndX = null;
+ var draggingDate = null;
+ var dateRange = null;
// Utility function to convert page-wide coordinates to canvas coords
var px = 0;
// Draw zoom rectangles when the mouse is down and the user moves around
Dygraph.addEvent(this.hidden_, 'mousemove', function(event) {
- if (mouseDown) {
+ if (isZooming) {
dragEndX = getX(event);
dragEndY = getY(event);
self.drawZoomRect_(dragStartX, dragEndX, prevEndX);
prevEndX = dragEndX;
+ } else if (isPanning) {
+ dragEndX = getX(event);
+ dragEndY = getY(event);
+
+ // Want to have it so that:
+ // 1. draggingDate appears at dragEndX
+ // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
+
+ self.dateWindow_[0] = draggingDate - (dragEndX / self.width_) * dateRange;
+ self.dateWindow_[1] = self.dateWindow_[0] + dateRange;
+ self.drawGraph_(self.rawData_);
}
});
// Track the beginning of drag events
Dygraph.addEvent(this.hidden_, 'mousedown', function(event) {
- mouseDown = true;
px = Dygraph.findPosX(self.canvas_);
py = Dygraph.findPosY(self.canvas_);
dragStartX = getX(event);
dragStartY = getY(event);
+
+ if (event.altKey || event.shiftKey) {
+ if (!self.dateWindow_) return; // have to be zoomed in to pan.
+ isPanning = true;
+ dateRange = self.dateWindow_[1] - self.dateWindow_[0];
+ draggingDate = (dragStartX / self.width_) * dateRange +
+ self.dateWindow_[0];
+ } else {
+ isZooming = true;
+ }
});
// If the user releases the mouse button during a drag, but not over the
// canvas, then it doesn't count as a zooming action.
Dygraph.addEvent(document, 'mouseup', function(event) {
- if (mouseDown) {
- mouseDown = false;
+ if (isZooming || isPanning) {
+ isZooming = false;
dragStartX = null;
dragStartY = null;
}
+
+ if (isPanning) {
+ isPanning = false;
+ draggingDate = null;
+ dateRange = null;
+ }
});
// Temporarily cancel the dragging event when the mouse leaves the graph
Dygraph.addEvent(this.hidden_, 'mouseout', function(event) {
- if (mouseDown) {
+ if (isZooming) {
dragEndX = null;
dragEndY = null;
}
// If the mouse is released on the canvas during a drag event, then it's a
// zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
Dygraph.addEvent(this.hidden_, 'mouseup', function(event) {
- if (mouseDown) {
- mouseDown = false;
+ if (isZooming) {
+ isZooming = false;
dragEndX = getX(event);
dragEndY = getY(event);
var regionWidth = Math.abs(dragEndX - dragStartX);
dragStartX = null;
dragStartY = null;
}
+
+ if (isPanning) {
+ isPanning = false;
+ draggingDate = null;
+ dateRange = null;
+ }
});
// Double-clicking zooms back out
Dygraph.SHORT_SPACINGS[Dygraph.TEN_MINUTELY] = 1000 * 60 * 10;
Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_MINUTELY] = 1000 * 60 * 30;
Dygraph.SHORT_SPACINGS[Dygraph.HOURLY] = 1000 * 3600;
-Dygraph.SHORT_SPACINGS[Dygraph.HOURLY] = 1000 * 3600 * 6;
+Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY] = 1000 * 3600 * 6;
Dygraph.SHORT_SPACINGS[Dygraph.DAILY] = 1000 * 86400;
Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY] = 1000 * 604800;
if (granularity < Dygraph.MONTHLY) {
// Generate one tick mark for every fixed interval of time.
var spacing = Dygraph.SHORT_SPACINGS[granularity];
- var format = '%d%b'; // e.g. "1 Jan"
+ var format = '%d%b'; // e.g. "1Jan"
// TODO(danvk): be smarter about making sure this really hits a "nice" time.
if (granularity < Dygraph.HOURLY) {
start_time = spacing * Math.floor(0.5 + start_time / spacing);
// Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
// Calculate the resulting tick spacing (i.e. this.height_ / nTicks).
// The first spacing greater than pixelsPerYLabel is what we use.
- var mults = [1, 2, 5];
+ if (self.attr_("labelsKMG2")) {
+ var mults = [1, 2, 4, 8];
+ } else {
+ var mults = [1, 2, 5];
+ }
var scale, low_val, high_val, nTicks;
// TODO(danvk): make it possible to set this for x- and y-axes independently.
var pixelsPerTick = self.attr_('pixelsPerYLabel');
for (var i = -10; i < 50; i++) {
- var base_scale = Math.pow(10, i);
+ if (self.attr_("labelsKMG2")) {
+ var base_scale = Math.pow(16, i);
+ } else {
+ var base_scale = Math.pow(10, i);
+ }
for (var j = 0; j < mults.length; j++) {
scale = base_scale * mults[j];
low_val = Math.floor(minV / scale) * scale;
// Construct labels for the ticks
var ticks = [];
+ var k;
+ var k_labels = [];
+ if (self.attr_("labelsKMB")) {
+ k = 1000;
+ k_labels = [ "K", "M", "B", "T" ];
+ }
+ if (self.attr_("labelsKMG2")) {
+ if (k) self.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
+ k = 1024;
+ k_labels = [ "k", "M", "G", "T" ];
+ }
+
for (var i = 0; i < nTicks; i++) {
var tickV = low_val + i * scale;
+ var absTickV = Math.abs(tickV);
var label = self.round_(tickV, 2);
- if (self.attr_("labelsKMB")) {
- var k = 1000;
- if (tickV >= k*k*k) {
- label = self.round_(tickV/(k*k*k), 1) + "B";
- } else if (tickV >= k*k) {
- label = self.round_(tickV/(k*k), 1) + "M";
- } else if (tickV >= k) {
- label = self.round_(tickV/k, 1) + "K";
+ if (k_labels.length) {
+ // Round up to an appropriate unit.
+ var n = k*k*k*k;
+ for (var j = 3; j >= 0; j--, n /= k) {
+ if (absTickV >= n) {
+ label = self.round_(tickV / n, 1) + k_labels[j];
+ break;
+ }
}
}
ticks.push( {label: label, v: tickV} );
} else {
for (var j = 0; j < series.length; j++) {
var y = series[j][1];
- if (!y) continue;
+ if (y === null || isNaN(y)) continue;
if (maxY == null || y > maxY) {
maxY = y;
}
this.addXTicks_();
// Tell PlotKit to use this new data and render itself
+ this.layout_.updateOptions({dateWindow: this.dateWindow_});
this.layout_.evaluateWithError();
this.plotter_.clear();
this.plotter_.render();
- this.canvas_.getContext('2d').clearRect(0, 0,
- this.canvas_.width, this.canvas_.height);
+ this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width,
+ this.canvas_.height);
};
/**
var ret = [];
for (var i = 0; i < rows; i++) {
var row = [];
- if (!data.getValue(i, 0)) continue;
+ if (typeof(data.getValue(i, 0)) === 'undefined' ||
+ data.getValue(i, 0) === null) {
+ this.warning("Ignoring row " + i +
+ " of DataTable because of undefined or null first column.");
+ continue;
+ }
+
if (indepType == 'date') {
row.push(data.getValue(i, 0).getTime());
} else {
Dygraph.update = function (self, o) {
if (typeof(o) != 'undefined' && o !== null) {
for (var k in o) {
- self[k] = o[k];
+ if (o.hasOwnProperty(k)) {
+ self[k] = o[k];
+ }
}
}
return self;
};
/**
+ * Resizes the dygraph. If no parameters are specified, resizes to fill the
+ * containing div (which has presumably changed size since the dygraph was
+ * instantiated. If the width/height are specified, the div will be resized.
+ *
+ * This is far more efficient than destroying and re-instantiating a
+ * Dygraph, since it doesn't have to reparse the underlying data.
+ *
+ * @param {Number} width Width (in pixels)
+ * @param {Number} height Height (in pixels)
+ */
+Dygraph.prototype.resize = function(width, height) {
+ if ((width === null) != (height === null)) {
+ this.warn("Dygraph.resize() should be called with zero parameters or " +
+ "two non-NULL parameters. Pretending it was zero.");
+ width = height = null;
+ }
+
+ // TODO(danvk): there should be a clear() method.
+ this.maindiv_.innerHTML = "";
+ this.attrs_.labelsDiv = null;
+
+ if (width) {
+ this.maindiv_.style.width = width + "px";
+ this.maindiv_.style.height = height + "px";
+ this.width_ = width;
+ this.height_ = height;
+ } else {
+ this.width_ = this.maindiv_.offsetWidth;
+ this.height_ = this.maindiv_.offsetHeight;
+ }
+
+ this.createInterface_();
+ this.drawGraph_(this.rawData_);
+};
+
+/**
* Adjusts the number of days in the rolling average. Updates the graph to
* reflect the new averaging period.
* @param {Number} length Number of days over which to average the data.