X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=30ea7a8a25be5f7b4b0a1198edd0b58686676450;hb=d33de6cabb99ea446961ee20da465e5974ebbdf9;hp=26aff3a19d83f0007f38e2efb6503a8e57b64f92;hpb=92fd68d8d81bbe54bae58e4c02c9a2466cab966c;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index 26aff3a..30ea7a8 100644 --- a/dygraph.js +++ b/dygraph.js @@ -258,6 +258,10 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.is_initial_draw_ = true; this.annotations_ = []; + // Zoomed indicators - These indicate when the graph has been zoomed and on what axis. + this.zoomed_x_ = false; + this.zoomed_y_ = false; + // Number of digits to use when labeling the x (if numeric) and y axis // ticks. this.numXDigits_ = 2; @@ -334,6 +338,22 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.start_(); }; +/** + * Returns the zoomed status of the chart for one or both axes. + * + * Axis is an optional parameter. Can be set to 'x' or 'y'. + * + * The zoomed status for an axis is set whenever a user zooms using the mouse + * or when the dateWindow or valueRange are updated (unless the isZoomedIgnoreProgrammaticZoom + * option is also specified). + */ +Dygraph.prototype.isZoomed = function(axis) { + if (axis == null) return this.zoomed_x_ || this.zoomed_y_; + if (axis == 'x') return this.zoomed_x_; + if (axis == 'y') return this.zoomed_y_; + throw "axis parameter to Dygraph.isZoomed must be missing, 'x' or 'y'."; +}; + Dygraph.prototype.toString = function() { var maindiv = this.maindiv_; var id = (maindiv && maindiv.id) ? maindiv.id : maindiv @@ -409,9 +429,14 @@ Dygraph.prototype.rollPeriod = function() { * 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_; + return this.dateWindow_ ? this.dateWindow_ : this.xAxisExtremes(); +}; - // The entire chart is visible. +/** + * Returns the lower- and upper-bound x-axis values of the + * data set. + */ +Dygraph.prototype.xAxisExtremes = function() { var left = this.rawData_[0][0]; var right = this.rawData_[this.rawData_.length - 1][0]; return [left, right]; @@ -564,7 +589,7 @@ Dygraph.prototype.toDataYCoord = function(y, axis) { /** * Converts a y for an axis to a percentage from the top to the - * bottom of the div. + * bottom of the drawing area. * * If the coordinate represents a value visible on the canvas, then * the value will be between 0 and 1, where 0 is the top of the canvas. @@ -585,8 +610,8 @@ Dygraph.prototype.toPercentYCoord = function(y, axis) { var pct; if (!this.axes_[axis].logscale) { - // yrange[1] - y is unit distance from the bottom. - // yrange[1] - yrange[0] is the scale of the range. + // yRange[1] - y is unit distance from the bottom. + // yRange[1] - yRange[0] is the scale of the range. // (yRange[1] - y) / (yRange[1] - yRange[0]) is the % from the bottom. pct = (yRange[1] - y) / (yRange[1] - yRange[0]); } else { @@ -597,6 +622,26 @@ Dygraph.prototype.toPercentYCoord = function(y, axis) { } /** + * Converts an x value to a percentage from the left to the right of + * the drawing area. + * + * If the coordinate represents a value visible on the canvas, then + * the value will be between 0 and 1, where 0 is the left of the canvas. + * However, this method will return values outside the range, as + * values can fall outside the canvas. + * + * If x is null, this returns null. + */ +Dygraph.prototype.toPercentXCoord = function(x) { + if (x == null) { + return null; + } + + var xRange = this.xAxisRange(); + return (x - xRange[0]) / (xRange[1] - xRange[0]); +} + +/** * Returns the number of columns (including the independent variable). */ Dygraph.prototype.numColumns = function() { @@ -1006,6 +1051,35 @@ Dygraph.startPan = function(event, g, context) { context.initialLeftmostDate = xRange[0]; context.xUnitsPerPixel = context.dateRange / (g.plotter_.area.w - 1); + if (g.attr_("panEdgeFraction")) { + var maxXPixelsToDraw = g.width_ * g.attr_("panEdgeFraction"); + var xExtremes = g.xAxisExtremes(); // I REALLY WANT TO CALL THIS xTremes! + + var boundedLeftX = g.toDomXCoord(xExtremes[0]) - maxXPixelsToDraw; + var boundedRightX = g.toDomXCoord(xExtremes[1]) + maxXPixelsToDraw; + + var boundedLeftDate = g.toDataXCoord(boundedLeftX); + var boundedRightDate = g.toDataXCoord(boundedRightX); + context.boundedDates = [boundedLeftDate, boundedRightDate]; + + var boundedValues = []; + var maxYPixelsToDraw = g.height_ * g.attr_("panEdgeFraction"); + + for (var i = 0; i < g.axes_.length; i++) { + var axis = g.axes_[i]; + var yExtremes = axis.extremeRange; + + var boundedTopY = g.toDomYCoord(yExtremes[0], i) + maxYPixelsToDraw; + var boundedBottomY = g.toDomYCoord(yExtremes[1], i) - maxYPixelsToDraw; + + var boundedTopValue = g.toDataYCoord(boundedTopY); + var boundedBottomValue = g.toDataYCoord(boundedBottomY); + + boundedValues[i] = [boundedTopValue, boundedBottomValue]; + } + context.boundedValues = boundedValues; + } + // Record the range of each y-axis at the start of the drag. // If any axis has a valueRange or valueWindow, then we want a 2D pan. context.is2DPan = false; @@ -1041,7 +1115,18 @@ Dygraph.movePan = function(event, g, context) { var minDate = context.initialLeftmostDate - (context.dragEndX - context.dragStartX) * context.xUnitsPerPixel; + if (context.boundedDates) { + minDate = Math.max(minDate, context.boundedDates[0]); + } var maxDate = minDate + context.dateRange; + if (context.boundedDates) { + if (maxDate > context.boundedDates[1]) { + // Adjust minDate, and recompute maxDate. + minDate = minDate - (maxDate - context.boundedDates[1]); + maxDate = minDate + context.dateRange; + } + } + g.dateWindow_ = [minDate, maxDate]; // y-axis scaling is automatic unless this is a full 2D pan. @@ -1052,10 +1137,22 @@ Dygraph.movePan = function(event, g, context) { var pixelsDragged = context.dragEndY - context.dragStartY; var unitsDragged = pixelsDragged * axis.unitsPerPixel; + + var boundedValue = context.boundedValues ? context.boundedValues[i] : null; // In log scale, maxValue and minValue are the logs of those values. var maxValue = axis.initialTopValue + unitsDragged; + if (boundedValue) { + maxValue = Math.min(maxValue, boundedValue[1]); + } var minValue = maxValue - axis.dragValueRange; + if (boundedValue) { + if (minValue < boundedValue[0]) { + // Adjust maxValue, and recompute minValue. + maxValue = maxValue - (minValue - boundedValue[0]); + minValue = maxValue - axis.dragValueRange; + } + } if (axis.logscale) { axis.valueWindow = [ Math.pow(Dygraph.LOG_SCALE, minValue), Math.pow(Dygraph.LOG_SCALE, maxValue) ]; @@ -1084,6 +1181,8 @@ Dygraph.endPan = function(event, g, context) { context.initialLeftmostDate = null; context.dateRange = null; context.valueRange = null; + context.boundedDates = null; + context.boundedValues = null; } // Called in response to an interaction model operation that @@ -1273,6 +1372,11 @@ Dygraph.prototype.createDragInterface_ = function() { px: 0, py: 0, + // Values for use with panEdgeFraction, which limit how far outside the + // graph's data boundaries it can be panned. + boundedDates: null, // [minDate, maxDate] + boundedValues: null, // [[minValue, maxValue] ...] + initializeMouseDown: function(event, g, context) { // prevents mouse drags from selecting page text. if (event.preventDefault) { @@ -1411,6 +1515,7 @@ Dygraph.prototype.doZoomX_ = function(lowX, highX) { */ Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) { this.dateWindow_ = [minDate, maxDate]; + this.zoomed_x_ = true; this.drawGraph_(); if (this.attr_("zoomCallback")) { this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges()); @@ -1438,9 +1543,11 @@ Dygraph.prototype.doZoomY_ = function(lowY, highY) { valueRanges.push([low, hi]); } + this.zoomed_y_ = true; this.drawGraph_(); if (this.attr_("zoomCallback")) { var xRange = this.xAxisRange(); + var yRange = this.yAxisRange(); this.attr_("zoomCallback")(xRange[0], xRange[1], this.yAxisRanges()); } }; @@ -1468,6 +1575,8 @@ Dygraph.prototype.doUnzoom_ = function() { if (dirty) { // Putting the drawing operation before the callback because it resets // yAxisRange. + this.zoomed_x_ = false; + this.zoomed_y_ = false; this.drawGraph_(); if (this.attr_("zoomCallback")) { var minDate = this.rawData_[0][0]; @@ -1485,12 +1594,12 @@ Dygraph.prototype.doUnzoom_ = function() { * @private */ Dygraph.prototype.mouseMove_ = function(event) { - var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_); - var points = this.layout_.points; - // This prevents JS errors when mousing over the canvas before data loads. + var points = this.layout_.points; if (points === undefined) return; + var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_); + var lastx = -1; var lasty = -1; @@ -2003,7 +2112,7 @@ Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) { if (i % year_mod != 0) continue; for (var j = 0; j < months.length; j++) { var date_str = i + "/" + zeropad(1 + months[j]) + "/01"; - var t = Date.parse(date_str); + var t = Dygraph.dateStrToMillis(date_str); if (t < start_time || t > end_time) continue; ticks.push({ v:t, label: formatter(new Date(t), granularity) }); } @@ -2492,15 +2601,20 @@ Dygraph.prototype.drawGraph_ = function() { this.layout_.addDataset(this.attr_("labels")[i], datasets[i]); } - this.computeYAxisRanges_(extremes); - this.layout_.updateOptions( { yAxes: this.axes_, - seriesToAxisMap: this.seriesToAxisMap_ - } ); - + if (datasets.length > 0) { + // TODO(danvk): this method doesn't need to return anything. + this.computeYAxisRanges_(extremes); + this.layout_.updateOptions( { yAxes: this.axes_, + seriesToAxisMap: this.seriesToAxisMap_ + } ); + } this.addXTicks_(); + // Save the X axis zoomed status as the updateOptions call will tend to set it errorneously + var tmp_zoomed_x = this.zoomed_x_; // Tell PlotKit to use this new data and render itself this.layout_.updateOptions({dateWindow: this.dateWindow_}); + this.zoomed_x_ = tmp_zoomed_x; this.layout_.evaluateWithError(); this.plotter_.clear(); this.plotter_.render(); @@ -2528,6 +2642,15 @@ Dygraph.prototype.drawGraph_ = function() { * indices are into the axes_ array. */ Dygraph.prototype.computeYAxes_ = function() { + var valueWindows; + if (this.axes_ != undefined) { + // Preserve valueWindow settings. + valueWindows = []; + for (var index = 0; index < this.axes_.length; index++) { + valueWindows.push(this.axes_[index].valueWindow); + } + } + this.axes_ = [{ yAxisId : 0, g : this }]; // always have at least one y-axis. this.seriesToAxisMap_ = {}; @@ -2604,6 +2727,13 @@ Dygraph.prototype.computeYAxes_ = function() { if (vis[i - 1]) seriesToAxisFiltered[s] = this.seriesToAxisMap_[s]; } this.seriesToAxisMap_ = seriesToAxisFiltered; + + if (valueWindows != undefined) { + // Restore valueWindow settings. + for (var index = 0; index < valueWindows.length; index++) { + this.axes_[index].valueWindow = valueWindows[index]; + } + } }; /** @@ -2652,25 +2782,35 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { // Compute extreme values, a span and tick marks for each axis. for (var i = 0; i < this.axes_.length; i++) { var axis = this.axes_[i]; - if (axis.valueWindow) { - // This is only set if the user has zoomed on the y-axis. It is never set - // by a user. It takes precedence over axis.valueRange because, if you set - // valueRange, you'd still expect to be able to pan. - axis.computedValueRange = [axis.valueWindow[0], axis.valueWindow[1]]; - } else if (axis.valueRange) { - // This is a user-set value range for this axis. - axis.computedValueRange = [axis.valueRange[0], axis.valueRange[1]]; - } else { + + { // Calculate the extremes of extremes. var series = seriesForAxis[i]; var minY = Infinity; // extremes[series[0]][0]; var maxY = -Infinity; // extremes[series[0]][1]; + var extremeMinY, extremeMaxY; for (var j = 0; j < series.length; j++) { - minY = Math.min(extremes[series[j]][0], minY); - maxY = Math.max(extremes[series[j]][1], maxY); + // Only use valid extremes to stop null data series' from corrupting the scale. + extremeMinY = extremes[series[j]][0]; + if (extremeMinY != null) { + minY = Math.min(extremeMinY, minY); + } + extremeMaxY = extremes[series[j]][1]; + if (extremeMaxY != null) { + maxY = Math.max(extremeMaxY, maxY); + } } if (axis.includeZero && minY > 0) minY = 0; + // Ensure we have a valid scale, otherwise defualt to zero for safety. + if (minY == Infinity) { + minY = 0; + } + + if (maxY == -Infinity) { + maxY = 0; + } + // Add some padding and round up to an integer to be human-friendly. var span = maxY - minY; // special case: if we have no sense of scale, use +/-10% of the sole value. @@ -2696,8 +2836,18 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { if (minY > 0) minAxisY = 0; } } - - axis.computedValueRange = [minAxisY, maxAxisY]; + axis.extremeRange = [minAxisY, maxAxisY]; + } + if (axis.valueWindow) { + // This is only set if the user has zoomed on the y-axis. It is never set + // by a user. It takes precedence over axis.valueRange because, if you set + // valueRange, you'd still expect to be able to pan. + axis.computedValueRange = [axis.valueWindow[0], axis.valueWindow[1]]; + } else if (axis.valueRange) { + // This is a user-set value range for this axis. + axis.computedValueRange = [axis.valueRange[0], axis.valueRange[1]]; + } else { + axis.computedValueRange = axis.extremeRange; } // Add ticks. By default, all axes inherit the tick positions of the @@ -2885,16 +3035,16 @@ Dygraph.dateParser = function(dateStr, self) { while (dateStrSlashed.search("-") != -1) { dateStrSlashed = dateStrSlashed.replace("-", "/"); } - d = Date.parse(dateStrSlashed); + d = Dygraph.dateStrToMillis(dateStrSlashed); } else if (dateStr.length == 8) { // e.g. '20090712' // TODO(danvk): remove support for this format. It's confusing. dateStrSlashed = dateStr.substr(0,4) + "/" + dateStr.substr(4,2) + "/" + dateStr.substr(6,2); - d = Date.parse(dateStrSlashed); + d = Dygraph.dateStrToMillis(dateStrSlashed); } else { // Any format that Date.parse will accept, e.g. "2009/07/12" or // "2009/07/12 12:34:56" - d = Date.parse(dateStr); + d = Dygraph.dateStrToMillis(dateStr); } if (!d || isNaN(d)) { @@ -3281,6 +3431,13 @@ Dygraph.prototype.parseDataTable_ = function(data) { } } +// This is identical to JavaScript's built-in Date.parse() method, except that +// it doesn't get replaced with an incompatible method by aggressive JS +// libraries like MooTools or Joomla. +Dygraph.dateStrToMillis = function(str) { + return new Date(str).getTime(); +}; + // These functions are all based on MochiKit. Dygraph.update = function (self, o) { if (typeof(o) != 'undefined' && o !== null) { @@ -3375,6 +3532,12 @@ Dygraph.prototype.start_ = function() { *
  • file: changes the source data for the graph
  • *
  • errorBars: changes whether the data contains stddev
  • * + * + * If the dateWindow or valueRange options are specified, the relevant zoomed_x_ + * or zoomed_y_ flags are set, unless the isZoomedIgnoreProgrammaticZoom option is also + * secified. This allows for the chart to be programmatically zoomed without + * altering the zoomed flags. + * * @param {Object} attrs The new properties and values */ Dygraph.prototype.updateOptions = function(attrs) { @@ -3384,6 +3547,12 @@ Dygraph.prototype.updateOptions = function(attrs) { } if ('dateWindow' in attrs) { this.dateWindow_ = attrs.dateWindow; + if (!('isZoomedIgnoreProgrammaticZoom' in attrs)) { + this.zoomed_x_ = attrs.dateWindow != null; + } + } + if ('valueRange' in attrs && !('isZoomedIgnoreProgrammaticZoom' in attrs)) { + this.zoomed_y_ = attrs.valueRange != null; } // TODO(danvk): validate per-series options. @@ -4034,6 +4203,19 @@ Dygraph.OPTIONS_REFERENCE = // "labels": ["Annotations"], "type": "boolean", "description": "Only applies when Dygraphs is used as a GViz chart. Causes string columns following a data series to be interpreted as annotations on points in that series. This is the same format used by Google's AnnotatedTimeLine chart." + }, + "panEdgeFraction": { + "default": "null", + "labels": ["Axis Display", "Interactive Elements"], + "type": "float", + "default": "null", + "description": "A value representing the farthest a graph may be panned, in percent of the display. For example, a value of 0.1 means that the graph can only be panned 10% pased the edges of the displayed values. null means no bounds." + }, + "isZoomedIgnoreProgrammaticZoom" : { + "default": "false", + "labels": ["Zooming"], + "type": "boolean", + "description" : "When this flag is passed along with either the dateWindow or valueRange options, the zoom flags are not changed to reflect a zoomed state. This is primarily useful for when the display area of a chart is changed programmatically and also where manual zooming is allowed and use is made of the isZoomed method to determine this." } } ; //