X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=16e96f114613916170ac5aa8e32fee5e8a1c7485;hb=1f75a05ce24d88587871a7305f1982010577cd33;hp=edc3eeabf997adfdf7ba6e82d14e19e9d87f6235;hpb=129569a500ce90f2b3d210791c53b09f53dda8de;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index edc3eea..16e96f1 100644 --- a/dygraph.js +++ b/dygraph.js @@ -126,7 +126,8 @@ Dygraph.DEFAULT_ATTRS = { stackedGraph: false, hideOverlayOnMouseOut: true, - stepPlot: false + stepPlot: false, + avoidMinZero: false }; // Various logging levels. @@ -135,6 +136,11 @@ Dygraph.INFO = 2; Dygraph.WARNING = 3; Dygraph.ERROR = 3; +// Directions for panning and zooming. Use bit operations when combined +// values are possible. +Dygraph.HORIZONTAL = 1; +Dygraph.VERTICAL = 2; + // Used for initializing annotation CSS rules only once. Dygraph.addedAnnotationCSS = false; @@ -171,7 +177,13 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.previousVerticalX_ = -1; this.fractions_ = attrs.fractions || false; this.dateWindow_ = attrs.dateWindow || null; + // valueRange and valueWindow are similar, but not the same. valueRange is a + // locally-stored copy of the attribute. valueWindow starts off the same as + // valueRange but is impacted by zoom or pan effects. valueRange is kept + // around to restore the original value back to valueRange. this.valueRange_ = attrs.valueRange || null; + this.valueWindow_ = this.valueRange_; + this.wilsonInterval_ = attrs.wilsonInterval || true; this.is_initial_draw_ = true; this.annotations_ = []; @@ -232,16 +244,19 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { // Make a note of whether labels will be pulled from the CSV file. this.labelsFromCSV_ = (this.attr_("labels") == null); - Dygraph.addAnnotationRule(); - // Create the containing DIV and other interactive elements this.createInterface_(); this.start_(); }; -Dygraph.prototype.attr_ = function(name) { - if (typeof(this.user_attrs_[name]) != 'undefined') { +Dygraph.prototype.attr_ = function(name, seriesName) { + if (seriesName && + typeof(this.user_attrs_[seriesName]) != 'undefined' && + this.user_attrs_[seriesName] != null && + typeof(this.user_attrs_[seriesName][name]) != 'undefined') { + return this.user_attrs_[seriesName][name]; + } else if (typeof(this.user_attrs_[name]) != 'undefined') { return this.user_attrs_[name]; } else if (typeof(this.attrs_[name]) != 'undefined') { return this.attrs_[name]; @@ -745,9 +760,22 @@ Dygraph.prototype.createDragInterface_ = function() { var dragStartY = null; var dragEndX = null; var dragEndY = null; + var dragDirection = null; var prevEndX = null; + var prevEndY = null; + var prevDragDirection = null; + + // draggingDate and draggingValue represent the [date,value] point on the + // graph at which the mouse was pressed. As the mouse moves while panning, + // the viewport must pan so that the mouse position points to + // [draggingDate, draggingValue] var draggingDate = null; + var draggingValue = null; + + // The range in second/value units that the viewport encompasses during a + // panning operation. var dateRange = null; + var valueRange = null; // Utility function to convert page-wide coordinates to canvas coords var px = 0; @@ -761,18 +789,41 @@ Dygraph.prototype.createDragInterface_ = function() { dragEndX = getX(event); dragEndY = getY(event); - self.drawZoomRect_(dragStartX, dragEndX, prevEndX); + var xDelta = Math.abs(dragStartX - dragEndX); + var yDelta = Math.abs(dragStartY - dragEndY); + + // drag direction threshold for y axis is twice as large as x axis + dragDirection = (xDelta < yDelta / 2) ? Dygraph.VERTICAL : Dygraph.HORIZONTAL; + + self.drawZoomRect_(dragDirection, dragStartX, dragEndX, dragStartY, dragEndY, + prevDragDirection, prevEndX, prevEndY); + prevEndX = dragEndX; + prevEndY = dragEndY; + prevDragDirection = dragDirection; } else if (isPanning) { dragEndX = getX(event); dragEndY = getY(event); // Want to have it so that: - // 1. draggingDate appears at dragEndX + // 1. draggingDate appears at dragEndX, draggingValue appears at dragEndY. // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered. + // 3. draggingValue appears at dragEndY. + // 4. valueRange is unaltered. - self.dateWindow_[0] = draggingDate - (dragEndX / self.width_) * dateRange; - self.dateWindow_[1] = self.dateWindow_[0] + dateRange; + var minDate = draggingDate - (dragEndX / self.width_) * dateRange; + var maxDate = minDate + dateRange; + self.dateWindow_ = [minDate, maxDate]; + + + // y-axis scaling is automatic unless a valueRange is defined or + // if the user zooms in on the y-axis. If neither is true, valueWindow_ + // will be null. + if (self.valueWindow_) { + var maxValue = draggingValue + (dragEndY / self.height_) * valueRange; + var minValue = maxValue - valueRange; + self.valueWindow_ = [ minValue, maxValue ]; + } self.drawGraph_(self.rawData_); } }); @@ -785,11 +836,21 @@ Dygraph.prototype.createDragInterface_ = function() { dragStartY = getY(event); if (event.altKey || event.shiftKey) { - if (!self.dateWindow_) return; // have to be zoomed in to pan. + // have to be zoomed in to pan. + if (!self.dateWindow_ && !self.valueWindow_) return; + isPanning = true; - dateRange = self.dateWindow_[1] - self.dateWindow_[0]; + var xRange = self.xAxisRange(); + dateRange = xRange[1] - xRange[0]; + var yRange = self.yAxisRange(); + valueRange = yRange[1] - yRange[0]; + + // TODO(konigsberg): Switch from all this math to toDataCoords? + // Seems to work for the dragging value. draggingDate = (dragStartX / self.width_) * dateRange + - self.dateWindow_[0]; + xRange[0]; + var r = self.toDataCoords(null, dragStartY); + draggingValue = r[1]; } else { isZooming = true; } @@ -807,7 +868,9 @@ Dygraph.prototype.createDragInterface_ = function() { if (isPanning) { isPanning = false; draggingDate = null; + draggingValue = null; dateRange = null; + valueRange = null; } }); @@ -857,9 +920,12 @@ Dygraph.prototype.createDragInterface_ = function() { } } - if (regionWidth >= 10) { - self.doZoom_(Math.min(dragStartX, dragEndX), + if (regionWidth >= 10 && dragDirection == Dygraph.HORIZONTAL) { + self.doZoomX_(Math.min(dragStartX, dragEndX), Math.max(dragStartX, dragEndX)); + } else if (regionHeight >= 10 && dragDirection == Dygraph.VERTICAL){ + self.doZoomY_(Math.min(dragStartY, dragEndY), + Math.max(dragStartY, dragEndY)); } else { self.canvas_.getContext("2d").clearRect(0, 0, self.canvas_.width, @@ -873,20 +939,18 @@ Dygraph.prototype.createDragInterface_ = function() { if (isPanning) { isPanning = false; draggingDate = null; + draggingValue = null; dateRange = null; + valueRange = null; } }); // Double-clicking zooms back out Dygraph.addEvent(this.mouseEventElement_, 'dblclick', function(event) { - if (self.dateWindow_ == null) return; - self.dateWindow_ = null; - self.drawGraph_(self.rawData_); - var minDate = self.rawData_[0][0]; - var maxDate = self.rawData_[self.rawData_.length - 1][0]; - if (self.attr_("zoomCallback")) { - self.attr_("zoomCallback")(minDate, maxDate); - } + // Disable zooming out if panning. + if (event.altKey || event.shiftKey) return; + + self.doUnzoom_(); }); }; @@ -895,49 +959,158 @@ Dygraph.prototype.createDragInterface_ = function() { * up any previous zoom rectangles that were drawn. This could be optimized to * avoid extra redrawing, but it's tricky to avoid interactions with the status * dots. + * + * @param {Number} direction the direction of the zoom rectangle. Acceptable + * values are Dygraph.HORIZONTAL and Dygraph.VERTICAL. * @param {Number} startX The X position where the drag started, in canvas * coordinates. * @param {Number} endX The current X position of the drag, in canvas coords. + * @param {Number} startY The Y position where the drag started, in canvas + * coordinates. + * @param {Number} endY The current Y position of the drag, in canvas coords. + * @param {Number} prevDirection the value of direction on the previous call to + * this function. Used to avoid excess redrawing * @param {Number} prevEndX The value of endX on the previous call to this * function. Used to avoid excess redrawing + * @param {Number} prevEndY The value of endY on the previous call to this + * function. Used to avoid excess redrawing * @private */ -Dygraph.prototype.drawZoomRect_ = function(startX, endX, prevEndX) { +Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY, endY, + prevDirection, prevEndX, prevEndY) { var ctx = this.canvas_.getContext("2d"); // Clean up from the previous rect if necessary - if (prevEndX) { + if (prevDirection == Dygraph.HORIZONTAL) { ctx.clearRect(Math.min(startX, prevEndX), 0, Math.abs(startX - prevEndX), this.height_); + } else if (prevDirection == Dygraph.VERTICAL){ + ctx.clearRect(0, Math.min(startY, prevEndY), + this.width_, Math.abs(startY - prevEndY)); } // Draw a light-grey rectangle to show the new viewing area - if (endX && startX) { - ctx.fillStyle = "rgba(128,128,128,0.33)"; - ctx.fillRect(Math.min(startX, endX), 0, - Math.abs(endX - startX), this.height_); + if (direction == Dygraph.HORIZONTAL) { + if (endX && startX) { + ctx.fillStyle = "rgba(128,128,128,0.33)"; + ctx.fillRect(Math.min(startX, endX), 0, + Math.abs(endX - startX), this.height_); + } + } + if (direction == Dygraph.VERTICAL) { + if (endY && startY) { + ctx.fillStyle = "rgba(128,128,128,0.33)"; + ctx.fillRect(0, Math.min(startY, endY), + this.width_, Math.abs(endY - startY)); + } } }; /** - * Zoom to something containing [lowX, highX]. These are pixel coordinates - * in the canvas. The exact zoom window may be slightly larger if there are no - * data points near lowX or highX. This function redraws the graph. + * Zoom to something containing [lowX, highX]. These are pixel coordinates in + * the canvas. The exact zoom window may be slightly larger if there are no data + * points near lowX or highX. Don't confuse this function with doZoomXDates, + * which accepts dates that match the raw data. This function redraws the graph. + * * @param {Number} lowX The leftmost pixel value that should be visible. * @param {Number} highX The rightmost pixel value that should be visible. * @private */ -Dygraph.prototype.doZoom_ = function(lowX, highX) { +Dygraph.prototype.doZoomX_ = function(lowX, highX) { // Find the earliest and latest dates contained in this canvasx range. + // Convert the call to date ranges of the raw data. var r = this.toDataCoords(lowX, null); var minDate = r[0]; r = this.toDataCoords(highX, null); var maxDate = r[0]; + this.doZoomXDates_(minDate, maxDate); +}; +/** + * Zoom to something containing [minDate, maxDate] values. Don't confuse this + * method with doZoomX which accepts pixel coordinates. This function redraws + * the graph. + * + * @param {Number} minDate The minimum date that should be visible. + * @param {Number} maxDate The maximum date that should be visible. + * @private + */ +Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) { this.dateWindow_ = [minDate, maxDate]; this.drawGraph_(this.rawData_); if (this.attr_("zoomCallback")) { - this.attr_("zoomCallback")(minDate, maxDate); + var yRange = this.yAxisRange(); + this.attr_("zoomCallback")(minDate, maxDate, yRange[0], yRange[1]); + } +}; + +/** + * Zoom to something containing [lowY, highY]. These are pixel coordinates in + * the canvas. The exact zoom window may be slightly larger if there are no + * data points near lowY or highY. Don't confuse this function with + * doZoomYValues, which accepts parameters that match the raw data. This + * function redraws the graph. + * + * @param {Number} lowY The topmost pixel value that should be visible. + * @param {Number} highY The lowest pixel value that should be visible. + * @private + */ +Dygraph.prototype.doZoomY_ = function(lowY, highY) { + // Find the highest and lowest values in pixel range. + var r = this.toDataCoords(null, lowY); + var maxValue = r[1]; + r = this.toDataCoords(null, highY); + var minValue = r[1]; + + this.doZoomYValues_(minValue, maxValue); +}; + +/** + * Zoom to something containing [minValue, maxValue] values. Don't confuse this + * method with doZoomY which accepts pixel coordinates. This function redraws + * the graph. + * + * @param {Number} minValue The minimum Value that should be visible. + * @param {Number} maxValue The maximum value that should be visible. + * @private + */ +Dygraph.prototype.doZoomYValues_ = function(minValue, maxValue) { + this.valueWindow_ = [minValue, maxValue]; + this.drawGraph_(this.rawData_); + if (this.attr_("zoomCallback")) { + var xRange = this.xAxisRange(); + this.attr_("zoomCallback")(xRange[0], xRange[1], minValue, maxValue); + } +}; + +/** + * Reset the zoom to the original view coordinates. This is the same as + * double-clicking on the graph. + * + * @private + */ +Dygraph.prototype.doUnzoom_ = function() { + var dirty = null; + if (this.dateWindow_ != null) { + dirty = 1; + this.dateWindow_ = null; + } + if (this.valueWindow_ != null) { + dirty = 1; + this.valueWindow_ = this.valueRange_; + } + + if (dirty) { + // Putting the drawing operation before the callback because it resets + // yAxisRange. + this.drawGraph_(this.rawData_); + if (this.attr_("zoomCallback")) { + var minDate = this.rawData_[0][0]; + var maxDate = this.rawData_[this.rawData_.length - 1][0]; + var minValue = this.yAxisRange()[0]; + var maxValue = this.yAxisRange()[1]; + this.attr_("zoomCallback")(minDate, maxDate, minValue, maxValue); + } } }; @@ -1017,11 +1190,18 @@ Dygraph.prototype.mouseMove_ = function(event) { */ Dygraph.prototype.updateSelection_ = function() { // Clear the previously drawn vertical, if there is one - var circleSize = this.attr_('highlightCircleSize'); var ctx = this.canvas_.getContext("2d"); if (this.previousVerticalX_ >= 0) { + // Determine the maximum highlight circle size. + var maxCircleSize = 0; + var labels = this.attr_('labels'); + for (var i = 1; i < labels.length; i++) { + var r = this.attr_('highlightCircleSize', labels[i]); + if (r > maxCircleSize) maxCircleSize = r; + } var px = this.previousVerticalX_; - ctx.clearRect(px - circleSize - 1, 0, 2 * circleSize + 2, this.height_); + ctx.clearRect(px - maxCircleSize - 1, 0, + 2 * maxCircleSize + 2, this.height_); } var isOK = function(x) { return x && !isNaN(x); }; @@ -1043,7 +1223,7 @@ Dygraph.prototype.updateSelection_ = function() { replace += "
"; } var point = this.selPoints_[i]; - var c = new RGBColor(this.colors_[i%clen]); + var c = new RGBColor(this.plotter_.colors[point.name]); var yval = fmtFunc(point.yval); replace += " " + point.name + ":" @@ -1057,6 +1237,8 @@ Dygraph.prototype.updateSelection_ = function() { ctx.save(); for (var i = 0; i < this.selPoints_.length; i++) { if (!isOK(this.selPoints_[i].canvasy)) continue; + var circleSize = + this.attr_('highlightCircleSize', this.selPoints_[i].name); ctx.beginPath(); ctx.fillStyle = this.plotter_.colors[this.selPoints_[i].name]; ctx.arc(canvasx, this.selPoints_[i].canvasy, circleSize, @@ -1087,7 +1269,13 @@ Dygraph.prototype.setSelection = function(row) { 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]); + var point = this.layout_.points[pos+row]; + + if (this.attr_("stackedGraph")) { + point = this.layout_.unstackPointAtIndex(pos+row); + } + + this.selPoints_.push(point); } pos += this.layout_.datasets[i].length; } @@ -1438,10 +1626,12 @@ Dygraph.dateTicker = function(startDate, endDate, self) { * Add ticks when the x axis has numbers on it (instead of dates) * @param {Number} startDate Start of the date window (millis since epoch) * @param {Number} endDate End of the date window (millis since epoch) + * @param self + * @param {function} formatter: Optional formatter to use for each tick value * @return {Array.} Array of {label, value} tuples. * @public */ -Dygraph.numericTicks = function(minV, maxV, self) { +Dygraph.numericTicks = function(minV, maxV, self, formatter) { // Basic idea: // Try labels every 1, 2, 5, 10, 20, 50, 100, etc. // Calculate the resulting tick spacing (i.e. this.height_ / nTicks). @@ -1493,7 +1683,12 @@ Dygraph.numericTicks = function(minV, maxV, self) { for (var i = 0; i < nTicks; i++) { var tickV = low_val + i * scale; var absTickV = Math.abs(tickV); - var label = Dygraph.round_(tickV, 2); + var label; + if (formatter != undefined) { + label = formatter(tickV); + } else { + label = Dygraph.round_(tickV, 2); + } if (k_labels.length) { // Round up to an appropriate unit. var n = k*k*k*k; @@ -1518,7 +1713,8 @@ Dygraph.numericTicks = function(minV, maxV, self) { Dygraph.prototype.addYTicks_ = function(minY, maxY) { // Set the number of ticks so that the labels are human-friendly. // TODO(danvk): make this an attribute as well. - var ticks = Dygraph.numericTicks(minY, maxY, this); + var formatter = this.attr_('yAxisLabelFormatter') ? this.attr_('yAxisLabelFormatter') : this.attr_('yValueFormatter'); + var ticks = Dygraph.numericTicks(minY, maxY, this, formatter); this.layout_.updateOptions( { yAxis: [minY, maxY], yTicks: ticks } ); }; @@ -1581,8 +1777,6 @@ Dygraph.prototype.drawGraph_ = function(data) { this.setColors_(); this.attrs_['pointSize'] = 0.5 * this.attr_('highlightCircleSize'); - 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. @@ -1593,6 +1787,8 @@ Dygraph.prototype.drawGraph_ = function(data) { for (var i = data[0].length - 1; i >= 1; i--) { if (!this.visibility()[i - 1]) continue; + var connectSeparatedPoints = this.attr_('connectSeparatedPoints', i); + var series = []; for (var j = 0; j < data.length; j++) { if (data[j][i] != null || !connectSeparatedPoints) { @@ -1637,8 +1833,8 @@ Dygraph.prototype.drawGraph_ = function(data) { var extremes = this.extremeValues_(series); var thisMinY = extremes[0]; var thisMaxY = extremes[1]; - if (minY === null || thisMinY < minY) minY = thisMinY; - if (maxY === null || thisMaxY > maxY) maxY = thisMaxY; + if (minY === null || (thisMinY != null && thisMinY < minY)) minY = thisMinY; + if (maxY === null || (thisMaxY != null && thisMaxY > maxY)) maxY = thisMaxY; if (bars) { for (var j=0; j 0) { @@ -1692,8 +1888,10 @@ Dygraph.prototype.drawGraph_ = function(data) { var minAxisY = minY - 0.1 * span; // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense. - if (minAxisY < 0 && minY >= 0) minAxisY = 0; - if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0; + if (!this.attr_("avoidMinZero")) { + if (minAxisY < 0 && minY >= 0) minAxisY = 0; + if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0; + } if (this.attr_("includeZero")) { if (maxY < 0) maxAxisY = 0; @@ -2137,6 +2335,7 @@ Dygraph.prototype.parseDataTable_ = function(data) { var labels = [data.getColumnLabel(0)]; for (var i = 0; i < colIdx.length; i++) { labels.push(data.getColumnLabel(colIdx[i])); + if (this.attr_("errorBars")) i += 1; } this.attrs_.labels = labels; cols = labels.length; @@ -2297,15 +2496,24 @@ Dygraph.prototype.start_ = function() { */ Dygraph.prototype.updateOptions = function(attrs) { // TODO(danvk): this is a mess. Rethink this function. - if (attrs.rollPeriod) { + if ('rollPeriod' in attrs) { this.rollPeriod_ = attrs.rollPeriod; } - if (attrs.dateWindow) { + if ('dateWindow' in attrs) { this.dateWindow_ = attrs.dateWindow; } - if (attrs.valueRange) { + if ('valueRange' in attrs) { this.valueRange_ = attrs.valueRange; + this.valueWindow_ = attrs.valueRange; } + + // TODO(danvk): validate per-series options. + // Supported: + // strokeWidth + // pointSize + // drawPoints + // highlightCircleSize + Dygraph.update(this.user_attrs_, attrs); Dygraph.update(this.renderOptions_, attrs); @@ -2406,6 +2614,8 @@ Dygraph.prototype.setVisibility = function(num, value) { * Update the list of annotations and redraw the chart. */ Dygraph.prototype.setAnnotations = function(ann, suppressDraw) { + // Only add the annotation CSS rule once we know it will be used. + Dygraph.addAnnotationRule(); this.annotations_ = ann; this.layout_.setAnnotations(this.annotations_); if (!suppressDraw) { @@ -2420,6 +2630,18 @@ Dygraph.prototype.annotations = function() { return this.annotations_; }; +/** + * Get the index of a series (column) given its name. The first column is the + * x-axis, so the data series start with index 1. + */ +Dygraph.prototype.indexFromSetName = function(name) { + var labels = this.attr_("labels"); + for (var i = 0; i < labels.length; i++) { + if (labels[i] == name) return i; + } + return null; +}; + Dygraph.addAnnotationRule = function() { if (Dygraph.addedAnnotationCSS) return; @@ -2440,7 +2662,8 @@ Dygraph.addAnnotationRule = function() { "background-color: white; " + "text-align: center;"; if (mysheet.insertRule) { // Firefox - mysheet.insertRule(".dygraphDefaultAnnotation { " + rule + " }", 0); + var idx = mysheet.cssRules ? mysheet.cssRules.length : 0; + mysheet.insertRule(".dygraphDefaultAnnotation { " + rule + " }", idx); } else if (mysheet.addRule) { // IE mysheet.addRule(".dygraphDefaultAnnotation", rule); } @@ -2456,7 +2679,7 @@ Dygraph.createCanvas = function() { var canvas = document.createElement("canvas"); isIE = (/MSIE/.test(navigator.userAgent) && !window.opera); - if (isIE) { + if (isIE && (typeof(G_vmlCanvasManager) != 'undefined')) { canvas = G_vmlCanvasManager.initElement(canvas); }