X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=7bcffd1fec05fb9e8cd4a6df1ae9060ec9589ebc;hb=ea4942ed6644e9ae7ce1e955ecbfb67666f051dc;hp=4d2d3d074a6fe07ddfa087b2ad72a2fd7e950e93;hpb=2ad87eaa004e722037fba951f4fe418d09c209cc;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index 4d2d3d0..7bcffd1 100644 --- a/dygraph.js +++ b/dygraph.js @@ -171,7 +171,6 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.previousVerticalX_ = -1; this.fractions_ = attrs.fractions || false; this.dateWindow_ = attrs.dateWindow || null; - this.valueRange_ = attrs.valueRange || null; this.wilsonInterval_ = attrs.wilsonInterval || true; this.is_initial_draw_ = true; this.annotations_ = []; @@ -240,8 +239,13 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { 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]; @@ -352,6 +356,32 @@ Dygraph.prototype.toDataCoords = function(x, y) { return ret; }; +/** + * Returns the number of columns (including the independent variable). + */ +Dygraph.prototype.numColumns = function() { + return this.rawData_[0].length; +}; + +/** + * Returns the number of rows (excluding any header/label row). + */ +Dygraph.prototype.numRows = function() { + return this.rawData_.length; +}; + +/** + * Returns the value in the given row and column. If the row and column exceed + * the bounds on the data, returns null. Also returns null if the value is + * missing. + */ +Dygraph.prototype.getValue = function(row, col) { + if (row < 0 || row > this.rawData_.length) return null; + if (col < 0 || col > this.rawData_[row].length) return null; + + return this.rawData_[row][col]; +}; + Dygraph.addEvent = function(el, evt, fn) { var normed_fn = function(e) { if (!e) var e = window.event; @@ -803,7 +833,8 @@ Dygraph.prototype.createDragInterface_ = function() { var regionWidth = Math.abs(dragEndX - dragStartX); var regionHeight = Math.abs(dragEndY - dragStartY); - if (regionWidth < 2 && regionHeight < 2 && self.lastx_ != undefined) { + if (regionWidth < 2 && regionHeight < 2 && + self.lastx_ != undefined && self.lastx_ != -1) { // TODO(danvk): pass along more info about the points, e.g. 'x' if (self.attr_('clickCallback') != null) { self.attr_('clickCallback')(event, self.lastx_, self.selPoints_); @@ -990,11 +1021,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); }; @@ -1010,7 +1048,7 @@ Dygraph.prototype.updateSelection_ = function() { if (this.attr_('showLabelsOnHighlight')) { // Set the status message to indicate the selected point(s) for (var i = 0; i < this.selPoints_.length; i++) { - if (!this.attr_("labelsShowZeroValues") && this.selPoints_[i].yval == 0) continue; + if (!this.attr_("labelsShowZeroValues") && this.selPoints_[i].yval == 0) continue; if (!isOK(this.selPoints_[i].canvasy)) continue; if (this.attr_("labelsSeparateLines")) { replace += "
"; @@ -1030,6 +1068,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, @@ -1414,22 +1454,25 @@ Dygraph.dateTicker = function(startDate, endDate, self) { * @return {Array.} Array of {label, value} tuples. * @public */ -Dygraph.numericTicks = function(minV, maxV, self) { +Dygraph.numericTicks = function(minV, maxV, self, attr) { + // This is a bit of a hack to allow per-axis attributes. + if (!attr) attr = self.attr_; + // Basic idea: // Try labels every 1, 2, 5, 10, 20, 50, 100, etc. // Calculate the resulting tick spacing (i.e. this.height_ / nTicks). // The first spacing greater than pixelsPerYLabel is what we use. // TODO(danvk): version that works on a log scale. - if (self.attr_("labelsKMG2")) { + if (attr("labelsKMG2")) { var mults = [1, 2, 4, 8]; } else { var mults = [1, 2, 5]; } var scale, low_val, high_val, nTicks; // TODO(danvk): make it possible to set this for x- and y-axes independently. - var pixelsPerTick = self.attr_('pixelsPerYLabel'); + var pixelsPerTick = attr('pixelsPerYLabel'); for (var i = -10; i < 50; i++) { - if (self.attr_("labelsKMG2")) { + if (attr("labelsKMG2")) { var base_scale = Math.pow(16, i); } else { var base_scale = Math.pow(10, i); @@ -1450,11 +1493,11 @@ Dygraph.numericTicks = function(minV, maxV, self) { var ticks = []; var k; var k_labels = []; - if (self.attr_("labelsKMB")) { + if (attr("labelsKMB")) { k = 1000; k_labels = [ "K", "M", "B", "T" ]; } - if (self.attr_("labelsKMG2")) { + if (attr("labelsKMG2")) { if (k) self.warn("Setting both labelsKMB and labelsKMG2. Pick one!"); k = 1024; k_labels = [ "k", "M", "G", "T" ]; @@ -1554,18 +1597,21 @@ 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. var cumulative_y = []; // For stacked series. var datasets = []; + var extremes = {}; // series name -> [low, high] + // Loop over all fields and create datasets for (var i = data[0].length - 1; i >= 1; i--) { if (!this.visibility()[i - 1]) continue; + var seriesName = this.attr_("labels")[i]; + var connectSeparatedPoints = this.attr_('connectSeparatedPoints', i); + var series = []; for (var j = 0; j < data.length; j++) { if (data[j][i] != null || !connectSeparatedPoints) { @@ -1607,9 +1653,10 @@ Dygraph.prototype.drawGraph_ = function(data) { this.boundaryIds_[i-1] = [0, series.length-1]; } - var extremes = this.extremeValues_(series); - var thisMinY = extremes[0]; - var thisMaxY = extremes[1]; + var seriesExtremes = this.extremeValues_(series); + extremes[seriesName] = seriesExtremes; + var thisMinY = seriesExtremes[0]; + var thisMaxY = seriesExtremes[1]; if (minY === null || thisMinY < minY) minY = thisMinY; if (maxY === null || thisMaxY > maxY) maxY = thisMaxY; @@ -1646,36 +1693,15 @@ Dygraph.prototype.drawGraph_ = function(data) { this.layout_.addDataset(this.attr_("labels")[i], datasets[i]); } - // Use some heuristics to come up with a good maxY value, unless it's been - // 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) { - minY = 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. - if (span == 0) { span = maxY; } - var maxAxisY = maxY + 0.1 * span; - 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_("includeZero")) { - if (maxY < 0) maxAxisY = 0; - if (minY > 0) minAxisY = 0; - } - - this.addYTicks_(minAxisY, maxAxisY); - this.displayedYRange_ = [minAxisY, maxAxisY]; - } + var out = this.computeYaxes_(extremes); + var axes = out[0]; + var seriesToAxisMap = out[1]; + this.displayedYRange_ = axes[0].valueRange; + this.layout_.updateOptions( { yAxis: axes[0].valueRange, + yTicks: axes[0].ticks, + yAxes: axes, + seriesToAxisMap: seriesToAxisMap + } ); this.addXTicks_(); @@ -1693,6 +1719,126 @@ Dygraph.prototype.drawGraph_ = function(data) { }; /** + * Determine all y-axes. + * Inputs: mapping from seriesName -> [low, high] for that series, + * (implicit) per-series axis attributes. + * Returns [ axes, seriesToAxisMap ] + * axes = [ { valueRange: [low, high], otherOptions: ..., ticks: [...] } ] + * seriesToAxisMap = { seriesName: 0, seriesName2: 1, ... } + * indices are into the axes array. + */ +Dygraph.prototype.computeYaxes_ = function(extremes) { + var axes = [{}]; // always have at least one y-axis. + var seriesToAxisMap = {}; + var seriesForAxis = [[]]; + + // all options which could be applied per-axis: + var axisOptions = [ + 'includeZero', + 'valueRange', + 'labelsKMB', + 'labelsKMG2', + 'pixelsPerYLabel', + 'yAxisLabelWidth', + 'axisLabelFontSize', + 'axisTickSize' + ]; + + // Copy global axis options over to the first axis. + for (var i = 0; i < axisOptions.length; i++) { + var k = axisOptions[i]; + var v = this.attr_(k); + if (v) axes[0][k] = v; + } + + // Go through once and add all the axes. + for (var seriesName in extremes) { + if (!extremes.hasOwnProperty(seriesName)) continue; + var axis = this.attr_("axis", seriesName); + if (axis == null) { + seriesToAxisMap[seriesName] = 0; + seriesForAxis[0].push(seriesName); + continue; + } + if (typeof(axis) == 'object') { + // Add a new axis, making a copy of its per-axis options. + var opts = {}; + Dygraph.update(opts, axes[0]); + Dygraph.update(opts, { valueRange: null }); // shouldn't inherit this. + Dygraph.update(opts, axis); + axes.push(opts); + seriesToAxisMap[seriesName] = axes.length - 1; + seriesForAxis.push([seriesName]); + } + } + + // Go through one more time and assign series to an axis defined by another + // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } } + for (var seriesName in extremes) { + if (!extremes.hasOwnProperty(seriesName)) continue; + var axis = this.attr_("axis", seriesName); + if (typeof(axis) == 'string') { + if (!seriesToAxisMap.hasOwnProperty(axis)) { + this.error("Series " + seriesName + " wants to share a y-axis with " + + "series " + axis + ", which does not define its own axis."); + return null; + } + var idx = seriesToAxisMap[axis]; + seriesToAxisMap[seriesName] = idx; + seriesForAxis[idx].push(seriesName); + } + } + + // Compute extreme values, a span and tick marks for each axis. + for (var i = 0; i < axes.length; i++) { + var axis = axes[i]; + if (!axis.valueRange) { + // Calcuate the extremes of extremes. + var series = seriesForAxis[i]; + var minY = Infinity; // extremes[series[0]][0]; + var maxY = -Infinity; // extremes[series[0]][1]; + for (var j = 0; j < series.length; j++) { + minY = Math.min(extremes[series[j]][0], minY); + maxY = Math.max(extremes[series[j]][1], maxY); + } + if (axis.includeZero && minY > 0) minY = 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. + if (span == 0) { span = maxY; } + var maxAxisY = maxY + 0.1 * span; + 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_("includeZero")) { + if (maxY < 0) maxAxisY = 0; + if (minY > 0) minAxisY = 0; + } + + axis.valueRange = [minAxisY, maxAxisY]; + } + + // Add ticks. + axis.ticks = + Dygraph.numericTicks(axis.valueRange[0], + axis.valueRange[1], + this, + function(self, axis) { + return function(a) { + if (axis.hasOwnProperty(a)) return axis[a]; + return self.attr_(a); + }; + }(this, axis)); + } + + return [axes, seriesToAxisMap]; +}; + +/** * Calculates the rolling average of a data set. * If originalData is [label, val], rolls the average of those. * If originalData is [label, [, it's interpreted as [value, stddev] @@ -2121,8 +2267,8 @@ Dygraph.prototype.parseDataTable_ = function(data) { var row = []; if (typeof(data.getValue(i, 0)) === 'undefined' || data.getValue(i, 0) === null) { - this.warning("Ignoring row " + i + - " of DataTable because of undefined or null first column."); + this.warn("Ignoring row " + i + + " of DataTable because of undefined or null first column."); continue; } @@ -2276,9 +2422,14 @@ Dygraph.prototype.updateOptions = function(attrs) { if (attrs.dateWindow) { this.dateWindow_ = attrs.dateWindow; } - if (attrs.valueRange) { - this.valueRange_ = attrs.valueRange; - } + + // TODO(danvk): validate per-series options. + // Supported: + // strokeWidth + // pointSize + // drawPoints + // highlightCircleSize + Dygraph.update(this.user_attrs_, attrs); Dygraph.update(this.renderOptions_, attrs); @@ -2393,6 +2544,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;