1 /**
  2  * @license
  3  * Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
  4  * MIT-licensed (http://opensource.org/licenses/MIT)
  5  */
  6 
  7 /**
  8  * @fileoverview Creates an interactive, zoomable graph based on a CSV file or
  9  * string. Dygraph can handle multiple series with or without error bars. The
 10  * date/value ranges will be automatically set. Dygraph uses the
 11  * <canvas> tag, so it only works in FF1.5+.
 12  * @author danvdk@gmail.com (Dan Vanderkam)
 13 
 14   Usage:
 15    <div id="graphdiv" style="width:800px; height:500px;"></div>
 16    <script type="text/javascript">
 17      new Dygraph(document.getElementById("graphdiv"),
 18                  "datafile.csv",  // CSV file with headers
 19                  { }); // options
 20    </script>
 21 
 22  The CSV file is of the form
 23 
 24    Date,SeriesA,SeriesB,SeriesC
 25    YYYYMMDD,A1,B1,C1
 26    YYYYMMDD,A2,B2,C2
 27 
 28  If the 'errorBars' option is set in the constructor, the input should be of
 29  the form
 30    Date,SeriesA,SeriesB,...
 31    YYYYMMDD,A1,sigmaA1,B1,sigmaB1,...
 32    YYYYMMDD,A2,sigmaA2,B2,sigmaB2,...
 33 
 34  If the 'fractions' option is set, the input should be of the form:
 35 
 36    Date,SeriesA,SeriesB,...
 37    YYYYMMDD,A1/B1,A2/B2,...
 38    YYYYMMDD,A1/B1,A2/B2,...
 39 
 40  And error bars will be calculated automatically using a binomial distribution.
 41 
 42  For further documentation and examples, see http://dygraphs.com/
 43 
 44  */
 45 
 46 /*jshint globalstrict: true */
 47 /*global DygraphLayout:false, DygraphCanvasRenderer:false, DygraphOptions:false, G_vmlCanvasManager:false,ActiveXObject:false */
 48 "use strict";
 49 
 50 /**
 51  * Creates an interactive, zoomable chart.
 52  *
 53  * @constructor
 54  * @param {div | String} div A div or the id of a div into which to construct
 55  * the chart.
 56  * @param {String | Function} file A file containing CSV data or a function
 57  * that returns this data. The most basic expected format for each line is
 58  * "YYYY/MM/DD,val1,val2,...". For more information, see
 59  * http://dygraphs.com/data.html.
 60  * @param {Object} attrs Various other attributes, e.g. errorBars determines
 61  * whether the input data contains error ranges. For a complete list of
 62  * options, see http://dygraphs.com/options.html.
 63  */
 64 var Dygraph = function(div, data, opts, opt_fourth_param) {
 65   // These have to go above the "Hack for IE" in __init__ since .ready() can be
 66   // called as soon as the constructor returns. Once support for OldIE is
 67   // dropped, this can go down with the rest of the initializers.
 68   this.is_initial_draw_ = true;
 69   this.readyFns_ = [];
 70 
 71   if (opt_fourth_param !== undefined) {
 72     // Old versions of dygraphs took in the series labels as a constructor
 73     // parameter. This doesn't make sense anymore, but it's easy to continue
 74     // to support this usage.
 75     this.warn("Using deprecated four-argument dygraph constructor");
 76     this.__old_init__(div, data, opts, opt_fourth_param);
 77   } else {
 78     this.__init__(div, data, opts);
 79   }
 80 };
 81 
 82 Dygraph.NAME = "Dygraph";
 83 Dygraph.VERSION = "1.0.0";
 84 Dygraph.__repr__ = function() {
 85   return "[" + this.NAME + " " + this.VERSION + "]";
 86 };
 87 
 88 /**
 89  * Returns information about the Dygraph class.
 90  */
 91 Dygraph.toString = function() {
 92   return this.__repr__();
 93 };
 94 
 95 // Various default values
 96 Dygraph.DEFAULT_ROLL_PERIOD = 1;
 97 Dygraph.DEFAULT_WIDTH = 480;
 98 Dygraph.DEFAULT_HEIGHT = 320;
 99 
100 // For max 60 Hz. animation:
101 Dygraph.ANIMATION_STEPS = 12;
102 Dygraph.ANIMATION_DURATION = 200;
103 
104 // Label constants for the labelsKMB and labelsKMG2 options.
105 // (i.e. '100000' -> '100K')
106 Dygraph.KMB_LABELS = [ 'K', 'M', 'B', 'T', 'Q' ];
107 Dygraph.KMG2_BIG_LABELS = [ 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y' ];
108 Dygraph.KMG2_SMALL_LABELS = [ 'm', 'u', 'n', 'p', 'f', 'a', 'z', 'y' ];
109 
110 // These are defined before DEFAULT_ATTRS so that it can refer to them.
111 /**
112  * @private
113  * Return a string version of a number. This respects the digitsAfterDecimal
114  * and maxNumberWidth options.
115  * @param {Number} x The number to be formatted
116  * @param {Dygraph} opts An options view
117  * @param {String} name The name of the point's data series
118  * @param {Dygraph} g The dygraph object
119  */
120 Dygraph.numberValueFormatter = function(x, opts, pt, g) {
121   var sigFigs = opts('sigFigs');
122 
123   if (sigFigs !== null) {
124     // User has opted for a fixed number of significant figures.
125     return Dygraph.floatFormat(x, sigFigs);
126   }
127 
128   var digits = opts('digitsAfterDecimal');
129   var maxNumberWidth = opts('maxNumberWidth');
130 
131   var kmb = opts('labelsKMB');
132   var kmg2 = opts('labelsKMG2');
133 
134   var label;
135 
136   // switch to scientific notation if we underflow or overflow fixed display.
137   if (x !== 0.0 &&
138       (Math.abs(x) >= Math.pow(10, maxNumberWidth) ||
139        Math.abs(x) < Math.pow(10, -digits))) {
140     label = x.toExponential(digits);
141   } else {
142     label = '' + Dygraph.round_(x, digits);
143   }
144 
145   if (kmb || kmg2) {
146     var k;
147     var k_labels = [];
148     var m_labels = [];
149     if (kmb) {
150       k = 1000;
151       k_labels = Dygraph.KMB_LABELS;
152     }
153     if (kmg2) {
154       if (kmb) Dygraph.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
155       k = 1024;
156       k_labels = Dygraph.KMG2_BIG_LABELS;
157       m_labels = Dygraph.KMG2_SMALL_LABELS;
158     }
159 
160     var absx = Math.abs(x);
161     var n = Dygraph.pow(k, k_labels.length);
162     for (var j = k_labels.length - 1; j >= 0; j--, n /= k) {
163       if (absx >= n) {
164         label = Dygraph.round_(x / n, digits) + k_labels[j];
165         break;
166       }
167     }
168     if (kmg2) {
169       // TODO(danvk): clean up this logic. Why so different than kmb?
170       var x_parts = String(x.toExponential()).split('e-');
171       if (x_parts.length === 2 && x_parts[1] >= 3 && x_parts[1] <= 24) {
172         if (x_parts[1] % 3 > 0) {
173           label = Dygraph.round_(x_parts[0] /
174               Dygraph.pow(10, (x_parts[1] % 3)),
175               digits);
176         } else {
177           label = Number(x_parts[0]).toFixed(2);
178         }
179         label += m_labels[Math.floor(x_parts[1] / 3) - 1];
180       }
181     }
182   }
183 
184   return label;
185 };
186 
187 /**
188  * variant for use as an axisLabelFormatter.
189  * @private
190  */
191 Dygraph.numberAxisLabelFormatter = function(x, granularity, opts, g) {
192   return Dygraph.numberValueFormatter(x, opts, g);
193 };
194 
195 /**
196  * Convert a JS date (millis since epoch) to YYYY/MM/DD
197  * @param {Number} date The JavaScript date (ms since epoch)
198  * @return {String} A date of the form "YYYY/MM/DD"
199  * @private
200  */
201 Dygraph.dateString_ = function(date) {
202   var zeropad = Dygraph.zeropad;
203   var d = new Date(date);
204 
205   // Get the year:
206   var year = "" + d.getFullYear();
207   // Get a 0 padded month string
208   var month = zeropad(d.getMonth() + 1);  //months are 0-offset, sigh
209   // Get a 0 padded day string
210   var day = zeropad(d.getDate());
211 
212   var ret = "";
213   var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
214   if (frac) ret = " " + Dygraph.hmsString_(date);
215 
216   return year + "/" + month + "/" + day + ret;
217 };
218 
219 /**
220  * Convert a JS date to a string appropriate to display on an axis that
221  * is displaying values at the stated granularity.
222  * @param {Date} date The date to format
223  * @param {Number} granularity One of the Dygraph granularity constants
224  * @return {String} The formatted date
225  * @private
226  */
227 Dygraph.dateAxisFormatter = function(date, granularity) {
228   if (granularity >= Dygraph.DECADAL) {
229     return date.strftime('%Y');
230   } else if (granularity >= Dygraph.MONTHLY) {
231     return date.strftime('%b %y');
232   } else {
233     var frac = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds();
234     if (frac === 0 || granularity >= Dygraph.DAILY) {
235       return new Date(date.getTime() + 3600*1000).strftime('%d%b');
236     } else {
237       return Dygraph.hmsString_(date.getTime());
238     }
239   }
240 };
241 
242 /**
243  * Standard plotters. These may be used by clients.
244  * Available plotters are:
245  * - Dygraph.Plotters.linePlotter: draws central lines (most common)
246  * - Dygraph.Plotters.errorPlotter: draws error bars
247  * - Dygraph.Plotters.fillPlotter: draws fills under lines (used with fillGraph)
248  *
249  * By default, the plotter is [fillPlotter, errorPlotter, linePlotter].
250  * This causes all the lines to be drawn over all the fills/error bars.
251  */
252 Dygraph.Plotters = DygraphCanvasRenderer._Plotters;
253 
254 
255 // Default attribute values.
256 Dygraph.DEFAULT_ATTRS = {
257   highlightCircleSize: 3,
258   highlightSeriesOpts: null,
259   highlightSeriesBackgroundAlpha: 0.5,
260 
261   labelsDivWidth: 250,
262   labelsDivStyles: {
263     // TODO(danvk): move defaults from createStatusMessage_ here.
264   },
265   labelsSeparateLines: false,
266   labelsShowZeroValues: true,
267   labelsKMB: false,
268   labelsKMG2: false,
269   showLabelsOnHighlight: true,
270 
271   digitsAfterDecimal: 2,
272   maxNumberWidth: 6,
273   sigFigs: null,
274 
275   strokeWidth: 1.0,
276   strokeBorderWidth: 0,
277   strokeBorderColor: "white",
278 
279   axisTickSize: 3,
280   axisLabelFontSize: 14,
281   xAxisLabelWidth: 50,
282   yAxisLabelWidth: 50,
283   rightGap: 5,
284 
285   showRoller: false,
286   xValueParser: Dygraph.dateParser,
287 
288   delimiter: ',',
289 
290   sigma: 2.0,
291   errorBars: false,
292   fractions: false,
293   wilsonInterval: true,  // only relevant if fractions is true
294   customBars: false,
295   fillGraph: false,
296   fillAlpha: 0.15,
297   connectSeparatedPoints: false,
298 
299   stackedGraph: false,
300   stackedGraphNaNFill: 'all',
301   hideOverlayOnMouseOut: true,
302 
303   // TODO(danvk): support 'onmouseover' and 'never', and remove synonyms.
304   legend: 'onmouseover',  // the only relevant value at the moment is 'always'.
305 
306   stepPlot: false,
307   avoidMinZero: false,
308   xRangePad: 0,
309   yRangePad: null,
310   drawAxesAtZero: false,
311 
312   // Sizes of the various chart labels.
313   titleHeight: 28,
314   xLabelHeight: 18,
315   yLabelWidth: 18,
316 
317   drawXAxis: true,
318   drawYAxis: true,
319   axisLineColor: "black",
320   axisLineWidth: 0.3,
321   gridLineWidth: 0.3,
322   axisLabelColor: "black",
323   axisLabelFont: "Arial",  // TODO(danvk): is this implemented?
324   axisLabelWidth: 50,
325   drawYGrid: true,
326   drawXGrid: true,
327   gridLineColor: "rgb(128,128,128)",
328 
329   interactionModel: null,  // will be set to Dygraph.Interaction.defaultModel
330   animatedZooms: false,  // (for now)
331 
332   // Range selector options
333   showRangeSelector: false,
334   rangeSelectorHeight: 40,
335   rangeSelectorPlotStrokeColor: "#808FAB",
336   rangeSelectorPlotFillColor: "#A7B1C4",
337 
338   // The ordering here ensures that central lines always appear above any
339   // fill bars/error bars.
340   plotter: [
341     Dygraph.Plotters.fillPlotter,
342     Dygraph.Plotters.errorPlotter,
343     Dygraph.Plotters.linePlotter
344   ],
345 
346   plugins: [ ],
347 
348   // per-axis options
349   axes: {
350     x: {
351       pixelsPerLabel: 60,
352       axisLabelFormatter: Dygraph.dateAxisFormatter,
353       valueFormatter: Dygraph.dateString_,
354       drawGrid: true,
355       independentTicks: true,
356       ticker: null  // will be set in dygraph-tickers.js
357     },
358     y: {
359       pixelsPerLabel: 30,
360       valueFormatter: Dygraph.numberValueFormatter,
361       axisLabelFormatter: Dygraph.numberAxisLabelFormatter,
362       drawGrid: true,
363       independentTicks: true,
364       ticker: null  // will be set in dygraph-tickers.js
365     },
366     y2: {
367       pixelsPerLabel: 30,
368       valueFormatter: Dygraph.numberValueFormatter,
369       axisLabelFormatter: Dygraph.numberAxisLabelFormatter,
370       drawGrid: false,
371       independentTicks: false,
372       ticker: null  // will be set in dygraph-tickers.js
373     }
374   }
375 };
376 
377 // Directions for panning and zooming. Use bit operations when combined
378 // values are possible.
379 Dygraph.HORIZONTAL = 1;
380 Dygraph.VERTICAL = 2;
381 
382 // Installed plugins, in order of precedence (most-general to most-specific).
383 // Plugins are installed after they are defined, in plugins/install.js.
384 Dygraph.PLUGINS = [
385 ];
386 
387 // Used for initializing annotation CSS rules only once.
388 Dygraph.addedAnnotationCSS = false;
389 
390 Dygraph.prototype.__old_init__ = function(div, file, labels, attrs) {
391   // Labels is no longer a constructor parameter, since it's typically set
392   // directly from the data source. It also conains a name for the x-axis,
393   // which the previous constructor form did not.
394   if (labels !== null) {
395     var new_labels = ["Date"];
396     for (var i = 0; i < labels.length; i++) new_labels.push(labels[i]);
397     Dygraph.update(attrs, { 'labels': new_labels });
398   }
399   this.__init__(div, file, attrs);
400 };
401 
402 /**
403  * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
404  * and context <canvas> inside of it. See the constructor for details.
405  * on the parameters.
406  * @param {Element} div the Element to render the graph into.
407  * @param {String | Function} file Source data
408  * @param {Object} attrs Miscellaneous other options
409  * @private
410  */
411 Dygraph.prototype.__init__ = function(div, file, attrs) {
412   // Hack for IE: if we're using excanvas and the document hasn't finished
413   // loading yet (and hence may not have initialized whatever it needs to
414   // initialize), then keep calling this routine periodically until it has.
415   if (/MSIE/.test(navigator.userAgent) && !window.opera &&
416       typeof(G_vmlCanvasManager) != 'undefined' &&
417       document.readyState != 'complete') {
418     var self = this;
419     setTimeout(function() { self.__init__(div, file, attrs); }, 100);
420     return;
421   }
422 
423   // Support two-argument constructor
424   if (attrs === null || attrs === undefined) { attrs = {}; }
425 
426   attrs = Dygraph.mapLegacyOptions_(attrs);
427 
428   if (typeof(div) == 'string') {
429     div = document.getElementById(div);
430   }
431 
432   if (!div) {
433     Dygraph.error("Constructing dygraph with a non-existent div!");
434     return;
435   }
436 
437   this.isUsingExcanvas_ = typeof(G_vmlCanvasManager) != 'undefined';
438 
439   // Copy the important bits into the object
440   // TODO(danvk): most of these should just stay in the attrs_ dictionary.
441   this.maindiv_ = div;
442   this.file_ = file;
443   this.rollPeriod_ = attrs.rollPeriod || Dygraph.DEFAULT_ROLL_PERIOD;
444   this.previousVerticalX_ = -1;
445   this.fractions_ = attrs.fractions || false;
446   this.dateWindow_ = attrs.dateWindow || null;
447 
448   this.annotations_ = [];
449 
450   // Zoomed indicators - These indicate when the graph has been zoomed and on what axis.
451   this.zoomed_x_ = false;
452   this.zoomed_y_ = false;
453 
454   // Clear the div. This ensure that, if multiple dygraphs are passed the same
455   // div, then only one will be drawn.
456   div.innerHTML = "";
457 
458   // For historical reasons, the 'width' and 'height' options trump all CSS
459   // rules _except_ for an explicit 'width' or 'height' on the div.
460   // As an added convenience, if the div has zero height (like <div></div> does
461   // without any styles), then we use a default height/width.
462   if (div.style.width === '' && attrs.width) {
463     div.style.width = attrs.width + "px";
464   }
465   if (div.style.height === '' && attrs.height) {
466     div.style.height = attrs.height + "px";
467   }
468   if (div.style.height === '' && div.clientHeight === 0) {
469     div.style.height = Dygraph.DEFAULT_HEIGHT + "px";
470     if (div.style.width === '') {
471       div.style.width = Dygraph.DEFAULT_WIDTH + "px";
472     }
473   }
474   // These will be zero if the dygraph's div is hidden. In that case,
475   // use the user-specified attributes if present. If not, use zero
476   // and assume the user will call resize to fix things later.
477   this.width_ = div.clientWidth || attrs.width || 0;
478   this.height_ = div.clientHeight || attrs.height || 0;
479 
480   // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
481   if (attrs.stackedGraph) {
482     attrs.fillGraph = true;
483     // TODO(nikhilk): Add any other stackedGraph checks here.
484   }
485 
486   // DEPRECATION WARNING: All option processing should be moved from
487   // attrs_ and user_attrs_ to options_, which holds all this information.
488   //
489   // Dygraphs has many options, some of which interact with one another.
490   // To keep track of everything, we maintain two sets of options:
491   //
492   //  this.user_attrs_   only options explicitly set by the user.
493   //  this.attrs_        defaults, options derived from user_attrs_, data.
494   //
495   // Options are then accessed this.attr_('attr'), which first looks at
496   // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
497   // defaults without overriding behavior that the user specifically asks for.
498   this.user_attrs_ = {};
499   Dygraph.update(this.user_attrs_, attrs);
500 
501   // This sequence ensures that Dygraph.DEFAULT_ATTRS is never modified.
502   this.attrs_ = {};
503   Dygraph.updateDeep(this.attrs_, Dygraph.DEFAULT_ATTRS);
504 
505   this.boundaryIds_ = [];
506   this.setIndexByName_ = {};
507   this.datasetIndex_ = [];
508 
509   this.registeredEvents_ = [];
510   this.eventListeners_ = {};
511 
512   this.attributes_ = new DygraphOptions(this);
513 
514   // Create the containing DIV and other interactive elements
515   this.createInterface_();
516 
517   // Activate plugins.
518   this.plugins_ = [];
519   var plugins = Dygraph.PLUGINS.concat(this.getOption('plugins'));
520   for (var i = 0; i < plugins.length; i++) {
521     var Plugin = plugins[i];
522     var pluginInstance = new Plugin();
523     var pluginDict = {
524       plugin: pluginInstance,
525       events: {},
526       options: {},
527       pluginOptions: {}
528     };
529 
530     var handlers = pluginInstance.activate(this);
531     for (var eventName in handlers) {
532       // TODO(danvk): validate eventName.
533       pluginDict.events[eventName] = handlers[eventName];
534     }
535 
536     this.plugins_.push(pluginDict);
537   }
538 
539   // At this point, plugins can no longer register event handlers.
540   // Construct a map from event -> ordered list of [callback, plugin].
541   for (var i = 0; i < this.plugins_.length; i++) {
542     var plugin_dict = this.plugins_[i];
543     for (var eventName in plugin_dict.events) {
544       if (!plugin_dict.events.hasOwnProperty(eventName)) continue;
545       var callback = plugin_dict.events[eventName];
546 
547       var pair = [plugin_dict.plugin, callback];
548       if (!(eventName in this.eventListeners_)) {
549         this.eventListeners_[eventName] = [pair];
550       } else {
551         this.eventListeners_[eventName].push(pair);
552       }
553     }
554   }
555 
556   this.createDragInterface_();
557 
558   this.start_();
559 };
560 
561 /**
562  * Triggers a cascade of events to the various plugins which are interested in them.
563  * Returns true if the "default behavior" should be performed, i.e. if none of
564  * the event listeners called event.preventDefault().
565  * @private
566  */
567 Dygraph.prototype.cascadeEvents_ = function(name, extra_props) {
568   if (!(name in this.eventListeners_)) return true;
569 
570   // QUESTION: can we use objects & prototypes to speed this up?
571   var e = {
572     dygraph: this,
573     cancelable: false,
574     defaultPrevented: false,
575     preventDefault: function() {
576       if (!e.cancelable) throw "Cannot call preventDefault on non-cancelable event.";
577       e.defaultPrevented = true;
578     },
579     propagationStopped: false,
580     stopPropagation: function() {
581       e.propagationStopped = true;
582     }
583   };
584   Dygraph.update(e, extra_props);
585 
586   var callback_plugin_pairs = this.eventListeners_[name];
587   if (callback_plugin_pairs) {
588     for (var i = callback_plugin_pairs.length - 1; i >= 0; i--) {
589       var plugin = callback_plugin_pairs[i][0];
590       var callback = callback_plugin_pairs[i][1];
591       callback.call(plugin, e);
592       if (e.propagationStopped) break;
593     }
594   }
595   return e.defaultPrevented;
596 };
597 
598 /**
599  * Returns the zoomed status of the chart for one or both axes.
600  *
601  * Axis is an optional parameter. Can be set to 'x' or 'y'.
602  *
603  * The zoomed status for an axis is set whenever a user zooms using the mouse
604  * or when the dateWindow or valueRange are updated (unless the
605  * isZoomedIgnoreProgrammaticZoom option is also specified).
606  */
607 Dygraph.prototype.isZoomed = function(axis) {
608   if (axis === null || axis === undefined) {
609     return this.zoomed_x_ || this.zoomed_y_;
610   }
611   if (axis === 'x') return this.zoomed_x_;
612   if (axis === 'y') return this.zoomed_y_;
613   throw "axis parameter is [" + axis + "] must be null, 'x' or 'y'.";
614 };
615 
616 /**
617  * Returns information about the Dygraph object, including its containing ID.
618  */
619 Dygraph.prototype.toString = function() {
620   var maindiv = this.maindiv_;
621   var id = (maindiv && maindiv.id) ? maindiv.id : maindiv;
622   return "[Dygraph " + id + "]";
623 };
624 
625 /**
626  * @private
627  * Returns the value of an option. This may be set by the user (either in the
628  * constructor or by calling updateOptions) or by dygraphs, and may be set to a
629  * per-series value.
630  * @param { String } name The name of the option, e.g. 'rollPeriod'.
631  * @param { String } [seriesName] The name of the series to which the option
632  * will be applied. If no per-series value of this option is available, then
633  * the global value is returned. This is optional.
634  * @return { ... } The value of the option.
635  */
636 Dygraph.prototype.attr_ = function(name, seriesName) {
637 // <REMOVE_FOR_COMBINED>
638   if (typeof(Dygraph.OPTIONS_REFERENCE) === 'undefined') {
639     this.error('Must include options reference JS for testing');
640   } else if (!Dygraph.OPTIONS_REFERENCE.hasOwnProperty(name)) {
641     this.error('Dygraphs is using property ' + name + ', which has no entry ' +
642                'in the Dygraphs.OPTIONS_REFERENCE listing.');
643     // Only log this error once.
644     Dygraph.OPTIONS_REFERENCE[name] = true;
645   }
646 // </REMOVE_FOR_COMBINED>
647   return seriesName ? this.attributes_.getForSeries(name, seriesName) : this.attributes_.get(name);
648 };
649 
650 /**
651  * Returns the current value for an option, as set in the constructor or via
652  * updateOptions. You may pass in an (optional) series name to get per-series
653  * values for the option.
654  *
655  * All values returned by this method should be considered immutable. If you
656  * modify them, there is no guarantee that the changes will be honored or that
657  * dygraphs will remain in a consistent state. If you want to modify an option,
658  * use updateOptions() instead.
659  *
660  * @param { String } name The name of the option (e.g. 'strokeWidth')
661  * @param { String } [opt_seriesName] Series name to get per-series values.
662  * @return { ... } The value of the option.
663  */
664 Dygraph.prototype.getOption = function(name, opt_seriesName) {
665   return this.attr_(name, opt_seriesName);
666 };
667 
668 Dygraph.prototype.getOptionForAxis = function(name, axis) {
669   return this.attributes_.getForAxis(name, axis);
670 };
671 
672 /**
673  * @private
674  * @param  String} axis The name of the axis (i.e. 'x', 'y' or 'y2')
675  * @return { ... } A function mapping string -> option value
676  */
677 Dygraph.prototype.optionsViewForAxis_ = function(axis) {
678   var self = this;
679   return function(opt) {
680     var axis_opts = self.user_attrs_.axes;
681     if (axis_opts && axis_opts[axis] && axis_opts[axis].hasOwnProperty(opt)) {
682       return axis_opts[axis][opt];
683     }
684     // user-specified attributes always trump defaults, even if they're less
685     // specific.
686     if (typeof(self.user_attrs_[opt]) != 'undefined') {
687       return self.user_attrs_[opt];
688     }
689 
690     axis_opts = self.attrs_.axes;
691     if (axis_opts && axis_opts[axis] && axis_opts[axis].hasOwnProperty(opt)) {
692       return axis_opts[axis][opt];
693     }
694     // check old-style axis options
695     // TODO(danvk): add a deprecation warning if either of these match.
696     if (axis == 'y' && self.axes_[0].hasOwnProperty(opt)) {
697       return self.axes_[0][opt];
698     } else if (axis == 'y2' && self.axes_[1].hasOwnProperty(opt)) {
699       return self.axes_[1][opt];
700     }
701     return self.attr_(opt);
702   };
703 };
704 
705 /**
706  * Returns the current rolling period, as set by the user or an option.
707  * @return {Number} The number of points in the rolling window
708  */
709 Dygraph.prototype.rollPeriod = function() {
710   return this.rollPeriod_;
711 };
712 
713 /**
714  * Returns the currently-visible x-range. This can be affected by zooming,
715  * panning or a call to updateOptions.
716  * Returns a two-element array: [left, right].
717  * If the Dygraph has dates on the x-axis, these will be millis since epoch.
718  */
719 Dygraph.prototype.xAxisRange = function() {
720   return this.dateWindow_ ? this.dateWindow_ : this.xAxisExtremes();
721 };
722 
723 /**
724  * Returns the lower- and upper-bound x-axis values of the
725  * data set.
726  */
727 Dygraph.prototype.xAxisExtremes = function() {
728   var pad = this.attr_('xRangePad') / this.plotter_.area.w;
729   if (this.numRows() === 0) {
730     return [0 - pad, 1 + pad];
731   }
732   var left = this.rawData_[0][0];
733   var right = this.rawData_[this.rawData_.length - 1][0];
734   if (pad) {
735     // Must keep this in sync with dygraph-layout _evaluateLimits()
736     var range = right - left;
737     left -= range * pad;
738     right += range * pad;
739   }
740   return [left, right];
741 };
742 
743 /**
744  * Returns the currently-visible y-range for an axis. This can be affected by
745  * zooming, panning or a call to updateOptions. Axis indices are zero-based. If
746  * called with no arguments, returns the range of the first axis.
747  * Returns a two-element array: [bottom, top].
748  */
749 Dygraph.prototype.yAxisRange = function(idx) {
750   if (typeof(idx) == "undefined") idx = 0;
751   if (idx < 0 || idx >= this.axes_.length) {
752     return null;
753   }
754   var axis = this.axes_[idx];
755   return [ axis.computedValueRange[0], axis.computedValueRange[1] ];
756 };
757 
758 /**
759  * Returns the currently-visible y-ranges for each axis. This can be affected by
760  * zooming, panning, calls to updateOptions, etc.
761  * Returns an array of [bottom, top] pairs, one for each y-axis.
762  */
763 Dygraph.prototype.yAxisRanges = function() {
764   var ret = [];
765   for (var i = 0; i < this.axes_.length; i++) {
766     ret.push(this.yAxisRange(i));
767   }
768   return ret;
769 };
770 
771 // TODO(danvk): use these functions throughout dygraphs.
772 /**
773  * Convert from data coordinates to canvas/div X/Y coordinates.
774  * If specified, do this conversion for the coordinate system of a particular
775  * axis. Uses the first axis by default.
776  * Returns a two-element array: [X, Y]
777  *
778  * Note: use toDomXCoord instead of toDomCoords(x, null) and use toDomYCoord
779  * instead of toDomCoords(null, y, axis).
780  */
781 Dygraph.prototype.toDomCoords = function(x, y, axis) {
782   return [ this.toDomXCoord(x), this.toDomYCoord(y, axis) ];
783 };
784 
785 /**
786  * Convert from data x coordinates to canvas/div X coordinate.
787  * If specified, do this conversion for the coordinate system of a particular
788  * axis.
789  * Returns a single value or null if x is null.
790  */
791 Dygraph.prototype.toDomXCoord = function(x) {
792   if (x === null) {
793     return null;
794   }
795 
796   var area = this.plotter_.area;
797   var xRange = this.xAxisRange();
798   return area.x + (x - xRange[0]) / (xRange[1] - xRange[0]) * area.w;
799 };
800 
801 /**
802  * Convert from data x coordinates to canvas/div Y coordinate and optional
803  * axis. Uses the first axis by default.
804  *
805  * returns a single value or null if y is null.
806  */
807 Dygraph.prototype.toDomYCoord = function(y, axis) {
808   var pct = this.toPercentYCoord(y, axis);
809 
810   if (pct === null) {
811     return null;
812   }
813   var area = this.plotter_.area;
814   return area.y + pct * area.h;
815 };
816 
817 /**
818  * Convert from canvas/div coords to data coordinates.
819  * If specified, do this conversion for the coordinate system of a particular
820  * axis. Uses the first axis by default.
821  * Returns a two-element array: [X, Y].
822  *
823  * Note: use toDataXCoord instead of toDataCoords(x, null) and use toDataYCoord
824  * instead of toDataCoords(null, y, axis).
825  */
826 Dygraph.prototype.toDataCoords = function(x, y, axis) {
827   return [ this.toDataXCoord(x), this.toDataYCoord(y, axis) ];
828 };
829 
830 /**
831  * Convert from canvas/div x coordinate to data coordinate.
832  *
833  * If x is null, this returns null.
834  */
835 Dygraph.prototype.toDataXCoord = function(x) {
836   if (x === null) {
837     return null;
838   }
839 
840   var area = this.plotter_.area;
841   var xRange = this.xAxisRange();
842   return xRange[0] + (x - area.x) / area.w * (xRange[1] - xRange[0]);
843 };
844 
845 /**
846  * Convert from canvas/div y coord to value.
847  *
848  * If y is null, this returns null.
849  * if axis is null, this uses the first axis.
850  */
851 Dygraph.prototype.toDataYCoord = function(y, axis) {
852   if (y === null) {
853     return null;
854   }
855 
856   var area = this.plotter_.area;
857   var yRange = this.yAxisRange(axis);
858 
859   if (typeof(axis) == "undefined") axis = 0;
860   if (!this.axes_[axis].logscale) {
861     return yRange[0] + (area.y + area.h - y) / area.h * (yRange[1] - yRange[0]);
862   } else {
863     // Computing the inverse of toDomCoord.
864     var pct = (y - area.y) / area.h;
865 
866     // Computing the inverse of toPercentYCoord. The function was arrived at with
867     // the following steps:
868     //
869     // Original calcuation:
870     // pct = (logr1 - Dygraph.log10(y)) / (logr1 - Dygraph.log10(yRange[0]));
871     //
872     // Move denominator to both sides:
873     // pct * (logr1 - Dygraph.log10(yRange[0])) = logr1 - Dygraph.log10(y);
874     //
875     // subtract logr1, and take the negative value.
876     // logr1 - (pct * (logr1 - Dygraph.log10(yRange[0]))) = Dygraph.log10(y);
877     //
878     // Swap both sides of the equation, and we can compute the log of the
879     // return value. Which means we just need to use that as the exponent in
880     // e^exponent.
881     // Dygraph.log10(y) = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0])));
882 
883     var logr1 = Dygraph.log10(yRange[1]);
884     var exponent = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0])));
885     var value = Math.pow(Dygraph.LOG_SCALE, exponent);
886     return value;
887   }
888 };
889 
890 /**
891  * Converts a y for an axis to a percentage from the top to the
892  * bottom of the drawing area.
893  *
894  * If the coordinate represents a value visible on the canvas, then
895  * the value will be between 0 and 1, where 0 is the top of the canvas.
896  * However, this method will return values outside the range, as
897  * values can fall outside the canvas.
898  *
899  * If y is null, this returns null.
900  * if axis is null, this uses the first axis.
901  *
902  * @param { Number } y The data y-coordinate.
903  * @param { Number } [axis] The axis number on which the data coordinate lives.
904  * @return { Number } A fraction in [0, 1] where 0 = the top edge.
905  */
906 Dygraph.prototype.toPercentYCoord = function(y, axis) {
907   if (y === null) {
908     return null;
909   }
910   if (typeof(axis) == "undefined") axis = 0;
911 
912   var yRange = this.yAxisRange(axis);
913 
914   var pct;
915   var logscale = this.attributes_.getForAxis("logscale", axis);
916   if (!logscale) {
917     // yRange[1] - y is unit distance from the bottom.
918     // yRange[1] - yRange[0] is the scale of the range.
919     // (yRange[1] - y) / (yRange[1] - yRange[0]) is the % from the bottom.
920     pct = (yRange[1] - y) / (yRange[1] - yRange[0]);
921   } else {
922     var logr1 = Dygraph.log10(yRange[1]);
923     pct = (logr1 - Dygraph.log10(y)) / (logr1 - Dygraph.log10(yRange[0]));
924   }
925   return pct;
926 };
927 
928 /**
929  * Converts an x value to a percentage from the left to the right of
930  * the drawing area.
931  *
932  * If the coordinate represents a value visible on the canvas, then
933  * the value will be between 0 and 1, where 0 is the left of the canvas.
934  * However, this method will return values outside the range, as
935  * values can fall outside the canvas.
936  *
937  * If x is null, this returns null.
938  * @param { Number } x The data x-coordinate.
939  * @return { Number } A fraction in [0, 1] where 0 = the left edge.
940  */
941 Dygraph.prototype.toPercentXCoord = function(x) {
942   if (x === null) {
943     return null;
944   }
945 
946   var xRange = this.xAxisRange();
947   return (x - xRange[0]) / (xRange[1] - xRange[0]);
948 };
949 
950 /**
951  * Returns the number of columns (including the independent variable).
952  * @return { Integer } The number of columns.
953  */
954 Dygraph.prototype.numColumns = function() {
955   if (!this.rawData_) return 0;
956   return this.rawData_[0] ? this.rawData_[0].length : this.attr_("labels").length;
957 };
958 
959 /**
960  * Returns the number of rows (excluding any header/label row).
961  * @return { Integer } The number of rows, less any header.
962  */
963 Dygraph.prototype.numRows = function() {
964   if (!this.rawData_) return 0;
965   return this.rawData_.length;
966 };
967 
968 /**
969  * Returns the value in the given row and column. If the row and column exceed
970  * the bounds on the data, returns null. Also returns null if the value is
971  * missing.
972  * @param { Number} row The row number of the data (0-based). Row 0 is the
973  * first row of data, not a header row.
974  * @param { Number} col The column number of the data (0-based)
975  * @return { Number } The value in the specified cell or null if the row/col
976  * were out of range.
977  */
978 Dygraph.prototype.getValue = function(row, col) {
979   if (row < 0 || row > this.rawData_.length) return null;
980   if (col < 0 || col > this.rawData_[row].length) return null;
981 
982   return this.rawData_[row][col];
983 };
984 
985 /**
986  * Generates interface elements for the Dygraph: a containing div, a div to
987  * display the current point, and a textbox to adjust the rolling average
988  * period. Also creates the Renderer/Layout elements.
989  * @private
990  */
991 Dygraph.prototype.createInterface_ = function() {
992   // Create the all-enclosing graph div
993   var enclosing = this.maindiv_;
994 
995   this.graphDiv = document.createElement("div");
996 
997   // TODO(danvk): any other styles that are useful to set here?
998   this.graphDiv.style.textAlign = 'left';  // This is a CSS "reset"
999   enclosing.appendChild(this.graphDiv);
1000 
1001   // Create the canvas for interactive parts of the chart.
1002   this.canvas_ = Dygraph.createCanvas();
1003   this.canvas_.style.position = "absolute";
1004 
1005   // ... and for static parts of the chart.
1006   this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
1007 
1008   this.resizeElements_();
1009 
1010   this.canvas_ctx_ = Dygraph.getContext(this.canvas_);
1011   this.hidden_ctx_ = Dygraph.getContext(this.hidden_);
1012 
1013   // The interactive parts of the graph are drawn on top of the chart.
1014   this.graphDiv.appendChild(this.hidden_);
1015   this.graphDiv.appendChild(this.canvas_);
1016   this.mouseEventElement_ = this.createMouseEventElement_();
1017 
1018   // Create the grapher
1019   this.layout_ = new DygraphLayout(this);
1020 
1021   var dygraph = this;
1022 
1023   this.mouseMoveHandler_ = function(e) {
1024     dygraph.mouseMove_(e);
1025   };
1026 
1027   this.mouseOutHandler_ = function(e) {
1028     // The mouse has left the chart if:
1029     // 1. e.target is inside the chart
1030     // 2. e.relatedTarget is outside the chart
1031     var target = e.target || e.fromElement;
1032     var relatedTarget = e.relatedTarget || e.toElement;
1033     if (Dygraph.isNodeContainedBy(target, dygraph.graphDiv) &&
1034         !Dygraph.isNodeContainedBy(relatedTarget, dygraph.graphDiv)) {
1035       dygraph.mouseOut_(e);
1036     }
1037   };
1038 
1039   this.addAndTrackEvent(window, 'mouseout', this.mouseOutHandler_);
1040   this.addAndTrackEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_);
1041 
1042   // Don't recreate and register the resize handler on subsequent calls.
1043   // This happens when the graph is resized.
1044   if (!this.resizeHandler_) {
1045     this.resizeHandler_ = function(e) {
1046       dygraph.resize();
1047     };
1048 
1049     // Update when the window is resized.
1050     // TODO(danvk): drop frames depending on complexity of the chart.
1051     this.addAndTrackEvent(window, 'resize', this.resizeHandler_);
1052   }
1053 };
1054 
1055 Dygraph.prototype.resizeElements_ = function() {
1056   this.graphDiv.style.width = this.width_ + "px";
1057   this.graphDiv.style.height = this.height_ + "px";
1058   this.canvas_.width = this.width_;
1059   this.canvas_.height = this.height_;
1060   this.canvas_.style.width = this.width_ + "px";    // for IE
1061   this.canvas_.style.height = this.height_ + "px";  // for IE
1062   this.hidden_.width = this.width_;
1063   this.hidden_.height = this.height_;
1064   this.hidden_.style.width = this.width_ + "px";    // for IE
1065   this.hidden_.style.height = this.height_ + "px";  // for IE
1066 };
1067 
1068 /**
1069  * Detach DOM elements in the dygraph and null out all data references.
1070  * Calling this when you're done with a dygraph can dramatically reduce memory
1071  * usage. See, e.g., the tests/perf.html example.
1072  */
1073 Dygraph.prototype.destroy = function() {
1074   this.canvas_ctx_.restore();
1075   this.hidden_ctx_.restore();
1076 
1077   var removeRecursive = function(node) {
1078     while (node.hasChildNodes()) {
1079       removeRecursive(node.firstChild);
1080       node.removeChild(node.firstChild);
1081     }
1082   };
1083 
1084   this.removeTrackedEvents_();
1085 
1086   // remove mouse event handlers (This may not be necessary anymore)
1087   Dygraph.removeEvent(window, 'mouseout', this.mouseOutHandler_);
1088   Dygraph.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_);
1089 
1090   // remove window handlers
1091   Dygraph.removeEvent(window,'resize',this.resizeHandler_);
1092   this.resizeHandler_ = null;
1093 
1094   removeRecursive(this.maindiv_);
1095 
1096   var nullOut = function(obj) {
1097     for (var n in obj) {
1098       if (typeof(obj[n]) === 'object') {
1099         obj[n] = null;
1100       }
1101     }
1102   };
1103   // These may not all be necessary, but it can't hurt...
1104   nullOut(this.layout_);
1105   nullOut(this.plotter_);
1106   nullOut(this);
1107 };
1108 
1109 /**
1110  * Creates the canvas on which the chart will be drawn. Only the Renderer ever
1111  * draws on this particular canvas. All Dygraph work (i.e. drawing hover dots
1112  * or the zoom rectangles) is done on this.canvas_.
1113  * @param {Object} canvas The Dygraph canvas over which to overlay the plot
1114  * @return {Object} The newly-created canvas
1115  * @private
1116  */
1117 Dygraph.prototype.createPlotKitCanvas_ = function(canvas) {
1118   var h = Dygraph.createCanvas();
1119   h.style.position = "absolute";
1120   // TODO(danvk): h should be offset from canvas. canvas needs to include
1121   // some extra area to make it easier to zoom in on the far left and far
1122   // right. h needs to be precisely the plot area, so that clipping occurs.
1123   h.style.top = canvas.style.top;
1124   h.style.left = canvas.style.left;
1125   h.width = this.width_;
1126   h.height = this.height_;
1127   h.style.width = this.width_ + "px";    // for IE
1128   h.style.height = this.height_ + "px";  // for IE
1129   return h;
1130 };
1131 
1132 /**
1133  * Creates an overlay element used to handle mouse events.
1134  * @return {Object} The mouse event element.
1135  * @private
1136  */
1137 Dygraph.prototype.createMouseEventElement_ = function() {
1138   if (this.isUsingExcanvas_) {
1139     var elem = document.createElement("div");
1140     elem.style.position = 'absolute';
1141     elem.style.backgroundColor = 'white';
1142     elem.style.filter = 'alpha(opacity=0)';
1143     elem.style.width = this.width_ + "px";
1144     elem.style.height = this.height_ + "px";
1145     this.graphDiv.appendChild(elem);
1146     return elem;
1147   } else {
1148     return this.canvas_;
1149   }
1150 };
1151 
1152 /**
1153  * Generate a set of distinct colors for the data series. This is done with a
1154  * color wheel. Saturation/Value are customizable, and the hue is
1155  * equally-spaced around the color wheel. If a custom set of colors is
1156  * specified, that is used instead.
1157  * @private
1158  */
1159 Dygraph.prototype.setColors_ = function() {
1160   var labels = this.getLabels();
1161   var num = labels.length - 1;
1162   this.colors_ = [];
1163   this.colorsMap_ = {};
1164   var colors = this.attr_('colors');
1165   var i;
1166   if (!colors) {
1167     var sat = this.attr_('colorSaturation') || 1.0;
1168     var val = this.attr_('colorValue') || 0.5;
1169     var half = Math.ceil(num / 2);
1170     for (i = 1; i <= num; i++) {
1171       if (!this.visibility()[i-1]) continue;
1172       // alternate colors for high contrast.
1173       var idx = i % 2 ? Math.ceil(i / 2) : (half + i / 2);
1174       var hue = (1.0 * idx/ (1 + num));
1175       var colorStr = Dygraph.hsvToRGB(hue, sat, val);
1176       this.colors_.push(colorStr);
1177       this.colorsMap_[labels[i]] = colorStr;
1178     }
1179   } else {
1180     for (i = 0; i < num; i++) {
1181       if (!this.visibility()[i]) continue;
1182       var colorStr = colors[i % colors.length];
1183       this.colors_.push(colorStr);
1184       this.colorsMap_[labels[1 + i]] = colorStr;
1185     }
1186   }
1187 };
1188 
1189 /**
1190  * Return the list of colors. This is either the list of colors passed in the
1191  * attributes or the autogenerated list of rgb(r,g,b) strings.
1192  * This does not return colors for invisible series.
1193  * @return {Array<string>} The list of colors.
1194  */
1195 Dygraph.prototype.getColors = function() {
1196   return this.colors_;
1197 };
1198 
1199 /**
1200  * Returns a few attributes of a series, i.e. its color, its visibility, which
1201  * axis it's assigned to, and its column in the original data.
1202  * Returns null if the series does not exist.
1203  * Otherwise, returns an object with column, visibility, color and axis properties.
1204  * The "axis" property will be set to 1 for y1 and 2 for y2.
1205  * The "column" property can be fed back into getValue(row, column) to get
1206  * values for this series.
1207  */
1208 Dygraph.prototype.getPropertiesForSeries = function(series_name) {
1209   var idx = -1;
1210   var labels = this.getLabels();
1211   for (var i = 1; i < labels.length; i++) {
1212     if (labels[i] == series_name) {
1213       idx = i;
1214       break;
1215     }
1216   }
1217   if (idx == -1) return null;
1218 
1219   return {
1220     name: series_name,
1221     column: idx,
1222     visible: this.visibility()[idx - 1],
1223     color: this.colorsMap_[series_name],
1224     axis: 1 + this.attributes_.axisForSeries(series_name)
1225   };
1226 };
1227 
1228 /**
1229  * Create the text box to adjust the averaging period
1230  * @private
1231  */
1232 Dygraph.prototype.createRollInterface_ = function() {
1233   // Create a roller if one doesn't exist already.
1234   if (!this.roller_) {
1235     this.roller_ = document.createElement("input");
1236     this.roller_.type = "text";
1237     this.roller_.style.display = "none";
1238     this.graphDiv.appendChild(this.roller_);
1239   }
1240 
1241   var display = this.attr_('showRoller') ? 'block' : 'none';
1242 
1243   var area = this.plotter_.area;
1244   var textAttr = { "position": "absolute",
1245                    "zIndex": 10,
1246                    "top": (area.y + area.h - 25) + "px",
1247                    "left": (area.x + 1) + "px",
1248                    "display": display
1249                   };
1250   this.roller_.size = "2";
1251   this.roller_.value = this.rollPeriod_;
1252   for (var name in textAttr) {
1253     if (textAttr.hasOwnProperty(name)) {
1254       this.roller_.style[name] = textAttr[name];
1255     }
1256   }
1257 
1258   var dygraph = this;
1259   this.roller_.onchange = function() { dygraph.adjustRoll(dygraph.roller_.value); };
1260 };
1261 
1262 /**
1263  * @private
1264  * Converts page the x-coordinate of the event to pixel x-coordinates on the
1265  * canvas (i.e. DOM Coords).
1266  */
1267 Dygraph.prototype.dragGetX_ = function(e, context) {
1268   return Dygraph.pageX(e) - context.px;
1269 };
1270 
1271 /**
1272  * @private
1273  * Converts page the y-coordinate of the event to pixel y-coordinates on the
1274  * canvas (i.e. DOM Coords).
1275  */
1276 Dygraph.prototype.dragGetY_ = function(e, context) {
1277   return Dygraph.pageY(e) - context.py;
1278 };
1279 
1280 /**
1281  * Set up all the mouse handlers needed to capture dragging behavior for zoom
1282  * events.
1283  * @private
1284  */
1285 Dygraph.prototype.createDragInterface_ = function() {
1286   var context = {
1287     // Tracks whether the mouse is down right now
1288     isZooming: false,
1289     isPanning: false,  // is this drag part of a pan?
1290     is2DPan: false,    // if so, is that pan 1- or 2-dimensional?
1291     dragStartX: null, // pixel coordinates
1292     dragStartY: null, // pixel coordinates
1293     dragEndX: null, // pixel coordinates
1294     dragEndY: null, // pixel coordinates
1295     dragDirection: null,
1296     prevEndX: null, // pixel coordinates
1297     prevEndY: null, // pixel coordinates
1298     prevDragDirection: null,
1299     cancelNextDblclick: false,  // see comment in dygraph-interaction-model.js
1300 
1301     // The value on the left side of the graph when a pan operation starts.
1302     initialLeftmostDate: null,
1303 
1304     // The number of units each pixel spans. (This won't be valid for log
1305     // scales)
1306     xUnitsPerPixel: null,
1307 
1308     // TODO(danvk): update this comment
1309     // The range in second/value units that the viewport encompasses during a
1310     // panning operation.
1311     dateRange: null,
1312 
1313     // Top-left corner of the canvas, in DOM coords
1314     // TODO(konigsberg): Rename topLeftCanvasX, topLeftCanvasY.
1315     px: 0,
1316     py: 0,
1317 
1318     // Values for use with panEdgeFraction, which limit how far outside the
1319     // graph's data boundaries it can be panned.
1320     boundedDates: null, // [minDate, maxDate]
1321     boundedValues: null, // [[minValue, maxValue] ...]
1322 
1323     // We cover iframes during mouse interactions. See comments in
1324     // dygraph-utils.js for more info on why this is a good idea.
1325     tarp: new Dygraph.IFrameTarp(),
1326 
1327     // contextB is the same thing as this context object but renamed.
1328     initializeMouseDown: function(event, g, contextB) {
1329       // prevents mouse drags from selecting page text.
1330       if (event.preventDefault) {
1331         event.preventDefault();  // Firefox, Chrome, etc.
1332       } else {
1333         event.returnValue = false;  // IE
1334         event.cancelBubble = true;
1335       }
1336 
1337       contextB.px = Dygraph.findPosX(g.canvas_);
1338       contextB.py = Dygraph.findPosY(g.canvas_);
1339       contextB.dragStartX = g.dragGetX_(event, contextB);
1340       contextB.dragStartY = g.dragGetY_(event, contextB);
1341       contextB.cancelNextDblclick = false;
1342       contextB.tarp.cover();
1343     }
1344   };
1345 
1346   var interactionModel = this.attr_("interactionModel");
1347 
1348   // Self is the graph.
1349   var self = this;
1350 
1351   // Function that binds the graph and context to the handler.
1352   var bindHandler = function(handler) {
1353     return function(event) {
1354       handler(event, self, context);
1355     };
1356   };
1357 
1358   for (var eventName in interactionModel) {
1359     if (!interactionModel.hasOwnProperty(eventName)) continue;
1360     this.addAndTrackEvent(this.mouseEventElement_, eventName,
1361         bindHandler(interactionModel[eventName]));
1362   }
1363 
1364   // If the user releases the mouse button during a drag, but not over the
1365   // canvas, then it doesn't count as a zooming action.
1366   var mouseUpHandler = function(event) {
1367     if (context.isZooming || context.isPanning) {
1368       context.isZooming = false;
1369       context.dragStartX = null;
1370       context.dragStartY = null;
1371     }
1372 
1373     if (context.isPanning) {
1374       context.isPanning = false;
1375       context.draggingDate = null;
1376       context.dateRange = null;
1377       for (var i = 0; i < self.axes_.length; i++) {
1378         delete self.axes_[i].draggingValue;
1379         delete self.axes_[i].dragValueRange;
1380       }
1381     }
1382 
1383     context.tarp.uncover();
1384   };
1385 
1386   this.addAndTrackEvent(document, 'mouseup', mouseUpHandler);
1387 };
1388 
1389 /**
1390  * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
1391  * up any previous zoom rectangles that were drawn. This could be optimized to
1392  * avoid extra redrawing, but it's tricky to avoid interactions with the status
1393  * dots.
1394  *
1395  * @param {Number} direction the direction of the zoom rectangle. Acceptable
1396  * values are Dygraph.HORIZONTAL and Dygraph.VERTICAL.
1397  * @param {Number} startX The X position where the drag started, in canvas
1398  * coordinates.
1399  * @param {Number} endX The current X position of the drag, in canvas coords.
1400  * @param {Number} startY The Y position where the drag started, in canvas
1401  * coordinates.
1402  * @param {Number} endY The current Y position of the drag, in canvas coords.
1403  * @param {Number} prevDirection the value of direction on the previous call to
1404  * this function. Used to avoid excess redrawing
1405  * @param {Number} prevEndX The value of endX on the previous call to this
1406  * function. Used to avoid excess redrawing
1407  * @param {Number} prevEndY The value of endY on the previous call to this
1408  * function. Used to avoid excess redrawing
1409  * @private
1410  */
1411 Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY,
1412                                            endY, prevDirection, prevEndX,
1413                                            prevEndY) {
1414   var ctx = this.canvas_ctx_;
1415 
1416   // Clean up from the previous rect if necessary
1417   if (prevDirection == Dygraph.HORIZONTAL) {
1418     ctx.clearRect(Math.min(startX, prevEndX), this.layout_.getPlotArea().y,
1419                   Math.abs(startX - prevEndX), this.layout_.getPlotArea().h);
1420   } else if (prevDirection == Dygraph.VERTICAL) {
1421     ctx.clearRect(this.layout_.getPlotArea().x, Math.min(startY, prevEndY),
1422                   this.layout_.getPlotArea().w, Math.abs(startY - prevEndY));
1423   }
1424 
1425   // Draw a light-grey rectangle to show the new viewing area
1426   if (direction == Dygraph.HORIZONTAL) {
1427     if (endX && startX) {
1428       ctx.fillStyle = "rgba(128,128,128,0.33)";
1429       ctx.fillRect(Math.min(startX, endX), this.layout_.getPlotArea().y,
1430                    Math.abs(endX - startX), this.layout_.getPlotArea().h);
1431     }
1432   } else if (direction == Dygraph.VERTICAL) {
1433     if (endY && startY) {
1434       ctx.fillStyle = "rgba(128,128,128,0.33)";
1435       ctx.fillRect(this.layout_.getPlotArea().x, Math.min(startY, endY),
1436                    this.layout_.getPlotArea().w, Math.abs(endY - startY));
1437     }
1438   }
1439 
1440   if (this.isUsingExcanvas_) {
1441     this.currentZoomRectArgs_ = [direction, startX, endX, startY, endY, 0, 0, 0];
1442   }
1443 };
1444 
1445 /**
1446  * Clear the zoom rectangle (and perform no zoom).
1447  * @private
1448  */
1449 Dygraph.prototype.clearZoomRect_ = function() {
1450   this.currentZoomRectArgs_ = null;
1451   this.canvas_ctx_.clearRect(0, 0, this.canvas_.width, this.canvas_.height);
1452 };
1453 
1454 /**
1455  * Zoom to something containing [lowX, highX]. These are pixel coordinates in
1456  * the canvas. The exact zoom window may be slightly larger if there are no data
1457  * points near lowX or highX. Don't confuse this function with doZoomXDates,
1458  * which accepts dates that match the raw data. This function redraws the graph.
1459  *
1460  * @param {Number} lowX The leftmost pixel value that should be visible.
1461  * @param {Number} highX The rightmost pixel value that should be visible.
1462  * @private
1463  */
1464 Dygraph.prototype.doZoomX_ = function(lowX, highX) {
1465   this.currentZoomRectArgs_ = null;
1466   // Find the earliest and latest dates contained in this canvasx range.
1467   // Convert the call to date ranges of the raw data.
1468   var minDate = this.toDataXCoord(lowX);
1469   var maxDate = this.toDataXCoord(highX);
1470   this.doZoomXDates_(minDate, maxDate);
1471 };
1472 
1473 /**
1474  * Transition function to use in animations. Returns values between 0.0
1475  * (totally old values) and 1.0 (totally new values) for each frame.
1476  * @private
1477  */
1478 Dygraph.zoomAnimationFunction = function(frame, numFrames) {
1479   var k = 1.5;
1480   return (1.0 - Math.pow(k, -frame)) / (1.0 - Math.pow(k, -numFrames));
1481 };
1482 
1483 /**
1484  * Zoom to something containing [minDate, maxDate] values. Don't confuse this
1485  * method with doZoomX which accepts pixel coordinates. This function redraws
1486  * the graph.
1487  *
1488  * @param {Number} minDate The minimum date that should be visible.
1489  * @param {Number} maxDate The maximum date that should be visible.
1490  * @private
1491  */
1492 Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) {
1493   // TODO(danvk): when yAxisRange is null (i.e. "fit to data", the animation
1494   // can produce strange effects. Rather than the y-axis transitioning slowly
1495   // between values, it can jerk around.)
1496   var old_window = this.xAxisRange();
1497   var new_window = [minDate, maxDate];
1498   this.zoomed_x_ = true;
1499   var that = this;
1500   this.doAnimatedZoom(old_window, new_window, null, null, function() {
1501     if (that.attr_("zoomCallback")) {
1502       that.attr_("zoomCallback")(minDate, maxDate, that.yAxisRanges());
1503     }
1504   });
1505 };
1506 
1507 /**
1508  * Zoom to something containing [lowY, highY]. These are pixel coordinates in
1509  * the canvas. This function redraws the graph.
1510  *
1511  * @param {Number} lowY The topmost pixel value that should be visible.
1512  * @param {Number} highY The lowest pixel value that should be visible.
1513  * @private
1514  */
1515 Dygraph.prototype.doZoomY_ = function(lowY, highY) {
1516   this.currentZoomRectArgs_ = null;
1517   // Find the highest and lowest values in pixel range for each axis.
1518   // Note that lowY (in pixels) corresponds to the max Value (in data coords).
1519   // This is because pixels increase as you go down on the screen, whereas data
1520   // coordinates increase as you go up the screen.
1521   var oldValueRanges = this.yAxisRanges();
1522   var newValueRanges = [];
1523   for (var i = 0; i < this.axes_.length; i++) {
1524     var hi = this.toDataYCoord(lowY, i);
1525     var low = this.toDataYCoord(highY, i);
1526     newValueRanges.push([low, hi]);
1527   }
1528 
1529   this.zoomed_y_ = true;
1530   var that = this;
1531   this.doAnimatedZoom(null, null, oldValueRanges, newValueRanges, function() {
1532     if (that.attr_("zoomCallback")) {
1533       var xRange = that.xAxisRange();
1534       that.attr_("zoomCallback")(xRange[0], xRange[1], that.yAxisRanges());
1535     }
1536   });
1537 };
1538 
1539 /**
1540  * Reset the zoom to the original view coordinates. This is the same as
1541  * double-clicking on the graph.
1542  */
1543 Dygraph.prototype.resetZoom = function() {
1544   var dirty = false, dirtyX = false, dirtyY = false;
1545   if (this.dateWindow_ !== null) {
1546     dirty = true;
1547     dirtyX = true;
1548   }
1549 
1550   for (var i = 0; i < this.axes_.length; i++) {
1551     if (typeof(this.axes_[i].valueWindow) !== 'undefined' && this.axes_[i].valueWindow !== null) {
1552       dirty = true;
1553       dirtyY = true;
1554     }
1555   }
1556 
1557   // Clear any selection, since it's likely to be drawn in the wrong place.
1558   this.clearSelection();
1559 
1560   if (dirty) {
1561     this.zoomed_x_ = false;
1562     this.zoomed_y_ = false;
1563 
1564     var minDate = this.rawData_[0][0];
1565     var maxDate = this.rawData_[this.rawData_.length - 1][0];
1566 
1567     // With only one frame, don't bother calculating extreme ranges.
1568     // TODO(danvk): merge this block w/ the code below.
1569     if (!this.attr_("animatedZooms")) {
1570       this.dateWindow_ = null;
1571       for (i = 0; i < this.axes_.length; i++) {
1572         if (this.axes_[i].valueWindow !== null) {
1573           delete this.axes_[i].valueWindow;
1574         }
1575       }
1576       this.drawGraph_();
1577       if (this.attr_("zoomCallback")) {
1578         this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges());
1579       }
1580       return;
1581     }
1582 
1583     var oldWindow=null, newWindow=null, oldValueRanges=null, newValueRanges=null;
1584     if (dirtyX) {
1585       oldWindow = this.xAxisRange();
1586       newWindow = [minDate, maxDate];
1587     }
1588 
1589     if (dirtyY) {
1590       oldValueRanges = this.yAxisRanges();
1591       // TODO(danvk): this is pretty inefficient
1592       var packed = this.gatherDatasets_(this.rolledSeries_, null);
1593       var extremes = packed.extremes;
1594 
1595       // this has the side-effect of modifying this.axes_.
1596       // this doesn't make much sense in this context, but it's convenient (we
1597       // need this.axes_[*].extremeValues) and not harmful since we'll be
1598       // calling drawGraph_ shortly, which clobbers these values.
1599       this.computeYAxisRanges_(extremes);
1600 
1601       newValueRanges = [];
1602       for (i = 0; i < this.axes_.length; i++) {
1603         var axis = this.axes_[i];
1604         newValueRanges.push((axis.valueRange !== null &&
1605                              axis.valueRange !== undefined) ?
1606                             axis.valueRange : axis.extremeRange);
1607       }
1608     }
1609 
1610     var that = this;
1611     this.doAnimatedZoom(oldWindow, newWindow, oldValueRanges, newValueRanges,
1612         function() {
1613           that.dateWindow_ = null;
1614           for (var i = 0; i < that.axes_.length; i++) {
1615             if (that.axes_[i].valueWindow !== null) {
1616               delete that.axes_[i].valueWindow;
1617             }
1618           }
1619           if (that.attr_("zoomCallback")) {
1620             that.attr_("zoomCallback")(minDate, maxDate, that.yAxisRanges());
1621           }
1622         });
1623   }
1624 };
1625 
1626 /**
1627  * Combined animation logic for all zoom functions.
1628  * either the x parameters or y parameters may be null.
1629  * @private
1630  */
1631 Dygraph.prototype.doAnimatedZoom = function(oldXRange, newXRange, oldYRanges, newYRanges, callback) {
1632   var steps = this.attr_("animatedZooms") ? Dygraph.ANIMATION_STEPS : 1;
1633 
1634   var windows = [];
1635   var valueRanges = [];
1636   var step, frac;
1637 
1638   if (oldXRange !== null && newXRange !== null) {
1639     for (step = 1; step <= steps; step++) {
1640       frac = Dygraph.zoomAnimationFunction(step, steps);
1641       windows[step-1] = [oldXRange[0]*(1-frac) + frac*newXRange[0],
1642                          oldXRange[1]*(1-frac) + frac*newXRange[1]];
1643     }
1644   }
1645 
1646   if (oldYRanges !== null && newYRanges !== null) {
1647     for (step = 1; step <= steps; step++) {
1648       frac = Dygraph.zoomAnimationFunction(step, steps);
1649       var thisRange = [];
1650       for (var j = 0; j < this.axes_.length; j++) {
1651         thisRange.push([oldYRanges[j][0]*(1-frac) + frac*newYRanges[j][0],
1652                         oldYRanges[j][1]*(1-frac) + frac*newYRanges[j][1]]);
1653       }
1654       valueRanges[step-1] = thisRange;
1655     }
1656   }
1657 
1658   var that = this;
1659   Dygraph.repeatAndCleanup(function(step) {
1660     if (valueRanges.length) {
1661       for (var i = 0; i < that.axes_.length; i++) {
1662         var w = valueRanges[step][i];
1663         that.axes_[i].valueWindow = [w[0], w[1]];
1664       }
1665     }
1666     if (windows.length) {
1667       that.dateWindow_ = windows[step];
1668     }
1669     that.drawGraph_();
1670   }, steps, Dygraph.ANIMATION_DURATION / steps, callback);
1671 };
1672 
1673 /**
1674  * Get the current graph's area object.
1675  *
1676  * Returns: {x, y, w, h}
1677  */
1678 Dygraph.prototype.getArea = function() {
1679   return this.plotter_.area;
1680 };
1681 
1682 /**
1683  * Convert a mouse event to DOM coordinates relative to the graph origin.
1684  *
1685  * Returns a two-element array: [X, Y].
1686  */
1687 Dygraph.prototype.eventToDomCoords = function(event) {
1688   if (event.offsetX && event.offsetY) {
1689     return [ event.offsetX, event.offsetY ];
1690   } else {
1691     var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_);
1692     var canvasy = Dygraph.pageY(event) - Dygraph.findPosY(this.mouseEventElement_);
1693     return [canvasx, canvasy];
1694   }
1695 };
1696 
1697 /**
1698  * Given a canvas X coordinate, find the closest row.
1699  * @param {Number} domX graph-relative DOM X coordinate
1700  * Returns: row number, integer
1701  * @private
1702  */
1703 Dygraph.prototype.findClosestRow = function(domX) {
1704   var minDistX = Infinity;
1705   var closestRow = -1;
1706   var sets = this.layout_.points;
1707   for (var i = 0; i < sets.length; i++) {
1708     var points = sets[i];
1709     var len = points.length;
1710     for (var j = 0; j < len; j++) {
1711       var point = points[j];
1712       if (!Dygraph.isValidPoint(point, true)) continue;
1713       var dist = Math.abs(point.canvasx - domX);
1714       if (dist < minDistX) {
1715         minDistX = dist;
1716         closestRow = point.idx;
1717       }
1718     }
1719   }
1720 
1721   return closestRow;
1722 };
1723 
1724 /**
1725  * Given canvas X,Y coordinates, find the closest point.
1726  *
1727  * This finds the individual data point across all visible series
1728  * that's closest to the supplied DOM coordinates using the standard
1729  * Euclidean X,Y distance.
1730  *
1731  * @param {Number} domX graph-relative DOM X coordinate
1732  * @param {Number} domY graph-relative DOM Y coordinate
1733  * Returns: {row, seriesName, point}
1734  * @private
1735  */
1736 Dygraph.prototype.findClosestPoint = function(domX, domY) {
1737   var minDist = Infinity;
1738   var dist, dx, dy, point, closestPoint, closestSeries, closestRow;
1739   for ( var setIdx = this.layout_.points.length - 1 ; setIdx >= 0 ; --setIdx ) {
1740     var points = this.layout_.points[setIdx];
1741     for (var i = 0; i < points.length; ++i) {
1742       point = points[i];
1743       if (!Dygraph.isValidPoint(point)) continue;
1744       dx = point.canvasx - domX;
1745       dy = point.canvasy - domY;
1746       dist = dx * dx + dy * dy;
1747       if (dist < minDist) {
1748         minDist = dist;
1749         closestPoint = point;
1750         closestSeries = setIdx;
1751         closestRow = point.idx;
1752       }
1753     }
1754   }
1755   var name = this.layout_.setNames[closestSeries];
1756   return {
1757     row: closestRow,
1758     seriesName: name,
1759     point: closestPoint
1760   };
1761 };
1762 
1763 /**
1764  * Given canvas X,Y coordinates, find the touched area in a stacked graph.
1765  *
1766  * This first finds the X data point closest to the supplied DOM X coordinate,
1767  * then finds the series which puts the Y coordinate on top of its filled area,
1768  * using linear interpolation between adjacent point pairs.
1769  *
1770  * @param {Number} domX graph-relative DOM X coordinate
1771  * @param {Number} domY graph-relative DOM Y coordinate
1772  * Returns: {row, seriesName, point}
1773  * @private
1774  */
1775 Dygraph.prototype.findStackedPoint = function(domX, domY) {
1776   var row = this.findClosestRow(domX);
1777   var closestPoint, closestSeries;
1778   for (var setIdx = 0; setIdx < this.layout_.points.length; ++setIdx) {
1779     var boundary = this.getLeftBoundary_(setIdx);
1780     var rowIdx = row - boundary;
1781     var points = this.layout_.points[setIdx];
1782     if (rowIdx >= points.length) continue;
1783     var p1 = points[rowIdx];
1784     if (!Dygraph.isValidPoint(p1)) continue;
1785     var py = p1.canvasy;
1786     if (domX > p1.canvasx && rowIdx + 1 < points.length) {
1787       // interpolate series Y value using next point
1788       var p2 = points[rowIdx + 1];
1789       if (Dygraph.isValidPoint(p2)) {
1790         var dx = p2.canvasx - p1.canvasx;
1791         if (dx > 0) {
1792           var r = (domX - p1.canvasx) / dx;
1793           py += r * (p2.canvasy - p1.canvasy);
1794         }
1795       }
1796     } else if (domX < p1.canvasx && rowIdx > 0) {
1797       // interpolate series Y value using previous point
1798       var p0 = points[rowIdx - 1];
1799       if (Dygraph.isValidPoint(p0)) {
1800         var dx = p1.canvasx - p0.canvasx;
1801         if (dx > 0) {
1802           var r = (p1.canvasx - domX) / dx;
1803           py += r * (p0.canvasy - p1.canvasy);
1804         }
1805       }
1806     }
1807     // Stop if the point (domX, py) is above this series' upper edge
1808     if (setIdx === 0 || py < domY) {
1809       closestPoint = p1;
1810       closestSeries = setIdx;
1811     }
1812   }
1813   var name = this.layout_.setNames[closestSeries];
1814   return {
1815     row: row,
1816     seriesName: name,
1817     point: closestPoint
1818   };
1819 };
1820 
1821 /**
1822  * When the mouse moves in the canvas, display information about a nearby data
1823  * point and draw dots over those points in the data series. This function
1824  * takes care of cleanup of previously-drawn dots.
1825  * @param {Object} event The mousemove event from the browser.
1826  * @private
1827  */
1828 Dygraph.prototype.mouseMove_ = function(event) {
1829   // This prevents JS errors when mousing over the canvas before data loads.
1830   var points = this.layout_.points;
1831   if (points === undefined || points === null) return;
1832 
1833   var canvasCoords = this.eventToDomCoords(event);
1834   var canvasx = canvasCoords[0];
1835   var canvasy = canvasCoords[1];
1836 
1837   var highlightSeriesOpts = this.attr_("highlightSeriesOpts");
1838   var selectionChanged = false;
1839   if (highlightSeriesOpts && !this.isSeriesLocked()) {
1840     var closest;
1841     if (this.attr_("stackedGraph")) {
1842       closest = this.findStackedPoint(canvasx, canvasy);
1843     } else {
1844       closest = this.findClosestPoint(canvasx, canvasy);
1845     }
1846     selectionChanged = this.setSelection(closest.row, closest.seriesName);
1847   } else {
1848     var idx = this.findClosestRow(canvasx);
1849     selectionChanged = this.setSelection(idx);
1850   }
1851 
1852   var callback = this.attr_("highlightCallback");
1853   if (callback && selectionChanged) {
1854     callback(event,
1855         this.lastx_,
1856         this.selPoints_,
1857         this.lastRow_,
1858         this.highlightSet_);
1859   }
1860 };
1861 
1862 /**
1863  * Fetch left offset from the specified set index or if not passed, the 
1864  * first defined boundaryIds record (see bug #236).
1865  * @private
1866  */
1867 Dygraph.prototype.getLeftBoundary_ = function(setIdx) {
1868   if (this.boundaryIds_[setIdx]) {
1869       return this.boundaryIds_[setIdx][0];
1870   } else {
1871     for (var i = 0; i < this.boundaryIds_.length; i++) {
1872       if (this.boundaryIds_[i] !== undefined) {
1873         return this.boundaryIds_[i][0];
1874       }
1875     }
1876     return 0;
1877   }
1878 };
1879 
1880 Dygraph.prototype.animateSelection_ = function(direction) {
1881   var totalSteps = 10;
1882   var millis = 30;
1883   if (this.fadeLevel === undefined) this.fadeLevel = 0;
1884   if (this.animateId === undefined) this.animateId = 0;
1885   var start = this.fadeLevel;
1886   var steps = direction < 0 ? start : totalSteps - start;
1887   if (steps <= 0) {
1888     if (this.fadeLevel) {
1889       this.updateSelection_(1.0);
1890     }
1891     return;
1892   }
1893 
1894   var thisId = ++this.animateId;
1895   var that = this;
1896   Dygraph.repeatAndCleanup(
1897     function(n) {
1898       // ignore simultaneous animations
1899       if (that.animateId != thisId) return;
1900 
1901       that.fadeLevel += direction;
1902       if (that.fadeLevel === 0) {
1903         that.clearSelection();
1904       } else {
1905         that.updateSelection_(that.fadeLevel / totalSteps);
1906       }
1907     },
1908     steps, millis, function() {});
1909 };
1910 
1911 /**
1912  * Draw dots over the selectied points in the data series. This function
1913  * takes care of cleanup of previously-drawn dots.
1914  * @private
1915  */
1916 Dygraph.prototype.updateSelection_ = function(opt_animFraction) {
1917   /*var defaultPrevented = */
1918   this.cascadeEvents_('select', {
1919     selectedX: this.lastx_,
1920     selectedPoints: this.selPoints_
1921   });
1922   // TODO(danvk): use defaultPrevented here?
1923 
1924   // Clear the previously drawn vertical, if there is one
1925   var i;
1926   var ctx = this.canvas_ctx_;
1927   if (this.attr_('highlightSeriesOpts')) {
1928     ctx.clearRect(0, 0, this.width_, this.height_);
1929     var alpha = 1.0 - this.attr_('highlightSeriesBackgroundAlpha');
1930     if (alpha) {
1931       // Activating background fade includes an animation effect for a gradual
1932       // fade. TODO(klausw): make this independently configurable if it causes
1933       // issues? Use a shared preference to control animations?
1934       var animateBackgroundFade = true;
1935       if (animateBackgroundFade) {
1936         if (opt_animFraction === undefined) {
1937           // start a new animation
1938           this.animateSelection_(1);
1939           return;
1940         }
1941         alpha *= opt_animFraction;
1942       }
1943       ctx.fillStyle = 'rgba(255,255,255,' + alpha + ')';
1944       ctx.fillRect(0, 0, this.width_, this.height_);
1945     }
1946 
1947     // Redraw only the highlighted series in the interactive canvas (not the
1948     // static plot canvas, which is where series are usually drawn).
1949     this.plotter_._renderLineChart(this.highlightSet_, ctx);
1950   } else if (this.previousVerticalX_ >= 0) {
1951     // Determine the maximum highlight circle size.
1952     var maxCircleSize = 0;
1953     var labels = this.attr_('labels');
1954     for (i = 1; i < labels.length; i++) {
1955       var r = this.attr_('highlightCircleSize', labels[i]);
1956       if (r > maxCircleSize) maxCircleSize = r;
1957     }
1958     var px = this.previousVerticalX_;
1959     ctx.clearRect(px - maxCircleSize - 1, 0,
1960                   2 * maxCircleSize + 2, this.height_);
1961   }
1962 
1963   if (this.isUsingExcanvas_ && this.currentZoomRectArgs_) {
1964     Dygraph.prototype.drawZoomRect_.apply(this, this.currentZoomRectArgs_);
1965   }
1966 
1967   if (this.selPoints_.length > 0) {
1968     // Draw colored circles over the center of each selected point
1969     var canvasx = this.selPoints_[0].canvasx;
1970     ctx.save();
1971     for (i = 0; i < this.selPoints_.length; i++) {
1972       var pt = this.selPoints_[i];
1973       if (!Dygraph.isOK(pt.canvasy)) continue;
1974 
1975       var circleSize = this.attr_('highlightCircleSize', pt.name);
1976       var callback = this.attr_("drawHighlightPointCallback", pt.name);
1977       var color = this.plotter_.colors[pt.name];
1978       if (!callback) {
1979         callback = Dygraph.Circles.DEFAULT;
1980       }
1981       ctx.lineWidth = this.attr_('strokeWidth', pt.name);
1982       ctx.strokeStyle = color;
1983       ctx.fillStyle = color;
1984       callback(this.g, pt.name, ctx, canvasx, pt.canvasy,
1985           color, circleSize, pt.idx);
1986     }
1987     ctx.restore();
1988 
1989     this.previousVerticalX_ = canvasx;
1990   }
1991 };
1992 
1993 /**
1994  * Manually set the selected points and display information about them in the
1995  * legend. The selection can be cleared using clearSelection() and queried
1996  * using getSelection().
1997  * @param { Integer } row number that should be highlighted (i.e. appear with
1998  * hover dots on the chart). Set to false to clear any selection.
1999  * @param { seriesName } optional series name to highlight that series with the
2000  * the highlightSeriesOpts setting.
2001  * @param { locked } optional If true, keep seriesName selected when mousing
2002  * over the graph, disabling closest-series highlighting. Call clearSelection()
2003  * to unlock it.
2004  */
2005 Dygraph.prototype.setSelection = function(row, opt_seriesName, opt_locked) {
2006   // Extract the points we've selected
2007   this.selPoints_ = [];
2008 
2009   var changed = false;
2010   if (row !== false && row >= 0) {
2011     if (row != this.lastRow_) changed = true;
2012     this.lastRow_ = row;
2013     for (var setIdx = 0; setIdx < this.layout_.points.length; ++setIdx) {
2014       var points = this.layout_.points[setIdx];
2015       var setRow = row - this.getLeftBoundary_(setIdx);
2016       if (setRow < points.length) {
2017         var point = points[setRow];
2018         if (point.yval !== null) this.selPoints_.push(point);
2019       }
2020     }
2021   } else {
2022     if (this.lastRow_ >= 0) changed = true;
2023     this.lastRow_ = -1;
2024   }
2025 
2026   if (this.selPoints_.length) {
2027     this.lastx_ = this.selPoints_[0].xval;
2028   } else {
2029     this.lastx_ = -1;
2030   }
2031 
2032   if (opt_seriesName !== undefined) {
2033     if (this.highlightSet_ !== opt_seriesName) changed = true;
2034     this.highlightSet_ = opt_seriesName;
2035   }
2036 
2037   if (opt_locked !== undefined) {
2038     this.lockedSet_ = opt_locked;
2039   }
2040 
2041   if (changed) {
2042     this.updateSelection_(undefined);
2043   }
2044   return changed;
2045 };
2046 
2047 /**
2048  * The mouse has left the canvas. Clear out whatever artifacts remain
2049  * @param {Object} event the mouseout event from the browser.
2050  * @private
2051  */
2052 Dygraph.prototype.mouseOut_ = function(event) {
2053   if (this.attr_("unhighlightCallback")) {
2054     this.attr_("unhighlightCallback")(event);
2055   }
2056 
2057   if (this.attr_("hideOverlayOnMouseOut") && !this.lockedSet_) {
2058     this.clearSelection();
2059   }
2060 };
2061 
2062 /**
2063  * Clears the current selection (i.e. points that were highlighted by moving
2064  * the mouse over the chart).
2065  */
2066 Dygraph.prototype.clearSelection = function() {
2067   this.cascadeEvents_('deselect', {});
2068 
2069   this.lockedSet_ = false;
2070   // Get rid of the overlay data
2071   if (this.fadeLevel) {
2072     this.animateSelection_(-1);
2073     return;
2074   }
2075   this.canvas_ctx_.clearRect(0, 0, this.width_, this.height_);
2076   this.fadeLevel = 0;
2077   this.selPoints_ = [];
2078   this.lastx_ = -1;
2079   this.lastRow_ = -1;
2080   this.highlightSet_ = null;
2081 };
2082 
2083 /**
2084  * Returns the number of the currently selected row. To get data for this row,
2085  * you can use the getValue method.
2086  * @return { Integer } row number, or -1 if nothing is selected
2087  */
2088 Dygraph.prototype.getSelection = function() {
2089   if (!this.selPoints_ || this.selPoints_.length < 1) {
2090     return -1;
2091   }
2092 
2093   for (var setIdx = 0; setIdx < this.layout_.points.length; setIdx++) {
2094     var points = this.layout_.points[setIdx];
2095     for (var row = 0; row < points.length; row++) {
2096       if (points[row].x == this.selPoints_[0].x) {
2097         return points[row].idx;
2098       }
2099     }
2100   }
2101   return -1;
2102 };
2103 
2104 /**
2105  * Returns the name of the currently-highlighted series.
2106  * Only available when the highlightSeriesOpts option is in use.
2107  */
2108 Dygraph.prototype.getHighlightSeries = function() {
2109   return this.highlightSet_;
2110 };
2111 
2112 /**
2113  * Returns true if the currently-highlighted series was locked
2114  * via setSelection(..., seriesName, true).
2115  */
2116 Dygraph.prototype.isSeriesLocked = function() {
2117   return this.lockedSet_;
2118 };
2119 
2120 /**
2121  * Fires when there's data available to be graphed.
2122  * @param {String} data Raw CSV data to be plotted
2123  * @private
2124  */
2125 Dygraph.prototype.loadedEvent_ = function(data) {
2126   this.rawData_ = this.parseCSV_(data);
2127   this.predraw_();
2128 };
2129 
2130 /**
2131  * Add ticks on the x-axis representing years, months, quarters, weeks, or days
2132  * @private
2133  */
2134 Dygraph.prototype.addXTicks_ = function() {
2135   // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
2136   var range;
2137   if (this.dateWindow_) {
2138     range = [this.dateWindow_[0], this.dateWindow_[1]];
2139   } else {
2140     range = this.xAxisExtremes();
2141   }
2142 
2143   var xAxisOptionsView = this.optionsViewForAxis_('x');
2144   var xTicks = xAxisOptionsView('ticker')(
2145       range[0],
2146       range[1],
2147       this.width_,  // TODO(danvk): should be area.width
2148       xAxisOptionsView,
2149       this);
2150   // var msg = 'ticker(' + range[0] + ', ' + range[1] + ', ' + this.width_ + ', ' + this.attr_('pixelsPerXLabel') + ') -> ' + JSON.stringify(xTicks);
2151   // console.log(msg);
2152   this.layout_.setXTicks(xTicks);
2153 };
2154 
2155 /**
2156  * @private
2157  * Computes the range of the data series (including confidence intervals).
2158  * @param { [Array] } series either [ [x1, y1], [x2, y2], ... ] or
2159  * [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
2160  * @return [low, high]
2161  */
2162 Dygraph.prototype.extremeValues_ = function(series) {
2163   var minY = null, maxY = null, j, y;
2164 
2165   var bars = this.attr_("errorBars") || this.attr_("customBars");
2166   if (bars) {
2167     // With custom bars, maxY is the max of the high values.
2168     for (j = 0; j < series.length; j++) {
2169       y = series[j][1][0];
2170       if (y === null || isNaN(y)) continue;
2171       var low = y - series[j][1][1];
2172       var high = y + series[j][1][2];
2173       if (low > y) low = y;    // this can happen with custom bars,
2174       if (high < y) high = y;  // e.g. in tests/custom-bars.html
2175       if (maxY === null || high > maxY) {
2176         maxY = high;
2177       }
2178       if (minY === null || low < minY) {
2179         minY = low;
2180       }
2181     }
2182   } else {
2183     for (j = 0; j < series.length; j++) {
2184       y = series[j][1];
2185       if (y === null || isNaN(y)) continue;
2186       if (maxY === null || y > maxY) {
2187         maxY = y;
2188       }
2189       if (minY === null || y < minY) {
2190         minY = y;
2191       }
2192     }
2193   }
2194 
2195   return [minY, maxY];
2196 };
2197 
2198 /**
2199  * @private
2200  * This function is called once when the chart's data is changed or the options
2201  * dictionary is updated. It is _not_ called when the user pans or zooms. The
2202  * idea is that values derived from the chart's data can be computed here,
2203  * rather than every time the chart is drawn. This includes things like the
2204  * number of axes, rolling averages, etc.
2205  */
2206 Dygraph.prototype.predraw_ = function() {
2207   var start = new Date();
2208 
2209   this.layout_.computePlotArea();
2210 
2211   // TODO(danvk): move more computations out of drawGraph_ and into here.
2212   this.computeYAxes_();
2213 
2214   // Create a new plotter.
2215   if (this.plotter_) {
2216     this.cascadeEvents_('clearChart');
2217     this.plotter_.clear();
2218   }
2219 
2220   if (!this.is_initial_draw_) {
2221     this.canvas_ctx_.restore();
2222     this.hidden_ctx_.restore();
2223   }
2224 
2225   this.canvas_ctx_.save();
2226   this.hidden_ctx_.save();
2227 
2228   this.plotter_ = new DygraphCanvasRenderer(this,
2229                                             this.hidden_,
2230                                             this.hidden_ctx_,
2231                                             this.layout_);
2232 
2233   // The roller sits in the bottom left corner of the chart. We don't know where
2234   // this will be until the options are available, so it's positioned here.
2235   this.createRollInterface_();
2236 
2237   this.cascadeEvents_('predraw');
2238 
2239   // Convert the raw data (a 2D array) into the internal format and compute
2240   // rolling averages.
2241   this.rolledSeries_ = [null];  // x-axis is the first series and it's special
2242   for (var i = 1; i < this.numColumns(); i++) {
2243     // var logScale = this.attr_('logscale', i); // TODO(klausw): this looks wrong // konigsberg thinks so too.
2244     var logScale = this.attr_('logscale');
2245     var series = this.extractSeries_(this.rawData_, i, logScale);
2246     series = this.rollingAverage(series, this.rollPeriod_);
2247     this.rolledSeries_.push(series);
2248   }
2249 
2250   // If the data or options have changed, then we'd better redraw.
2251   this.drawGraph_();
2252 
2253   // This is used to determine whether to do various animations.
2254   var end = new Date();
2255   this.drawingTimeMs_ = (end - start);
2256 };
2257 
2258 /**
2259  * Point structure.
2260  *
2261  * xval_* and yval_* are the original unscaled data values,
2262  * while x_* and y_* are scaled to the range (0.0-1.0) for plotting.
2263  * yval_stacked is the cumulative Y value used for stacking graphs,
2264  * and bottom/top/minus/plus are used for error bar graphs.
2265  *
2266  * @typedef {{
2267  *     idx: number,
2268  *     name: string,
2269  *     x: ?number,
2270  *     xval: ?number,
2271  *     y_bottom: ?number,
2272  *     y: ?number,
2273  *     y_stacked: ?number,
2274  *     y_top: ?number,
2275  *     yval_minus: ?number,
2276  *     yval: ?number,
2277  *     yval_plus: ?number,
2278  *     yval_stacked
2279  * }}
2280  */
2281 Dygraph.PointType = undefined;
2282 
2283 // TODO(bhs): these loops are a hot-spot for high-point-count charts. In fact,
2284 // on chrome+linux, they are 6 times more expensive than iterating through the
2285 // points and drawing the lines. The brunt of the cost comes from allocating
2286 // the |point| structures.
2287 /**
2288  * Converts a series to a Point array.
2289  *
2290  * @private
2291  * @param {Array.<Array.<(?number|Array<?number>)>} series Array where
2292  *     series[row] = [x,y] or [x, [y, err]] or [x, [y, yplus, yminus]].
2293  * @param {boolean} bars True if error bars or custom bars are being drawn.
2294  * @param {string} setName Name of the series.
2295  * @param {number} boundaryIdStart Index offset of the first point, equal to
2296  *     the number of skipped points left of the date window minimum (if any).
2297  * @return {Array.<Dygraph.PointType>} List of points for this series.
2298  */
2299 Dygraph.seriesToPoints_ = function(series, bars, setName, boundaryIdStart) {
2300   var points = [];
2301   for (var i = 0; i < series.length; ++i) {
2302     var item = series[i];
2303     var yraw = bars ? item[1][0] : item[1];
2304     var yval = yraw === null ? null : DygraphLayout.parseFloat_(yraw);
2305     var point = {
2306       x: NaN,
2307       y: NaN,
2308       xval: DygraphLayout.parseFloat_(item[0]),
2309       yval: yval,
2310       name: setName,  // TODO(danvk): is this really necessary?
2311       idx: i + boundaryIdStart
2312     };
2313 
2314     if (bars) {
2315       point.y_top = NaN;
2316       point.y_bottom = NaN;
2317       point.yval_minus = DygraphLayout.parseFloat_(item[1][1]);
2318       point.yval_plus = DygraphLayout.parseFloat_(item[1][2]);
2319     }
2320     points.push(point);
2321   }
2322   return points;
2323 };
2324 
2325 
2326 /**
2327  * Calculates point stacking for stackedGraph=true.
2328  *
2329  * For stacking purposes, interpolate or extend neighboring data across
2330  * NaN values based on stackedGraphNaNFill settings. This is for display
2331  * only, the underlying data value as shown in the legend remains NaN.
2332  *
2333  * @param {Array.<Dygraph.PointType>} points Point array for a single series.
2334  *     Updates each Point's yval_stacked property.
2335  * @param {Array.<number>} cumulativeYval Accumulated top-of-graph stacked Y
2336  *     values for the series seen so far. Index is the row number. Updated
2337  *     based on the current series's values.
2338  * @param {Array.<number>} seriesExtremes Min and max values, updated
2339  *     to reflect the stacked values.
2340  * @param {string} fillMethod Interpolation method, one of 'all', 'inside', or
2341  *     'none'.
2342  * @private
2343  */
2344 Dygraph.stackPoints_ = function(
2345     points, cumulativeYval, seriesExtremes, fillMethod) {
2346   var lastXval = null;
2347   var prevPoint = null;
2348   var nextPoint = null;
2349   var nextPointIdx = -1;
2350 
2351   // Find the next stackable point starting from the given index.
2352   var updateNextPoint = function(idx) {
2353     // If we've previously found a non-NaN point and haven't gone past it yet,
2354     // just use that.
2355     if (nextPointIdx >= idx) return;
2356 
2357     // We haven't found a non-NaN point yet or have moved past it,
2358     // look towards the right to find a non-NaN point.
2359     for (var j = idx; j < points.length; ++j) {
2360       // Clear out a previously-found point (if any) since it's no longer
2361       // valid, we shouldn't use it for interpolation anymore.
2362       nextPoint = null;
2363       if (!isNaN(points[j].yval) && points[j].yval !== null) {
2364         nextPointIdx = j;
2365         nextPoint = points[j];
2366         break;
2367       }
2368     }
2369   };
2370 
2371   for (var i = 0; i < points.length; ++i) {
2372     var point = points[i];
2373     var xval = point.xval;
2374     if (cumulativeYval[xval] === undefined) {
2375       cumulativeYval[xval] = 0;
2376     }
2377 
2378     var actualYval = point.yval;
2379     if (isNaN(actualYval) || actualYval === null) {
2380       // Interpolate/extend for stacking purposes if possible.
2381       updateNextPoint(i);
2382       if (prevPoint && nextPoint && fillMethod != 'none') {
2383         // Use linear interpolation between prevPoint and nextPoint.
2384         actualYval = prevPoint.yval + (nextPoint.yval - prevPoint.yval) *
2385             ((xval - prevPoint.xval) / (nextPoint.xval - prevPoint.xval));
2386       } else if (prevPoint && fillMethod == 'all') {
2387         actualYval = prevPoint.yval;
2388       } else if (nextPoint && fillMethod == 'all') {
2389         actualYval = nextPoint.yval;
2390       } else {
2391         actualYval = 0;
2392       }
2393     } else {
2394       prevPoint = point;
2395     }
2396 
2397     var stackedYval = cumulativeYval[xval];
2398     if (lastXval != xval) {
2399       // If an x-value is repeated, we ignore the duplicates.
2400       stackedYval += actualYval;
2401       cumulativeYval[xval] = stackedYval;
2402     }
2403     lastXval = xval;
2404 
2405     point.yval_stacked = stackedYval;
2406 
2407     if (stackedYval > seriesExtremes[1]) {
2408       seriesExtremes[1] = stackedYval;
2409     }
2410     if (stackedYval < seriesExtremes[0]) {
2411       seriesExtremes[0] = stackedYval;
2412     }
2413   }
2414 };
2415 
2416 
2417 /**
2418  * Loop over all fields and create datasets, calculating extreme y-values for
2419  * each series and extreme x-indices as we go.
2420  *
2421  * dateWindow is passed in as an explicit parameter so that we can compute
2422  * extreme values "speculatively", i.e. without actually setting state on the
2423  * dygraph.
2424  *
2425  * @param {Array.<Array.<Array.<(number|Array<number>)>>} rolledSeries, where
2426  *     rolledSeries[seriesIndex][row] = raw point, where
2427  *     seriesIndex is the column number starting with 1, and
2428  *     rawPoint is [x,y] or [x, [y, err]] or [x, [y, yminus, yplus]].
2429  * @param {?Array.<number>} dateWindow [xmin, xmax] pair, or null.
2430  * @return {{
2431  *     points: Array.<Array.<Dygraph.PointType>>,
2432  *     seriesExtremes: Array.<Array.<number>>,
2433  *     boundaryIds: Array.<number>}}
2434  * @private
2435  */
2436 Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) {
2437   var boundaryIds = [];
2438   var points = [];
2439   var cumulativeYval = [];  // For stacked series.
2440   var extremes = {};  // series name -> [low, high]
2441   var i, k;
2442   var errorBars = this.attr_("errorBars");
2443   var customBars = this.attr_("customBars");
2444   var bars = errorBars || customBars;
2445   var isValueNull = function(sample) {
2446     if (!bars) {
2447       return sample[1] === null;
2448     } else {
2449       return customBars ? sample[1][1] === null : 
2450         errorBars ? sample[1][0] === null : false;
2451     }
2452   };
2453 
2454   // Loop over the fields (series).  Go from the last to the first,
2455   // because if they're stacked that's how we accumulate the values.
2456   var num_series = rolledSeries.length - 1;
2457   var series;
2458   for (i = num_series; i >= 1; i--) {
2459     if (!this.visibility()[i - 1]) continue;
2460 
2461     // Prune down to the desired range, if necessary (for zooming)
2462     // Because there can be lines going to points outside of the visible area,
2463     // we actually prune to visible points, plus one on either side.
2464     if (dateWindow) {
2465       series = rolledSeries[i];
2466       var low = dateWindow[0];
2467       var high = dateWindow[1];
2468 
2469       // TODO(danvk): do binary search instead of linear search.
2470       // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
2471       var firstIdx = null, lastIdx = null;
2472       for (k = 0; k < series.length; k++) {
2473         if (series[k][0] >= low && firstIdx === null) {
2474           firstIdx = k;
2475         }
2476         if (series[k][0] <= high) {
2477           lastIdx = k;
2478         }
2479       }
2480 
2481       if (firstIdx === null) firstIdx = 0;
2482       var correctedFirstIdx = firstIdx;
2483       var isInvalidValue = true;
2484       while (isInvalidValue && correctedFirstIdx > 0) {
2485         correctedFirstIdx--;
2486         isInvalidValue = isValueNull(series[correctedFirstIdx]);
2487       }
2488 
2489       if (lastIdx === null) lastIdx = series.length - 1;
2490       var correctedLastIdx = lastIdx;
2491       isInvalidValue = true;
2492       while (isInvalidValue && correctedLastIdx < series.length - 1) {
2493         correctedLastIdx++;
2494         isInvalidValue = isValueNull(series[correctedLastIdx]);
2495       }
2496 
2497 
2498       if (correctedFirstIdx!==firstIdx) {
2499         firstIdx = correctedFirstIdx;
2500       }
2501       if (correctedLastIdx !== lastIdx) {
2502         lastIdx = correctedLastIdx;
2503       }
2504       
2505       boundaryIds[i-1] = [firstIdx, lastIdx];
2506       
2507       // .slice's end is exclusive, we want to include lastIdx.
2508       series = series.slice(firstIdx, lastIdx + 1);
2509     } else {
2510       series = rolledSeries[i];
2511       boundaryIds[i-1] = [0, series.length-1];
2512     }
2513 
2514     var seriesName = this.attr_("labels")[i];
2515     var seriesExtremes = this.extremeValues_(series);
2516 
2517     var seriesPoints = Dygraph.seriesToPoints_(
2518         series, bars, seriesName, boundaryIds[i-1][0]);
2519 
2520     if (this.attr_("stackedGraph")) {
2521       Dygraph.stackPoints_(seriesPoints, cumulativeYval, seriesExtremes,
2522                            this.attr_("stackedGraphNaNFill"));
2523     }
2524 
2525     extremes[seriesName] = seriesExtremes;
2526     points[i] = seriesPoints;
2527   }
2528 
2529   return { points: points, extremes: extremes, boundaryIds: boundaryIds };
2530 };
2531 
2532 /**
2533  * Update the graph with new data. This method is called when the viewing area
2534  * has changed. If the underlying data or options have changed, predraw_ will
2535  * be called before drawGraph_ is called.
2536  *
2537  * @private
2538  */
2539 Dygraph.prototype.drawGraph_ = function() {
2540   var start = new Date();
2541 
2542   // This is used to set the second parameter to drawCallback, below.
2543   var is_initial_draw = this.is_initial_draw_;
2544   this.is_initial_draw_ = false;
2545 
2546   this.layout_.removeAllDatasets();
2547   this.setColors_();
2548   this.attrs_.pointSize = 0.5 * this.attr_('highlightCircleSize');
2549 
2550   var packed = this.gatherDatasets_(this.rolledSeries_, this.dateWindow_);
2551   var points = packed.points;
2552   var extremes = packed.extremes;
2553   this.boundaryIds_ = packed.boundaryIds;
2554 
2555   this.setIndexByName_ = {};
2556   var labels = this.attr_("labels");
2557   if (labels.length > 0) {
2558     this.setIndexByName_[labels[0]] = 0;
2559   }
2560   var dataIdx = 0;
2561   for (var i = 1; i < points.length; i++) {
2562     this.setIndexByName_[labels[i]] = i;
2563     if (!this.visibility()[i - 1]) continue;
2564     this.layout_.addDataset(labels[i], points[i]);
2565     this.datasetIndex_[i] = dataIdx++;
2566   }
2567 
2568   this.computeYAxisRanges_(extremes);
2569   this.layout_.setYAxes(this.axes_);
2570 
2571   this.addXTicks_();
2572 
2573   // Save the X axis zoomed status as the updateOptions call will tend to set it erroneously
2574   var tmp_zoomed_x = this.zoomed_x_;
2575   // Tell PlotKit to use this new data and render itself
2576   this.zoomed_x_ = tmp_zoomed_x;
2577   this.layout_.evaluate();
2578   this.renderGraph_(is_initial_draw);
2579 
2580   if (this.attr_("timingName")) {
2581     var end = new Date();
2582     Dygraph.info(this.attr_("timingName") + " - drawGraph: " + (end - start) + "ms");
2583   }
2584 };
2585 
2586 /**
2587  * This does the work of drawing the chart. It assumes that the layout and axis
2588  * scales have already been set (e.g. by predraw_).
2589  *
2590  * @private
2591  */
2592 Dygraph.prototype.renderGraph_ = function(is_initial_draw) {
2593   this.cascadeEvents_('clearChart');
2594   this.plotter_.clear();
2595 
2596   if (this.attr_('underlayCallback')) {
2597     // NOTE: we pass the dygraph object to this callback twice to avoid breaking
2598     // users who expect a deprecated form of this callback.
2599     this.attr_('underlayCallback')(
2600         this.hidden_ctx_, this.layout_.getPlotArea(), this, this);
2601   }
2602 
2603   var e = {
2604     canvas: this.hidden_,
2605     drawingContext: this.hidden_ctx_
2606   };
2607   this.cascadeEvents_('willDrawChart', e);
2608   this.plotter_.render();
2609   this.cascadeEvents_('didDrawChart', e);
2610   this.lastRow_ = -1;  // because plugins/legend.js clears the legend
2611 
2612   // TODO(danvk): is this a performance bottleneck when panning?
2613   // The interaction canvas should already be empty in that situation.
2614   this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width,
2615                                           this.canvas_.height);
2616 
2617   if (this.attr_("drawCallback") !== null) {
2618     this.attr_("drawCallback")(this, is_initial_draw);
2619   }
2620   if (is_initial_draw) {
2621     this.readyFired_ = true;
2622     while (this.readyFns_.length > 0) {
2623       var fn = this.readyFns_.pop();
2624       fn(this);
2625     }
2626   }
2627 };
2628 
2629 /**
2630  * @private
2631  * Determine properties of the y-axes which are independent of the data
2632  * currently being displayed. This includes things like the number of axes and
2633  * the style of the axes. It does not include the range of each axis and its
2634  * tick marks.
2635  * This fills in this.axes_.
2636  * axes_ = [ { options } ]
2637  *   indices are into the axes_ array.
2638  */
2639 Dygraph.prototype.computeYAxes_ = function() {
2640   // Preserve valueWindow settings if they exist, and if the user hasn't
2641   // specified a new valueRange.
2642   var valueWindows, axis, index, opts, v;
2643   if (this.axes_ !== undefined && this.user_attrs_.hasOwnProperty("valueRange") === false) {
2644     valueWindows = [];
2645     for (index = 0; index < this.axes_.length; index++) {
2646       valueWindows.push(this.axes_[index].valueWindow);
2647     }
2648   }
2649 
2650   // this.axes_ doesn't match this.attributes_.axes_.options. It's used for
2651   // data computation as well as options storage.
2652   // Go through once and add all the axes.
2653   this.axes_ = [];
2654 
2655   for (axis = 0; axis < this.attributes_.numAxes(); axis++) {
2656     // Add a new axis, making a copy of its per-axis options.
2657     opts = { g : this };
2658     Dygraph.update(opts, this.attributes_.axisOptions(axis));
2659     this.axes_[axis] = opts;
2660   }
2661 
2662 
2663   // Copy global valueRange option over to the first axis.
2664   // NOTE(konigsberg): Are these two statements necessary?
2665   // I tried removing it. The automated tests pass, and manually
2666   // messing with tests/zoom.html showed no trouble.
2667   v = this.attr_('valueRange');
2668   if (v) this.axes_[0].valueRange = v;
2669 
2670   if (valueWindows !== undefined) {
2671     // Restore valueWindow settings.
2672 
2673     // When going from two axes back to one, we only restore
2674     // one axis.
2675     var idxCount = Math.min(valueWindows.length, this.axes_.length);
2676 
2677     for (index = 0; index < idxCount; index++) {
2678       this.axes_[index].valueWindow = valueWindows[index];
2679     }
2680   }
2681 
2682   for (axis = 0; axis < this.axes_.length; axis++) {
2683     if (axis === 0) {
2684       opts = this.optionsViewForAxis_('y' + (axis ? '2' : ''));
2685       v = opts("valueRange");
2686       if (v) this.axes_[axis].valueRange = v;
2687     } else {  // To keep old behavior
2688       var axes = this.user_attrs_.axes;
2689       if (axes && axes.y2) {
2690         v = axes.y2.valueRange;
2691         if (v) this.axes_[axis].valueRange = v;
2692       }
2693     }
2694   }
2695 };
2696 
2697 /**
2698  * Returns the number of y-axes on the chart.
2699  * @return {Number} the number of axes.
2700  */
2701 Dygraph.prototype.numAxes = function() {
2702   return this.attributes_.numAxes();
2703 };
2704 
2705 /**
2706  * @private
2707  * Returns axis properties for the given series.
2708  * @param { String } setName The name of the series for which to get axis
2709  * properties, e.g. 'Y1'.
2710  * @return { Object } The axis properties.
2711  */
2712 Dygraph.prototype.axisPropertiesForSeries = function(series) {
2713   // TODO(danvk): handle errors.
2714   return this.axes_[this.attributes_.axisForSeries(series)];
2715 };
2716 
2717 /**
2718  * @private
2719  * Determine the value range and tick marks for each axis.
2720  * @param {Object} extremes A mapping from seriesName -> [low, high]
2721  * This fills in the valueRange and ticks fields in each entry of this.axes_.
2722  */
2723 Dygraph.prototype.computeYAxisRanges_ = function(extremes) {
2724   var isNullUndefinedOrNaN = function(num) {
2725     return isNaN(parseFloat(num));
2726   };
2727   var numAxes = this.attributes_.numAxes();
2728   var ypadCompat, span, series, ypad;
2729   
2730   var p_axis;
2731 
2732   // Compute extreme values, a span and tick marks for each axis.
2733   for (var i = 0; i < numAxes; i++) {
2734     var axis = this.axes_[i];
2735     var logscale = this.attributes_.getForAxis("logscale", i);
2736     var includeZero = this.attributes_.getForAxis("includeZero", i);
2737     var independentTicks = this.attributes_.getForAxis("independentTicks", i);
2738     series = this.attributes_.seriesForAxis(i);
2739 
2740     // Add some padding. This supports two Y padding operation modes:
2741     //
2742     // - backwards compatible (yRangePad not set):
2743     //   10% padding for automatic Y ranges, but not for user-supplied
2744     //   ranges, and move a close-to-zero edge to zero except if
2745     //   avoidMinZero is set, since drawing at the edge results in
2746     //   invisible lines. Unfortunately lines drawn at the edge of a
2747     //   user-supplied range will still be invisible. If logscale is
2748     //   set, add a variable amount of padding at the top but
2749     //   none at the bottom.
2750     //
2751     // - new-style (yRangePad set by the user):
2752     //   always add the specified Y padding.
2753     //
2754     ypadCompat = true;
2755     ypad = 0.1; // add 10%
2756     if (this.attr_('yRangePad') !== null) {
2757       ypadCompat = false;
2758       // Convert pixel padding to ratio
2759       ypad = this.attr_('yRangePad') / this.plotter_.area.h;
2760     }
2761 
2762     if (series.length === 0) {
2763       // If no series are defined or visible then use a reasonable default
2764       axis.extremeRange = [0, 1];
2765     } else {
2766       // Calculate the extremes of extremes.
2767       var minY = Infinity;  // extremes[series[0]][0];
2768       var maxY = -Infinity;  // extremes[series[0]][1];
2769       var extremeMinY, extremeMaxY;
2770 
2771       for (var j = 0; j < series.length; j++) {
2772         // this skips invisible series
2773         if (!extremes.hasOwnProperty(series[j])) continue;
2774 
2775         // Only use valid extremes to stop null data series' from corrupting the scale.
2776         extremeMinY = extremes[series[j]][0];
2777         if (extremeMinY !== null) {
2778           minY = Math.min(extremeMinY, minY);
2779         }
2780         extremeMaxY = extremes[series[j]][1];
2781         if (extremeMaxY !== null) {
2782           maxY = Math.max(extremeMaxY, maxY);
2783         }
2784       }
2785 
2786       // Include zero if requested by the user.
2787       if (includeZero && !logscale) {
2788         if (minY > 0) minY = 0;
2789         if (maxY < 0) maxY = 0;
2790       }
2791 
2792       // Ensure we have a valid scale, otherwise default to [0, 1] for safety.
2793       if (minY == Infinity) minY = 0;
2794       if (maxY == -Infinity) maxY = 1;
2795 
2796       span = maxY - minY;
2797       // special case: if we have no sense of scale, center on the sole value.
2798       if (span === 0) {
2799         if (maxY !== 0) {
2800           span = Math.abs(maxY);
2801         } else {
2802           // ... and if the sole value is zero, use range 0-1.
2803           maxY = 1;
2804           span = 1;
2805         }
2806       }
2807 
2808       var maxAxisY, minAxisY;
2809       if (logscale) {
2810         if (ypadCompat) {
2811           maxAxisY = maxY + ypad * span;
2812           minAxisY = minY;
2813         } else {
2814           var logpad = Math.exp(Math.log(span) * ypad);
2815           maxAxisY = maxY * logpad;
2816           minAxisY = minY / logpad;
2817         }
2818       } else {
2819         maxAxisY = maxY + ypad * span;
2820         minAxisY = minY - ypad * span;
2821 
2822         // Backwards-compatible behavior: Move the span to start or end at zero if it's
2823         // close to zero, but not if avoidMinZero is set.
2824         if (ypadCompat && !this.attr_("avoidMinZero")) {
2825           if (minAxisY < 0 && minY >= 0) minAxisY = 0;
2826           if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0;
2827         }
2828       }
2829       axis.extremeRange = [minAxisY, maxAxisY];
2830     }
2831     if (axis.valueWindow) {
2832       // This is only set if the user has zoomed on the y-axis. It is never set
2833       // by a user. It takes precedence over axis.valueRange because, if you set
2834       // valueRange, you'd still expect to be able to pan.
2835       axis.computedValueRange = [axis.valueWindow[0], axis.valueWindow[1]];
2836     } else if (axis.valueRange) {
2837       // This is a user-set value range for this axis.
2838       var y0 = isNullUndefinedOrNaN(axis.valueRange[0]) ? axis.extremeRange[0] : axis.valueRange[0];
2839       var y1 = isNullUndefinedOrNaN(axis.valueRange[1]) ? axis.extremeRange[1] : axis.valueRange[1];
2840       if (!ypadCompat) {
2841         if (axis.logscale) {
2842           var logpad = Math.exp(Math.log(span) * ypad);
2843           y0 *= logpad;
2844           y1 /= logpad;
2845         } else {
2846           span = y1 - y0;
2847           y0 -= span * ypad;
2848           y1 += span * ypad;
2849         }
2850       }
2851       axis.computedValueRange = [y0, y1];
2852     } else {
2853       axis.computedValueRange = axis.extremeRange;
2854     }
2855     
2856     
2857     if (independentTicks) {
2858       axis.independentTicks = independentTicks;
2859       var opts = this.optionsViewForAxis_('y' + (i ? '2' : ''));
2860       var ticker = opts('ticker');
2861       axis.ticks = ticker(axis.computedValueRange[0],
2862               axis.computedValueRange[1],
2863               this.height_,  // TODO(danvk): should be area.height
2864               opts,
2865               this);
2866       // Define the first independent axis as primary axis.
2867       if (!p_axis) p_axis = axis;
2868     }
2869   }
2870   if (p_axis === undefined) {
2871     throw ("Configuration Error: At least one axis has to have the \"independentTicks\" option activated.");
2872   }
2873   // Add ticks. By default, all axes inherit the tick positions of the
2874   // primary axis. However, if an axis is specifically marked as having
2875   // independent ticks, then that is permissible as well.
2876   for (var i = 0; i < numAxes; i++) {
2877     var axis = this.axes_[i];
2878     
2879     if (!axis.independentTicks) {
2880       var opts = this.optionsViewForAxis_('y' + (i ? '2' : ''));
2881       var ticker = opts('ticker');
2882       var p_ticks = p_axis.ticks;
2883       var p_scale = p_axis.computedValueRange[1] - p_axis.computedValueRange[0];
2884       var scale = axis.computedValueRange[1] - axis.computedValueRange[0];
2885       var tick_values = [];
2886       for (var k = 0; k < p_ticks.length; k++) {
2887         var y_frac = (p_ticks[k].v - p_axis.computedValueRange[0]) / p_scale;
2888         var y_val = axis.computedValueRange[0] + y_frac * scale;
2889         tick_values.push(y_val);
2890       }
2891 
2892       axis.ticks = ticker(axis.computedValueRange[0],
2893                           axis.computedValueRange[1],
2894                           this.height_,  // TODO(danvk): should be area.height
2895                           opts,
2896                           this,
2897                           tick_values);
2898     }
2899   }
2900 };
2901 
2902 /**
2903  * Extracts one series from the raw data (a 2D array) into an array of (date,
2904  * value) tuples.
2905  *
2906  * This is where undesirable points (i.e. negative values on log scales and
2907  * missing values through which we wish to connect lines) are dropped.
2908  * TODO(danvk): the "missing values" bit above doesn't seem right.
2909  *
2910  * @private
2911  * @param {Array.<Array.<(number|Array<Number>)>>} rawData Input data. Rectangular
2912  *     grid of points, where rawData[row][0] is the X value for the row,
2913  *     and rawData[row][i] is the Y data for series #i.
2914  * @param {number} i Series index, starting from 1.
2915  * @param {boolean} logScale True if using logarithmic Y scale.
2916  * @return {Array.<Array.<(?number|Array<?number>)>} Series array, where
2917  *     series[row] = [x,y] or [x, [y, err]] or [x, [y, yplus, yminus]].
2918  */
2919 Dygraph.prototype.extractSeries_ = function(rawData, i, logScale) {
2920   // TODO(danvk): pre-allocate series here.
2921   var series = [];
2922   var errorBars = this.attr_("errorBars");
2923   var customBars =  this.attr_("customBars");
2924   for (var j = 0; j < rawData.length; j++) {
2925     var x = rawData[j][0];
2926     var point = rawData[j][i];
2927     if (logScale) {
2928       // On the log scale, points less than zero do not exist.
2929       // This will create a gap in the chart.
2930       if (errorBars || customBars) {
2931         // point.length is either 2 (errorBars) or 3 (customBars)
2932         for (var k = 0; k < point.length; k++) {
2933           if (point[k] <= 0) {
2934             point = null;
2935             break;
2936           }
2937         }
2938       } else if (point <= 0) {
2939         point = null;
2940       }
2941     }
2942     // Fix null points to fit the display type standard.
2943     if (point !== null) {
2944       series.push([x, point]);
2945     } else {
2946       series.push([x, errorBars ? [null, null] : customBars ? [null, null, null] : point]);
2947     }
2948   }
2949   return series;
2950 };
2951 
2952 /**
2953  * @private
2954  * Calculates the rolling average of a data set.
2955  * If originalData is [label, val], rolls the average of those.
2956  * If originalData is [label, [, it's interpreted as [value, stddev]
2957  *   and the roll is returned in the same form, with appropriately reduced
2958  *   stddev for each value.
2959  * Note that this is where fractional input (i.e. '5/10') is converted into
2960  *   decimal values.
2961  * @param {Array} originalData The data in the appropriate format (see above)
2962  * @param {Number} rollPeriod The number of points over which to average the
2963  *                            data
2964  */
2965 Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) {
2966   rollPeriod = Math.min(rollPeriod, originalData.length);
2967   var rollingData = [];
2968   var sigma = this.attr_("sigma");
2969 
2970   var low, high, i, j, y, sum, num_ok, stddev;
2971   if (this.fractions_) {
2972     var num = 0;
2973     var den = 0;  // numerator/denominator
2974     var mult = 100.0;
2975     for (i = 0; i < originalData.length; i++) {
2976       num += originalData[i][1][0];
2977       den += originalData[i][1][1];
2978       if (i - rollPeriod >= 0) {
2979         num -= originalData[i - rollPeriod][1][0];
2980         den -= originalData[i - rollPeriod][1][1];
2981       }
2982 
2983       var date = originalData[i][0];
2984       var value = den ? num / den : 0.0;
2985       if (this.attr_("errorBars")) {
2986         if (this.attr_("wilsonInterval")) {
2987           // For more details on this confidence interval, see:
2988           // http://en.wikipedia.org/wiki/Binomial_confidence_interval
2989           if (den) {
2990             var p = value < 0 ? 0 : value, n = den;
2991             var pm = sigma * Math.sqrt(p*(1-p)/n + sigma*sigma/(4*n*n));
2992             var denom = 1 + sigma * sigma / den;
2993             low  = (p + sigma * sigma / (2 * den) - pm) / denom;
2994             high = (p + sigma * sigma / (2 * den) + pm) / denom;
2995             rollingData[i] = [date,
2996                               [p * mult, (p - low) * mult, (high - p) * mult]];
2997           } else {
2998             rollingData[i] = [date, [0, 0, 0]];
2999           }
3000         } else {
3001           stddev = den ? sigma * Math.sqrt(value * (1 - value) / den) : 1.0;
3002           rollingData[i] = [date, [mult * value, mult * stddev, mult * stddev]];
3003         }
3004       } else {
3005         rollingData[i] = [date, mult * value];
3006       }
3007     }
3008   } else if (this.attr_("customBars")) {
3009     low = 0;
3010     var mid = 0;
3011     high = 0;
3012     var count = 0;
3013     for (i = 0; i < originalData.length; i++) {
3014       var data = originalData[i][1];
3015       y = data[1];
3016       rollingData[i] = [originalData[i][0], [y, y - data[0], data[2] - y]];
3017 
3018       if (y !== null && !isNaN(y)) {
3019         low += data[0];
3020         mid += y;
3021         high += data[2];
3022         count += 1;
3023       }
3024       if (i - rollPeriod >= 0) {
3025         var prev = originalData[i - rollPeriod];
3026         if (prev[1][1] !== null && !isNaN(prev[1][1])) {
3027           low -= prev[1][0];
3028           mid -= prev[1][1];
3029           high -= prev[1][2];
3030           count -= 1;
3031         }
3032       }
3033       if (count) {
3034         rollingData[i] = [originalData[i][0], [ 1.0 * mid / count,
3035                                                 1.0 * (mid - low) / count,
3036                                                 1.0 * (high - mid) / count ]];
3037       } else {
3038         rollingData[i] = [originalData[i][0], [null, null, null]];
3039       }
3040     }
3041   } else {
3042     // Calculate the rolling average for the first rollPeriod - 1 points where
3043     // there is not enough data to roll over the full number of points
3044     if (!this.attr_("errorBars")) {
3045       if (rollPeriod == 1) {
3046         return originalData;
3047       }
3048 
3049       for (i = 0; i < originalData.length; i++) {
3050         sum = 0;
3051         num_ok = 0;
3052         for (j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
3053           y = originalData[j][1];
3054           if (y === null || isNaN(y)) continue;
3055           num_ok++;
3056           sum += originalData[j][1];
3057         }
3058         if (num_ok) {
3059           rollingData[i] = [originalData[i][0], sum / num_ok];
3060         } else {
3061           rollingData[i] = [originalData[i][0], null];
3062         }
3063       }
3064 
3065     } else {
3066       for (i = 0; i < originalData.length; i++) {
3067         sum = 0;
3068         var variance = 0;
3069         num_ok = 0;
3070         for (j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
3071           y = originalData[j][1][0];
3072           if (y === null || isNaN(y)) continue;
3073           num_ok++;
3074           sum += originalData[j][1][0];
3075           variance += Math.pow(originalData[j][1][1], 2);
3076         }
3077         if (num_ok) {
3078           stddev = Math.sqrt(variance) / num_ok;
3079           rollingData[i] = [originalData[i][0],
3080                             [sum / num_ok, sigma * stddev, sigma * stddev]];
3081         } else {
3082           // This explicitly preserves NaNs to aid with "independent series".
3083           // See testRollingAveragePreservesNaNs.
3084           var v = (rollPeriod == 1) ? originalData[i][1][0] : null;
3085           rollingData[i] = [originalData[i][0], [v, v, v]];
3086         }
3087       }
3088     }
3089   }
3090 
3091   return rollingData;
3092 };
3093 
3094 /**
3095  * Detects the type of the str (date or numeric) and sets the various
3096  * formatting attributes in this.attrs_ based on this type.
3097  * @param {String} str An x value.
3098  * @private
3099  */
3100 Dygraph.prototype.detectTypeFromString_ = function(str) {
3101   var isDate = false;
3102   var dashPos = str.indexOf('-');  // could be 2006-01-01 _or_ 1.0e-2
3103   if ((dashPos > 0 && (str[dashPos-1] != 'e' && str[dashPos-1] != 'E')) ||
3104       str.indexOf('/') >= 0 ||
3105       isNaN(parseFloat(str))) {
3106     isDate = true;
3107   } else if (str.length == 8 && str > '19700101' && str < '20371231') {
3108     // TODO(danvk): remove support for this format.
3109     isDate = true;
3110   }
3111 
3112   this.setXAxisOptions_(isDate);
3113 };
3114 
3115 Dygraph.prototype.setXAxisOptions_ = function(isDate) {
3116   if (isDate) {
3117     this.attrs_.xValueParser = Dygraph.dateParser;
3118     this.attrs_.axes.x.valueFormatter = Dygraph.dateString_;
3119     this.attrs_.axes.x.ticker = Dygraph.dateTicker;
3120     this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisFormatter;
3121   } else {
3122     /** @private (shut up, jsdoc!) */
3123     this.attrs_.xValueParser = function(x) { return parseFloat(x); };
3124     // TODO(danvk): use Dygraph.numberValueFormatter here?
3125     /** @private (shut up, jsdoc!) */
3126     this.attrs_.axes.x.valueFormatter = function(x) { return x; };
3127     this.attrs_.axes.x.ticker = Dygraph.numericLinearTicks;
3128     this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter;
3129   }
3130 };
3131 
3132 /**
3133  * Parses the value as a floating point number. This is like the parseFloat()
3134  * built-in, but with a few differences:
3135  * - the empty string is parsed as null, rather than NaN.
3136  * - if the string cannot be parsed at all, an error is logged.
3137  * If the string can't be parsed, this method returns null.
3138  * @param {String} x The string to be parsed
3139  * @param {Number} opt_line_no The line number from which the string comes.
3140  * @param {String} opt_line The text of the line from which the string comes.
3141  * @private
3142  */
3143 
3144 // Parse the x as a float or return null if it's not a number.
3145 Dygraph.prototype.parseFloat_ = function(x, opt_line_no, opt_line) {
3146   var val = parseFloat(x);
3147   if (!isNaN(val)) return val;
3148 
3149   // Try to figure out what happeend.
3150   // If the value is the empty string, parse it as null.
3151   if (/^ *$/.test(x)) return null;
3152 
3153   // If it was actually "NaN", return it as NaN.
3154   if (/^ *nan *$/i.test(x)) return NaN;
3155 
3156   // Looks like a parsing error.
3157   var msg = "Unable to parse '" + x + "' as a number";
3158   if (opt_line !== null && opt_line_no !== null) {
3159     msg += " on line " + (1+opt_line_no) + " ('" + opt_line + "') of CSV.";
3160   }
3161   this.error(msg);
3162 
3163   return null;
3164 };
3165 
3166 /**
3167  * @private
3168  * Parses a string in a special csv format.  We expect a csv file where each
3169  * line is a date point, and the first field in each line is the date string.
3170  * We also expect that all remaining fields represent series.
3171  * if the errorBars attribute is set, then interpret the fields as:
3172  * date, series1, stddev1, series2, stddev2, ...
3173  * @param {[Object]} data See above.
3174  *
3175  * @return [Object] An array with one entry for each row. These entries
3176  * are an array of cells in that row. The first entry is the parsed x-value for
3177  * the row. The second, third, etc. are the y-values. These can take on one of
3178  * three forms, depending on the CSV and constructor parameters:
3179  * 1. numeric value
3180  * 2. [ value, stddev ]
3181  * 3. [ low value, center value, high value ]
3182  */
3183 Dygraph.prototype.parseCSV_ = function(data) {
3184   var ret = [];
3185   var line_delimiter = Dygraph.detectLineDelimiter(data);
3186   var lines = data.split(line_delimiter || "\n");
3187   var vals, j;
3188 
3189   // Use the default delimiter or fall back to a tab if that makes sense.
3190   var delim = this.attr_('delimiter');
3191   if (lines[0].indexOf(delim) == -1 && lines[0].indexOf('\t') >= 0) {
3192     delim = '\t';
3193   }
3194 
3195   var start = 0;
3196   if (!('labels' in this.user_attrs_)) {
3197     // User hasn't explicitly set labels, so they're (presumably) in the CSV.
3198     start = 1;
3199     this.attrs_.labels = lines[0].split(delim);  // NOTE: _not_ user_attrs_.
3200     this.attributes_.reparseSeries();
3201   }
3202   var line_no = 0;
3203 
3204   var xParser;
3205   var defaultParserSet = false;  // attempt to auto-detect x value type
3206   var expectedCols = this.attr_("labels").length;
3207   var outOfOrder = false;
3208   for (var i = start; i < lines.length; i++) {
3209     var line = lines[i];
3210     line_no = i;
3211     if (line.length === 0) continue;  // skip blank lines
3212     if (line[0] == '#') continue;    // skip comment lines
3213     var inFields = line.split(delim);
3214     if (inFields.length < 2) continue;
3215 
3216     var fields = [];
3217     if (!defaultParserSet) {
3218       this.detectTypeFromString_(inFields[0]);
3219       xParser = this.attr_("xValueParser");
3220       defaultParserSet = true;
3221     }
3222     fields[0] = xParser(inFields[0], this);
3223 
3224     // If fractions are expected, parse the numbers as "A/B"
3225     if (this.fractions_) {
3226       for (j = 1; j < inFields.length; j++) {
3227         // TODO(danvk): figure out an appropriate way to flag parse errors.
3228         vals = inFields[j].split("/");
3229         if (vals.length != 2) {
3230           this.error('Expected fractional "num/den" values in CSV data ' +
3231                      "but found a value '" + inFields[j] + "' on line " +
3232                      (1 + i) + " ('" + line + "') which is not of this form.");
3233           fields[j] = [0, 0];
3234         } else {
3235           fields[j] = [this.parseFloat_(vals[0], i, line),
3236                        this.parseFloat_(vals[1], i, line)];
3237         }
3238       }
3239     } else if (this.attr_("errorBars")) {
3240       // If there are error bars, values are (value, stddev) pairs
3241       if (inFields.length % 2 != 1) {
3242         this.error('Expected alternating (value, stdev.) pairs in CSV data ' +
3243                    'but line ' + (1 + i) + ' has an odd number of values (' +
3244                    (inFields.length - 1) + "): '" + line + "'");
3245       }
3246       for (j = 1; j < inFields.length; j += 2) {
3247         fields[(j + 1) / 2] = [this.parseFloat_(inFields[j], i, line),
3248                                this.parseFloat_(inFields[j + 1], i, line)];
3249       }
3250     } else if (this.attr_("customBars")) {
3251       // Bars are a low;center;high tuple
3252       for (j = 1; j < inFields.length; j++) {
3253         var val = inFields[j];
3254         if (/^ *$/.test(val)) {
3255           fields[j] = [null, null, null];
3256         } else {
3257           vals = val.split(";");
3258           if (vals.length == 3) {
3259             fields[j] = [ this.parseFloat_(vals[0], i, line),
3260                           this.parseFloat_(vals[1], i, line),
3261                           this.parseFloat_(vals[2], i, line) ];
3262           } else {
3263             this.warn('When using customBars, values must be either blank ' +
3264                       'or "low;center;high" tuples (got "' + val +
3265                       '" on line ' + (1+i));
3266           }
3267         }
3268       }
3269     } else {
3270       // Values are just numbers
3271       for (j = 1; j < inFields.length; j++) {
3272         fields[j] = this.parseFloat_(inFields[j], i, line);
3273       }
3274     }
3275     if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
3276       outOfOrder = true;
3277     }
3278 
3279     if (fields.length != expectedCols) {
3280       this.error("Number of columns in line " + i + " (" + fields.length +
3281                  ") does not agree with number of labels (" + expectedCols +
3282                  ") " + line);
3283     }
3284 
3285     // If the user specified the 'labels' option and none of the cells of the
3286     // first row parsed correctly, then they probably double-specified the
3287     // labels. We go with the values set in the option, discard this row and
3288     // log a warning to the JS console.
3289     if (i === 0 && this.attr_('labels')) {
3290       var all_null = true;
3291       for (j = 0; all_null && j < fields.length; j++) {
3292         if (fields[j]) all_null = false;
3293       }
3294       if (all_null) {
3295         this.warn("The dygraphs 'labels' option is set, but the first row of " +
3296                   "CSV data ('" + line + "') appears to also contain labels. " +
3297                   "Will drop the CSV labels and use the option labels.");
3298         continue;
3299       }
3300     }
3301     ret.push(fields);
3302   }
3303 
3304   if (outOfOrder) {
3305     this.warn("CSV is out of order; order it correctly to speed loading.");
3306     ret.sort(function(a,b) { return a[0] - b[0]; });
3307   }
3308 
3309   return ret;
3310 };
3311 
3312 /**
3313  * @private
3314  * The user has provided their data as a pre-packaged JS array. If the x values
3315  * are numeric, this is the same as dygraphs' internal format. If the x values
3316  * are dates, we need to convert them from Date objects to ms since epoch.
3317  * @param {[Object]} data
3318  * @return {[Object]} data with numeric x values.
3319  */
3320 Dygraph.prototype.parseArray_ = function(data) {
3321   // Peek at the first x value to see if it's numeric.
3322   if (data.length === 0) {
3323     this.error("Can't plot empty data set");
3324     return null;
3325   }
3326   if (data[0].length === 0) {
3327     this.error("Data set cannot contain an empty row");
3328     return null;
3329   }
3330 
3331   var i;
3332   if (this.attr_("labels") === null) {
3333     this.warn("Using default labels. Set labels explicitly via 'labels' " +
3334               "in the options parameter");
3335     this.attrs_.labels = [ "X" ];
3336     for (i = 1; i < data[0].length; i++) {
3337       this.attrs_.labels.push("Y" + i); // Not user_attrs_.
3338     }
3339     this.attributes_.reparseSeries();
3340   } else {
3341     var num_labels = this.attr_("labels");
3342     if (num_labels.length != data[0].length) {
3343       this.error("Mismatch between number of labels (" + num_labels +
3344           ") and number of columns in array (" + data[0].length + ")");
3345       return null;
3346     }
3347   }
3348 
3349   if (Dygraph.isDateLike(data[0][0])) {
3350     // Some intelligent defaults for a date x-axis.
3351     this.attrs_.axes.x.valueFormatter = Dygraph.dateString_;
3352     this.attrs_.axes.x.ticker = Dygraph.dateTicker;
3353     this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisFormatter;
3354 
3355     // Assume they're all dates.
3356     var parsedData = Dygraph.clone(data);
3357     for (i = 0; i < data.length; i++) {
3358       if (parsedData[i].length === 0) {
3359         this.error("Row " + (1 + i) + " of data is empty");
3360         return null;
3361       }
3362       if (parsedData[i][0] === null ||
3363           typeof(parsedData[i][0].getTime) != 'function' ||
3364           isNaN(parsedData[i][0].getTime())) {
3365         this.error("x value in row " + (1 + i) + " is not a Date");
3366         return null;
3367       }
3368       parsedData[i][0] = parsedData[i][0].getTime();
3369     }
3370     return parsedData;
3371   } else {
3372     // Some intelligent defaults for a numeric x-axis.
3373     /** @private (shut up, jsdoc!) */
3374     this.attrs_.axes.x.valueFormatter = function(x) { return x; };
3375     this.attrs_.axes.x.ticker = Dygraph.numericLinearTicks;
3376     this.attrs_.axes.x.axisLabelFormatter = Dygraph.numberAxisLabelFormatter;
3377     return data;
3378   }
3379 };
3380 
3381 /**
3382  * Parses a DataTable object from gviz.
3383  * The data is expected to have a first column that is either a date or a
3384  * number. All subsequent columns must be numbers. If there is a clear mismatch
3385  * between this.xValueParser_ and the type of the first column, it will be
3386  * fixed. Fills out rawData_.
3387  * @param {[Object]} data See above.
3388  * @private
3389  */
3390 Dygraph.prototype.parseDataTable_ = function(data) {
3391   var shortTextForAnnotationNum = function(num) {
3392     // converts [0-9]+ [A-Z][a-z]*
3393     // example: 0=A, 1=B, 25=Z, 26=Aa, 27=Ab
3394     // and continues like.. Ba Bb .. Za .. Zz..Aaa...Zzz Aaaa Zzzz
3395     var shortText = String.fromCharCode(65 /* A */ + num % 26);
3396     num = Math.floor(num / 26);
3397     while ( num > 0 ) {
3398       shortText = String.fromCharCode(65 /* A */ + (num - 1) % 26 ) + shortText.toLowerCase();
3399       num = Math.floor((num - 1) / 26);
3400     }
3401     return shortText;
3402   };
3403 
3404   var cols = data.getNumberOfColumns();
3405   var rows = data.getNumberOfRows();
3406 
3407   var indepType = data.getColumnType(0);
3408   if (indepType == 'date' || indepType == 'datetime') {
3409     this.attrs_.xValueParser = Dygraph.dateParser;
3410     this.attrs_.axes.x.valueFormatter = Dygraph.dateString_;
3411     this.attrs_.axes.x.ticker = Dygraph.dateTicker;
3412     this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisFormatter;
3413   } else if (indepType == 'number') {
3414     this.attrs_.xValueParser = function(x) { return parseFloat(x); };
3415     this.attrs_.axes.x.valueFormatter = function(x) { return x; };
3416     this.attrs_.axes.x.ticker = Dygraph.numericLinearTicks;
3417     this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter;
3418   } else {
3419     this.error("only 'date', 'datetime' and 'number' types are supported for " +
3420                "column 1 of DataTable input (Got '" + indepType + "')");
3421     return null;
3422   }
3423 
3424   // Array of the column indices which contain data (and not annotations).
3425   var colIdx = [];
3426   var annotationCols = {};  // data index -> [annotation cols]
3427   var hasAnnotations = false;
3428   var i, j;
3429   for (i = 1; i < cols; i++) {
3430     var type = data.getColumnType(i);
3431     if (type == 'number') {
3432       colIdx.push(i);
3433     } else if (type == 'string' && this.attr_('displayAnnotations')) {
3434       // This is OK -- it's an annotation column.
3435       var dataIdx = colIdx[colIdx.length - 1];
3436       if (!annotationCols.hasOwnProperty(dataIdx)) {
3437         annotationCols[dataIdx] = [i];
3438       } else {
3439         annotationCols[dataIdx].push(i);
3440       }
3441       hasAnnotations = true;
3442     } else {
3443       this.error("Only 'number' is supported as a dependent type with Gviz." +
3444                  " 'string' is only supported if displayAnnotations is true");
3445     }
3446   }
3447 
3448   // Read column labels
3449   // TODO(danvk): add support back for errorBars
3450   var labels = [data.getColumnLabel(0)];
3451   for (i = 0; i < colIdx.length; i++) {
3452     labels.push(data.getColumnLabel(colIdx[i]));
3453     if (this.attr_("errorBars")) i += 1;
3454   }
3455   this.attrs_.labels = labels;
3456   cols = labels.length;
3457 
3458   var ret = [];
3459   var outOfOrder = false;
3460   var annotations = [];
3461   for (i = 0; i < rows; i++) {
3462     var row = [];
3463     if (typeof(data.getValue(i, 0)) === 'undefined' ||
3464         data.getValue(i, 0) === null) {
3465       this.warn("Ignoring row " + i +
3466                 " of DataTable because of undefined or null first column.");
3467       continue;
3468     }
3469 
3470     if (indepType == 'date' || indepType == 'datetime') {
3471       row.push(data.getValue(i, 0).getTime());
3472     } else {
3473       row.push(data.getValue(i, 0));
3474     }
3475     if (!this.attr_("errorBars")) {
3476       for (j = 0; j < colIdx.length; j++) {
3477         var col = colIdx[j];
3478         row.push(data.getValue(i, col));
3479         if (hasAnnotations &&
3480             annotationCols.hasOwnProperty(col) &&
3481             data.getValue(i, annotationCols[col][0]) !== null) {
3482           var ann = {};
3483           ann.series = data.getColumnLabel(col);
3484           ann.xval = row[0];
3485           ann.shortText = shortTextForAnnotationNum(annotations.length);
3486           ann.text = '';
3487           for (var k = 0; k < annotationCols[col].length; k++) {
3488             if (k) ann.text += "\n";
3489             ann.text += data.getValue(i, annotationCols[col][k]);
3490           }
3491           annotations.push(ann);
3492         }
3493       }
3494 
3495       // Strip out infinities, which give dygraphs problems later on.
3496       for (j = 0; j < row.length; j++) {
3497         if (!isFinite(row[j])) row[j] = null;
3498       }
3499     } else {
3500       for (j = 0; j < cols - 1; j++) {
3501         row.push([ data.getValue(i, 1 + 2 * j), data.getValue(i, 2 + 2 * j) ]);
3502       }
3503     }
3504     if (ret.length > 0 && row[0] < ret[ret.length - 1][0]) {
3505       outOfOrder = true;
3506     }
3507     ret.push(row);
3508   }
3509 
3510   if (outOfOrder) {
3511     this.warn("DataTable is out of order; order it correctly to speed loading.");
3512     ret.sort(function(a,b) { return a[0] - b[0]; });
3513   }
3514   this.rawData_ = ret;
3515 
3516   if (annotations.length > 0) {
3517     this.setAnnotations(annotations, true);
3518   }
3519   this.attributes_.reparseSeries();
3520 };
3521 
3522 /**
3523  * Get the CSV data. If it's in a function, call that function. If it's in a
3524  * file, do an XMLHttpRequest to get it.
3525  * @private
3526  */
3527 Dygraph.prototype.start_ = function() {
3528   var data = this.file_;
3529 
3530   // Functions can return references of all other types.
3531   if (typeof data == 'function') {
3532     data = data();
3533   }
3534 
3535   if (Dygraph.isArrayLike(data)) {
3536     this.rawData_ = this.parseArray_(data);
3537     this.predraw_();
3538   } else if (typeof data == 'object' &&
3539              typeof data.getColumnRange == 'function') {
3540     // must be a DataTable from gviz.
3541     this.parseDataTable_(data);
3542     this.predraw_();
3543   } else if (typeof data == 'string') {
3544     // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
3545     var line_delimiter = Dygraph.detectLineDelimiter(data);
3546     if (line_delimiter) {
3547       this.loadedEvent_(data);
3548     } else {
3549       // REMOVE_FOR_IE
3550       var req;
3551       if (window.XMLHttpRequest) {
3552         // Firefox, Opera, IE7, and other browsers will use the native object
3553         req = new XMLHttpRequest();
3554       } else {
3555         // IE 5 and 6 will use the ActiveX control
3556         req = new ActiveXObject("Microsoft.XMLHTTP");
3557       }
3558 
3559       var caller = this;
3560       req.onreadystatechange = function () {
3561         if (req.readyState == 4) {
3562           if (req.status === 200 ||  // Normal http
3563               req.status === 0) {    // Chrome w/ --allow-file-access-from-files
3564             caller.loadedEvent_(req.responseText);
3565           }
3566         }
3567       };
3568 
3569       req.open("GET", data, true);
3570       req.send(null);
3571     }
3572   } else {
3573     this.error("Unknown data format: " + (typeof data));
3574   }
3575 };
3576 
3577 /**
3578  * Changes various properties of the graph. These can include:
3579  * <ul>
3580  * <li>file: changes the source data for the graph</li>
3581  * <li>errorBars: changes whether the data contains stddev</li>
3582  * </ul>
3583  *
3584  * There's a huge variety of options that can be passed to this method. For a
3585  * full list, see http://dygraphs.com/options.html.
3586  *
3587  * @param {Object} attrs The new properties and values
3588  * @param {Boolean} [block_redraw] Usually the chart is redrawn after every
3589  * call to updateOptions(). If you know better, you can pass true to explicitly
3590  * block the redraw. This can be useful for chaining updateOptions() calls,
3591  * avoiding the occasional infinite loop and preventing redraws when it's not
3592  * necessary (e.g. when updating a callback).
3593  */
3594 Dygraph.prototype.updateOptions = function(input_attrs, block_redraw) {
3595   if (typeof(block_redraw) == 'undefined') block_redraw = false;
3596 
3597   // mapLegacyOptions_ drops the "file" parameter as a convenience to us.
3598   var file = input_attrs.file;
3599   var attrs = Dygraph.mapLegacyOptions_(input_attrs);
3600 
3601   // TODO(danvk): this is a mess. Move these options into attr_.
3602   if ('rollPeriod' in attrs) {
3603     this.rollPeriod_ = attrs.rollPeriod;
3604   }
3605   if ('dateWindow' in attrs) {
3606     this.dateWindow_ = attrs.dateWindow;
3607     if (!('isZoomedIgnoreProgrammaticZoom' in attrs)) {
3608       this.zoomed_x_ = (attrs.dateWindow !== null);
3609     }
3610   }
3611   if ('valueRange' in attrs && !('isZoomedIgnoreProgrammaticZoom' in attrs)) {
3612     this.zoomed_y_ = (attrs.valueRange !== null);
3613   }
3614 
3615   // TODO(danvk): validate per-series options.
3616   // Supported:
3617   // strokeWidth
3618   // pointSize
3619   // drawPoints
3620   // highlightCircleSize
3621 
3622   // Check if this set options will require new points.
3623   var requiresNewPoints = Dygraph.isPixelChangingOptionList(this.attr_("labels"), attrs);
3624 
3625   Dygraph.updateDeep(this.user_attrs_, attrs);
3626 
3627   this.attributes_.reparseSeries();
3628 
3629   if (file) {
3630     this.file_ = file;
3631     if (!block_redraw) this.start_();
3632   } else {
3633     if (!block_redraw) {
3634       if (requiresNewPoints) {
3635         this.predraw_();
3636       } else {
3637         this.renderGraph_(false);
3638       }
3639     }
3640   }
3641 };
3642 
3643 /**
3644  * Returns a copy of the options with deprecated names converted into current
3645  * names. Also drops the (potentially-large) 'file' attribute. If the caller is
3646  * interested in that, they should save a copy before calling this.
3647  * @private
3648  */
3649 Dygraph.mapLegacyOptions_ = function(attrs) {
3650   var my_attrs = {};
3651   for (var k in attrs) {
3652     if (k == 'file') continue;
3653     if (attrs.hasOwnProperty(k)) my_attrs[k] = attrs[k];
3654   }
3655 
3656   var set = function(axis, opt, value) {
3657     if (!my_attrs.axes) my_attrs.axes = {};
3658     if (!my_attrs.axes[axis]) my_attrs.axes[axis] = {};
3659     my_attrs.axes[axis][opt] = value;
3660   };
3661   var map = function(opt, axis, new_opt) {
3662     if (typeof(attrs[opt]) != 'undefined') {
3663       Dygraph.warn("Option " + opt + " is deprecated. Use the " +
3664           new_opt + " option for the " + axis + " axis instead. " +
3665           "(e.g. { axes : { " + axis + " : { " + new_opt + " : ... } } } " +
3666           "(see http://dygraphs.com/per-axis.html for more information.");
3667       set(axis, new_opt, attrs[opt]);
3668       delete my_attrs[opt];
3669     }
3670   };
3671 
3672   // This maps, e.g., xValueFormater -> axes: { x: { valueFormatter: ... } }
3673   map('xValueFormatter', 'x', 'valueFormatter');
3674   map('pixelsPerXLabel', 'x', 'pixelsPerLabel');
3675   map('xAxisLabelFormatter', 'x', 'axisLabelFormatter');
3676   map('xTicker', 'x', 'ticker');
3677   map('yValueFormatter', 'y', 'valueFormatter');
3678   map('pixelsPerYLabel', 'y', 'pixelsPerLabel');
3679   map('yAxisLabelFormatter', 'y', 'axisLabelFormatter');
3680   map('yTicker', 'y', 'ticker');
3681   return my_attrs;
3682 };
3683 
3684 /**
3685  * Resizes the dygraph. If no parameters are specified, resizes to fill the
3686  * containing div (which has presumably changed size since the dygraph was
3687  * instantiated. If the width/height are specified, the div will be resized.
3688  *
3689  * This is far more efficient than destroying and re-instantiating a
3690  * Dygraph, since it doesn't have to reparse the underlying data.
3691  *
3692  * @param {Number} [width] Width (in pixels)
3693  * @param {Number} [height] Height (in pixels)
3694  */
3695 Dygraph.prototype.resize = function(width, height) {
3696   if (this.resize_lock) {
3697     return;
3698   }
3699   this.resize_lock = true;
3700 
3701   if ((width === null) != (height === null)) {
3702     this.warn("Dygraph.resize() should be called with zero parameters or " +
3703               "two non-NULL parameters. Pretending it was zero.");
3704     width = height = null;
3705   }
3706 
3707   var old_width = this.width_;
3708   var old_height = this.height_;
3709 
3710   if (width) {
3711     this.maindiv_.style.width = width + "px";
3712     this.maindiv_.style.height = height + "px";
3713     this.width_ = width;
3714     this.height_ = height;
3715   } else {
3716     this.width_ = this.maindiv_.clientWidth;
3717     this.height_ = this.maindiv_.clientHeight;
3718   }
3719 
3720   if (old_width != this.width_ || old_height != this.height_) {
3721     // Resizing a canvas erases it, even when the size doesn't change, so
3722     // any resize needs to be followed by a redraw.
3723     this.resizeElements_();
3724     this.predraw_();
3725   }
3726 
3727   this.resize_lock = false;
3728 };
3729 
3730 /**
3731  * Adjusts the number of points in the rolling average. Updates the graph to
3732  * reflect the new averaging period.
3733  * @param {Number} length Number of points over which to average the data.
3734  */
3735 Dygraph.prototype.adjustRoll = function(length) {
3736   this.rollPeriod_ = length;
3737   this.predraw_();
3738 };
3739 
3740 /**
3741  * Returns a boolean array of visibility statuses.
3742  */
3743 Dygraph.prototype.visibility = function() {
3744   // Do lazy-initialization, so that this happens after we know the number of
3745   // data series.
3746   if (!this.attr_("visibility")) {
3747     this.attrs_.visibility = [];
3748   }
3749   // TODO(danvk): it looks like this could go into an infinite loop w/ user_attrs.
3750   while (this.attr_("visibility").length < this.numColumns() - 1) {
3751     this.attrs_.visibility.push(true);
3752   }
3753   return this.attr_("visibility");
3754 };
3755 
3756 /**
3757  * Changes the visiblity of a series.
3758  */
3759 Dygraph.prototype.setVisibility = function(num, value) {
3760   var x = this.visibility();
3761   if (num < 0 || num >= x.length) {
3762     this.warn("invalid series number in setVisibility: " + num);
3763   } else {
3764     x[num] = value;
3765     this.predraw_();
3766   }
3767 };
3768 
3769 /**
3770  * How large of an area will the dygraph render itself in?
3771  * This is used for testing.
3772  * @return A {width: w, height: h} object.
3773  * @private
3774  */
3775 Dygraph.prototype.size = function() {
3776   return { width: this.width_, height: this.height_ };
3777 };
3778 
3779 /**
3780  * Update the list of annotations and redraw the chart.
3781  * See dygraphs.com/annotations.html for more info on how to use annotations.
3782  * @param ann {Array} An array of annotation objects.
3783  * @param suppressDraw {Boolean} Set to "true" to block chart redraw (optional).
3784  */
3785 Dygraph.prototype.setAnnotations = function(ann, suppressDraw) {
3786   // Only add the annotation CSS rule once we know it will be used.
3787   Dygraph.addAnnotationRule();
3788   this.annotations_ = ann;
3789   if (!this.layout_) {
3790     this.warn("Tried to setAnnotations before dygraph was ready. " +
3791               "Try setting them in a ready() block. See " +
3792               "dygraphs.com/tests/annotation.html");
3793     return;
3794   }
3795 
3796   this.layout_.setAnnotations(this.annotations_);
3797   if (!suppressDraw) {
3798     this.predraw_();
3799   }
3800 };
3801 
3802 /**
3803  * Return the list of annotations.
3804  */
3805 Dygraph.prototype.annotations = function() {
3806   return this.annotations_;
3807 };
3808 
3809 /**
3810  * Get the list of label names for this graph. The first column is the
3811  * x-axis, so the data series names start at index 1.
3812  *
3813  * Returns null when labels have not yet been defined.
3814  */
3815 Dygraph.prototype.getLabels = function() {
3816   var labels = this.attr_("labels");
3817   return labels ? labels.slice() : null;
3818 };
3819 
3820 /**
3821  * Get the index of a series (column) given its name. The first column is the
3822  * x-axis, so the data series start with index 1.
3823  */
3824 Dygraph.prototype.indexFromSetName = function(name) {
3825   return this.setIndexByName_[name];
3826 };
3827 
3828 /**
3829  * Trigger a callback when the dygraph has drawn itself and is ready to be
3830  * manipulated. This is primarily useful when dygraphs has to do an XHR for the
3831  * data (i.e. a URL is passed as the data source) and the chart is drawn
3832  * asynchronously. If the chart has already drawn, the callback will fire
3833  * immediately.
3834  *
3835  * This is a good place to call setAnnotation().
3836  *
3837  * @param {function(!Dygraph)} callback The callback to trigger when the chart
3838  *     is ready.
3839  */
3840 Dygraph.prototype.ready = function(callback) {
3841   if (this.is_initial_draw_) {
3842     this.readyFns_.push(callback);
3843   } else {
3844     callback(this);
3845   }
3846 };
3847 
3848 /**
3849  * @private
3850  * Adds a default style for the annotation CSS classes to the document. This is
3851  * only executed when annotations are actually used. It is designed to only be
3852  * called once -- all calls after the first will return immediately.
3853  */
3854 Dygraph.addAnnotationRule = function() {
3855   // TODO(danvk): move this function into plugins/annotations.js?
3856   if (Dygraph.addedAnnotationCSS) return;
3857 
3858   var rule = "border: 1px solid black; " +
3859              "background-color: white; " +
3860              "text-align: center;";
3861 
3862   var styleSheetElement = document.createElement("style");
3863   styleSheetElement.type = "text/css";
3864   document.getElementsByTagName("head")[0].appendChild(styleSheetElement);
3865 
3866   // Find the first style sheet that we can access.
3867   // We may not add a rule to a style sheet from another domain for security
3868   // reasons. This sometimes comes up when using gviz, since the Google gviz JS
3869   // adds its own style sheets from google.com.
3870   for (var i = 0; i < document.styleSheets.length; i++) {
3871     if (document.styleSheets[i].disabled) continue;
3872     var mysheet = document.styleSheets[i];
3873     try {
3874       if (mysheet.insertRule) {  // Firefox
3875         var idx = mysheet.cssRules ? mysheet.cssRules.length : 0;
3876         mysheet.insertRule(".dygraphDefaultAnnotation { " + rule + " }", idx);
3877       } else if (mysheet.addRule) {  // IE
3878         mysheet.addRule(".dygraphDefaultAnnotation", rule);
3879       }
3880       Dygraph.addedAnnotationCSS = true;
3881       return;
3882     } catch(err) {
3883       // Was likely a security exception.
3884     }
3885   }
3886 
3887   this.warn("Unable to add default annotation CSS rule; display may be off.");
3888 };
3889 
3890 // Older pages may still use this name.
3891 var DateGraph = Dygraph;
3892