X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=ee5306ca1ef322b4de09e7e91ab70d28599466ba;hb=e17f6ca5747de4c634a9080693ea7e822ab5dd9c;hp=d20d348571bf9712fdaba60dc1e97541cc5e3da4;hpb=103b7292caaf98d842c99086c413945b1bd7f982;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index d20d348..ee5306c 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, @@ -99,6 +102,7 @@ Dygraph.DEFAULT_ATTRS = { axisLabelFontSize: 14, xAxisLabelWidth: 50, yAxisLabelWidth: 50, + xAxisLabelFormatter: Dygraph.dateAxisFormatter, rightGap: 5, showRoller: false, @@ -116,9 +120,12 @@ Dygraph.DEFAULT_ATTRS = { customBars: false, fillGraph: false, fillAlpha: 0.15, + connectSeparatedPoints: false, stackedGraph: false, - hideOverlayOnMouseOut: true + hideOverlayOnMouseOut: true, + + stepPlot: false }; // Various logging levels. @@ -143,8 +150,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 */ @@ -181,11 +188,17 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { // The div might have been specified as percent of the current window size, // convert that to an appropriate number of pixels. if (div.style.width.indexOf("%") == div.style.width.length - 1) { - // Minus ten pixels keeps scrollbars from showing up for a 100% width div. - this.width_ = (this.width_ * self.innerWidth / 100) - 10; + this.width_ = div.offsetWidth; } if (div.style.height.indexOf("%") == div.style.height.length - 1) { - this.height_ = (this.height_ * self.innerHeight / 100) - 10; + this.height_ = div.offsetHeight; + } + + if (this.width_ == 0) { + this.error("dygraph has zero width. Please specify a width in pixels."); + } + if (this.height_ == 0) { + this.error("dygraph has zero height. Please specify a height in pixels."); } // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_. @@ -209,6 +222,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); @@ -380,20 +395,24 @@ Dygraph.prototype.createInterface_ = function() { this.canvas_.height = this.height_; this.canvas_.style.width = this.width_ + "px"; // for IE this.canvas_.style.height = this.height_ + "px"; // for IE - this.graphDiv.appendChild(this.canvas_); // ... and for static parts of the chart. this.hidden_ = this.createPlotKitCanvas_(this.canvas_); + // The interactive parts of the graph are drawn on top of the chart. + this.graphDiv.appendChild(this.hidden_); + this.graphDiv.appendChild(this.canvas_); + this.mouseEventElement_ = 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.addEvent(this.mouseEventElement_, 'mousemove', function(e) { dygraph.mouseMove_(e); }); - Dygraph.addEvent(this.hidden_, 'mouseout', function(e) { + Dygraph.addEvent(this.mouseEventElement_, 'mouseout', function(e) { dygraph.mouseOut_(e); }); @@ -469,7 +488,6 @@ Dygraph.prototype.createPlotKitCanvas_ = function(canvas) { h.height = this.height_; h.style.width = this.width_ + "px"; // for IE h.style.height = this.height_ + "px"; // for IE - this.graphDiv.appendChild(h); return h; }; @@ -521,10 +539,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)); } @@ -558,7 +577,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) @@ -699,7 +718,7 @@ Dygraph.prototype.createDragInterface_ = function() { var getY = function(e) { return Dygraph.pageX(e) - py }; // Draw zoom rectangles when the mouse is down and the user moves around - Dygraph.addEvent(this.hidden_, 'mousemove', function(event) { + Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(event) { if (isZooming) { dragEndX = getX(event); dragEndY = getY(event); @@ -721,7 +740,7 @@ Dygraph.prototype.createDragInterface_ = function() { }); // Track the beginning of drag events - Dygraph.addEvent(this.hidden_, 'mousedown', function(event) { + Dygraph.addEvent(this.mouseEventElement_, 'mousedown', function(event) { px = Dygraph.findPosX(self.canvas_); py = Dygraph.findPosY(self.canvas_); dragStartX = getX(event); @@ -755,7 +774,7 @@ Dygraph.prototype.createDragInterface_ = function() { }); // Temporarily cancel the dragging event when the mouse leaves the graph - Dygraph.addEvent(this.hidden_, 'mouseout', function(event) { + Dygraph.addEvent(this.mouseEventElement_, 'mouseout', function(event) { if (isZooming) { dragEndX = null; dragEndY = null; @@ -764,7 +783,7 @@ Dygraph.prototype.createDragInterface_ = function() { // 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) { + Dygraph.addEvent(this.mouseEventElement_, 'mouseup', function(event) { if (isZooming) { isZooming = false; dragEndX = getX(event); @@ -800,7 +819,7 @@ Dygraph.prototype.createDragInterface_ = function() { }); // Double-clicking zooms back out - Dygraph.addEvent(this.hidden_, 'dblclick', function(event) { + Dygraph.addEvent(this.mouseEventElement_, 'dblclick', function(event) { if (self.dateWindow_ == null) return; self.dateWindow_ = null; self.drawGraph_(self.rawData_); @@ -871,7 +890,7 @@ Dygraph.prototype.doZoom_ = function(lowX, highX) { * @private */ Dygraph.prototype.mouseMove_ = function(event) { - var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.hidden_); + var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_); var points = this.layout_.points; var lastx = -1; @@ -883,7 +902,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; } @@ -894,36 +913,41 @@ Dygraph.prototype.mouseMove_ = function(event) { // Extract the points we've selected this.selPoints_ = []; - for (var i = 0; i < points.length; i++) { - if (points[i].xval == lastx) { - this.selPoints_.push(points[i]); + var l = points.length; + if (!this.attr_("stackedGraph")) { + for (var i = 0; i < l; i++) { + if (points[i].xval == lastx) { + this.selPoints_.push(points[i]); + } + } + } else { + // Need to 'unstack' points starting from the bottom + var cumulative_sum = 0; + for (var i = l - 1; i >= 0; i--) { + if (points[i].xval == lastx) { + var p = {}; // Clone the point since we modify it + for (var k in points[i]) { + p[k] = points[i][k]; + } + p.yval -= cumulative_sum; + cumulative_sum += p.yval; + this.selPoints_.push(p); + } } + this.selPoints_.reverse(); } if (this.attr_("highlightCallback")) { - var px = this.lastHighlightCallbackX; + var px = this.lastx_; 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_(); }; @@ -948,27 +972,34 @@ Dygraph.prototype.updateSelection_ = function() { // Set the status message to indicate the selected point(s) 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; } - 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(); } @@ -988,13 +1019,21 @@ 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) { - this.selPoints_.push(this.layout_.points[pos+row]); + 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 { @@ -1010,6 +1049,10 @@ Dygraph.prototype.setSelection = function(row) { * @private */ Dygraph.prototype.mouseOut_ = function(event) { + if (this.attr_("unhighlightCallback")) { + this.attr_("unhighlightCallback")(event); + } + if (this.attr_("hideOverlayOnMouseOut")) { this.clearSelection(); } @@ -1037,10 +1080,10 @@ Dygraph.prototype.getSelection = function() { if (!this.selPoints_ || this.selPoints_.length < 1) { return -1; } - + for (var row=0; row= Dygraph.MONTHLY) { + return date.strftime('%b %y'); + } else { + var frac = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds(); + if (frac == 0 || granularity >= Dygraph.DAILY) { + return new Date(date.getTime() + 3600*1000).strftime('%d%b'); + } else { + return Dygraph.hmsString_(date.getTime()); + } + } +} + +/** * Convert a JS date (millis since epoch) to YYYY/MM/DD * @param {Number} date The JavaScript date (ms since epoch) * @return {String} A date of the form "YYYY/MM/DD" * @private - * TODO(danvk): why is this part of the prototype? */ Dygraph.dateString_ = function(date, self) { var zeropad = Dygraph.zeropad; @@ -1088,7 +1151,7 @@ Dygraph.dateString_ = function(date, self) { var ret = ""; var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds(); - if (frac) ret = " " + self.hmsString_(date); + if (frac) ret = " " + Dygraph.hmsString_(date); return year + "/" + month + "/" + day + ret; }; @@ -1100,7 +1163,7 @@ Dygraph.dateString_ = function(date, self) { * @return {Number} The rounded number * @private */ -Dygraph.prototype.round_ = function(num, places) { +Dygraph.round_ = function(num, places) { var shift = Math.pow(10, places); return Math.round(num * shift)/shift; }; @@ -1210,6 +1273,7 @@ Dygraph.prototype.NumXTicks = function(start_time, end_time, granularity) { // Returns an array containing {v: millis, label: label} dictionaries. // Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) { + var formatter = this.attr_("xAxisLabelFormatter"); var ticks = []; if (granularity < Dygraph.MONTHLY) { // Generate one tick mark for every fixed interval of time. @@ -1246,14 +1310,7 @@ Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) { start_time = d.getTime(); for (var t = start_time; t <= end_time; t += spacing) { - var d = new Date(t); - var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds(); - if (frac == 0 || granularity >= Dygraph.DAILY) { - // the extra hour covers DST problems. - ticks.push({ v:t, label: new Date(t + 3600*1000).strftime(format) }); - } else { - ticks.push({ v:t, label: this.hmsString_(t) }); - } + ticks.push({ v:t, label: formatter(new Date(t), granularity) }); } } else { // Display a tick mark on the first of a set of months of each year. @@ -1284,7 +1341,7 @@ Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) { var date_str = i + "/" + zeropad(1 + months[j]) + "/01"; var t = Date.parse(date_str); if (t < start_time || t > end_time) continue; - ticks.push({ v:t, label: new Date(t).strftime('%b %y') }); + ticks.push({ v:t, label: formatter(new Date(t), granularity) }); } } } @@ -1348,7 +1405,7 @@ Dygraph.numericTicks = function(minV, maxV, self) { scale = base_scale * mults[j]; low_val = Math.floor(minV / scale) * scale; high_val = Math.ceil(maxV / scale) * scale; - nTicks = (high_val - low_val) / scale; + nTicks = Math.abs(high_val - low_val) / scale; var spacing = self.height_ / nTicks; // wish I could break out of both loops at once... if (spacing > pixelsPerTick) break; @@ -1370,16 +1427,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; } } @@ -1461,18 +1521,24 @@ Dygraph.prototype.drawGraph_ = function(data) { this.setColors_(); this.attrs_['pointSize'] = 0.5 * this.attr_('highlightCircleSize'); - // For stacked series. - var cumulative_y = []; - var stacked_datasets = []; + var connectSeparatedPoints = this.attr_('connectSeparatedPoints'); + + // Loop over the fields (series). Go from the last to the first, + // because if they're stacked that's how we accumulate the values. - // Loop over all fields in the dataset - for (var i = 1; i < data[0].length; i++) { + var cumulative_y = []; // For stacked series. + var datasets = []; + + // Loop over all fields and create datasets + for (var i = data[0].length - 1; i >= 1; 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] != null || !connectSeparatedPoints) { + var date = data[j][0]; + series.push([date, data[j][i]]); + } } series = this.rollingAverage(series, this.rollPeriod_); @@ -1499,51 +1565,51 @@ Dygraph.prototype.drawGraph_ = function(data) { 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); var thisMinY = extremes[0]; var thisMaxY = extremes[1]; - if (!minY || thisMinY < minY) minY = thisMinY; - if (!maxY || thisMaxY > maxY) maxY = thisMaxY; + if (minY === null || thisMinY < minY) minY = thisMinY; + if (maxY === null || thisMaxY > maxY) maxY = thisMaxY; if (bars) { - var vals = []; - for (var j=0; j maxY) - maxY = cumulative_y[series[j][0]]; + if (!maxY || cumulative_y[x] > maxY) + maxY = cumulative_y[x]; } - 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); } + + datasets[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]); - } + for (var i = 1; i < datasets.length; i++) { + this.layout_.addDataset(this.attr_("labels")[i], datasets[i]); } // Use some heuristics to come up with a good maxY value, unless it's been @@ -1782,10 +1848,12 @@ Dygraph.prototype.detectTypeFromString_ = function(str) { this.attrs_.xValueFormatter = Dygraph.dateString_; this.attrs_.xValueParser = Dygraph.dateParser; this.attrs_.xTicker = Dygraph.dateTicker; + this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter; } else { this.attrs_.xValueFormatter = function(x) { return x; }; this.attrs_.xValueParser = function(x) { return parseFloat(x); }; this.attrs_.xTicker = Dygraph.numericTicks; + this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter; } }; @@ -1917,6 +1985,7 @@ Dygraph.prototype.parseArray_ = function(data) { if (Dygraph.isDateLike(data[0][0])) { // Some intelligent defaults for a date x-axis. this.attrs_.xValueFormatter = Dygraph.dateString_; + this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter; this.attrs_.xTicker = Dygraph.dateTicker; // Assume they're all dates. @@ -1970,10 +2039,12 @@ Dygraph.prototype.parseDataTable_ = function(data) { this.attrs_.xValueFormatter = Dygraph.dateString_; this.attrs_.xValueParser = Dygraph.dateParser; this.attrs_.xTicker = Dygraph.dateTicker; + this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter; } else if (indepType == 'number') { this.attrs_.xValueFormatter = function(x) { return x; }; this.attrs_.xValueParser = function(x) { return parseFloat(x); }; this.attrs_.xTicker = Dygraph.numericTicks; + this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter; } else { this.error("only 'date', 'datetime' and 'number' types are supported for " + "column 1 of DataTable input (Got '" + indepType + "')"); @@ -2131,7 +2202,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 { @@ -2151,6 +2222,11 @@ Dygraph.prototype.updateOptions = function(attrs) { * @param {Number} height Height (in pixels) */ Dygraph.prototype.resize = function(width, height) { + if (this.resize_lock) { + return; + } + this.resize_lock = true; + if ((width === null) != (height === null)) { this.warn("Dygraph.resize() should be called with zero parameters or " + "two non-NULL parameters. Pretending it was zero."); @@ -2173,6 +2249,8 @@ Dygraph.prototype.resize = function(width, height) { this.createInterface_(); this.drawGraph_(this.rawData_); + + this.resize_lock = false; }; /** @@ -2244,8 +2322,7 @@ Dygraph.GVizChart.prototype.draw = function(data, options) { /** * Google charts compatible setSelection - * Only row selection is supported, all points in the - * row will be highlighted + * Only row selection is supported, all points in the row will be highlighted * @param {Array} array of the selected cells * @public */ @@ -2264,11 +2341,11 @@ Dygraph.GVizChart.prototype.setSelection = function(selection_array) { */ 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});