+ } else if (granularity == Dygraph.CENTENNIAL) {
+ months = [ 0 ];
+ year_mod = 100;
+ } else {
+ this.warn("Span of dates is too long");
+ }
+
+ var start_year = new Date(start_time).getFullYear();
+ var end_year = new Date(end_time).getFullYear();
+ var zeropad = Dygraph.zeropad;
+ for (var i = start_year; i <= end_year; i++) {
+ 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 = Dygraph.dateStrToMillis(date_str);
+ if (t < start_time || t > end_time) continue;
+ ticks.push({ v:t, label: formatter(new Date(t), granularity) });
+ }
+ }
+ }
+
+ return ticks;
+};
+
+
+/**
+ * Add ticks to the x-axis based on a date range.
+ * @param {Number} startDate Start of the date window (millis since epoch)
+ * @param {Number} endDate End of the date window (millis since epoch)
+ * @param {Dygraph} self The dygraph object
+ * @return { [Object] } Array of {label, value} tuples.
+ * @public
+ */
+Dygraph.dateTicker = function(startDate, endDate, self) {
+ // TODO(danvk): why does this take 'self' as a param?
+ var chosen = -1;
+ for (var i = 0; i < Dygraph.NUM_GRANULARITIES; i++) {
+ var num_ticks = self.NumXTicks(startDate, endDate, i);
+ if (self.width_ / num_ticks >= self.attr_('pixelsPerXLabel')) {
+ chosen = i;
+ break;
+ }
+ }
+
+ if (chosen >= 0) {
+ return self.GetXAxis(startDate, endDate, chosen);
+ } else {
+ // TODO(danvk): signal error.
+ }
+};
+
+/**
+ * @private
+ * This is a list of human-friendly values at which to show tick marks on a log
+ * scale. It is k * 10^n, where k=1..9 and n=-39..+39, so:
+ * ..., 1, 2, 3, 4, 5, ..., 9, 10, 20, 30, ..., 90, 100, 200, 300, ...
+ * NOTE: this assumes that Dygraph.LOG_SCALE = 10.
+ */
+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;
+}();
+
+// TODO(konigsberg): Update comment.
+/**
+ * Add ticks when the x axis has numbers on it (instead of dates)
+ *
+ * @param {Number} minV minimum value
+ * @param {Number} maxV maximum value
+ * @param self
+ * @param {function} attribute accessor function.
+ * @return {[Object]} Array of {label, value} tuples.
+ */
+Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) {
+ var attr = function(k) {
+ if (axis_props && axis_props.hasOwnProperty(k)) return axis_props[k];
+ return self.attr_(k);
+ };
+
+ var ticks = [];
+ if (vals) {
+ for (var i = 0; i < vals.length; i++) {
+ ticks.push({v: vals[i]});
+ }
+ } else {
+ 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 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;
+ }
+ 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).
+ // The first spacing greater than pixelsPerYLabel is what we use.
+ // TODO(danvk): version that works on a log scale.
+ 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 = attr('pixelsPerYLabel');
+ for (var i = -10; i < 50; i++) {
+ if (attr("labelsKMG2")) {
+ var base_scale = Math.pow(16, i);
+ } else {
+ var base_scale = Math.pow(10, i);
+ }
+ for (var j = 0; j < mults.length; j++) {
+ scale = base_scale * mults[j];
+ low_val = Math.floor(minV / scale) * scale;
+ high_val = Math.ceil(maxV / scale) * scale;
+ nTicks = Math.abs(high_val - low_val) / scale;
+ var spacing = self.height_ / nTicks;
+ // wish I could break out of both loops at once...
+ if (spacing > pixelsPerTick) break;
+ }
+ if (spacing > pixelsPerTick) break;
+ }
+
+ // Construct the set of ticks.
+ // Allow reverse y-axis if it's explicitly requested.
+ if (low_val > high_val) scale *= -1;
+ for (var i = 0; i < nTicks; i++) {
+ var tickV = low_val + i * scale;
+ ticks.push( {v: tickV} );
+ }
+ }
+ }
+
+ // 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');
+
+ // 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(tickV, self);
+ 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 = Dygraph.round_(tickV / n, attr('digitsAfterDecimal')) + k_labels[j];
+ break;
+ }
+ }
+ }
+ ticks[i].label = label;
+ }
+
+ return ticks;
+};
+
+/**
+ * @private
+ * Computes the range of the data series (including confidence intervals).
+ * @param { [Array] } series either [ [x1, y1], [x2, y2], ... ] or
+ * [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
+ * @return [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];
+};
+
+/**
+ * @private
+ * 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.hidden_ctx_,
+ this.layout_);
+
+ // 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.
+ *
+ * clearSelection, when undefined or true, causes this.clearSelection to be
+ * called at the end of the draw operation. This should rarely be defined,
+ * and never true (that is it should be undefined most of the time, and
+ * rarely false.)
+ *
+ * @private
+ */
+Dygraph.prototype.drawGraph_ = function(clearSelection) {
+ if (typeof(clearSelection) === 'undefined') {
+ clearSelection = true;
+ }
+
+ 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]);
+ }
+ }