+
+ // Add formatted labels to the ticks.
+ var k;
+ var k_labels = [];
+ if (attr("labelsKMB")) {
+ k = 1000;
+ k_labels = [ "K", "M", "B", "T" ];
+ }
+ if (attr("labelsKMG2")) {
+ if (k) self.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
+ k = 1024;
+ k_labels = [ "k", "M", "G", "T" ];
+ }
+ var formatter = attr('yAxisLabelFormatter') ?
+ attr('yAxisLabelFormatter') : attr('yValueFormatter');
+
+ // Determine the number of decimal places needed for the labels below by
+ // taking the maximum number of significant figures for any label. We must
+ // take the max because we can't tell if trailing 0s are significant.
+ var numDigits = 0;
+ for (var i = 0; i < ticks.length; i++) {
+ numDigits = Math.max(Dygraph.significantFigures(ticks[i].v), numDigits);
+ }
+
+ // Add labels to the ticks.
+ for (var i = 0; i < ticks.length; i++) {
+ if (ticks[i].label !== undefined) continue; // Use current label.
+ var tickV = ticks[i].v;
+ var absTickV = Math.abs(tickV);
+ var label = (formatter !== undefined) ?
+ formatter(tickV, numDigits) : tickV.toPrecision(numDigits);
+ if (k_labels.length > 0) {
+ // 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 = formatter(tickV / n, numDigits) + k_labels[j];
+ break;
+ }
+ }
+ }
+ ticks[i].label = label;
+ }
+
+ return {ticks: ticks, numDigits: numDigits};
+};
+
+// Computes the range of the data series (including confidence intervals).
+// series is either [ [x1, y1], [x2, y2], ... ] or
+// [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
+// Returns [low, high]
+Dygraph.prototype.extremeValues_ = function(series) {
+ var minY = null, maxY = null;
+
+ var bars = this.attr_("errorBars") || this.attr_("customBars");
+ if (bars) {
+ // With custom bars, maxY is the max of the high values.
+ for (var j = 0; j < series.length; j++) {
+ var y = series[j][1][0];
+ if (!y) continue;
+ var low = y - series[j][1][1];
+ var high = y + series[j][1][2];
+ if (low > y) low = y; // this can happen with custom bars,
+ if (high < y) high = y; // e.g. in tests/custom-bars.html
+ if (maxY == null || high > maxY) {
+ maxY = high;
+ }
+ if (minY == null || low < minY) {
+ minY = low;
+ }
+ }
+ } else {
+ for (var j = 0; j < series.length; j++) {
+ var y = series[j][1];
+ if (y === null || isNaN(y)) continue;
+ if (maxY == null || y > maxY) {
+ maxY = y;
+ }
+ if (minY == null || y < minY) {
+ minY = y;
+ }
+ }
+ }
+
+ return [minY, maxY];
+};
+
+/**
+ * This function is called once when the chart's data is changed or the options
+ * dictionary is updated. It is _not_ called when the user pans or zooms. The
+ * idea is that values derived from the chart's data can be computed here,
+ * rather than every time the chart is drawn. This includes things like the
+ * number of axes, rolling averages, etc.
+ */
+Dygraph.prototype.predraw_ = function() {
+ // TODO(danvk): move more computations out of drawGraph_ and into here.
+ this.computeYAxes_();
+
+ // Create a new plotter.
+ if (this.plotter_) this.plotter_.clear();
+ this.plotter_ = new DygraphCanvasRenderer(this,
+ this.hidden_, this.layout_,
+ this.renderOptions_);
+
+ // The roller sits in the bottom left corner of the chart. We don't know where
+ // this will be until the options are available, so it's positioned here.
+ this.createRollInterface_();
+
+ // Same thing applies for the labelsDiv. It's right edge should be flush with
+ // the right edge of the charting area (which may not be the same as the right
+ // edge of the div, if we have two y-axes.
+ this.positionLabelsDiv_();
+
+ // If the data or options have changed, then we'd better redraw.
+ this.drawGraph_();
+};
+
+/**
+ * Update the graph with new data. This method is called when the viewing area
+ * has changed. If the underlying data or options have changed, predraw_ will
+ * be called before drawGraph_ is called.
+ * @private
+ */
+Dygraph.prototype.drawGraph_ = function() {
+ var data = this.rawData_;
+
+ // This is used to set the second parameter to drawCallback, below.
+ var is_initial_draw = this.is_initial_draw_;
+ this.is_initial_draw_ = false;
+
+ var minY = null, maxY = null;
+ this.layout_.removeAllDatasets();
+ this.setColors_();
+ this.attrs_['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
+
+ // 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 logScale = this.attr_('logscale', i);
+
+ var series = [];
+ for (var j = 0; j < data.length; j++) {
+ 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]);
+ }
+ }
+ }
+
+ // TODO(danvk): move this into predraw_. It's insane to do it here.
+ series = this.rollingAverage(series, this.rollPeriod_);
+
+ // Prune down to the desired range, if necessary (for zooming)
+ // Because there can be lines going to points outside of the visible area,
+ // we actually prune to visible points, plus one on either side.
+ var bars = this.attr_("errorBars") || this.attr_("customBars");
+ if (this.dateWindow_) {
+ var low = this.dateWindow_[0];
+ var high= this.dateWindow_[1];
+ var pruned = [];
+ // TODO(danvk): do binary search instead of linear search.
+ // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
+ var firstIdx = null, lastIdx = null;
+ for (var k = 0; k < series.length; k++) {
+ if (series[k][0] >= low && firstIdx === null) {
+ firstIdx = k;
+ }
+ if (series[k][0] <= high) {
+ lastIdx = k;
+ }
+ }
+ if (firstIdx === null) firstIdx = 0;
+ 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 seriesExtremes = this.extremeValues_(series);
+
+ if (bars) {
+ for (var j=0; j<series.length; j++) {
+ val = [series[j][0], series[j][1][0], series[j][1][1], series[j][1][2]];
+ series[j] = val;
+ }
+ } else if (this.attr_("stackedGraph")) {
+ var l = series.length;
+ var actual_y;
+ for (var j = 0; j < l; j++) {
+ // If one data set has a NaN, let all subsequent stacked
+ // sets inherit the NaN -- only start at 0 for the first set.
+ var x = series[j][0];
+ if (cumulative_y[x] === undefined) {
+ cumulative_y[x] = 0;
+ }
+
+ actual_y = series[j][1];
+ cumulative_y[x] += actual_y;
+
+ series[j] = [x, cumulative_y[x]]
+
+ if (cumulative_y[x] > seriesExtremes[1]) {
+ seriesExtremes[1] = cumulative_y[x];
+ }
+ if (cumulative_y[x] < seriesExtremes[0]) {
+ seriesExtremes[0] = cumulative_y[x];
+ }
+ }
+ }
+ extremes[seriesName] = seriesExtremes;
+
+ datasets[i] = series;
+ }
+
+ for (var i = 1; i < datasets.length; i++) {
+ if (!this.visibility()[i - 1]) continue;
+ this.layout_.addDataset(this.attr_("labels")[i], datasets[i]);
+ }
+
+ this.computeYAxisRanges_(extremes);
+ this.layout_.updateOptions( { yAxes: this.axes_,
+ seriesToAxisMap: this.seriesToAxisMap_
+ } );
+
+ this.addXTicks_();
+
+ // Tell PlotKit to use this new data and render itself
+ this.layout_.updateOptions({dateWindow: this.dateWindow_});
+ this.layout_.evaluateWithError();
+ this.plotter_.clear();
+ this.plotter_.render();
+ this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width,
+ this.canvas_.height);
+
+ if (this.attr_("drawCallback") !== null) {
+ this.attr_("drawCallback")(this, is_initial_draw);
+ }