+ 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]);
+ }
+
+ if (datasets.length > 0) {
+ // TODO(danvk): this method doesn't need to return anything.
+ var out = this.computeYAxisRanges_(extremes);
+ var axes = out[0];
+ var seriesToAxisMap = out[1];
+ this.layout_.updateOptions( { yAxes: axes,
+ seriesToAxisMap: 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();
+ 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);
+ }
+};
+
+/**
+ * Determine properties of the y-axes which are independent of the data
+ * currently being displayed. This includes things like the number of axes and
+ * the style of the axes. It does not include the range of each axis and its
+ * tick marks.
+ * This fills in this.axes_ and this.seriesToAxisMap_.
+ * axes_ = [ { options } ]
+ * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... }
+ * 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_ = {};
+
+ // Get a list of series names.
+ var labels = this.attr_("labels");
+ var series = {};
+ for (var i = 1; i < labels.length; i++) series[labels[i]] = (i - 1);
+
+ // all options which could be applied per-axis:
+ var axisOptions = [
+ 'includeZero',
+ 'valueRange',
+ 'labelsKMB',
+ 'labelsKMG2',
+ 'pixelsPerYLabel',
+ 'yAxisLabelWidth',
+ 'axisLabelFontSize',
+ 'axisTickSize',
+ 'logscale'
+ ];
+
+ // 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) this.axes_[0][k] = v;
+ }
+
+ // Go through once and add all the axes.
+ for (var seriesName in series) {
+ if (!series.hasOwnProperty(seriesName)) continue;
+ var axis = this.attr_("axis", seriesName);
+ if (axis == null) {
+ this.seriesToAxisMap_[seriesName] = 0;
+ continue;
+ }
+ if (typeof(axis) == 'object') {
+ // Add a new axis, making a copy of its per-axis options.
+ 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] = yAxisId;
+ }