X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=954d067b919f984ae3741e914a29fc24f820a510;hb=2aa21213e0c8f56985595ebd106dad77583a6b4e;hp=6b1a99cebc28c5f1a4e093e92a5302bc965be992;hpb=3a909ec56945357aa074c1f4b268c8d995bcde20;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index 6b1a99c..954d067 100644 --- a/dygraph.js +++ b/dygraph.js @@ -92,6 +92,9 @@ Dygraph.DEFAULT_ATTRS = { labelsSeparateLines: false, labelsKMB: false, labelsKMG2: false, + showLabelsOnHighlight: true, + + yValueFormatter: function(x) { return Dygraph.round_(x, 2); }, strokeWidth: 1.0, @@ -116,6 +119,7 @@ Dygraph.DEFAULT_ATTRS = { customBars: false, fillGraph: false, fillAlpha: 0.15, + connectSeparatedPoints: false, stackedGraph: false, hideOverlayOnMouseOut: true @@ -143,8 +147,8 @@ Dygraph.prototype.__old_init__ = function(div, file, labels, attrs) { * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit * and interaction <canvas> inside of it. See the constructor for details * on the parameters. + * @param {Element} div the Element to render the graph into. * @param {String | Function} file Source data - * @param {Array.} labels Names of the data series * @param {Object} attrs Miscellaneous other options * @private */ @@ -209,6 +213,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); @@ -280,6 +286,56 @@ 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; +}; + +// 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; @@ -471,10 +527,11 @@ Dygraph.prototype.setColors_ = function() { if (!colors) { var sat = this.attr_('colorSaturation') || 1.0; var val = this.attr_('colorValue') || 0.5; + var half = Math.ceil(num / 2); for (var i = 1; i <= num; i++) { if (!this.visibility()[i-1]) continue; // alternate colors for high contrast. - var idx = i - parseInt(i % 2 ? i / 2 : (i - num)/2, 10); + var idx = i % 2 ? Math.ceil(i / 2) : (half + i / 2); var hue = (1.0 * idx/ (1 + num)); this.colors_.push(Dygraph.hsvToRGB(hue, sat, val)); } @@ -508,7 +565,7 @@ Dygraph.prototype.getColors = function() { Dygraph.findPosX = function(obj) { var curleft = 0; if(obj.offsetParent) - while(1) + while(1) { curleft += obj.offsetLeft; if(!obj.offsetParent) @@ -801,19 +858,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_); @@ -842,7 +890,7 @@ Dygraph.prototype.mouseMove_ = function(event) { var idx = -1; for (var i = 0; i < points.length; i++) { var dist = Math.abs(points[i].canvasx - canvasx); - if (dist > minDist) break; + if (dist > minDist) continue; minDist = dist; idx = i; } @@ -853,9 +901,24 @@ Dygraph.prototype.mouseMove_ = function(event) { // Extract the points we've selected this.selPoints_ = []; - for (var i = 0; i < points.length; i++) { + var cumulative_sum = 0; // used only if we have a stackedGraph. + var l = points.length; + var isStacked = this.attr_("stackedGraph"); + for (var i = l - 1; i >= 0; i--) { if (points[i].xval == lastx) { - this.selPoints_.push(points[i]); + if (!isStacked) { + this.selPoints_.unshift(points[i]); + } else { + // Clone the point, since we need to 'unstack' it below. Stacked points + // are in reverse order. + var p = {}; + for (var k in points[i]) { + p[k] = points[i][k]; + } + p.yval -= cumulative_sum; + cumulative_sum += p.yval; + this.selPoints_.push(p); + } } } @@ -864,22 +927,22 @@ Dygraph.prototype.mouseMove_ = function(event) { 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); - } + this.attr_("highlightCallback")(event, lastx, this.selPoints_); } } + // 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"); @@ -894,31 +957,35 @@ 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 fmtFunc = this.attr_('yValueFormatter'); var clen = this.colors_.length; - for (var i = 0; i < this.selPoints_.length; i++) { - if (!isOK(this.selPoints_[i].canvasy)) continue; - if (this.attr_("labelsSeparateLines")) { - replace += "
"; + + if (this.attr_('showLabelsOnHighlight')) { + // Set the status message to indicate the selected point(s) + for (var i = 0; i < this.selPoints_.length; i++) { + if (!isOK(this.selPoints_[i].canvasy)) continue; + if (this.attr_("labelsSeparateLines")) { + replace += "
"; + } + var point = this.selPoints_[i]; + var c = new RGBColor(this.colors_[i%clen]); + var yval = fmtFunc(point.yval); + replace += " " + + point.name + ":" + + yval; } - var point = this.selPoints_[i]; - var c = new RGBColor(this.colors_[i%clen]); - replace += " " - + point.name + ":" - + this.round_(point.yval, 2); - } - this.attr_("labelsDiv").innerHTML = replace; - // Save last x position for callbacks. - this.lastx_ = lastx; + this.attr_("labelsDiv").innerHTML = replace; + } // Draw colored circles over the center of each selected point ctx.save(); for (var i = 0; i < this.selPoints_.length; i++) { - if (!isOK(this.selPoints_[i%clen].canvasy)) continue; + if (!isOK(this.selPoints_[i].canvasy)) continue; ctx.beginPath(); - ctx.fillStyle = this.colors_[i%clen]; - ctx.arc(canvasx, this.selPoints_[i%clen].canvasy, circleSize, + ctx.fillStyle = this.plotter_.colors[this.selPoints_[i].name]; + ctx.arc(canvasx, this.selPoints_[i].canvasy, circleSize, 0, 2 * Math.PI, false); ctx.fill(); } @@ -929,19 +996,81 @@ 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")) { - // 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.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; @@ -1266,16 +1395,19 @@ 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); - var label = self.round_(tickV, 2); + var label = Dygraph.round_(tickV, 2); 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]; + label = Dygraph.round_(tickV / n, 1) + k_labels[j]; break; } } @@ -1357,34 +1489,55 @@ Dygraph.prototype.drawGraph_ = function(data) { this.setColors_(); this.attrs_['pointSize'] = 0.5 * this.attr_('highlightCircleSize'); + var connectSeparatedPoints = this.attr_('connectSeparatedPoints'); + // 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; var series = []; for (var j = 0; j < data.length; j++) { - var date = data[j][0]; - series[j] = [date, data[j][i]]; + if (data[j][i] || !connectSeparatedPoints) { + var date = data[j][0]; + series.push([date, data[j][i]]); + } } 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); @@ -1432,6 +1585,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) { @@ -1455,6 +1609,7 @@ Dygraph.prototype.drawGraph_ = function(data) { } this.addYTicks_(minAxisY, maxAxisY); + this.displayedYRange_ = [minAxisY, maxAxisY]; } this.addXTicks_(); @@ -1809,7 +1964,7 @@ Dygraph.prototype.parseArray_ = function(data) { if (parsedData[i][0] == null || typeof(parsedData[i][0].getTime) != 'function' || isNaN(parsedData[i][0].getTime())) { - this.error("x value in row " << (1 + i) << " is not a Date"); + this.error("x value in row " + (1 + i) + " is not a Date"); return null; } parsedData[i][0] = parsedData[i][0].getTime(); @@ -2011,7 +2166,7 @@ Dygraph.prototype.updateOptions = function(attrs) { // TODO(danvk): this doesn't match the constructor logic this.layout_.updateOptions({ 'errorBars': this.attr_("errorBars") }); - if (attrs['file'] && attrs['file'] != this.file_) { + if (attrs['file']) { this.file_ = attrs['file']; this.start_(); } else { @@ -2122,5 +2277,40 @@ 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;