X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=8f6933799e0f374c32a2fef4ec5851d51aad79c5;hb=ed898bdd829370f5172a34d5f3d64858aeea16be;hp=d738e54129c784dd03456e627a28decce533c883;hpb=d59b6f341a39df8b051e0777ec7885f93fab25f5;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index d738e54..8f69337 100644 --- a/dygraph.js +++ b/dygraph.js @@ -24,7 +24,6 @@ If the 'errorBars' option is set in the constructor, the input should be of the form - Date,SeriesA,SeriesB,... YYYYMMDD,A1,sigmaA1,B1,sigmaB1,... YYYYMMDD,A2,sigmaA2,B2,sigmaB2,... @@ -80,9 +79,9 @@ Dygraph.DEFAULT_HEIGHT = 320; Dygraph.AXIS_LINE_WIDTH = 0.3; Dygraph.LOG_SCALE = 10; -Dygraph.LOG_BASE_E_OF_TEN = Math.log(Dygraph.LOG_SCALE); +Dygraph.LN_TEN = Math.log(Dygraph.LOG_SCALE); Dygraph.log10 = function(x) { - return Math.log(x) / Dygraph.LOG_BASE_E_OF_TEN; + return Math.log(x) / Dygraph.LN_TEN; } // Default attribute values. @@ -119,7 +118,6 @@ Dygraph.DEFAULT_ATTRS = { delimiter: ',', - logScale: false, sigma: 2.0, errorBars: false, fractions: false, @@ -362,7 +360,7 @@ Dygraph.prototype.yAxisRanges = function() { * axis. Uses the first axis by default. * Returns a two-element array: [X, Y] * - * Note: use toDomXCoord instead of toDomCoords(x. null) and use toDomYCoord + * Note: use toDomXCoord instead of toDomCoords(x, null) and use toDomYCoord * instead of toDomCoords(null, y, axis). */ Dygraph.prototype.toDomCoords = function(x, y, axis) { @@ -372,8 +370,8 @@ Dygraph.prototype.toDomCoords = function(x, y, axis) { /** * Convert from data x coordinates to canvas/div X coordinate. * If specified, do this conversion for the coordinate system of a particular - * axis. Uses the first axis by default. - * returns a single value or null if x is null. + * axis. + * Returns a single value or null if x is null. */ Dygraph.prototype.toDomXCoord = function(x) { if (x == null) { @@ -392,11 +390,12 @@ Dygraph.prototype.toDomXCoord = function(x) { * returns a single value or null if y is null. */ Dygraph.prototype.toDomYCoord = function(y, axis) { - var pct = toPercentYCoord(y, axis); + var pct = this.toPercentYCoord(y, axis); if (pct == null) { return null; } + var area = this.plotter_.area; return area.y + pct * area.h; } @@ -406,7 +405,7 @@ Dygraph.prototype.toDomYCoord = function(y, axis) { * axis. Uses the first axis by default. * Returns a two-element array: [X, Y]. * - * Note: use toDataXCoord instead of toDataCoords(x. null) and use toDataYCoord + * Note: use toDataXCoord instead of toDataCoords(x, null) and use toDataYCoord * instead of toDataCoords(null, y, axis). */ Dygraph.prototype.toDataCoords = function(x, y, axis) { @@ -442,7 +441,8 @@ Dygraph.prototype.toDataYCoord = function(y, axis) { var area = this.plotter_.area; var yRange = this.yAxisRange(axis); - if (!this.attr_("logscale")) { + if (typeof(axis) == "undefined") axis = 0; + if (!this.axes_[axis].logscale) { return yRange[0] + (area.h - y) / area.h * (yRange[1] - yRange[0]); } else { // Computing the inverse of toDomCoord. @@ -488,12 +488,13 @@ Dygraph.prototype.toPercentYCoord = function(y, axis) { if (y == null) { return null; } + if (typeof(axis) == "undefined") axis = 0; var area = this.plotter_.area; var yRange = this.yAxisRange(axis); var pct; - if (!this.attr_("logscale")) { + 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) / (yRange[1] - yRange[0]) is the % from the bottom. @@ -911,6 +912,8 @@ Dygraph.startPan = function(event, g, context) { context.isPanning = true; var xRange = g.xAxisRange(); context.dateRange = xRange[1] - xRange[0]; + context.initialLeftmostDate = xRange[0]; + context.xUnitsPerPixel = context.dateRange / (g.plotter_.area.w - 1); // 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. @@ -918,14 +921,20 @@ Dygraph.startPan = function(event, g, context) { for (var i = 0; i < g.axes_.length; i++) { var axis = g.axes_[i]; var yRange = g.yAxisRange(i); - axis.dragValueRange = yRange[1] - yRange[0]; - axis.draggingValue = g.toDataYCoord(context.dragStartY, i); + // TODO(konigsberg): These values should be in |context|. + // In log scale, initialTopValue, dragValueRange and unitsPerPixel are log scale. + if (axis.logscale) { + axis.initialTopValue = Dygraph.log10(yRange[1]); + axis.dragValueRange = Dygraph.log10(yRange[1]) - Dygraph.log10(yRange[0]); + } else { + axis.initialTopValue = yRange[1]; + axis.dragValueRange = yRange[1] - yRange[0]; + } + axis.unitsPerPixel = axis.dragValueRange / (g.plotter_.area.h - 1); + + // While calculating axes, set 2dpan. if (axis.valueWindow || axis.valueRange) context.is2DPan = true; } - - // TODO(konigsberg): Switch from all this math to toDataCoords? - // Seems to work for the dragging value. - context.draggingDate = (context.dragStartX / g.width_) * context.dateRange + xRange[0]; }; // Called in response to an interaction model operation that @@ -939,33 +948,28 @@ Dygraph.movePan = function(event, g, context) { context.dragEndX = g.dragGetX_(event, context); context.dragEndY = g.dragGetY_(event, context); - // TODO(danvk): update this comment - // Want to have it so that: - // 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. - - var minDate = context.draggingDate - (context.dragEndX / g.width_) * context.dateRange; + var minDate = context.initialLeftmostDate - + (context.dragEndX - context.dragStartX) * context.xUnitsPerPixel; var maxDate = minDate + context.dateRange; g.dateWindow_ = [minDate, maxDate]; // y-axis scaling is automatic unless this is a full 2D pan. if (context.is2DPan) { // Adjust each axis appropriately. - // NOTE(konigsberg): I don't think this computation for y_frac is correct. - // I think it doesn't take into account the display of the x axis. - // See, when I tested this with console.log(y_frac), and move the mouse - // cursor to the botom, the largest y_frac was 0.94, and not 1.0. That - // could also explain why panning tends to start with a small jumpy shift. - var y_frac = context.dragEndY / g.height_; - for (var i = 0; i < g.axes_.length; i++) { var axis = g.axes_[i]; - var maxValue = axis.draggingValue + y_frac * axis.dragValueRange; + + var pixelsDragged = context.dragEndY - context.dragStartY; + var unitsDragged = pixelsDragged * axis.unitsPerPixel; + + // In log scale, maxValue and minValue are the logs of those values. + var maxValue = axis.initialTopValue + unitsDragged; var minValue = maxValue - axis.dragValueRange; - console.log(axis.draggingValue, axis.dragValueRange, minValue, maxValue, y_frac); - axis.valueWindow = [ minValue, maxValue ]; + if (axis.logscale) { + axis.valueWindow = [ Math.pow(10, minValue), Math.pow(10, maxValue) ]; + } else { + axis.valueWindow = [ minValue, maxValue ]; + } } } @@ -980,9 +984,12 @@ Dygraph.movePan = function(event, g, context) { // panning behavior. // Dygraph.endPan = function(event, g, context) { + // TODO(konigsberg): Clear the context data from the axis. + // TODO(konigsberg): mouseup should just delete the + // context object, and mousedown should create a new one. context.isPanning = false; context.is2DPan = false; - context.draggingDate = null; + context.initialLeftmostDate = null; context.dateRange = null; context.valueRange = null; } @@ -1158,12 +1165,12 @@ Dygraph.prototype.createDragInterface_ = function() { prevEndY: null, prevDragDirection: null, - // TODO(danvk): update this comment - // 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] - draggingDate: null, + // The value on the left side of the graph when a pan operation starts. + initialLeftmostDate: null, + + // The number of units each pixel spans. (This won't be valid for log + // scales) + xUnitsPerPixel: null, // TODO(danvk): update this comment // The range in second/value units that the viewport encompasses during a @@ -1914,12 +1921,71 @@ Dygraph.dateTicker = function(startDate, endDate, self) { } }; +Dygraph.PREFERRED_LOG_TICK_VALUES = function() { + var vals = []; + for (var power = -39; power <= 39; power++) { + var range = Math.pow(10, power); + for (var mult = 1; mult <= 9; mult++) { + var val = range * mult; + vals.push(val); + } + } + return vals; +}(); + +// val is the value to search for +// arry is the value over which to search +// if abs > 0, find the lowest entry greater than val +// if abs < 0, find the highest entry less than val +// if abs == 0, find the entry that equals val. +// Currently does not work when val is outside the range of arry's values. +Dygraph.binarySearch = function(val, arry, abs, low, high) { + if (low == null || high == null) { + low = 0; + high = arry.length - 1; + } + if (low > high) { + return -1; + } + if (abs == null) { + abs = 0; + } + var validIndex = function(idx) { + return idx >= 0 && idx < arry.length; + } + var mid = parseInt((low + high) / 2); + var element = arry[mid]; + if (element == val) { + return mid; + } + if (element > val) { + if (abs > 0) { + // Accept if element > val, but also if prior element < val. + var idx = mid - 1; + if (validIndex(idx) && arry[idx] < val) { + return mid; + } + } + return Dygraph.binarySearch(val, arry, abs, low, mid - 1); + } + if (element < val) { + if (abs < 0) { + // Accept if element < val, but also if prior element > val. + var idx = mid + 1; + if (validIndex(idx) && arry[idx] > val) { + return mid; + } + } + return Dygraph.binarySearch(val, arry, abs, mid + 1, high); + } +} + /** * Add ticks when the x axis has numbers on it (instead of dates) * TODO(konigsberg): Update comment. * - * @param {Number} startDate Start of the date window (millis since epoch) - * @param {Number} endDate End of the date window (millis since epoch) + * @param {Number} minV minimum value + * @param {Number} maxV maximum value * @param self * @param {function} attribute accessor function. * @return {Array.} Array of {label, value} tuples. @@ -1937,23 +2003,50 @@ Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) { ticks.push({v: vals[i]}); } } else { - if (self.attr_("logscale")) { - // As opposed to the other ways for computing ticks, we're just going - // for nearby values. There's no reasonable way to scale the values - // (unless we want to show strings like "log(" + x + ")") in which case - // x can be integer values. - - // so compute height / pixelsPerTick and move on. + if (axis_props && attr("logscale")) { var pixelsPerTick = attr('pixelsPerYLabel'); + // NOTE(konigsberg): Dan, should self.height_ be self.plotter_.area.h? var nTicks = Math.floor(self.height_ / pixelsPerTick); - var vv = minV; - - // Construct the set of ticks. - for (var i = 0; i < nTicks; i++) { - ticks.push( {v: vv} ); - vv = vv * Dygraph.LOG_SCALE; + var minIdx = Dygraph.binarySearch(minV, Dygraph.PREFERRED_LOG_TICK_VALUES, 1); + var maxIdx = Dygraph.binarySearch(maxV, Dygraph.PREFERRED_LOG_TICK_VALUES, -1); + if (minIdx == -1) { + minIdx = 0; } - } else { + if (maxIdx == -1) { + maxIdx = Dygraph.PREFERRED_LOG_TICK_VALUES.length - 1; + } + // Count the number of tick values would appear, if we can get at least + // nTicks / 4 accept them. + var lastDisplayed = null; + if (maxIdx - minIdx >= nTicks / 4) { + var axisId = axis_props.yAxisId; + for (var idx = maxIdx; idx >= minIdx; idx--) { + var tickValue = Dygraph.PREFERRED_LOG_TICK_VALUES[idx]; + var domCoord = axis_props.g.toDomYCoord(tickValue, axisId); + var tick = { v: tickValue }; + if (lastDisplayed == null) { + lastDisplayed = { + tickValue : tickValue, + domCoord : domCoord + }; + } else { + if (domCoord - lastDisplayed.domCoord >= pixelsPerTick) { + lastDisplayed = { + tickValue : tickValue, + domCoord : domCoord + }; + } else { + tick.label = ""; + } + } + ticks.push(tick); + } + // Since we went in backwards order. + ticks.reverse(); + } + } + // ticks.length won't be 0 if the log scale function finds values to insert. + if (ticks.length == 0) { // Basic idea: // Try labels every 1, 2, 5, 10, 20, 50, 100, etc. // Calculate the resulting tick spacing (i.e. this.height_ / nTicks). @@ -2009,26 +2102,29 @@ Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) { } var formatter = attr('yAxisLabelFormatter') ? attr('yAxisLabelFormatter') : attr('yValueFormatter'); + // Add labels to the ticks. for (var i = 0; i < ticks.length; i++) { - var tickV = ticks[i].v; - var absTickV = Math.abs(tickV); - 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; - for (var j = 3; j >= 0; j--, n /= k) { - if (absTickV >= n) { - label = Dygraph.round_(tickV / n, 1) + k_labels[j]; - break; + if (ticks[i].label == null) { + var tickV = ticks[i].v; + var absTickV = Math.abs(tickV); + 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; + for (var j = 3; j >= 0; j--, n /= k) { + if (absTickV >= n) { + label = Dygraph.round_(tickV / n, 1) + k_labels[j]; + break; + } } } + ticks[i].label = label; } - ticks[i].label = label; } return ticks; }; @@ -2081,7 +2177,7 @@ Dygraph.prototype.extremeValues_ = function(series) { * number of axes, rolling averages, etc. */ Dygraph.prototype.predraw_ = function() { - // TODO(danvk): move more computations out of drawGraph_ and into here. + // TODO(danvk): movabilitye more computations out of drawGraph_ and into here. this.computeYAxes_(); // Create a new plotter. @@ -2135,12 +2231,24 @@ Dygraph.prototype.drawGraph_ = function() { var seriesName = this.attr_("labels")[i]; var connectSeparatedPoints = this.attr_('connectSeparatedPoints', i); + var logScale = this.attr_('logscale', i); var series = []; for (var j = 0; j < data.length; j++) { - if (data[j][i] != null || !connectSeparatedPoints) { - var date = data[j][0]; - series.push([date, data[j][i]]); + var date = data[j][0]; + var point = data[j][i]; + if (logScale) { + // On the log scale, points less than zero do not exist. + // This will create a gap in the chart. Note that this ignores + // connectSeparatedPoints. + if (point < 0) { + point = null; + } + series.push([date, point]); + } else { + if (point != null || !connectSeparatedPoints) { + series.push([date, point]); + } } } @@ -2254,7 +2362,7 @@ Dygraph.prototype.drawGraph_ = function() { * indices are into the axes_ array. */ Dygraph.prototype.computeYAxes_ = function() { - this.axes_ = [{}]; // always have at least one y-axis. + this.axes_ = [{ yAxisId : 0, g : this }]; // always have at least one y-axis. this.seriesToAxisMap_ = {}; // Get a list of series names. @@ -2271,7 +2379,8 @@ Dygraph.prototype.computeYAxes_ = function() { 'pixelsPerYLabel', 'yAxisLabelWidth', 'axisLabelFontSize', - 'axisTickSize' + 'axisTickSize', + 'logscale' ]; // Copy global axis options over to the first axis. @@ -2294,9 +2403,12 @@ Dygraph.prototype.computeYAxes_ = function() { var opts = {}; Dygraph.update(opts, this.axes_[0]); Dygraph.update(opts, { valueRange: null }); // shouldn't inherit this. + var yAxisId = this.axes_.length; + opts.yAxisId = yAxisId; + opts.g = this; Dygraph.update(opts, axis); this.axes_.push(opts); - this.seriesToAxisMap_[seriesName] = this.axes_.length - 1; + this.seriesToAxisMap_[seriesName] = yAxisId; } } @@ -2359,7 +2471,6 @@ 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 isLogScale = this.attr_("logscale"); 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 @@ -2387,7 +2498,7 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { var maxAxisY; var minAxisY; - if (isLogScale) { + if (axis.logscale) { var maxAxisY = maxY + 0.1 * span; var minAxisY = minY; } else {