X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=a0e224e47d65e47c2f72a11174fec991ad876b7f;hb=e21e69e2937b64a6282de67d7f9558e24611b568;hp=1df7cc1b8761d719bb72d90ad6d3a4d4b14beffc;hpb=d053ab5a1f2a48d598857724f7d571fd6df36e20;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index 1df7cc1..a0e224e 100644 --- a/dygraph.js +++ b/dygraph.js @@ -114,7 +114,11 @@ Dygraph.DEFAULT_ATTRS = { fractions: false, wilsonInterval: true, // only relevant if fractions is true customBars: false, - fillGraph: false + fillGraph: false, + fillAlpha: 0.15, + + stackedGraph: false, + hideOverlayOnMouseOut: true }; // Various logging levels. @@ -158,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. @@ -183,6 +188,12 @@ 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. + } + // Dygraphs has many options, some of which interact with one another. // To keep track of everything, we maintain two sets of options: // @@ -197,6 +208,8 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.attrs_ = {}; Dygraph.update(this.attrs_, Dygraph.DEFAULT_ATTRS); + + this.boundaryIds_ = []; // Make a note of whether labels will be pulled from the CSV file. this.labelsFromCSV_ = (this.attr_("labels") == null); @@ -254,6 +267,71 @@ Dygraph.prototype.rollPeriod = function() { return this.rollPeriod_; }; +/** + * Returns the currently-visible x-range. This can be affected by zooming, + * panning or a call to updateOptions. + * Returns a two-element array: [left, right]. + * If the Dygraph has dates on the x-axis, these will be millis since epoch. + */ +Dygraph.prototype.xAxisRange = function() { + if (this.dateWindow_) return this.dateWindow_; + + // The entire chart is visible. + var left = this.rawData_[0][0]; + var right = this.rawData_[this.rawData_.length - 1][0]; + 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; +}; + +// TODO(danvk): use these functions throughout dygraphs. +/** + * 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; @@ -266,6 +344,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 @@ -281,8 +366,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_; @@ -294,6 +387,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); @@ -325,7 +422,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 @@ -335,9 +460,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_; @@ -398,8 +525,10 @@ Dygraph.prototype.setColors_ = function() { var val = this.attr_('colorValue') || 0.5; for (var i = 1; i <= num; i++) { if (!this.visibility()[i-1]) continue; - var hue = (1.0*i/(1+num)); - this.colors_.push( Dygraph.hsvToRGB(hue, sat, val) ); + // alternate colors for high contrast. + var idx = i - parseInt(i % 2 ? i / 2 : (i - num)/2, 10); + var hue = (1.0 * idx/ (1 + num)); + this.colors_.push(Dygraph.hsvToRGB(hue, sat, val)); } } else { for (var i = 0; i < num; i++) { @@ -416,34 +545,50 @@ Dygraph.prototype.setColors_ = function() { Dygraph.update(this.layoutOptions_, this.attrs_); } -// The following functions are from quirksmode.org +/** + * Return the list of colors. This is either the list of colors passed in the + * attributes, or the autogenerated list of rgb(r,g,b) strings. + * @return {Array} The list of colors. + */ +Dygraph.prototype.getColors = function() { + return this.colors_; +}; + +// The following functions are from quirksmode.org with a modification for Safari from +// http://blog.firetree.net/2005/07/04/javascript-find-position/ // http://www.quirksmode.org/js/findpos.html Dygraph.findPosX = function(obj) { var curleft = 0; - if (obj.offsetParent) { - while (obj.offsetParent) { + if(obj.offsetParent) + while(1) + { curleft += obj.offsetLeft; + if(!obj.offsetParent) + break; obj = obj.offsetParent; } - } - else if (obj.x) + else if(obj.x) curleft += obj.x; return curleft; }; Dygraph.findPosY = function(obj) { var curtop = 0; - if (obj.offsetParent) { - while (obj.offsetParent) { + if(obj.offsetParent) + while(1) + { curtop += obj.offsetTop; + if(!obj.offsetParent) + break; obj = obj.offsetParent; } - } - else if (obj.y) + else if(obj.y) curtop += obj.y; return curtop; }; + + /** * Create the div that contains information on the selected point(s) * This goes in the top right of the canvas, unless an external div has already @@ -708,19 +853,10 @@ Dygraph.prototype.drawZoomRect_ = function(startX, endX, prevEndX) { */ Dygraph.prototype.doZoom_ = function(lowX, highX) { // Find the earliest and latest dates contained in this canvasx range. - var points = this.layout_.points; - var minDate = null; - var maxDate = null; - // Find the nearest [minDate, maxDate] that contains [lowX, highX] - for (var i = 0; i < points.length; i++) { - var cx = points[i].canvasx; - var x = points[i].xval; - if (cx < lowX && (minDate == null || x > minDate)) minDate = x; - if (cx > highX && (maxDate == null || x < maxDate)) maxDate = x; - } - // Use the extremes if either is missing - if (minDate == null) minDate = points[0].xval; - if (maxDate == null) maxDate = points[points.length-1].xval; + var r = this.toDataCoords(lowX, null); + var minDate = r[0]; + r = this.toDataCoords(highX, null); + var maxDate = r[0]; this.dateWindow_ = [minDate, maxDate]; this.drawGraph_(this.rawData_); @@ -767,9 +903,38 @@ Dygraph.prototype.mouseMove_ = function(event) { } if (this.attr_("highlightCallback")) { - this.attr_("highlightCallback")(event, lastx, this.selPoints_); + var px = this.lastHighlightCallbackX; + if (px !== null && lastx != px) { + // only fire if the selected point has changed. + this.lastHighlightCallbackX = lastx; + 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); + } + } } + // Save last x position for callbacks. + this.lastx_ = lastx; + + this.updateSelection_(); +}; + +/** + * Draw dots over the selectied points in the data series. This function + * takes care of cleanup of previously-drawn dots. + * @private + */ +Dygraph.prototype.updateSelection_ = function() { // Clear the previously drawn vertical, if there is one var circleSize = this.attr_('highlightCircleSize'); var ctx = this.canvas_.getContext("2d"); @@ -784,7 +949,7 @@ Dygraph.prototype.mouseMove_ = function(event) { var canvasx = this.selPoints_[0].canvasx; // Set the status message to indicate the selected point(s) - var replace = this.attr_('xValueFormatter')(lastx, this) + ":"; + var replace = this.attr_('xValueFormatter')(this.lastx_, this) + ":"; var clen = this.colors_.length; for (var i = 0; i < this.selPoints_.length; i++) { if (!isOK(this.selPoints_[i].canvasy)) continue; @@ -799,11 +964,8 @@ Dygraph.prototype.mouseMove_ = function(event) { } this.attr_("labelsDiv").innerHTML = replace; - // Save last x position for callbacks. - this.lastx_ = lastx; - // Draw colored circles over the center of each selected point - ctx.save() + ctx.save(); for (var i = 0; i < this.selPoints_.length; i++) { if (!isOK(this.selPoints_[i%clen].canvasy)) continue; ctx.beginPath(); @@ -819,16 +981,80 @@ Dygraph.prototype.mouseMove_ = function(event) { }; /** + * Set manually set selected dots, and display information about them + * @param int row number that should by highlighted + * false value clears the selection + * @public + */ +Dygraph.prototype.setSelection = function(row) { + // Extract the points we've selected + this.selPoints_ = []; + var pos = 0; + + if (row !== false) { + row = row-this.boundaryIds_[0][0]; + } + + if (row !== false && row >= 0) { + for (var i in this.layout_.datasets) { + if (row < this.layout_.datasets[i].length) { + this.selPoints_.push(this.layout_.points[pos+row]); + } + pos += this.layout_.datasets[i].length; + } + } + + if (this.selPoints_.length) { + this.lastx_ = this.selPoints_[0].xval; + this.updateSelection_(); + } else { + this.lastx_ = -1; + this.clearSelection(); + } + +}; + +/** * The mouse has left the canvas. Clear out whatever artifacts remain * @param {Object} event the mouseout event from the browser. * @private */ Dygraph.prototype.mouseOut_ = function(event) { + if (this.attr_("hideOverlayOnMouseOut")) { + this.clearSelection(); + } +}; + +/** + * Remove all selection from the canvas + * @public + */ +Dygraph.prototype.clearSelection = function() { // Get rid of the overlay data var ctx = this.canvas_.getContext("2d"); ctx.clearRect(0, 0, this.width_, this.height_); this.attr_("labelsDiv").innerHTML = ""; -}; + this.selPoints_ = []; + this.lastx_ = -1; +} + +/** + * Returns the number of the currently selected row + * @return int row number, of -1 if nothing is selected + * @public + */ +Dygraph.prototype.getSelection = function() { + if (!this.selPoints_ || this.selPoints_.length < 1) { + return -1; + } + + for (var row=0; row pixelsPerTick) break; @@ -1156,6 +1380,9 @@ Dygraph.numericTicks = function(minV, maxV, self) { k_labels = [ "k", "M", "G", "T" ]; } + // Allow reverse y-axis if it's explicitly requested. + if (low_val > high_val) scale *= -1; + for (var i = 0; i < nTicks; i++) { var tickV = low_val + i * scale; var absTickV = Math.abs(tickV); @@ -1238,11 +1465,19 @@ 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_(); this.attrs_['pointSize'] = 0.5 * this.attr_('highlightCircleSize'); + // For stacked series. + var cumulative_y = []; + 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; @@ -1255,17 +1490,35 @@ 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++; + this.boundaryIds_[i-1] = [firstIdx, lastIdx]; + for (var k = firstIdx; k <= lastIdx; k++) { + pruned.push(series[k]); } series = pruned; + } else { + this.boundaryIds_[i-1] = [0, series.length-1]; } var extremes = this.extremeValues_(series); @@ -1280,15 +1533,40 @@ Dygraph.prototype.drawGraph_ = function(data) { vals[j] = [series[j][0], series[j][1][0], series[j][1][1], series[j][1][2]]; this.layout_.addDataset(this.attr_("labels")[i], vals); + } else if (this.attr_("stackedGraph")) { + var vals = []; + var l = series.length; + var actual_y; + for (var j = 0; j < l; j++) { + if (cumulative_y[series[j][0]] === undefined) + cumulative_y[series[j][0]] = 0; + + actual_y = series[j][1]; + cumulative_y[series[j][0]] += actual_y; + + vals[j] = [series[j][0], cumulative_y[series[j][0]]] + + if (!maxY || cumulative_y[series[j][0]] > maxY) + maxY = cumulative_y[series[j][0]]; + } + 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); } } + 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]); + } + } + // Use some heuristics to come up with a good maxY value, unless it's been // 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) { @@ -1312,6 +1590,7 @@ Dygraph.prototype.drawGraph_ = function(data) { } this.addYTicks_(minAxisY, maxAxisY); + this.displayedYRange_ = [minAxisY, maxAxisY]; } this.addXTicks_(); @@ -1323,6 +1602,10 @@ Dygraph.prototype.drawGraph_ = function(data) { this.plotter_.render(); this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width, this.canvas_.height); + + if (this.attr_("drawCallback") !== null) { + this.attr_("drawCallback")(this, is_initial_draw); + } }; /** @@ -1471,7 +1754,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("-", "/"); @@ -1660,8 +1943,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(); @@ -1974,5 +2258,41 @@ Dygraph.GVizChart.prototype.draw = function(data, options) { this.date_graph = new Dygraph(this.container, data, options); } +/** + * Google charts compatible setSelection + * Only row selection is supported, all points in the + * row will be highlighted + * @param {Array} array of the selected cells + * @public + */ +Dygraph.GVizChart.prototype.setSelection = function(selection_array) { + var row = false; + if (selection_array.length) { + row = selection_array[0].row; + } + this.date_graph.setSelection(row); +} + +/** + * Google charts compatible getSelection implementation + * @return {Array} array of the selected cells + * @public + */ +Dygraph.GVizChart.prototype.getSelection = function() { + var selection = []; + + var row = this.date_graph.getSelection(); + + if (row < 0) return selection; + + col = 1; + for (var i in this.date_graph.layout_.datasets) { + selection.push({row: row, column: col}); + col++; + } + + return selection; +} + // Older pages may still use this name. DateGraph = Dygraph;