return [left, right];
};
+/**
+ * Returns the currently-visible y-range. This can be affected by zooming,
+ * panning or a call to updateOptions.
+ * Returns a two-element array: [bottom, top].
+ */
+Dygraph.prototype.yAxisRange = function() {
+ return this.displayedYRange_;
+};
+
+/**
+ * Convert from data coordinates to canvas/div X/Y coordinates.
+ * Returns a two-element array: [X, Y]
+ */
+Dygraph.prototype.toDomCoords = function(x, y) {
+ var ret = [null, null];
+ var area = this.plotter_.area;
+ if (x !== null) {
+ var xRange = this.xAxisRange();
+ ret[0] = area.x + (x - xRange[0]) / (xRange[1] - xRange[0]) * area.w;
+ }
+
+ if (y !== null) {
+ var yRange = this.yAxisRange();
+ ret[1] = area.y + (yRange[1] - y) / (yRange[1] - yRange[0]) * area.h;
+ }
+
+ return ret;
+};
+
+/**
+ * Convert from canvas/div coords to data coordinates.
+ * Returns a two-element array: [X, Y]
+ */
+Dygraph.prototype.toDataCoords = function(x, y) {
+ var ret = [null, null];
+ var area = this.plotter_.area;
+ if (x !== null) {
+ var xRange = this.xAxisRange();
+ ret[0] = xRange[0] + (x - area.x) / area.w * (xRange[1] - xRange[0]);
+ }
+
+ if (y !== null) {
+ var yRange = this.yAxisRange();
+ ret[1] = yRange[0] + (area.h - y) / area.h * (yRange[1] - yRange[0]);
+ }
+
+ return ret;
+};
+
Dygraph.addEvent = function(el, evt, fn) {
var normed_fn = function(e) {
if (!e) var e = window.event;
}
};
+Dygraph.clipCanvas_ = function(cnv, clip) {
+ var ctx = cnv.getContext("2d");
+ ctx.beginPath();
+ ctx.rect(clip.left, clip.top, clip.width, clip.height);
+ ctx.clip();
+};
+
/**
* Generates interface elements for the Dygraph: a containing div, a div to
* display the current point, and a textbox to adjust the rolling average
this.graphDiv.style.height = this.height_ + "px";
enclosing.appendChild(this.graphDiv);
+ var clip = {
+ top: 0,
+ left: this.attr_("yAxisLabelWidth") + 2 * this.attr_("axisTickSize")
+ };
+ clip.width = this.width_ - clip.left - this.attr_("rightGap");
+ clip.height = this.height_ - this.attr_("axisLabelFontSize")
+ - 2 * this.attr_("axisTickSize");
+ this.clippingArea_ = clip;
+
// Create the canvas for interactive parts of the chart.
- // this.canvas_ = document.createElement("canvas");
this.canvas_ = Dygraph.createCanvas();
this.canvas_.style.position = "absolute";
this.canvas_.width = this.width_;
// ... and for static parts of the chart.
this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
+ // Make sure we don't overdraw.
+ Dygraph.clipCanvas_(this.hidden_, this.clippingArea_);
+ Dygraph.clipCanvas_(this.canvas_, this.clippingArea_);
+
var dygraph = this;
Dygraph.addEvent(this.hidden_, 'mousemove', function(e) {
dygraph.mouseMove_(e);
* @private
*/
Dygraph.prototype.createPlotKitCanvas_ = function(canvas) {
- // var h = document.createElement("canvas");
var h = Dygraph.createCanvas();
h.style.position = "absolute";
+ // TODO(danvk): h should be offset from canvas. canvas needs to include
+ // some extra area to make it easier to zoom in on the far left and far
+ // right. h needs to be precisely the plot area, so that clipping occurs.
h.style.top = canvas.style.top;
h.style.left = canvas.style.left;
h.width = this.width_;
var stacked_datasets = [];
// Loop over all fields in the dataset
-
for (var i = 1; i < data[0].length; i++) {
if (!this.visibility()[i - 1]) continue;
series = this.rollingAverage(series, this.rollPeriod_);
// Prune down to the desired range, if necessary (for zooming)
+ // Because there can be lines going to points outside of the visible area,
+ // we actually prune to visible points, plus one on either side.
var bars = this.attr_("errorBars") || this.attr_("customBars");
if (this.dateWindow_) {
var low = this.dateWindow_[0];
var high= this.dateWindow_[1];
var pruned = [];
+ // TODO(danvk): do binary search instead of linear search.
+ // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
+ var firstIdx = null, lastIdx = null;
for (var k = 0; k < series.length; k++) {
- if (series[k][0] >= low && series[k][0] <= high) {
- pruned.push(series[k]);
+ if (series[k][0] >= low && firstIdx === null) {
+ firstIdx = k;
+ }
+ if (series[k][0] <= high) {
+ lastIdx = k;
}
}
+ if (firstIdx === null) firstIdx = 0;
+ if (firstIdx > 0) firstIdx--;
+ if (lastIdx === null) lastIdx = series.length - 1;
+ if (lastIdx < series.length - 1) lastIdx++;
+ for (var k = firstIdx; k <= lastIdx; k++) {
+ pruned.push(series[k]);
+ }
series = pruned;
}
// set explicitly by the user.
if (this.valueRange_ != null) {
this.addYTicks_(this.valueRange_[0], this.valueRange_[1]);
+ this.displayedYRange_ = this.valueRange_;
} else {
// This affects the calculation of span, below.
if (this.attr_("includeZero") && minY > 0) {
}
this.addYTicks_(minAxisY, maxAxisY);
+ this.displayedYRange_ = [minAxisY, maxAxisY];
}
this.addXTicks_();
return null;
}
if (parsedData[i][0] == null
- || typeof(parsedData[i][0].getTime) != 'function') {
- this.error("x value in row " << (1 + i) << " is not a Date");
+ || typeof(parsedData[i][0].getTime) != 'function'
+ || isNaN(parsedData[i][0].getTime())) {
+ this.error("x value in row " + (1 + i) + " is not a Date");
return null;
}
parsedData[i][0] = parsedData[i][0].getTime();