X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=6bacea2d080e2471c689503c304101de336a1a44;hb=891ad846715bece6a69959f97e0a42e57f559ff3;hp=aa6f24c5ef858b05c9bf0a3dc1454f50e940544a;hpb=71a11a8ef16b4719cf735d25b0aac8a357a9de2f;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index aa6f24c..6bacea2 100644 --- a/dygraph.js +++ b/dygraph.js @@ -162,6 +162,7 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.dateWindow_ = attrs.dateWindow || null; this.valueRange_ = attrs.valueRange || null; this.wilsonInterval_ = attrs.wilsonInterval || true; + this.is_initial_draw_ = true; // Clear the div. This ensure that, if multiple dygraphs are passed the same // div, then only one will be drawn. @@ -187,6 +188,7 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.height_ = (this.height_ * self.innerHeight / 100) - 10; } + // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_. if (attrs['stackedGraph']) { attrs['fillGraph'] = true; // TODO(nikhilk): Add any other stackedGraph checks here. @@ -278,6 +280,55 @@ Dygraph.prototype.xAxisRange = function() { 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; @@ -290,6 +341,13 @@ Dygraph.addEvent = function(el, evt, fn) { } }; +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 @@ -305,8 +363,16 @@ Dygraph.prototype.createInterface_ = function() { 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_; @@ -318,6 +384,10 @@ Dygraph.prototype.createInterface_ = function() { // ... 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); @@ -349,7 +419,35 @@ Dygraph.prototype.createInterface_ = function() { this.createStatusMessage_(); this.createRollInterface_(); this.createDragInterface_(); -} +}; + +/** + * Detach DOM elements in the dygraph and null out all data references. + * Calling this when you're done with a dygraph can dramatically reduce memory + * usage. See, e.g., the tests/perf.html example. + */ +Dygraph.prototype.destroy = function() { + var removeRecursive = function(node) { + while (node.hasChildNodes()) { + removeRecursive(node.firstChild); + node.removeChild(node.firstChild); + } + }; + removeRecursive(this.maindiv_); + + var nullOut = function(obj) { + for (var n in obj) { + if (typeof(obj[n]) === 'object') { + obj[n] = null; + } + } + }; + + // These may not all be necessary, but it can't hurt... + nullOut(this.layout_); + nullOut(this.plotter_); + nullOut(this); +}; /** * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on @@ -359,9 +457,11 @@ Dygraph.prototype.createInterface_ = function() { * @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_; @@ -808,23 +908,24 @@ Dygraph.prototype.mouseMove_ = function(event) { } } - // MERGE: check if this breaks compatibility. if (this.attr_("highlightCallback")) { var px = this.lastHighlightCallbackX; if (px !== null && lastx != px) { + // only fire if the selected point has changed. this.lastHighlightCallbackX = lastx; - this.attr_("highlightCallback")(event, lastx, this.selPoints_); - var callbackPoints = this.selPoints_.map( - function(p) { return {xval: p.xval, yval: p.yval, name: p.name} }); - if (this.attr_("stackedGraph")) { + if (!this.attr_("stackedGraph")) { + this.attr_("highlightCallback")(event, lastx, this.selPoints_); + } else { // "unstack" the points. + var callbackPoints = this.selPoints_.map( + function(p) { return {xval: p.xval, yval: p.yval, name: p.name} }); var cumulative_sum = 0; for (var j = callbackPoints.length - 1; j >= 0; j--) { callbackPoints[j].yval -= cumulative_sum; cumulative_sum += callbackPoints[j].yval; } + this.attr_("highlightCallback")(event, lastx, callbackPoints); } - this.attr_("highlightCallback")(event, lastx, callbackPoints); } } @@ -907,10 +1008,8 @@ Dygraph.prototype.hmsString_ = function(date) { return zeropad(d.getHours()) + ":" + zeropad(d.getMinutes()) + ":" + zeropad(d.getSeconds()); - } else if (d.getMinutes()) { - return zeropad(d.getHours()) + ":" + zeropad(d.getMinutes()); } else { - return zeropad(d.getHours()); + return zeropad(d.getHours()) + ":" + zeropad(d.getMinutes()); } } @@ -1298,6 +1397,10 @@ Dygraph.prototype.extremeValues_ = function(series) { * @private */ Dygraph.prototype.drawGraph_ = function(data) { + // This is used to set the second parameter to drawCallback, below. + var is_initial_draw = this.is_initial_draw_; + this.is_initial_draw_ = false; + var minY = null, maxY = null; this.layout_.removeAllDatasets(); this.setColors_(); @@ -1305,10 +1408,9 @@ Dygraph.prototype.drawGraph_ = function(data) { // For stacked series. var cumulative_y = []; - var datasets = []; + 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; @@ -1320,16 +1422,31 @@ Dygraph.prototype.drawGraph_ = function(data) { 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; } @@ -1361,17 +1478,16 @@ Dygraph.prototype.drawGraph_ = function(data) { if (!maxY || cumulative_y[series[j][0]] > maxY) maxY = cumulative_y[series[j][0]]; } - datasets.push([this.attr_("labels")[i], vals]); + stacked_datasets.push([this.attr_("labels")[i], vals]); //this.layout_.addDataset(this.attr_("labels")[i], vals); } else { this.layout_.addDataset(this.attr_("labels")[i], series); } } -// MERGE: move up into the stackedGraph section. - if (datasets.length > 0) { - for (var i = (datasets.length - 1); i >= 0; i--) { - this.layout_.addDataset(datasets[i][0], datasets[i][1]); + if (stacked_datasets.length > 0) { + for (var i = (stacked_datasets.length - 1); i >= 0; i--) { + this.layout_.addDataset(stacked_datasets[i][0], stacked_datasets[i][1]); } } @@ -1379,6 +1495,7 @@ Dygraph.prototype.drawGraph_ = function(data) { // 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) { @@ -1402,6 +1519,7 @@ Dygraph.prototype.drawGraph_ = function(data) { } this.addYTicks_(minAxisY, maxAxisY); + this.displayedYRange_ = [minAxisY, maxAxisY]; } this.addXTicks_(); @@ -1415,7 +1533,7 @@ Dygraph.prototype.drawGraph_ = function(data) { this.canvas_.height); if (this.attr_("drawCallback") !== null) { - this.attr_("drawCallback")(this); + this.attr_("drawCallback")(this, is_initial_draw); } }; @@ -1565,7 +1683,7 @@ Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) { Dygraph.dateParser = function(dateStr, self) { var dateStrSlashed; var d; - if (dateStr.length == 10 && dateStr.search("-") != -1) { // e.g. '2009-07-12' + if (dateStr.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12' dateStrSlashed = dateStr.replace("-", "/", "g"); while (dateStrSlashed.search("-") != -1) { dateStrSlashed = dateStrSlashed.replace("-", "/"); @@ -1754,8 +1872,9 @@ Dygraph.prototype.parseArray_ = function(data) { 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();