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