794fecf5c77e00a6b6e0d75b3f3861ebe65a313b
[dygraphs.git] / dygraph.js
1 // Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
2 // All Rights Reserved.
3
4 /**
5 * @fileoverview Creates an interactive, zoomable graph based on a CSV file or
6 * string. Dygraph can handle multiple series with or without error bars. The
7 * date/value ranges will be automatically set. Dygraph uses the
8 * <canvas> tag, so it only works in FF1.5+.
9 * @author danvdk@gmail.com (Dan Vanderkam)
10
11 Usage:
12 <div id="graphdiv" style="width:800px; height:500px;"></div>
13 <script type="text/javascript">
14 new Dygraph(document.getElementById("graphdiv"),
15 "datafile.csv", // CSV file with headers
16 { }); // options
17 </script>
18
19 The CSV file is of the form
20
21 Date,SeriesA,SeriesB,SeriesC
22 YYYYMMDD,A1,B1,C1
23 YYYYMMDD,A2,B2,C2
24
25 If the 'errorBars' option is set in the constructor, the input should be of
26 the form
27 Date,SeriesA,SeriesB,...
28 YYYYMMDD,A1,sigmaA1,B1,sigmaB1,...
29 YYYYMMDD,A2,sigmaA2,B2,sigmaB2,...
30
31 If the 'fractions' option is set, the input should be of the form:
32
33 Date,SeriesA,SeriesB,...
34 YYYYMMDD,A1/B1,A2/B2,...
35 YYYYMMDD,A1/B1,A2/B2,...
36
37 And error bars will be calculated automatically using a binomial distribution.
38
39 For further documentation and examples, see http://dygraphs.com/
40
41 */
42
43 /**
44 * Creates an interactive, zoomable chart.
45 *
46 * @constructor
47 * @param {div | String} div A div or the id of a div into which to construct
48 * the chart.
49 * @param {String | Function} file A file containing CSV data or a function
50 * that returns this data. The most basic expected format for each line is
51 * "YYYY/MM/DD,val1,val2,...". For more information, see
52 * http://dygraphs.com/data.html.
53 * @param {Object} attrs Various other attributes, e.g. errorBars determines
54 * whether the input data contains error ranges. For a complete list of
55 * options, see http://dygraphs.com/options.html.
56 */
57 Dygraph = function(div, data, opts) {
58 if (arguments.length > 0) {
59 if (arguments.length == 4) {
60 // Old versions of dygraphs took in the series labels as a constructor
61 // parameter. This doesn't make sense anymore, but it's easy to continue
62 // to support this usage.
63 this.warn("Using deprecated four-argument dygraph constructor");
64 this.__old_init__(div, data, arguments[2], arguments[3]);
65 } else {
66 this.__init__(div, data, opts);
67 }
68 }
69 };
70
71 Dygraph.NAME = "Dygraph";
72 Dygraph.VERSION = "1.2";
73 Dygraph.__repr__ = function() {
74 return "[" + this.NAME + " " + this.VERSION + "]";
75 };
76
77 /**
78 * Returns information about the Dygraph class.
79 */
80 Dygraph.toString = function() {
81 return this.__repr__();
82 };
83
84 // Various default values
85 Dygraph.DEFAULT_ROLL_PERIOD = 1;
86 Dygraph.DEFAULT_WIDTH = 480;
87 Dygraph.DEFAULT_HEIGHT = 320;
88 Dygraph.AXIS_LINE_WIDTH = 0.3;
89
90 Dygraph.LOG_SCALE = 10;
91 Dygraph.LN_TEN = Math.log(Dygraph.LOG_SCALE);
92 /** @private */
93 Dygraph.log10 = function(x) {
94 return Math.log(x) / Dygraph.LN_TEN;
95 }
96
97 // Default attribute values.
98 Dygraph.DEFAULT_ATTRS = {
99 highlightCircleSize: 3,
100 pixelsPerXLabel: 60,
101 pixelsPerYLabel: 30,
102
103 labelsDivWidth: 250,
104 labelsDivStyles: {
105 // TODO(danvk): move defaults from createStatusMessage_ here.
106 },
107 labelsSeparateLines: false,
108 labelsShowZeroValues: true,
109 labelsKMB: false,
110 labelsKMG2: false,
111 showLabelsOnHighlight: true,
112
113 yValueFormatter: function(a,b) { return Dygraph.numberFormatter(a,b); },
114 digitsAfterDecimal: 2,
115 maxNumberWidth: 6,
116 sigFigs: null,
117
118 strokeWidth: 1.0,
119
120 axisTickSize: 3,
121 axisLabelFontSize: 14,
122 xAxisLabelWidth: 50,
123 yAxisLabelWidth: 50,
124 xAxisLabelFormatter: Dygraph.dateAxisFormatter,
125 rightGap: 5,
126
127 showRoller: false,
128 xValueFormatter: Dygraph.dateString_,
129 xValueParser: Dygraph.dateParser,
130 xTicker: Dygraph.dateTicker,
131
132 delimiter: ',',
133
134 sigma: 2.0,
135 errorBars: false,
136 fractions: false,
137 wilsonInterval: true, // only relevant if fractions is true
138 customBars: false,
139 fillGraph: false,
140 fillAlpha: 0.15,
141 connectSeparatedPoints: false,
142
143 stackedGraph: false,
144 hideOverlayOnMouseOut: true,
145
146 // TODO(danvk): support 'onmouseover' and 'never', and remove synonyms.
147 legend: 'onmouseover', // the only relevant value at the moment is 'always'.
148
149 stepPlot: false,
150 avoidMinZero: false,
151
152 // Sizes of the various chart labels.
153 titleHeight: 28,
154 xLabelHeight: 18,
155 yLabelWidth: 18,
156
157 // From renderer
158 drawXAxis: true,
159 drawYAxis: true,
160 axisLineColor: "black",
161 "axisLineWidth": 0.5,
162 "axisLabelColor": "black",
163 "axisLabelFont": "Arial", // TODO(danvk): is this implemented?
164 "axisLabelWidth": 50,
165 "drawYGrid": true,
166 "drawXGrid": true,
167 "gridLineColor": "rgb(128,128,128)",
168
169 interactionModel: null // will be set to Dygraph.defaultInteractionModel.
170 };
171
172 // Various logging levels.
173 Dygraph.DEBUG = 1;
174 Dygraph.INFO = 2;
175 Dygraph.WARNING = 3;
176 Dygraph.ERROR = 3;
177
178 // Directions for panning and zooming. Use bit operations when combined
179 // values are possible.
180 Dygraph.HORIZONTAL = 1;
181 Dygraph.VERTICAL = 2;
182
183 // Used for initializing annotation CSS rules only once.
184 Dygraph.addedAnnotationCSS = false;
185
186 /**
187 * @private
188 * Return the 2d context for a dygraph canvas.
189 *
190 * This method is only exposed for the sake of replacing the function in
191 * automated tests, e.g.
192 *
193 * var oldFunc = Dygraph.getContext();
194 * Dygraph.getContext = function(canvas) {
195 * var realContext = oldFunc(canvas);
196 * return new Proxy(realContext);
197 * };
198 */
199 Dygraph.getContext = function(canvas) {
200 return canvas.getContext("2d");
201 };
202
203 Dygraph.prototype.__old_init__ = function(div, file, labels, attrs) {
204 // Labels is no longer a constructor parameter, since it's typically set
205 // directly from the data source. It also conains a name for the x-axis,
206 // which the previous constructor form did not.
207 if (labels != null) {
208 var new_labels = ["Date"];
209 for (var i = 0; i < labels.length; i++) new_labels.push(labels[i]);
210 Dygraph.update(attrs, { 'labels': new_labels });
211 }
212 this.__init__(div, file, attrs);
213 };
214
215 /**
216 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
217 * and context &lt;canvas&gt; inside of it. See the constructor for details.
218 * on the parameters.
219 * @param {Element} div the Element to render the graph into.
220 * @param {String | Function} file Source data
221 * @param {Object} attrs Miscellaneous other options
222 * @private
223 */
224 Dygraph.prototype.__init__ = function(div, file, attrs) {
225 // Hack for IE: if we're using excanvas and the document hasn't finished
226 // loading yet (and hence may not have initialized whatever it needs to
227 // initialize), then keep calling this routine periodically until it has.
228 if (/MSIE/.test(navigator.userAgent) && !window.opera &&
229 typeof(G_vmlCanvasManager) != 'undefined' &&
230 document.readyState != 'complete') {
231 var self = this;
232 setTimeout(function() { self.__init__(div, file, attrs) }, 100);
233 }
234
235 // Support two-argument constructor
236 if (attrs == null) { attrs = {}; }
237
238 // Copy the important bits into the object
239 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
240 this.maindiv_ = div;
241 this.file_ = file;
242 this.rollPeriod_ = attrs.rollPeriod || Dygraph.DEFAULT_ROLL_PERIOD;
243 this.previousVerticalX_ = -1;
244 this.fractions_ = attrs.fractions || false;
245 this.dateWindow_ = attrs.dateWindow || null;
246
247 this.wilsonInterval_ = attrs.wilsonInterval || true;
248 this.is_initial_draw_ = true;
249 this.annotations_ = [];
250
251 // Zoomed indicators - These indicate when the graph has been zoomed and on what axis.
252 this.zoomed_x_ = false;
253 this.zoomed_y_ = false;
254
255 // Clear the div. This ensure that, if multiple dygraphs are passed the same
256 // div, then only one will be drawn.
257 div.innerHTML = "";
258
259 // If the div isn't already sized then inherit from our attrs or
260 // give it a default size.
261 if (div.style.width == '') {
262 div.style.width = (attrs.width || Dygraph.DEFAULT_WIDTH) + "px";
263 }
264 if (div.style.height == '') {
265 div.style.height = (attrs.height || Dygraph.DEFAULT_HEIGHT) + "px";
266 }
267 this.width_ = parseInt(div.style.width, 10);
268 this.height_ = parseInt(div.style.height, 10);
269 // The div might have been specified as percent of the current window size,
270 // convert that to an appropriate number of pixels.
271 if (div.style.width.indexOf("%") == div.style.width.length - 1) {
272 this.width_ = div.offsetWidth;
273 }
274 if (div.style.height.indexOf("%") == div.style.height.length - 1) {
275 this.height_ = div.offsetHeight;
276 }
277
278 if (this.width_ == 0) {
279 this.error("dygraph has zero width. Please specify a width in pixels.");
280 }
281 if (this.height_ == 0) {
282 this.error("dygraph has zero height. Please specify a height in pixels.");
283 }
284
285 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
286 if (attrs['stackedGraph']) {
287 attrs['fillGraph'] = true;
288 // TODO(nikhilk): Add any other stackedGraph checks here.
289 }
290
291 // Dygraphs has many options, some of which interact with one another.
292 // To keep track of everything, we maintain two sets of options:
293 //
294 // this.user_attrs_ only options explicitly set by the user.
295 // this.attrs_ defaults, options derived from user_attrs_, data.
296 //
297 // Options are then accessed this.attr_('attr'), which first looks at
298 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
299 // defaults without overriding behavior that the user specifically asks for.
300 this.user_attrs_ = {};
301 Dygraph.update(this.user_attrs_, attrs);
302
303 this.attrs_ = {};
304 Dygraph.update(this.attrs_, Dygraph.DEFAULT_ATTRS);
305
306 this.boundaryIds_ = [];
307
308 // Make a note of whether labels will be pulled from the CSV file.
309 this.labelsFromCSV_ = (this.attr_("labels") == null);
310
311 // Create the containing DIV and other interactive elements
312 this.createInterface_();
313
314 this.start_();
315 };
316
317 /**
318 * Returns the zoomed status of the chart for one or both axes.
319 *
320 * Axis is an optional parameter. Can be set to 'x' or 'y'.
321 *
322 * The zoomed status for an axis is set whenever a user zooms using the mouse
323 * or when the dateWindow or valueRange are updated (unless the isZoomedIgnoreProgrammaticZoom
324 * option is also specified).
325 */
326 Dygraph.prototype.isZoomed = function(axis) {
327 if (axis == null) return this.zoomed_x_ || this.zoomed_y_;
328 if (axis == 'x') return this.zoomed_x_;
329 if (axis == 'y') return this.zoomed_y_;
330 throw "axis parameter to Dygraph.isZoomed must be missing, 'x' or 'y'.";
331 };
332
333 /**
334 * Returns information about the Dygraph object, including its containing ID.
335 */
336 Dygraph.prototype.toString = function() {
337 var maindiv = this.maindiv_;
338 var id = (maindiv && maindiv.id) ? maindiv.id : maindiv
339 return "[Dygraph " + id + "]";
340 }
341
342 /**
343 * @private
344 * Returns the value of an option. This may be set by the user (either in the
345 * constructor or by calling updateOptions) or by dygraphs, and may be set to a
346 * per-series value.
347 * @param { String } name The name of the option, e.g. 'rollPeriod'.
348 * @param { String } [seriesName] The name of the series to which the option
349 * will be applied. If no per-series value of this option is available, then
350 * the global value is returned. This is optional.
351 * @return { ... } The value of the option.
352 */
353 Dygraph.prototype.attr_ = function(name, seriesName) {
354 // <REMOVE_FOR_COMBINED>
355 if (typeof(Dygraph.OPTIONS_REFERENCE) === 'undefined') {
356 this.error('Must include options reference JS for testing');
357 } else if (!Dygraph.OPTIONS_REFERENCE.hasOwnProperty(name)) {
358 this.error('Dygraphs is using property ' + name + ', which has no entry ' +
359 'in the Dygraphs.OPTIONS_REFERENCE listing.');
360 // Only log this error once.
361 Dygraph.OPTIONS_REFERENCE[name] = true;
362 }
363 // </REMOVE_FOR_COMBINED>
364 if (seriesName &&
365 typeof(this.user_attrs_[seriesName]) != 'undefined' &&
366 this.user_attrs_[seriesName] != null &&
367 typeof(this.user_attrs_[seriesName][name]) != 'undefined') {
368 return this.user_attrs_[seriesName][name];
369 } else if (typeof(this.user_attrs_[name]) != 'undefined') {
370 return this.user_attrs_[name];
371 } else if (typeof(this.attrs_[name]) != 'undefined') {
372 return this.attrs_[name];
373 } else {
374 return null;
375 }
376 };
377
378 // TODO(danvk): any way I can get the line numbers to be this.warn call?
379 /**
380 * @private
381 * Log an error on the JS console at the given severity.
382 * @param { Integer } severity One of Dygraph.{DEBUG,INFO,WARNING,ERROR}
383 * @param { String } The message to log.
384 */
385 Dygraph.prototype.log = function(severity, message) {
386 if (typeof(console) != 'undefined') {
387 switch (severity) {
388 case Dygraph.DEBUG:
389 console.debug('dygraphs: ' + message);
390 break;
391 case Dygraph.INFO:
392 console.info('dygraphs: ' + message);
393 break;
394 case Dygraph.WARNING:
395 console.warn('dygraphs: ' + message);
396 break;
397 case Dygraph.ERROR:
398 console.error('dygraphs: ' + message);
399 break;
400 }
401 }
402 };
403
404 /** @private */
405 Dygraph.prototype.info = function(message) {
406 this.log(Dygraph.INFO, message);
407 };
408
409 /** @private */
410 Dygraph.prototype.warn = function(message) {
411 this.log(Dygraph.WARNING, message);
412 };
413
414 /** @private */
415 Dygraph.prototype.error = function(message) {
416 this.log(Dygraph.ERROR, message);
417 };
418
419 /**
420 * Returns the current rolling period, as set by the user or an option.
421 * @return {Number} The number of points in the rolling window
422 */
423 Dygraph.prototype.rollPeriod = function() {
424 return this.rollPeriod_;
425 };
426
427 /**
428 * Returns the currently-visible x-range. This can be affected by zooming,
429 * panning or a call to updateOptions.
430 * Returns a two-element array: [left, right].
431 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
432 */
433 Dygraph.prototype.xAxisRange = function() {
434 return this.dateWindow_ ? this.dateWindow_ : this.xAxisExtremes();
435 };
436
437 /**
438 * Returns the lower- and upper-bound x-axis values of the
439 * data set.
440 */
441 Dygraph.prototype.xAxisExtremes = function() {
442 var left = this.rawData_[0][0];
443 var right = this.rawData_[this.rawData_.length - 1][0];
444 return [left, right];
445 };
446
447 /**
448 * Returns the currently-visible y-range for an axis. This can be affected by
449 * zooming, panning or a call to updateOptions. Axis indices are zero-based. If
450 * called with no arguments, returns the range of the first axis.
451 * Returns a two-element array: [bottom, top].
452 */
453 Dygraph.prototype.yAxisRange = function(idx) {
454 if (typeof(idx) == "undefined") idx = 0;
455 if (idx < 0 || idx >= this.axes_.length) return null;
456 return [ this.axes_[idx].computedValueRange[0],
457 this.axes_[idx].computedValueRange[1] ];
458 };
459
460 /**
461 * Returns the currently-visible y-ranges for each axis. This can be affected by
462 * zooming, panning, calls to updateOptions, etc.
463 * Returns an array of [bottom, top] pairs, one for each y-axis.
464 */
465 Dygraph.prototype.yAxisRanges = function() {
466 var ret = [];
467 for (var i = 0; i < this.axes_.length; i++) {
468 ret.push(this.yAxisRange(i));
469 }
470 return ret;
471 };
472
473 // TODO(danvk): use these functions throughout dygraphs.
474 /**
475 * Convert from data coordinates to canvas/div X/Y coordinates.
476 * If specified, do this conversion for the coordinate system of a particular
477 * axis. Uses the first axis by default.
478 * Returns a two-element array: [X, Y]
479 *
480 * Note: use toDomXCoord instead of toDomCoords(x, null) and use toDomYCoord
481 * instead of toDomCoords(null, y, axis).
482 */
483 Dygraph.prototype.toDomCoords = function(x, y, axis) {
484 return [ this.toDomXCoord(x), this.toDomYCoord(y, axis) ];
485 };
486
487 /**
488 * Convert from data x coordinates to canvas/div X coordinate.
489 * If specified, do this conversion for the coordinate system of a particular
490 * axis.
491 * Returns a single value or null if x is null.
492 */
493 Dygraph.prototype.toDomXCoord = function(x) {
494 if (x == null) {
495 return null;
496 };
497
498 var area = this.plotter_.area;
499 var xRange = this.xAxisRange();
500 return area.x + (x - xRange[0]) / (xRange[1] - xRange[0]) * area.w;
501 }
502
503 /**
504 * Convert from data x coordinates to canvas/div Y coordinate and optional
505 * axis. Uses the first axis by default.
506 *
507 * returns a single value or null if y is null.
508 */
509 Dygraph.prototype.toDomYCoord = function(y, axis) {
510 var pct = this.toPercentYCoord(y, axis);
511
512 if (pct == null) {
513 return null;
514 }
515 var area = this.plotter_.area;
516 return area.y + pct * area.h;
517 }
518
519 /**
520 * Convert from canvas/div coords to data coordinates.
521 * If specified, do this conversion for the coordinate system of a particular
522 * axis. Uses the first axis by default.
523 * Returns a two-element array: [X, Y].
524 *
525 * Note: use toDataXCoord instead of toDataCoords(x, null) and use toDataYCoord
526 * instead of toDataCoords(null, y, axis).
527 */
528 Dygraph.prototype.toDataCoords = function(x, y, axis) {
529 return [ this.toDataXCoord(x), this.toDataYCoord(y, axis) ];
530 };
531
532 /**
533 * Convert from canvas/div x coordinate to data coordinate.
534 *
535 * If x is null, this returns null.
536 */
537 Dygraph.prototype.toDataXCoord = function(x) {
538 if (x == null) {
539 return null;
540 }
541
542 var area = this.plotter_.area;
543 var xRange = this.xAxisRange();
544 return xRange[0] + (x - area.x) / area.w * (xRange[1] - xRange[0]);
545 };
546
547 /**
548 * Convert from canvas/div y coord to value.
549 *
550 * If y is null, this returns null.
551 * if axis is null, this uses the first axis.
552 */
553 Dygraph.prototype.toDataYCoord = function(y, axis) {
554 if (y == null) {
555 return null;
556 }
557
558 var area = this.plotter_.area;
559 var yRange = this.yAxisRange(axis);
560
561 if (typeof(axis) == "undefined") axis = 0;
562 if (!this.axes_[axis].logscale) {
563 return yRange[0] + (area.h - y) / area.h * (yRange[1] - yRange[0]);
564 } else {
565 // Computing the inverse of toDomCoord.
566 var pct = (y - area.y) / area.h
567
568 // Computing the inverse of toPercentYCoord. The function was arrived at with
569 // the following steps:
570 //
571 // Original calcuation:
572 // pct = (logr1 - Dygraph.log10(y)) / (logr1 - Dygraph.log10(yRange[0]));
573 //
574 // Move denominator to both sides:
575 // pct * (logr1 - Dygraph.log10(yRange[0])) = logr1 - Dygraph.log10(y);
576 //
577 // subtract logr1, and take the negative value.
578 // logr1 - (pct * (logr1 - Dygraph.log10(yRange[0]))) = Dygraph.log10(y);
579 //
580 // Swap both sides of the equation, and we can compute the log of the
581 // return value. Which means we just need to use that as the exponent in
582 // e^exponent.
583 // Dygraph.log10(y) = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0])));
584
585 var logr1 = Dygraph.log10(yRange[1]);
586 var exponent = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0])));
587 var value = Math.pow(Dygraph.LOG_SCALE, exponent);
588 return value;
589 }
590 };
591
592 /**
593 * Converts a y for an axis to a percentage from the top to the
594 * bottom of the drawing area.
595 *
596 * If the coordinate represents a value visible on the canvas, then
597 * the value will be between 0 and 1, where 0 is the top of the canvas.
598 * However, this method will return values outside the range, as
599 * values can fall outside the canvas.
600 *
601 * If y is null, this returns null.
602 * if axis is null, this uses the first axis.
603 *
604 * @param { Number } y The data y-coordinate.
605 * @param { Number } [axis] The axis number on which the data coordinate lives.
606 * @return { Number } A fraction in [0, 1] where 0 = the top edge.
607 */
608 Dygraph.prototype.toPercentYCoord = function(y, axis) {
609 if (y == null) {
610 return null;
611 }
612 if (typeof(axis) == "undefined") axis = 0;
613
614 var area = this.plotter_.area;
615 var yRange = this.yAxisRange(axis);
616
617 var pct;
618 if (!this.axes_[axis].logscale) {
619 // yRange[1] - y is unit distance from the bottom.
620 // yRange[1] - yRange[0] is the scale of the range.
621 // (yRange[1] - y) / (yRange[1] - yRange[0]) is the % from the bottom.
622 pct = (yRange[1] - y) / (yRange[1] - yRange[0]);
623 } else {
624 var logr1 = Dygraph.log10(yRange[1]);
625 pct = (logr1 - Dygraph.log10(y)) / (logr1 - Dygraph.log10(yRange[0]));
626 }
627 return pct;
628 }
629
630 /**
631 * Converts an x value to a percentage from the left to the right of
632 * the drawing area.
633 *
634 * If the coordinate represents a value visible on the canvas, then
635 * the value will be between 0 and 1, where 0 is the left of the canvas.
636 * However, this method will return values outside the range, as
637 * values can fall outside the canvas.
638 *
639 * If x is null, this returns null.
640 * @param { Number } x The data x-coordinate.
641 * @return { Number } A fraction in [0, 1] where 0 = the left edge.
642 */
643 Dygraph.prototype.toPercentXCoord = function(x) {
644 if (x == null) {
645 return null;
646 }
647
648 var xRange = this.xAxisRange();
649 return (x - xRange[0]) / (xRange[1] - xRange[0]);
650 };
651
652 /**
653 * Returns the number of columns (including the independent variable).
654 * @return { Integer } The number of columns.
655 */
656 Dygraph.prototype.numColumns = function() {
657 return this.rawData_[0].length;
658 };
659
660 /**
661 * Returns the number of rows (excluding any header/label row).
662 * @return { Integer } The number of rows, less any header.
663 */
664 Dygraph.prototype.numRows = function() {
665 return this.rawData_.length;
666 };
667
668 /**
669 * Returns the value in the given row and column. If the row and column exceed
670 * the bounds on the data, returns null. Also returns null if the value is
671 * missing.
672 * @param { Number} row The row number of the data (0-based). Row 0 is the
673 * first row of data, not a header row.
674 * @param { Number} col The column number of the data (0-based)
675 * @return { Number } The value in the specified cell or null if the row/col
676 * were out of range.
677 */
678 Dygraph.prototype.getValue = function(row, col) {
679 if (row < 0 || row > this.rawData_.length) return null;
680 if (col < 0 || col > this.rawData_[row].length) return null;
681
682 return this.rawData_[row][col];
683 };
684
685 /**
686 * @private
687 * Add an event handler. This smooths a difference between IE and the rest of
688 * the world.
689 * @param { DOM element } el The element to add the event to.
690 * @param { String } evt The name of the event, e.g. 'click' or 'mousemove'.
691 * @param { Function } fn The function to call on the event. The function takes
692 * one parameter: the event object.
693 */
694 Dygraph.addEvent = function(el, evt, fn) {
695 var normed_fn = function(e) {
696 if (!e) var e = window.event;
697 fn(e);
698 };
699 if (window.addEventListener) { // Mozilla, Netscape, Firefox
700 el.addEventListener(evt, normed_fn, false);
701 } else { // IE
702 el.attachEvent('on' + evt, normed_fn);
703 }
704 };
705
706
707 /**
708 * @private
709 * Cancels further processing of an event. This is useful to prevent default
710 * browser actions, e.g. highlighting text on a double-click.
711 * Based on the article at
712 * http://www.switchonthecode.com/tutorials/javascript-tutorial-the-scroll-wheel
713 * @param { Event } e The event whose normal behavior should be canceled.
714 */
715 Dygraph.cancelEvent = function(e) {
716 e = e ? e : window.event;
717 if (e.stopPropagation) {
718 e.stopPropagation();
719 }
720 if (e.preventDefault) {
721 e.preventDefault();
722 }
723 e.cancelBubble = true;
724 e.cancel = true;
725 e.returnValue = false;
726 return false;
727 };
728
729
730 /**
731 * Generates interface elements for the Dygraph: a containing div, a div to
732 * display the current point, and a textbox to adjust the rolling average
733 * period. Also creates the Renderer/Layout elements.
734 * @private
735 */
736 Dygraph.prototype.createInterface_ = function() {
737 // Create the all-enclosing graph div
738 var enclosing = this.maindiv_;
739
740 this.graphDiv = document.createElement("div");
741 this.graphDiv.style.width = this.width_ + "px";
742 this.graphDiv.style.height = this.height_ + "px";
743 enclosing.appendChild(this.graphDiv);
744
745 // Create the canvas for interactive parts of the chart.
746 this.canvas_ = Dygraph.createCanvas();
747 this.canvas_.style.position = "absolute";
748 this.canvas_.width = this.width_;
749 this.canvas_.height = this.height_;
750 this.canvas_.style.width = this.width_ + "px"; // for IE
751 this.canvas_.style.height = this.height_ + "px"; // for IE
752
753 this.canvas_ctx_ = Dygraph.getContext(this.canvas_);
754
755 // ... and for static parts of the chart.
756 this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
757 this.hidden_ctx_ = Dygraph.getContext(this.hidden_);
758
759 // The interactive parts of the graph are drawn on top of the chart.
760 this.graphDiv.appendChild(this.hidden_);
761 this.graphDiv.appendChild(this.canvas_);
762 this.mouseEventElement_ = this.canvas_;
763
764 var dygraph = this;
765 Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(e) {
766 dygraph.mouseMove_(e);
767 });
768 Dygraph.addEvent(this.mouseEventElement_, 'mouseout', function(e) {
769 dygraph.mouseOut_(e);
770 });
771
772 // Create the grapher
773 this.layout_ = new DygraphLayout(this);
774
775 // TODO(danvk): why does the Renderer need its own set of options?
776 this.renderOptions_ = { colorScheme: this.colors_,
777 strokeColor: null,
778 axisLineWidth: Dygraph.AXIS_LINE_WIDTH };
779 Dygraph.update(this.renderOptions_, this.attrs_);
780 Dygraph.update(this.renderOptions_, this.user_attrs_);
781
782 this.createStatusMessage_();
783 this.createDragInterface_();
784 };
785
786 /**
787 * Detach DOM elements in the dygraph and null out all data references.
788 * Calling this when you're done with a dygraph can dramatically reduce memory
789 * usage. See, e.g., the tests/perf.html example.
790 */
791 Dygraph.prototype.destroy = function() {
792 var removeRecursive = function(node) {
793 while (node.hasChildNodes()) {
794 removeRecursive(node.firstChild);
795 node.removeChild(node.firstChild);
796 }
797 };
798 removeRecursive(this.maindiv_);
799
800 var nullOut = function(obj) {
801 for (var n in obj) {
802 if (typeof(obj[n]) === 'object') {
803 obj[n] = null;
804 }
805 }
806 };
807
808 // These may not all be necessary, but it can't hurt...
809 nullOut(this.layout_);
810 nullOut(this.plotter_);
811 nullOut(this);
812 };
813
814 /**
815 * Creates the canvas on which the chart will be drawn. Only the Renderer ever
816 * draws on this particular canvas. All Dygraph work (i.e. drawing hover dots
817 * or the zoom rectangles) is done on this.canvas_.
818 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
819 * @return {Object} The newly-created canvas
820 * @private
821 */
822 Dygraph.prototype.createPlotKitCanvas_ = function(canvas) {
823 var h = Dygraph.createCanvas();
824 h.style.position = "absolute";
825 // TODO(danvk): h should be offset from canvas. canvas needs to include
826 // some extra area to make it easier to zoom in on the far left and far
827 // right. h needs to be precisely the plot area, so that clipping occurs.
828 h.style.top = canvas.style.top;
829 h.style.left = canvas.style.left;
830 h.width = this.width_;
831 h.height = this.height_;
832 h.style.width = this.width_ + "px"; // for IE
833 h.style.height = this.height_ + "px"; // for IE
834 return h;
835 };
836
837 /**
838 * Convert hsv values to an rgb(r,g,b) string. Taken from MochiKit.Color. This
839 * is used to generate default series colors which are evenly spaced on the
840 * color wheel.
841 * @param { Number } hue Range is 0.0-1.0.
842 * @param { Number } saturation Range is 0.0-1.0.
843 * @param { Number } value Range is 0.0-1.0.
844 * @return { String } "rgb(r,g,b)" where r, g and b range from 0-255.
845 * @private
846 */
847 Dygraph.hsvToRGB = function (hue, saturation, value) {
848 var red;
849 var green;
850 var blue;
851 if (saturation === 0) {
852 red = value;
853 green = value;
854 blue = value;
855 } else {
856 var i = Math.floor(hue * 6);
857 var f = (hue * 6) - i;
858 var p = value * (1 - saturation);
859 var q = value * (1 - (saturation * f));
860 var t = value * (1 - (saturation * (1 - f)));
861 switch (i) {
862 case 1: red = q; green = value; blue = p; break;
863 case 2: red = p; green = value; blue = t; break;
864 case 3: red = p; green = q; blue = value; break;
865 case 4: red = t; green = p; blue = value; break;
866 case 5: red = value; green = p; blue = q; break;
867 case 6: // fall through
868 case 0: red = value; green = t; blue = p; break;
869 }
870 }
871 red = Math.floor(255 * red + 0.5);
872 green = Math.floor(255 * green + 0.5);
873 blue = Math.floor(255 * blue + 0.5);
874 return 'rgb(' + red + ',' + green + ',' + blue + ')';
875 };
876
877
878 /**
879 * Generate a set of distinct colors for the data series. This is done with a
880 * color wheel. Saturation/Value are customizable, and the hue is
881 * equally-spaced around the color wheel. If a custom set of colors is
882 * specified, that is used instead.
883 * @private
884 */
885 Dygraph.prototype.setColors_ = function() {
886 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
887 // away with this.renderOptions_.
888 var num = this.attr_("labels").length - 1;
889 this.colors_ = [];
890 var colors = this.attr_('colors');
891 if (!colors) {
892 var sat = this.attr_('colorSaturation') || 1.0;
893 var val = this.attr_('colorValue') || 0.5;
894 var half = Math.ceil(num / 2);
895 for (var i = 1; i <= num; i++) {
896 if (!this.visibility()[i-1]) continue;
897 // alternate colors for high contrast.
898 var idx = i % 2 ? Math.ceil(i / 2) : (half + i / 2);
899 var hue = (1.0 * idx/ (1 + num));
900 this.colors_.push(Dygraph.hsvToRGB(hue, sat, val));
901 }
902 } else {
903 for (var i = 0; i < num; i++) {
904 if (!this.visibility()[i]) continue;
905 var colorStr = colors[i % colors.length];
906 this.colors_.push(colorStr);
907 }
908 }
909
910 // TODO(danvk): update this w/r/t/ the new options system.
911 this.renderOptions_.colorScheme = this.colors_;
912 Dygraph.update(this.plotter_.options, this.renderOptions_);
913 };
914
915 /**
916 * Return the list of colors. This is either the list of colors passed in the
917 * attributes or the autogenerated list of rgb(r,g,b) strings.
918 * @return {Array<string>} The list of colors.
919 */
920 Dygraph.prototype.getColors = function() {
921 return this.colors_;
922 };
923
924 // The following functions are from quirksmode.org with a modification for Safari from
925 // http://blog.firetree.net/2005/07/04/javascript-find-position/
926 // http://www.quirksmode.org/js/findpos.html
927
928 /** @private */
929 Dygraph.findPosX = function(obj) {
930 var curleft = 0;
931 if(obj.offsetParent)
932 while(1)
933 {
934 curleft += obj.offsetLeft;
935 if(!obj.offsetParent)
936 break;
937 obj = obj.offsetParent;
938 }
939 else if(obj.x)
940 curleft += obj.x;
941 return curleft;
942 };
943
944
945 /** @private */
946 Dygraph.findPosY = function(obj) {
947 var curtop = 0;
948 if(obj.offsetParent)
949 while(1)
950 {
951 curtop += obj.offsetTop;
952 if(!obj.offsetParent)
953 break;
954 obj = obj.offsetParent;
955 }
956 else if(obj.y)
957 curtop += obj.y;
958 return curtop;
959 };
960
961
962 /**
963 * Create the div that contains information on the selected point(s)
964 * This goes in the top right of the canvas, unless an external div has already
965 * been specified.
966 * @private
967 */
968 Dygraph.prototype.createStatusMessage_ = function() {
969 var userLabelsDiv = this.user_attrs_["labelsDiv"];
970 if (userLabelsDiv && null != userLabelsDiv
971 && (typeof(userLabelsDiv) == "string" || userLabelsDiv instanceof String)) {
972 this.user_attrs_["labelsDiv"] = document.getElementById(userLabelsDiv);
973 }
974 if (!this.attr_("labelsDiv")) {
975 var divWidth = this.attr_('labelsDivWidth');
976 var messagestyle = {
977 "position": "absolute",
978 "fontSize": "14px",
979 "zIndex": 10,
980 "width": divWidth + "px",
981 "top": "0px",
982 "left": (this.width_ - divWidth - 2) + "px",
983 "background": "white",
984 "textAlign": "left",
985 "overflow": "hidden"};
986 Dygraph.update(messagestyle, this.attr_('labelsDivStyles'));
987 var div = document.createElement("div");
988 for (var name in messagestyle) {
989 if (messagestyle.hasOwnProperty(name)) {
990 div.style[name] = messagestyle[name];
991 }
992 }
993 this.graphDiv.appendChild(div);
994 this.attrs_.labelsDiv = div;
995 }
996 };
997
998 /**
999 * Position the labels div so that:
1000 * - its right edge is flush with the right edge of the charting area
1001 * - its top edge is flush with the top edge of the charting area
1002 * @private
1003 */
1004 Dygraph.prototype.positionLabelsDiv_ = function() {
1005 // Don't touch a user-specified labelsDiv.
1006 if (this.user_attrs_.hasOwnProperty("labelsDiv")) return;
1007
1008 var area = this.plotter_.area;
1009 var div = this.attr_("labelsDiv");
1010 div.style.left = area.x + area.w - this.attr_("labelsDivWidth") - 1 + "px";
1011 div.style.top = area.y + "px";
1012 };
1013
1014 /**
1015 * Create the text box to adjust the averaging period
1016 * @private
1017 */
1018 Dygraph.prototype.createRollInterface_ = function() {
1019 // Create a roller if one doesn't exist already.
1020 if (!this.roller_) {
1021 this.roller_ = document.createElement("input");
1022 this.roller_.type = "text";
1023 this.roller_.style.display = "none";
1024 this.graphDiv.appendChild(this.roller_);
1025 }
1026
1027 var display = this.attr_('showRoller') ? 'block' : 'none';
1028
1029 var area = this.plotter_.area;
1030 var textAttr = { "position": "absolute",
1031 "zIndex": 10,
1032 "top": (area.y + area.h - 25) + "px",
1033 "left": (area.x + 1) + "px",
1034 "display": display
1035 };
1036 this.roller_.size = "2";
1037 this.roller_.value = this.rollPeriod_;
1038 for (var name in textAttr) {
1039 if (textAttr.hasOwnProperty(name)) {
1040 this.roller_.style[name] = textAttr[name];
1041 }
1042 }
1043
1044 var dygraph = this;
1045 this.roller_.onchange = function() { dygraph.adjustRoll(dygraph.roller_.value); };
1046 };
1047
1048 /**
1049 * @private
1050 * Returns the x-coordinate of the event in a coordinate system where the
1051 * top-left corner of the page (not the window) is (0,0).
1052 * Taken from MochiKit.Signal
1053 */
1054 Dygraph.pageX = function(e) {
1055 if (e.pageX) {
1056 return (!e.pageX || e.pageX < 0) ? 0 : e.pageX;
1057 } else {
1058 var de = document;
1059 var b = document.body;
1060 return e.clientX +
1061 (de.scrollLeft || b.scrollLeft) -
1062 (de.clientLeft || 0);
1063 }
1064 };
1065
1066 /**
1067 * @private
1068 * Returns the y-coordinate of the event in a coordinate system where the
1069 * top-left corner of the page (not the window) is (0,0).
1070 * Taken from MochiKit.Signal
1071 */
1072 Dygraph.pageY = function(e) {
1073 if (e.pageY) {
1074 return (!e.pageY || e.pageY < 0) ? 0 : e.pageY;
1075 } else {
1076 var de = document;
1077 var b = document.body;
1078 return e.clientY +
1079 (de.scrollTop || b.scrollTop) -
1080 (de.clientTop || 0);
1081 }
1082 };
1083
1084 /**
1085 * @private
1086 * Converts page the x-coordinate of the event to pixel x-coordinates on the
1087 * canvas (i.e. DOM Coords).
1088 */
1089 Dygraph.prototype.dragGetX_ = function(e, context) {
1090 return Dygraph.pageX(e) - context.px
1091 };
1092
1093 /**
1094 * @private
1095 * Converts page the y-coordinate of the event to pixel y-coordinates on the
1096 * canvas (i.e. DOM Coords).
1097 */
1098 Dygraph.prototype.dragGetY_ = function(e, context) {
1099 return Dygraph.pageY(e) - context.py
1100 };
1101
1102 /**
1103 * A collection of functions to facilitate build custom interaction models.
1104 * @class
1105 */
1106 Dygraph.Interaction = {};
1107
1108 /**
1109 * Called in response to an interaction model operation that
1110 * should start the default panning behavior.
1111 *
1112 * It's used in the default callback for "mousedown" operations.
1113 * Custom interaction model builders can use it to provide the default
1114 * panning behavior.
1115 *
1116 * @param { Event } event the event object which led to the startPan call.
1117 * @param { Dygraph} g The dygraph on which to act.
1118 * @param { Object} context The dragging context object (with
1119 * dragStartX/dragStartY/etc. properties). This function modifies the context.
1120 */
1121 Dygraph.Interaction.startPan = function(event, g, context) {
1122 context.isPanning = true;
1123 var xRange = g.xAxisRange();
1124 context.dateRange = xRange[1] - xRange[0];
1125 context.initialLeftmostDate = xRange[0];
1126 context.xUnitsPerPixel = context.dateRange / (g.plotter_.area.w - 1);
1127
1128 if (g.attr_("panEdgeFraction")) {
1129 var maxXPixelsToDraw = g.width_ * g.attr_("panEdgeFraction");
1130 var xExtremes = g.xAxisExtremes(); // I REALLY WANT TO CALL THIS xTremes!
1131
1132 var boundedLeftX = g.toDomXCoord(xExtremes[0]) - maxXPixelsToDraw;
1133 var boundedRightX = g.toDomXCoord(xExtremes[1]) + maxXPixelsToDraw;
1134
1135 var boundedLeftDate = g.toDataXCoord(boundedLeftX);
1136 var boundedRightDate = g.toDataXCoord(boundedRightX);
1137 context.boundedDates = [boundedLeftDate, boundedRightDate];
1138
1139 var boundedValues = [];
1140 var maxYPixelsToDraw = g.height_ * g.attr_("panEdgeFraction");
1141
1142 for (var i = 0; i < g.axes_.length; i++) {
1143 var axis = g.axes_[i];
1144 var yExtremes = axis.extremeRange;
1145
1146 var boundedTopY = g.toDomYCoord(yExtremes[0], i) + maxYPixelsToDraw;
1147 var boundedBottomY = g.toDomYCoord(yExtremes[1], i) - maxYPixelsToDraw;
1148
1149 var boundedTopValue = g.toDataYCoord(boundedTopY);
1150 var boundedBottomValue = g.toDataYCoord(boundedBottomY);
1151
1152 boundedValues[i] = [boundedTopValue, boundedBottomValue];
1153 }
1154 context.boundedValues = boundedValues;
1155 }
1156
1157 // Record the range of each y-axis at the start of the drag.
1158 // If any axis has a valueRange or valueWindow, then we want a 2D pan.
1159 context.is2DPan = false;
1160 for (var i = 0; i < g.axes_.length; i++) {
1161 var axis = g.axes_[i];
1162 var yRange = g.yAxisRange(i);
1163 // TODO(konigsberg): These values should be in |context|.
1164 // In log scale, initialTopValue, dragValueRange and unitsPerPixel are log scale.
1165 if (axis.logscale) {
1166 axis.initialTopValue = Dygraph.log10(yRange[1]);
1167 axis.dragValueRange = Dygraph.log10(yRange[1]) - Dygraph.log10(yRange[0]);
1168 } else {
1169 axis.initialTopValue = yRange[1];
1170 axis.dragValueRange = yRange[1] - yRange[0];
1171 }
1172 axis.unitsPerPixel = axis.dragValueRange / (g.plotter_.area.h - 1);
1173
1174 // While calculating axes, set 2dpan.
1175 if (axis.valueWindow || axis.valueRange) context.is2DPan = true;
1176 }
1177 };
1178
1179 /**
1180 * Called in response to an interaction model operation that
1181 * responds to an event that pans the view.
1182 *
1183 * It's used in the default callback for "mousemove" operations.
1184 * Custom interaction model builders can use it to provide the default
1185 * panning behavior.
1186 *
1187 * @param { Event } event the event object which led to the movePan call.
1188 * @param { Dygraph} g The dygraph on which to act.
1189 * @param { Object} context The dragging context object (with
1190 * dragStartX/dragStartY/etc. properties). This function modifies the context.
1191 */
1192 Dygraph.Interaction.movePan = function(event, g, context) {
1193 context.dragEndX = g.dragGetX_(event, context);
1194 context.dragEndY = g.dragGetY_(event, context);
1195
1196 var minDate = context.initialLeftmostDate -
1197 (context.dragEndX - context.dragStartX) * context.xUnitsPerPixel;
1198 if (context.boundedDates) {
1199 minDate = Math.max(minDate, context.boundedDates[0]);
1200 }
1201 var maxDate = minDate + context.dateRange;
1202 if (context.boundedDates) {
1203 if (maxDate > context.boundedDates[1]) {
1204 // Adjust minDate, and recompute maxDate.
1205 minDate = minDate - (maxDate - context.boundedDates[1]);
1206 maxDate = minDate + context.dateRange;
1207 }
1208 }
1209
1210 g.dateWindow_ = [minDate, maxDate];
1211
1212 // y-axis scaling is automatic unless this is a full 2D pan.
1213 if (context.is2DPan) {
1214 // Adjust each axis appropriately.
1215 for (var i = 0; i < g.axes_.length; i++) {
1216 var axis = g.axes_[i];
1217
1218 var pixelsDragged = context.dragEndY - context.dragStartY;
1219 var unitsDragged = pixelsDragged * axis.unitsPerPixel;
1220
1221 var boundedValue = context.boundedValues ? context.boundedValues[i] : null;
1222
1223 // In log scale, maxValue and minValue are the logs of those values.
1224 var maxValue = axis.initialTopValue + unitsDragged;
1225 if (boundedValue) {
1226 maxValue = Math.min(maxValue, boundedValue[1]);
1227 }
1228 var minValue = maxValue - axis.dragValueRange;
1229 if (boundedValue) {
1230 if (minValue < boundedValue[0]) {
1231 // Adjust maxValue, and recompute minValue.
1232 maxValue = maxValue - (minValue - boundedValue[0]);
1233 minValue = maxValue - axis.dragValueRange;
1234 }
1235 }
1236 if (axis.logscale) {
1237 axis.valueWindow = [ Math.pow(Dygraph.LOG_SCALE, minValue),
1238 Math.pow(Dygraph.LOG_SCALE, maxValue) ];
1239 } else {
1240 axis.valueWindow = [ minValue, maxValue ];
1241 }
1242 }
1243 }
1244
1245 g.drawGraph_();
1246 };
1247
1248 /**
1249 * Called in response to an interaction model operation that
1250 * responds to an event that ends panning.
1251 *
1252 * It's used in the default callback for "mouseup" operations.
1253 * Custom interaction model builders can use it to provide the default
1254 * panning behavior.
1255 *
1256 * @param { Event } event the event object which led to the startZoom call.
1257 * @param { Dygraph} g The dygraph on which to act.
1258 * @param { Object} context The dragging context object (with
1259 * dragStartX/dragStartY/etc. properties). This function modifies the context.
1260 */
1261 Dygraph.Interaction.endPan = function(event, g, context) {
1262 // TODO(konigsberg): Clear the context data from the axis.
1263 // TODO(konigsberg): mouseup should just delete the
1264 // context object, and mousedown should create a new one.
1265 context.isPanning = false;
1266 context.is2DPan = false;
1267 context.initialLeftmostDate = null;
1268 context.dateRange = null;
1269 context.valueRange = null;
1270 context.boundedDates = null;
1271 context.boundedValues = null;
1272 };
1273
1274 /**
1275 * Called in response to an interaction model operation that
1276 * responds to an event that starts zooming.
1277 *
1278 * It's used in the default callback for "mousedown" operations.
1279 * Custom interaction model builders can use it to provide the default
1280 * zooming behavior.
1281 *
1282 * @param { Event } event the event object which led to the startZoom call.
1283 * @param { Dygraph} g The dygraph on which to act.
1284 * @param { Object} context The dragging context object (with
1285 * dragStartX/dragStartY/etc. properties). This function modifies the context.
1286 */
1287 Dygraph.Interaction.startZoom = function(event, g, context) {
1288 context.isZooming = true;
1289 };
1290
1291 /**
1292 * Called in response to an interaction model operation that
1293 * responds to an event that defines zoom boundaries.
1294 *
1295 * It's used in the default callback for "mousemove" operations.
1296 * Custom interaction model builders can use it to provide the default
1297 * zooming behavior.
1298 *
1299 * @param { Event } event the event object which led to the moveZoom call.
1300 * @param { Dygraph} g The dygraph on which to act.
1301 * @param { Object} context The dragging context object (with
1302 * dragStartX/dragStartY/etc. properties). This function modifies the context.
1303 */
1304 Dygraph.Interaction.moveZoom = function(event, g, context) {
1305 context.dragEndX = g.dragGetX_(event, context);
1306 context.dragEndY = g.dragGetY_(event, context);
1307
1308 var xDelta = Math.abs(context.dragStartX - context.dragEndX);
1309 var yDelta = Math.abs(context.dragStartY - context.dragEndY);
1310
1311 // drag direction threshold for y axis is twice as large as x axis
1312 context.dragDirection = (xDelta < yDelta / 2) ? Dygraph.VERTICAL : Dygraph.HORIZONTAL;
1313
1314 g.drawZoomRect_(
1315 context.dragDirection,
1316 context.dragStartX,
1317 context.dragEndX,
1318 context.dragStartY,
1319 context.dragEndY,
1320 context.prevDragDirection,
1321 context.prevEndX,
1322 context.prevEndY);
1323
1324 context.prevEndX = context.dragEndX;
1325 context.prevEndY = context.dragEndY;
1326 context.prevDragDirection = context.dragDirection;
1327 };
1328
1329 /**
1330 * Called in response to an interaction model operation that
1331 * responds to an event that performs a zoom based on previously defined
1332 * bounds..
1333 *
1334 * It's used in the default callback for "mouseup" operations.
1335 * Custom interaction model builders can use it to provide the default
1336 * zooming behavior.
1337 *
1338 * @param { Event } event the event object which led to the endZoom call.
1339 * @param { Dygraph} g The dygraph on which to end the zoom.
1340 * @param { Object} context The dragging context object (with
1341 * dragStartX/dragStartY/etc. properties). This function modifies the context.
1342 */
1343 Dygraph.Interaction.endZoom = function(event, g, context) {
1344 // TODO(konigsberg): Refactor or rename this fn -- it deals with clicks, too.
1345 context.isZooming = false;
1346 context.dragEndX = g.dragGetX_(event, context);
1347 context.dragEndY = g.dragGetY_(event, context);
1348 var regionWidth = Math.abs(context.dragEndX - context.dragStartX);
1349 var regionHeight = Math.abs(context.dragEndY - context.dragStartY);
1350
1351 if (regionWidth < 2 && regionHeight < 2 &&
1352 g.lastx_ != undefined && g.lastx_ != -1) {
1353 // TODO(danvk): pass along more info about the points, e.g. 'x'
1354 if (g.attr_('clickCallback') != null) {
1355 g.attr_('clickCallback')(event, g.lastx_, g.selPoints_);
1356 }
1357 if (g.attr_('pointClickCallback')) {
1358 // check if the click was on a particular point.
1359 var closestIdx = -1;
1360 var closestDistance = 0;
1361 for (var i = 0; i < g.selPoints_.length; i++) {
1362 var p = g.selPoints_[i];
1363 var distance = Math.pow(p.canvasx - context.dragEndX, 2) +
1364 Math.pow(p.canvasy - context.dragEndY, 2);
1365 if (closestIdx == -1 || distance < closestDistance) {
1366 closestDistance = distance;
1367 closestIdx = i;
1368 }
1369 }
1370
1371 // Allow any click within two pixels of the dot.
1372 var radius = g.attr_('highlightCircleSize') + 2;
1373 if (closestDistance <= 5 * 5) {
1374 g.attr_('pointClickCallback')(event, g.selPoints_[closestIdx]);
1375 }
1376 }
1377 }
1378
1379 if (regionWidth >= 10 && context.dragDirection == Dygraph.HORIZONTAL) {
1380 g.doZoomX_(Math.min(context.dragStartX, context.dragEndX),
1381 Math.max(context.dragStartX, context.dragEndX));
1382 } else if (regionHeight >= 10 && context.dragDirection == Dygraph.VERTICAL) {
1383 g.doZoomY_(Math.min(context.dragStartY, context.dragEndY),
1384 Math.max(context.dragStartY, context.dragEndY));
1385 } else {
1386 g.canvas_ctx_.clearRect(0, 0, g.canvas_.width, g.canvas_.height);
1387 }
1388 context.dragStartX = null;
1389 context.dragStartY = null;
1390 };
1391
1392 /**
1393 * Default interation model for dygraphs. You can refer to specific elements of
1394 * this when constructing your own interaction model, e.g.:
1395 * g.updateOptions( {
1396 * interactionModel: {
1397 * mousedown: Dygraph.defaultInteractionModel.mousedown
1398 * }
1399 * } );
1400 */
1401 Dygraph.Interaction.defaultModel = {
1402 // Track the beginning of drag events
1403 mousedown: function(event, g, context) {
1404 context.initializeMouseDown(event, g, context);
1405
1406 if (event.altKey || event.shiftKey) {
1407 Dygraph.startPan(event, g, context);
1408 } else {
1409 Dygraph.startZoom(event, g, context);
1410 }
1411 },
1412
1413 // Draw zoom rectangles when the mouse is down and the user moves around
1414 mousemove: function(event, g, context) {
1415 if (context.isZooming) {
1416 Dygraph.moveZoom(event, g, context);
1417 } else if (context.isPanning) {
1418 Dygraph.movePan(event, g, context);
1419 }
1420 },
1421
1422 mouseup: function(event, g, context) {
1423 if (context.isZooming) {
1424 Dygraph.endZoom(event, g, context);
1425 } else if (context.isPanning) {
1426 Dygraph.endPan(event, g, context);
1427 }
1428 },
1429
1430 // Temporarily cancel the dragging event when the mouse leaves the graph
1431 mouseout: function(event, g, context) {
1432 if (context.isZooming) {
1433 context.dragEndX = null;
1434 context.dragEndY = null;
1435 }
1436 },
1437
1438 // Disable zooming out if panning.
1439 dblclick: function(event, g, context) {
1440 if (event.altKey || event.shiftKey) {
1441 return;
1442 }
1443 // TODO(konigsberg): replace g.doUnzoom()_ with something that is
1444 // friendlier to public use.
1445 g.doUnzoom_();
1446 }
1447 };
1448
1449 Dygraph.DEFAULT_ATTRS.interactionModel = Dygraph.Interaction.defaultModel;
1450
1451 // old ways of accessing these methods/properties
1452 Dygraph.defaultInteractionModel = Dygraph.Interaction.defaultModel;
1453 Dygraph.endZoom = Dygraph.Interaction.endZoom;
1454 Dygraph.moveZoom = Dygraph.Interaction.moveZoom;
1455 Dygraph.startZoom = Dygraph.Interaction.startZoom;
1456 Dygraph.endPan = Dygraph.Interaction.endPan;
1457 Dygraph.movePan = Dygraph.Interaction.movePan;
1458 Dygraph.startPan = Dygraph.Interaction.startPan;
1459
1460 /**
1461 * Set up all the mouse handlers needed to capture dragging behavior for zoom
1462 * events.
1463 * @private
1464 */
1465 Dygraph.prototype.createDragInterface_ = function() {
1466 var context = {
1467 // Tracks whether the mouse is down right now
1468 isZooming: false,
1469 isPanning: false, // is this drag part of a pan?
1470 is2DPan: false, // if so, is that pan 1- or 2-dimensional?
1471 dragStartX: null,
1472 dragStartY: null,
1473 dragEndX: null,
1474 dragEndY: null,
1475 dragDirection: null,
1476 prevEndX: null,
1477 prevEndY: null,
1478 prevDragDirection: null,
1479
1480 // The value on the left side of the graph when a pan operation starts.
1481 initialLeftmostDate: null,
1482
1483 // The number of units each pixel spans. (This won't be valid for log
1484 // scales)
1485 xUnitsPerPixel: null,
1486
1487 // TODO(danvk): update this comment
1488 // The range in second/value units that the viewport encompasses during a
1489 // panning operation.
1490 dateRange: null,
1491
1492 // Utility function to convert page-wide coordinates to canvas coords
1493 px: 0,
1494 py: 0,
1495
1496 // Values for use with panEdgeFraction, which limit how far outside the
1497 // graph's data boundaries it can be panned.
1498 boundedDates: null, // [minDate, maxDate]
1499 boundedValues: null, // [[minValue, maxValue] ...]
1500
1501 initializeMouseDown: function(event, g, context) {
1502 // prevents mouse drags from selecting page text.
1503 if (event.preventDefault) {
1504 event.preventDefault(); // Firefox, Chrome, etc.
1505 } else {
1506 event.returnValue = false; // IE
1507 event.cancelBubble = true;
1508 }
1509
1510 context.px = Dygraph.findPosX(g.canvas_);
1511 context.py = Dygraph.findPosY(g.canvas_);
1512 context.dragStartX = g.dragGetX_(event, context);
1513 context.dragStartY = g.dragGetY_(event, context);
1514 }
1515 };
1516
1517 var interactionModel = this.attr_("interactionModel");
1518
1519 // Self is the graph.
1520 var self = this;
1521
1522 // Function that binds the graph and context to the handler.
1523 var bindHandler = function(handler) {
1524 return function(event) {
1525 handler(event, self, context);
1526 };
1527 };
1528
1529 for (var eventName in interactionModel) {
1530 if (!interactionModel.hasOwnProperty(eventName)) continue;
1531 Dygraph.addEvent(this.mouseEventElement_, eventName,
1532 bindHandler(interactionModel[eventName]));
1533 }
1534
1535 // If the user releases the mouse button during a drag, but not over the
1536 // canvas, then it doesn't count as a zooming action.
1537 Dygraph.addEvent(document, 'mouseup', function(event) {
1538 if (context.isZooming || context.isPanning) {
1539 context.isZooming = false;
1540 context.dragStartX = null;
1541 context.dragStartY = null;
1542 }
1543
1544 if (context.isPanning) {
1545 context.isPanning = false;
1546 context.draggingDate = null;
1547 context.dateRange = null;
1548 for (var i = 0; i < self.axes_.length; i++) {
1549 delete self.axes_[i].draggingValue;
1550 delete self.axes_[i].dragValueRange;
1551 }
1552 }
1553 });
1554 };
1555
1556
1557 /**
1558 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
1559 * up any previous zoom rectangles that were drawn. This could be optimized to
1560 * avoid extra redrawing, but it's tricky to avoid interactions with the status
1561 * dots.
1562 *
1563 * @param {Number} direction the direction of the zoom rectangle. Acceptable
1564 * values are Dygraph.HORIZONTAL and Dygraph.VERTICAL.
1565 * @param {Number} startX The X position where the drag started, in canvas
1566 * coordinates.
1567 * @param {Number} endX The current X position of the drag, in canvas coords.
1568 * @param {Number} startY The Y position where the drag started, in canvas
1569 * coordinates.
1570 * @param {Number} endY The current Y position of the drag, in canvas coords.
1571 * @param {Number} prevDirection the value of direction on the previous call to
1572 * this function. Used to avoid excess redrawing
1573 * @param {Number} prevEndX The value of endX on the previous call to this
1574 * function. Used to avoid excess redrawing
1575 * @param {Number} prevEndY The value of endY on the previous call to this
1576 * function. Used to avoid excess redrawing
1577 * @private
1578 */
1579 Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY,
1580 endY, prevDirection, prevEndX,
1581 prevEndY) {
1582 var ctx = this.canvas_ctx_;
1583
1584 // Clean up from the previous rect if necessary
1585 if (prevDirection == Dygraph.HORIZONTAL) {
1586 ctx.clearRect(Math.min(startX, prevEndX), 0,
1587 Math.abs(startX - prevEndX), this.height_);
1588 } else if (prevDirection == Dygraph.VERTICAL){
1589 ctx.clearRect(0, Math.min(startY, prevEndY),
1590 this.width_, Math.abs(startY - prevEndY));
1591 }
1592
1593 // Draw a light-grey rectangle to show the new viewing area
1594 if (direction == Dygraph.HORIZONTAL) {
1595 if (endX && startX) {
1596 ctx.fillStyle = "rgba(128,128,128,0.33)";
1597 ctx.fillRect(Math.min(startX, endX), 0,
1598 Math.abs(endX - startX), this.height_);
1599 }
1600 }
1601 if (direction == Dygraph.VERTICAL) {
1602 if (endY && startY) {
1603 ctx.fillStyle = "rgba(128,128,128,0.33)";
1604 ctx.fillRect(0, Math.min(startY, endY),
1605 this.width_, Math.abs(endY - startY));
1606 }
1607 }
1608 };
1609
1610 /**
1611 * Zoom to something containing [lowX, highX]. These are pixel coordinates in
1612 * the canvas. The exact zoom window may be slightly larger if there are no data
1613 * points near lowX or highX. Don't confuse this function with doZoomXDates,
1614 * which accepts dates that match the raw data. This function redraws the graph.
1615 *
1616 * @param {Number} lowX The leftmost pixel value that should be visible.
1617 * @param {Number} highX The rightmost pixel value that should be visible.
1618 * @private
1619 */
1620 Dygraph.prototype.doZoomX_ = function(lowX, highX) {
1621 // Find the earliest and latest dates contained in this canvasx range.
1622 // Convert the call to date ranges of the raw data.
1623 var minDate = this.toDataXCoord(lowX);
1624 var maxDate = this.toDataXCoord(highX);
1625 this.doZoomXDates_(minDate, maxDate);
1626 };
1627
1628 /**
1629 * Zoom to something containing [minDate, maxDate] values. Don't confuse this
1630 * method with doZoomX which accepts pixel coordinates. This function redraws
1631 * the graph.
1632 *
1633 * @param {Number} minDate The minimum date that should be visible.
1634 * @param {Number} maxDate The maximum date that should be visible.
1635 * @private
1636 */
1637 Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) {
1638 this.dateWindow_ = [minDate, maxDate];
1639 this.zoomed_x_ = true;
1640 this.drawGraph_();
1641 if (this.attr_("zoomCallback")) {
1642 this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges());
1643 }
1644 };
1645
1646 /**
1647 * Zoom to something containing [lowY, highY]. These are pixel coordinates in
1648 * the canvas. This function redraws the graph.
1649 *
1650 * @param {Number} lowY The topmost pixel value that should be visible.
1651 * @param {Number} highY The lowest pixel value that should be visible.
1652 * @private
1653 */
1654 Dygraph.prototype.doZoomY_ = function(lowY, highY) {
1655 // Find the highest and lowest values in pixel range for each axis.
1656 // Note that lowY (in pixels) corresponds to the max Value (in data coords).
1657 // This is because pixels increase as you go down on the screen, whereas data
1658 // coordinates increase as you go up the screen.
1659 var valueRanges = [];
1660 for (var i = 0; i < this.axes_.length; i++) {
1661 var hi = this.toDataYCoord(lowY, i);
1662 var low = this.toDataYCoord(highY, i);
1663 this.axes_[i].valueWindow = [low, hi];
1664 valueRanges.push([low, hi]);
1665 }
1666
1667 this.zoomed_y_ = true;
1668 this.drawGraph_();
1669 if (this.attr_("zoomCallback")) {
1670 var xRange = this.xAxisRange();
1671 var yRange = this.yAxisRange();
1672 this.attr_("zoomCallback")(xRange[0], xRange[1], this.yAxisRanges());
1673 }
1674 };
1675
1676 /**
1677 * Reset the zoom to the original view coordinates. This is the same as
1678 * double-clicking on the graph.
1679 *
1680 * @private
1681 */
1682 Dygraph.prototype.doUnzoom_ = function() {
1683 var dirty = false;
1684 if (this.dateWindow_ != null) {
1685 dirty = true;
1686 this.dateWindow_ = null;
1687 }
1688
1689 for (var i = 0; i < this.axes_.length; i++) {
1690 if (this.axes_[i].valueWindow != null) {
1691 dirty = true;
1692 delete this.axes_[i].valueWindow;
1693 }
1694 }
1695
1696 if (dirty) {
1697 // Putting the drawing operation before the callback because it resets
1698 // yAxisRange.
1699 this.zoomed_x_ = false;
1700 this.zoomed_y_ = false;
1701 this.drawGraph_();
1702 if (this.attr_("zoomCallback")) {
1703 var minDate = this.rawData_[0][0];
1704 var maxDate = this.rawData_[this.rawData_.length - 1][0];
1705 this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges());
1706 }
1707 }
1708 };
1709
1710 /**
1711 * When the mouse moves in the canvas, display information about a nearby data
1712 * point and draw dots over those points in the data series. This function
1713 * takes care of cleanup of previously-drawn dots.
1714 * @param {Object} event The mousemove event from the browser.
1715 * @private
1716 */
1717 Dygraph.prototype.mouseMove_ = function(event) {
1718 // This prevents JS errors when mousing over the canvas before data loads.
1719 var points = this.layout_.points;
1720 if (points === undefined) return;
1721
1722 var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_);
1723
1724 var lastx = -1;
1725 var lasty = -1;
1726
1727 // Loop through all the points and find the date nearest to our current
1728 // location.
1729 var minDist = 1e+100;
1730 var idx = -1;
1731 for (var i = 0; i < points.length; i++) {
1732 var point = points[i];
1733 if (point == null) continue;
1734 var dist = Math.abs(point.canvasx - canvasx);
1735 if (dist > minDist) continue;
1736 minDist = dist;
1737 idx = i;
1738 }
1739 if (idx >= 0) lastx = points[idx].xval;
1740
1741 // Extract the points we've selected
1742 this.selPoints_ = [];
1743 var l = points.length;
1744 if (!this.attr_("stackedGraph")) {
1745 for (var i = 0; i < l; i++) {
1746 if (points[i].xval == lastx) {
1747 this.selPoints_.push(points[i]);
1748 }
1749 }
1750 } else {
1751 // Need to 'unstack' points starting from the bottom
1752 var cumulative_sum = 0;
1753 for (var i = l - 1; i >= 0; i--) {
1754 if (points[i].xval == lastx) {
1755 var p = {}; // Clone the point since we modify it
1756 for (var k in points[i]) {
1757 p[k] = points[i][k];
1758 }
1759 p.yval -= cumulative_sum;
1760 cumulative_sum += p.yval;
1761 this.selPoints_.push(p);
1762 }
1763 }
1764 this.selPoints_.reverse();
1765 }
1766
1767 if (this.attr_("highlightCallback")) {
1768 var px = this.lastx_;
1769 if (px !== null && lastx != px) {
1770 // only fire if the selected point has changed.
1771 this.attr_("highlightCallback")(event, lastx, this.selPoints_, this.idxToRow_(idx));
1772 }
1773 }
1774
1775 // Save last x position for callbacks.
1776 this.lastx_ = lastx;
1777
1778 this.updateSelection_();
1779 };
1780
1781 /**
1782 * Transforms layout_.points index into data row number.
1783 * @param int layout_.points index
1784 * @return int row number, or -1 if none could be found.
1785 * @private
1786 */
1787 Dygraph.prototype.idxToRow_ = function(idx) {
1788 if (idx < 0) return -1;
1789
1790 for (var i in this.layout_.datasets) {
1791 if (idx < this.layout_.datasets[i].length) {
1792 return this.boundaryIds_[0][0]+idx;
1793 }
1794 idx -= this.layout_.datasets[i].length;
1795 }
1796 return -1;
1797 };
1798
1799 /**
1800 * @private
1801 * @param { Number } x The number to consider.
1802 * @return { Boolean } Whether the number is zero or NaN.
1803 */
1804 // TODO(danvk): rename this function to something like 'isNonZeroNan'.
1805 Dygraph.isOK = function(x) {
1806 return x && !isNaN(x);
1807 };
1808
1809 /**
1810 * @private
1811 * Generates HTML for the legend which is displayed when hovering over the
1812 * chart. If no selected points are specified, a default legend is returned
1813 * (this may just be the empty string).
1814 * @param { Number } [x] The x-value of the selected points.
1815 * @param { [Object] } [sel_points] List of selected points for the given
1816 * x-value. Should have properties like 'name', 'yval' and 'canvasy'.
1817 */
1818 Dygraph.prototype.generateLegendHTML_ = function(x, sel_points) {
1819 // If no points are selected, we display a default legend. Traditionally,
1820 // this has been blank. But a better default would be a conventional legend,
1821 // which provides essential information for a non-interactive chart.
1822 if (typeof(x) === 'undefined') {
1823 if (this.attr_('legend') != 'always') return '';
1824
1825 var sepLines = this.attr_('labelsSeparateLines');
1826 var labels = this.attr_('labels');
1827 var html = '';
1828 for (var i = 1; i < labels.length; i++) {
1829 if (!this.visibility()[i - 1]) continue;
1830 var c = this.plotter_.colors[labels[i]];
1831 if (html != '') html += (sepLines ? '<br/>' : ' ');
1832 html += "<b><span style='color: " + c + ";'>&mdash;" + labels[i] +
1833 "</span></b>";
1834 }
1835 return html;
1836 }
1837
1838 var html = this.attr_('xValueFormatter')(x) + ":";
1839
1840 var fmtFunc = this.attr_('yValueFormatter');
1841 var showZeros = this.attr_("labelsShowZeroValues");
1842 var sepLines = this.attr_("labelsSeparateLines");
1843 for (var i = 0; i < this.selPoints_.length; i++) {
1844 var pt = this.selPoints_[i];
1845 if (pt.yval == 0 && !showZeros) continue;
1846 if (!Dygraph.isOK(pt.canvasy)) continue;
1847 if (sepLines) html += "<br/>";
1848
1849 var c = this.plotter_.colors[pt.name];
1850 var yval = fmtFunc(pt.yval, this);
1851 // TODO(danvk): use a template string here and make it an attribute.
1852 html += " <b><span style='color: " + c + ";'>"
1853 + pt.name + "</span></b>:"
1854 + yval;
1855 }
1856 return html;
1857 };
1858
1859 /**
1860 * @private
1861 * Displays information about the selected points in the legend. If there is no
1862 * selection, the legend will be cleared.
1863 * @param { Number } [x] The x-value of the selected points.
1864 * @param { [Object] } [sel_points] List of selected points for the given
1865 * x-value. Should have properties like 'name', 'yval' and 'canvasy'.
1866 */
1867 Dygraph.prototype.setLegendHTML_ = function(x, sel_points) {
1868 var html = this.generateLegendHTML_(x, sel_points);
1869 var labelsDiv = this.attr_("labelsDiv");
1870 if (labelsDiv !== null) {
1871 labelsDiv.innerHTML = html;
1872 } else {
1873 if (typeof(this.shown_legend_error_) == 'undefined') {
1874 this.error('labelsDiv is set to something nonexistent; legend will not be shown.');
1875 this.shown_legend_error_ = true;
1876 }
1877 }
1878 };
1879
1880 /**
1881 * Draw dots over the selectied points in the data series. This function
1882 * takes care of cleanup of previously-drawn dots.
1883 * @private
1884 */
1885 Dygraph.prototype.updateSelection_ = function() {
1886 // Clear the previously drawn vertical, if there is one
1887 var ctx = this.canvas_ctx_;
1888 if (this.previousVerticalX_ >= 0) {
1889 // Determine the maximum highlight circle size.
1890 var maxCircleSize = 0;
1891 var labels = this.attr_('labels');
1892 for (var i = 1; i < labels.length; i++) {
1893 var r = this.attr_('highlightCircleSize', labels[i]);
1894 if (r > maxCircleSize) maxCircleSize = r;
1895 }
1896 var px = this.previousVerticalX_;
1897 ctx.clearRect(px - maxCircleSize - 1, 0,
1898 2 * maxCircleSize + 2, this.height_);
1899 }
1900
1901 if (this.selPoints_.length > 0) {
1902 // Set the status message to indicate the selected point(s)
1903 if (this.attr_('showLabelsOnHighlight')) {
1904 this.setLegendHTML_(this.lastx_, this.selPoints_);
1905 }
1906
1907 // Draw colored circles over the center of each selected point
1908 var canvasx = this.selPoints_[0].canvasx;
1909 ctx.save();
1910 for (var i = 0; i < this.selPoints_.length; i++) {
1911 var pt = this.selPoints_[i];
1912 if (!Dygraph.isOK(pt.canvasy)) continue;
1913
1914 var circleSize = this.attr_('highlightCircleSize', pt.name);
1915 ctx.beginPath();
1916 ctx.fillStyle = this.plotter_.colors[pt.name];
1917 ctx.arc(canvasx, pt.canvasy, circleSize, 0, 2 * Math.PI, false);
1918 ctx.fill();
1919 }
1920 ctx.restore();
1921
1922 this.previousVerticalX_ = canvasx;
1923 }
1924 };
1925
1926 /**
1927 * Manually set the selected points and display information about them in the
1928 * legend. The selection can be cleared using clearSelection() and queried
1929 * using getSelection().
1930 * @param { Integer } row number that should be highlighted (i.e. appear with
1931 * hover dots on the chart). Set to false to clear any selection.
1932 */
1933 Dygraph.prototype.setSelection = function(row) {
1934 // Extract the points we've selected
1935 this.selPoints_ = [];
1936 var pos = 0;
1937
1938 if (row !== false) {
1939 row = row-this.boundaryIds_[0][0];
1940 }
1941
1942 if (row !== false && row >= 0) {
1943 for (var i in this.layout_.datasets) {
1944 if (row < this.layout_.datasets[i].length) {
1945 var point = this.layout_.points[pos+row];
1946
1947 if (this.attr_("stackedGraph")) {
1948 point = this.layout_.unstackPointAtIndex(pos+row);
1949 }
1950
1951 this.selPoints_.push(point);
1952 }
1953 pos += this.layout_.datasets[i].length;
1954 }
1955 }
1956
1957 if (this.selPoints_.length) {
1958 this.lastx_ = this.selPoints_[0].xval;
1959 this.updateSelection_();
1960 } else {
1961 this.clearSelection();
1962 }
1963
1964 };
1965
1966 /**
1967 * The mouse has left the canvas. Clear out whatever artifacts remain
1968 * @param {Object} event the mouseout event from the browser.
1969 * @private
1970 */
1971 Dygraph.prototype.mouseOut_ = function(event) {
1972 if (this.attr_("unhighlightCallback")) {
1973 this.attr_("unhighlightCallback")(event);
1974 }
1975
1976 if (this.attr_("hideOverlayOnMouseOut")) {
1977 this.clearSelection();
1978 }
1979 };
1980
1981 /**
1982 * Clears the current selection (i.e. points that were highlighted by moving
1983 * the mouse over the chart).
1984 */
1985 Dygraph.prototype.clearSelection = function() {
1986 // Get rid of the overlay data
1987 this.canvas_ctx_.clearRect(0, 0, this.width_, this.height_);
1988 this.setLegendHTML_();
1989 this.selPoints_ = [];
1990 this.lastx_ = -1;
1991 }
1992
1993 /**
1994 * Returns the number of the currently selected row. To get data for this row,
1995 * you can use the getValue method.
1996 * @return { Integer } row number, or -1 if nothing is selected
1997 */
1998 Dygraph.prototype.getSelection = function() {
1999 if (!this.selPoints_ || this.selPoints_.length < 1) {
2000 return -1;
2001 }
2002
2003 for (var row=0; row<this.layout_.points.length; row++ ) {
2004 if (this.layout_.points[row].x == this.selPoints_[0].x) {
2005 return row + this.boundaryIds_[0][0];
2006 }
2007 }
2008 return -1;
2009 };
2010
2011 /**
2012 * Number formatting function which mimicks the behavior of %g in printf, i.e.
2013 * either exponential or fixed format (without trailing 0s) is used depending on
2014 * the length of the generated string. The advantage of this format is that
2015 * there is a predictable upper bound on the resulting string length,
2016 * significant figures are not dropped, and normal numbers are not displayed in
2017 * exponential notation.
2018 *
2019 * NOTE: JavaScript's native toPrecision() is NOT a drop-in replacement for %g.
2020 * It creates strings which are too long for absolute values between 10^-4 and
2021 * 10^-6, e.g. '0.00001' instead of '1e-5'. See tests/number-format.html for
2022 * output examples.
2023 *
2024 * @param {Number} x The number to format
2025 * @param {Number} opt_precision The precision to use, default 2.
2026 * @return {String} A string formatted like %g in printf. The max generated
2027 * string length should be precision + 6 (e.g 1.123e+300).
2028 */
2029 Dygraph.floatFormat = function(x, opt_precision) {
2030 // Avoid invalid precision values; [1, 21] is the valid range.
2031 var p = Math.min(Math.max(1, opt_precision || 2), 21);
2032
2033 // This is deceptively simple. The actual algorithm comes from:
2034 //
2035 // Max allowed length = p + 4
2036 // where 4 comes from 'e+n' and '.'.
2037 //
2038 // Length of fixed format = 2 + y + p
2039 // where 2 comes from '0.' and y = # of leading zeroes.
2040 //
2041 // Equating the two and solving for y yields y = 2, or 0.00xxxx which is
2042 // 1.0e-3.
2043 //
2044 // Since the behavior of toPrecision() is identical for larger numbers, we
2045 // don't have to worry about the other bound.
2046 //
2047 // Finally, the argument for toExponential() is the number of trailing digits,
2048 // so we take off 1 for the value before the '.'.
2049 return (Math.abs(x) < 1.0e-3 && x != 0.0) ?
2050 x.toExponential(p - 1) : x.toPrecision(p);
2051 };
2052
2053 /**
2054 * @private
2055 * Return a string version of a number. This respects the digitsAfterDecimal
2056 * and maxNumberWidth options.
2057 * @param {Number} x The number to be formatted
2058 * @param {Dygraph} g The dygraph object
2059 */
2060 Dygraph.numberFormatter = function(x, g) {
2061 var sigFigs = g.attr_('sigFigs');
2062
2063 if (sigFigs !== null) {
2064 // User has opted for a fixed number of significant figures.
2065 return Dygraph.floatFormat(x, sigFigs);
2066 }
2067
2068 var digits = g.attr_('digitsAfterDecimal');
2069 var maxNumberWidth = g.attr_('maxNumberWidth');
2070
2071 // switch to scientific notation if we underflow or overflow fixed display.
2072 if (x !== 0.0 &&
2073 (Math.abs(x) >= Math.pow(10, maxNumberWidth) ||
2074 Math.abs(x) < Math.pow(10, -digits))) {
2075 return x.toExponential(digits);
2076 } else {
2077 return '' + Dygraph.round_(x, digits);
2078 }
2079 };
2080
2081 /**
2082 * @private
2083 * Converts '9' to '09' (useful for dates)
2084 */
2085 Dygraph.zeropad = function(x) {
2086 if (x < 10) return "0" + x; else return "" + x;
2087 };
2088
2089 /**
2090 * Return a string version of the hours, minutes and seconds portion of a date.
2091 * @param {Number} date The JavaScript date (ms since epoch)
2092 * @return {String} A time of the form "HH:MM:SS"
2093 * @private
2094 */
2095 Dygraph.hmsString_ = function(date) {
2096 var zeropad = Dygraph.zeropad;
2097 var d = new Date(date);
2098 if (d.getSeconds()) {
2099 return zeropad(d.getHours()) + ":" +
2100 zeropad(d.getMinutes()) + ":" +
2101 zeropad(d.getSeconds());
2102 } else {
2103 return zeropad(d.getHours()) + ":" + zeropad(d.getMinutes());
2104 }
2105 };
2106
2107 /**
2108 * Convert a JS date to a string appropriate to display on an axis that
2109 * is displaying values at the stated granularity.
2110 * @param {Date} date The date to format
2111 * @param {Number} granularity One of the Dygraph granularity constants
2112 * @return {String} The formatted date
2113 * @private
2114 */
2115 Dygraph.dateAxisFormatter = function(date, granularity) {
2116 if (granularity >= Dygraph.DECADAL) {
2117 return date.strftime('%Y');
2118 } else if (granularity >= Dygraph.MONTHLY) {
2119 return date.strftime('%b %y');
2120 } else {
2121 var frac = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds();
2122 if (frac == 0 || granularity >= Dygraph.DAILY) {
2123 return new Date(date.getTime() + 3600*1000).strftime('%d%b');
2124 } else {
2125 return Dygraph.hmsString_(date.getTime());
2126 }
2127 }
2128 };
2129
2130 /**
2131 * Convert a JS date (millis since epoch) to YYYY/MM/DD
2132 * @param {Number} date The JavaScript date (ms since epoch)
2133 * @return {String} A date of the form "YYYY/MM/DD"
2134 * @private
2135 */
2136 Dygraph.dateString_ = function(date) {
2137 var zeropad = Dygraph.zeropad;
2138 var d = new Date(date);
2139
2140 // Get the year:
2141 var year = "" + d.getFullYear();
2142 // Get a 0 padded month string
2143 var month = zeropad(d.getMonth() + 1); //months are 0-offset, sigh
2144 // Get a 0 padded day string
2145 var day = zeropad(d.getDate());
2146
2147 var ret = "";
2148 var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
2149 if (frac) ret = " " + Dygraph.hmsString_(date);
2150
2151 return year + "/" + month + "/" + day + ret;
2152 };
2153
2154 /**
2155 * Round a number to the specified number of digits past the decimal point.
2156 * @param {Number} num The number to round
2157 * @param {Number} places The number of decimals to which to round
2158 * @return {Number} The rounded number
2159 * @private
2160 */
2161 Dygraph.round_ = function(num, places) {
2162 var shift = Math.pow(10, places);
2163 return Math.round(num * shift)/shift;
2164 };
2165
2166 /**
2167 * Fires when there's data available to be graphed.
2168 * @param {String} data Raw CSV data to be plotted
2169 * @private
2170 */
2171 Dygraph.prototype.loadedEvent_ = function(data) {
2172 this.rawData_ = this.parseCSV_(data);
2173 this.predraw_();
2174 };
2175
2176 Dygraph.prototype.months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
2177 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
2178 Dygraph.prototype.quarters = ["Jan", "Apr", "Jul", "Oct"];
2179
2180 /**
2181 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
2182 * @private
2183 */
2184 Dygraph.prototype.addXTicks_ = function() {
2185 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
2186 var range;
2187 if (this.dateWindow_) {
2188 range = [this.dateWindow_[0], this.dateWindow_[1]];
2189 } else {
2190 range = [this.rawData_[0][0], this.rawData_[this.rawData_.length - 1][0]];
2191 }
2192
2193 var xTicks = this.attr_('xTicker')(range[0], range[1], this);
2194 this.layout_.setXTicks(xTicks);
2195 };
2196
2197 // Time granularity enumeration
2198 Dygraph.SECONDLY = 0;
2199 Dygraph.TWO_SECONDLY = 1;
2200 Dygraph.FIVE_SECONDLY = 2;
2201 Dygraph.TEN_SECONDLY = 3;
2202 Dygraph.THIRTY_SECONDLY = 4;
2203 Dygraph.MINUTELY = 5;
2204 Dygraph.TWO_MINUTELY = 6;
2205 Dygraph.FIVE_MINUTELY = 7;
2206 Dygraph.TEN_MINUTELY = 8;
2207 Dygraph.THIRTY_MINUTELY = 9;
2208 Dygraph.HOURLY = 10;
2209 Dygraph.TWO_HOURLY = 11;
2210 Dygraph.SIX_HOURLY = 12;
2211 Dygraph.DAILY = 13;
2212 Dygraph.WEEKLY = 14;
2213 Dygraph.MONTHLY = 15;
2214 Dygraph.QUARTERLY = 16;
2215 Dygraph.BIANNUAL = 17;
2216 Dygraph.ANNUAL = 18;
2217 Dygraph.DECADAL = 19;
2218 Dygraph.CENTENNIAL = 20;
2219 Dygraph.NUM_GRANULARITIES = 21;
2220
2221 Dygraph.SHORT_SPACINGS = [];
2222 Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY] = 1000 * 1;
2223 Dygraph.SHORT_SPACINGS[Dygraph.TWO_SECONDLY] = 1000 * 2;
2224 Dygraph.SHORT_SPACINGS[Dygraph.FIVE_SECONDLY] = 1000 * 5;
2225 Dygraph.SHORT_SPACINGS[Dygraph.TEN_SECONDLY] = 1000 * 10;
2226 Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_SECONDLY] = 1000 * 30;
2227 Dygraph.SHORT_SPACINGS[Dygraph.MINUTELY] = 1000 * 60;
2228 Dygraph.SHORT_SPACINGS[Dygraph.TWO_MINUTELY] = 1000 * 60 * 2;
2229 Dygraph.SHORT_SPACINGS[Dygraph.FIVE_MINUTELY] = 1000 * 60 * 5;
2230 Dygraph.SHORT_SPACINGS[Dygraph.TEN_MINUTELY] = 1000 * 60 * 10;
2231 Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_MINUTELY] = 1000 * 60 * 30;
2232 Dygraph.SHORT_SPACINGS[Dygraph.HOURLY] = 1000 * 3600;
2233 Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY] = 1000 * 3600 * 2;
2234 Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY] = 1000 * 3600 * 6;
2235 Dygraph.SHORT_SPACINGS[Dygraph.DAILY] = 1000 * 86400;
2236 Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY] = 1000 * 604800;
2237
2238 /**
2239 * @private
2240 * If we used this time granularity, how many ticks would there be?
2241 * This is only an approximation, but it's generally good enough.
2242 */
2243 Dygraph.prototype.NumXTicks = function(start_time, end_time, granularity) {
2244 if (granularity < Dygraph.MONTHLY) {
2245 // Generate one tick mark for every fixed interval of time.
2246 var spacing = Dygraph.SHORT_SPACINGS[granularity];
2247 return Math.floor(0.5 + 1.0 * (end_time - start_time) / spacing);
2248 } else {
2249 var year_mod = 1; // e.g. to only print one point every 10 years.
2250 var num_months = 12;
2251 if (granularity == Dygraph.QUARTERLY) num_months = 3;
2252 if (granularity == Dygraph.BIANNUAL) num_months = 2;
2253 if (granularity == Dygraph.ANNUAL) num_months = 1;
2254 if (granularity == Dygraph.DECADAL) { num_months = 1; year_mod = 10; }
2255 if (granularity == Dygraph.CENTENNIAL) { num_months = 1; year_mod = 100; }
2256
2257 var msInYear = 365.2524 * 24 * 3600 * 1000;
2258 var num_years = 1.0 * (end_time - start_time) / msInYear;
2259 return Math.floor(0.5 + 1.0 * num_years * num_months / year_mod);
2260 }
2261 };
2262
2263 /**
2264 * @private
2265 *
2266 * Construct an x-axis of nicely-formatted times on meaningful boundaries
2267 * (e.g. 'Jan 09' rather than 'Jan 22, 2009').
2268 *
2269 * Returns an array containing {v: millis, label: label} dictionaries.
2270 */
2271 Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) {
2272 var formatter = this.attr_("xAxisLabelFormatter");
2273 var ticks = [];
2274 if (granularity < Dygraph.MONTHLY) {
2275 // Generate one tick mark for every fixed interval of time.
2276 var spacing = Dygraph.SHORT_SPACINGS[granularity];
2277 var format = '%d%b'; // e.g. "1Jan"
2278
2279 // Find a time less than start_time which occurs on a "nice" time boundary
2280 // for this granularity.
2281 var g = spacing / 1000;
2282 var d = new Date(start_time);
2283 if (g <= 60) { // seconds
2284 var x = d.getSeconds(); d.setSeconds(x - x % g);
2285 } else {
2286 d.setSeconds(0);
2287 g /= 60;
2288 if (g <= 60) { // minutes
2289 var x = d.getMinutes(); d.setMinutes(x - x % g);
2290 } else {
2291 d.setMinutes(0);
2292 g /= 60;
2293
2294 if (g <= 24) { // days
2295 var x = d.getHours(); d.setHours(x - x % g);
2296 } else {
2297 d.setHours(0);
2298 g /= 24;
2299
2300 if (g == 7) { // one week
2301 d.setDate(d.getDate() - d.getDay());
2302 }
2303 }
2304 }
2305 }
2306 start_time = d.getTime();
2307
2308 for (var t = start_time; t <= end_time; t += spacing) {
2309 ticks.push({ v:t, label: formatter(new Date(t), granularity) });
2310 }
2311 } else {
2312 // Display a tick mark on the first of a set of months of each year.
2313 // Years get a tick mark iff y % year_mod == 0. This is useful for
2314 // displaying a tick mark once every 10 years, say, on long time scales.
2315 var months;
2316 var year_mod = 1; // e.g. to only print one point every 10 years.
2317
2318 if (granularity == Dygraph.MONTHLY) {
2319 months = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
2320 } else if (granularity == Dygraph.QUARTERLY) {
2321 months = [ 0, 3, 6, 9 ];
2322 } else if (granularity == Dygraph.BIANNUAL) {
2323 months = [ 0, 6 ];
2324 } else if (granularity == Dygraph.ANNUAL) {
2325 months = [ 0 ];
2326 } else if (granularity == Dygraph.DECADAL) {
2327 months = [ 0 ];
2328 year_mod = 10;
2329 } else if (granularity == Dygraph.CENTENNIAL) {
2330 months = [ 0 ];
2331 year_mod = 100;
2332 } else {
2333 this.warn("Span of dates is too long");
2334 }
2335
2336 var start_year = new Date(start_time).getFullYear();
2337 var end_year = new Date(end_time).getFullYear();
2338 var zeropad = Dygraph.zeropad;
2339 for (var i = start_year; i <= end_year; i++) {
2340 if (i % year_mod != 0) continue;
2341 for (var j = 0; j < months.length; j++) {
2342 var date_str = i + "/" + zeropad(1 + months[j]) + "/01";
2343 var t = Dygraph.dateStrToMillis(date_str);
2344 if (t < start_time || t > end_time) continue;
2345 ticks.push({ v:t, label: formatter(new Date(t), granularity) });
2346 }
2347 }
2348 }
2349
2350 return ticks;
2351 };
2352
2353
2354 /**
2355 * Add ticks to the x-axis based on a date range.
2356 * @param {Number} startDate Start of the date window (millis since epoch)
2357 * @param {Number} endDate End of the date window (millis since epoch)
2358 * @param {Dygraph} self The dygraph object
2359 * @return { [Object] } Array of {label, value} tuples.
2360 * @public
2361 */
2362 Dygraph.dateTicker = function(startDate, endDate, self) {
2363 // TODO(danvk): why does this take 'self' as a param?
2364 var chosen = -1;
2365 for (var i = 0; i < Dygraph.NUM_GRANULARITIES; i++) {
2366 var num_ticks = self.NumXTicks(startDate, endDate, i);
2367 if (self.width_ / num_ticks >= self.attr_('pixelsPerXLabel')) {
2368 chosen = i;
2369 break;
2370 }
2371 }
2372
2373 if (chosen >= 0) {
2374 return self.GetXAxis(startDate, endDate, chosen);
2375 } else {
2376 // TODO(danvk): signal error.
2377 }
2378 };
2379
2380 /**
2381 * @private
2382 * This is a list of human-friendly values at which to show tick marks on a log
2383 * scale. It is k * 10^n, where k=1..9 and n=-39..+39, so:
2384 * ..., 1, 2, 3, 4, 5, ..., 9, 10, 20, 30, ..., 90, 100, 200, 300, ...
2385 * NOTE: this assumes that Dygraph.LOG_SCALE = 10.
2386 */
2387 Dygraph.PREFERRED_LOG_TICK_VALUES = function() {
2388 var vals = [];
2389 for (var power = -39; power <= 39; power++) {
2390 var range = Math.pow(10, power);
2391 for (var mult = 1; mult <= 9; mult++) {
2392 var val = range * mult;
2393 vals.push(val);
2394 }
2395 }
2396 return vals;
2397 }();
2398
2399 /**
2400 * @private
2401 * Implementation of binary search over an array.
2402 * Currently does not work when val is outside the range of arry's values.
2403 * @param { Integer } val the value to search for
2404 * @param { Integer[] } arry is the value over which to search
2405 * @param { Integer } abs If abs > 0, find the lowest entry greater than val
2406 * If abs < 0, find the highest entry less than val.
2407 * if abs == 0, find the entry that equals val.
2408 * @param { Integer } [low] The first index in arry to consider (optional)
2409 * @param { Integer } [high] The last index in arry to consider (optional)
2410 */
2411 Dygraph.binarySearch = function(val, arry, abs, low, high) {
2412 if (low == null || high == null) {
2413 low = 0;
2414 high = arry.length - 1;
2415 }
2416 if (low > high) {
2417 return -1;
2418 }
2419 if (abs == null) {
2420 abs = 0;
2421 }
2422 var validIndex = function(idx) {
2423 return idx >= 0 && idx < arry.length;
2424 }
2425 var mid = parseInt((low + high) / 2);
2426 var element = arry[mid];
2427 if (element == val) {
2428 return mid;
2429 }
2430 if (element > val) {
2431 if (abs > 0) {
2432 // Accept if element > val, but also if prior element < val.
2433 var idx = mid - 1;
2434 if (validIndex(idx) && arry[idx] < val) {
2435 return mid;
2436 }
2437 }
2438 return Dygraph.binarySearch(val, arry, abs, low, mid - 1);
2439 }
2440 if (element < val) {
2441 if (abs < 0) {
2442 // Accept if element < val, but also if prior element > val.
2443 var idx = mid + 1;
2444 if (validIndex(idx) && arry[idx] > val) {
2445 return mid;
2446 }
2447 }
2448 return Dygraph.binarySearch(val, arry, abs, mid + 1, high);
2449 }
2450 };
2451
2452 // TODO(konigsberg): Update comment.
2453 /**
2454 * Add ticks when the x axis has numbers on it (instead of dates)
2455 *
2456 * @param {Number} minV minimum value
2457 * @param {Number} maxV maximum value
2458 * @param self
2459 * @param {function} attribute accessor function.
2460 * @return {[Object]} Array of {label, value} tuples.
2461 */
2462 Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) {
2463 var attr = function(k) {
2464 if (axis_props && axis_props.hasOwnProperty(k)) return axis_props[k];
2465 return self.attr_(k);
2466 };
2467
2468 var ticks = [];
2469 if (vals) {
2470 for (var i = 0; i < vals.length; i++) {
2471 ticks.push({v: vals[i]});
2472 }
2473 } else {
2474 if (axis_props && attr("logscale")) {
2475 var pixelsPerTick = attr('pixelsPerYLabel');
2476 // NOTE(konigsberg): Dan, should self.height_ be self.plotter_.area.h?
2477 var nTicks = Math.floor(self.height_ / pixelsPerTick);
2478 var minIdx = Dygraph.binarySearch(minV, Dygraph.PREFERRED_LOG_TICK_VALUES, 1);
2479 var maxIdx = Dygraph.binarySearch(maxV, Dygraph.PREFERRED_LOG_TICK_VALUES, -1);
2480 if (minIdx == -1) {
2481 minIdx = 0;
2482 }
2483 if (maxIdx == -1) {
2484 maxIdx = Dygraph.PREFERRED_LOG_TICK_VALUES.length - 1;
2485 }
2486 // Count the number of tick values would appear, if we can get at least
2487 // nTicks / 4 accept them.
2488 var lastDisplayed = null;
2489 if (maxIdx - minIdx >= nTicks / 4) {
2490 var axisId = axis_props.yAxisId;
2491 for (var idx = maxIdx; idx >= minIdx; idx--) {
2492 var tickValue = Dygraph.PREFERRED_LOG_TICK_VALUES[idx];
2493 var domCoord = axis_props.g.toDomYCoord(tickValue, axisId);
2494 var tick = { v: tickValue };
2495 if (lastDisplayed == null) {
2496 lastDisplayed = {
2497 tickValue : tickValue,
2498 domCoord : domCoord
2499 };
2500 } else {
2501 if (domCoord - lastDisplayed.domCoord >= pixelsPerTick) {
2502 lastDisplayed = {
2503 tickValue : tickValue,
2504 domCoord : domCoord
2505 };
2506 } else {
2507 tick.label = "";
2508 }
2509 }
2510 ticks.push(tick);
2511 }
2512 // Since we went in backwards order.
2513 ticks.reverse();
2514 }
2515 }
2516
2517 // ticks.length won't be 0 if the log scale function finds values to insert.
2518 if (ticks.length == 0) {
2519 // Basic idea:
2520 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
2521 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks).
2522 // The first spacing greater than pixelsPerYLabel is what we use.
2523 // TODO(danvk): version that works on a log scale.
2524 if (attr("labelsKMG2")) {
2525 var mults = [1, 2, 4, 8];
2526 } else {
2527 var mults = [1, 2, 5];
2528 }
2529 var scale, low_val, high_val, nTicks;
2530 // TODO(danvk): make it possible to set this for x- and y-axes independently.
2531 var pixelsPerTick = attr('pixelsPerYLabel');
2532 for (var i = -10; i < 50; i++) {
2533 if (attr("labelsKMG2")) {
2534 var base_scale = Math.pow(16, i);
2535 } else {
2536 var base_scale = Math.pow(10, i);
2537 }
2538 for (var j = 0; j < mults.length; j++) {
2539 scale = base_scale * mults[j];
2540 low_val = Math.floor(minV / scale) * scale;
2541 high_val = Math.ceil(maxV / scale) * scale;
2542 nTicks = Math.abs(high_val - low_val) / scale;
2543 var spacing = self.height_ / nTicks;
2544 // wish I could break out of both loops at once...
2545 if (spacing > pixelsPerTick) break;
2546 }
2547 if (spacing > pixelsPerTick) break;
2548 }
2549
2550 // Construct the set of ticks.
2551 // Allow reverse y-axis if it's explicitly requested.
2552 if (low_val > high_val) scale *= -1;
2553 for (var i = 0; i < nTicks; i++) {
2554 var tickV = low_val + i * scale;
2555 ticks.push( {v: tickV} );
2556 }
2557 }
2558 }
2559
2560 // Add formatted labels to the ticks.
2561 var k;
2562 var k_labels = [];
2563 if (attr("labelsKMB")) {
2564 k = 1000;
2565 k_labels = [ "K", "M", "B", "T" ];
2566 }
2567 if (attr("labelsKMG2")) {
2568 if (k) self.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
2569 k = 1024;
2570 k_labels = [ "k", "M", "G", "T" ];
2571 }
2572 var formatter = attr('yAxisLabelFormatter') ?
2573 attr('yAxisLabelFormatter') : attr('yValueFormatter');
2574
2575 // Add labels to the ticks.
2576 for (var i = 0; i < ticks.length; i++) {
2577 if (ticks[i].label !== undefined) continue; // Use current label.
2578 var tickV = ticks[i].v;
2579 var absTickV = Math.abs(tickV);
2580 var label = formatter(tickV, self);
2581 if (k_labels.length > 0) {
2582 // Round up to an appropriate unit.
2583 var n = k*k*k*k;
2584 for (var j = 3; j >= 0; j--, n /= k) {
2585 if (absTickV >= n) {
2586 label = Dygraph.round_(tickV / n, attr('digitsAfterDecimal')) + k_labels[j];
2587 break;
2588 }
2589 }
2590 }
2591 ticks[i].label = label;
2592 }
2593
2594 return ticks;
2595 };
2596
2597 /**
2598 * @private
2599 * Computes the range of the data series (including confidence intervals).
2600 * @param { [Array] } series either [ [x1, y1], [x2, y2], ... ] or
2601 * [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
2602 * @return [low, high]
2603 */
2604 Dygraph.prototype.extremeValues_ = function(series) {
2605 var minY = null, maxY = null;
2606
2607 var bars = this.attr_("errorBars") || this.attr_("customBars");
2608 if (bars) {
2609 // With custom bars, maxY is the max of the high values.
2610 for (var j = 0; j < series.length; j++) {
2611 var y = series[j][1][0];
2612 if (!y) continue;
2613 var low = y - series[j][1][1];
2614 var high = y + series[j][1][2];
2615 if (low > y) low = y; // this can happen with custom bars,
2616 if (high < y) high = y; // e.g. in tests/custom-bars.html
2617 if (maxY == null || high > maxY) {
2618 maxY = high;
2619 }
2620 if (minY == null || low < minY) {
2621 minY = low;
2622 }
2623 }
2624 } else {
2625 for (var j = 0; j < series.length; j++) {
2626 var y = series[j][1];
2627 if (y === null || isNaN(y)) continue;
2628 if (maxY == null || y > maxY) {
2629 maxY = y;
2630 }
2631 if (minY == null || y < minY) {
2632 minY = y;
2633 }
2634 }
2635 }
2636
2637 return [minY, maxY];
2638 };
2639
2640 /**
2641 * @private
2642 * This function is called once when the chart's data is changed or the options
2643 * dictionary is updated. It is _not_ called when the user pans or zooms. The
2644 * idea is that values derived from the chart's data can be computed here,
2645 * rather than every time the chart is drawn. This includes things like the
2646 * number of axes, rolling averages, etc.
2647 */
2648 Dygraph.prototype.predraw_ = function() {
2649 // TODO(danvk): move more computations out of drawGraph_ and into here.
2650 this.computeYAxes_();
2651
2652 // Create a new plotter.
2653 if (this.plotter_) this.plotter_.clear();
2654 this.plotter_ = new DygraphCanvasRenderer(this,
2655 this.hidden_,
2656 this.hidden_ctx_,
2657 this.layout_,
2658 this.renderOptions_);
2659
2660 // The roller sits in the bottom left corner of the chart. We don't know where
2661 // this will be until the options are available, so it's positioned here.
2662 this.createRollInterface_();
2663
2664 // Same thing applies for the labelsDiv. It's right edge should be flush with
2665 // the right edge of the charting area (which may not be the same as the right
2666 // edge of the div, if we have two y-axes.
2667 this.positionLabelsDiv_();
2668
2669 // If the data or options have changed, then we'd better redraw.
2670 this.drawGraph_();
2671 };
2672
2673 /**
2674 * Update the graph with new data. This method is called when the viewing area
2675 * has changed. If the underlying data or options have changed, predraw_ will
2676 * be called before drawGraph_ is called.
2677 * @private
2678 */
2679 Dygraph.prototype.drawGraph_ = function() {
2680 var data = this.rawData_;
2681
2682 // This is used to set the second parameter to drawCallback, below.
2683 var is_initial_draw = this.is_initial_draw_;
2684 this.is_initial_draw_ = false;
2685
2686 var minY = null, maxY = null;
2687 this.layout_.removeAllDatasets();
2688 this.setColors_();
2689 this.attrs_['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
2690
2691 // Loop over the fields (series). Go from the last to the first,
2692 // because if they're stacked that's how we accumulate the values.
2693
2694 var cumulative_y = []; // For stacked series.
2695 var datasets = [];
2696
2697 var extremes = {}; // series name -> [low, high]
2698
2699 // Loop over all fields and create datasets
2700 for (var i = data[0].length - 1; i >= 1; i--) {
2701 if (!this.visibility()[i - 1]) continue;
2702
2703 var seriesName = this.attr_("labels")[i];
2704 var connectSeparatedPoints = this.attr_('connectSeparatedPoints', i);
2705 var logScale = this.attr_('logscale', i);
2706
2707 var series = [];
2708 for (var j = 0; j < data.length; j++) {
2709 var date = data[j][0];
2710 var point = data[j][i];
2711 if (logScale) {
2712 // On the log scale, points less than zero do not exist.
2713 // This will create a gap in the chart. Note that this ignores
2714 // connectSeparatedPoints.
2715 if (point <= 0) {
2716 point = null;
2717 }
2718 series.push([date, point]);
2719 } else {
2720 if (point != null || !connectSeparatedPoints) {
2721 series.push([date, point]);
2722 }
2723 }
2724 }
2725
2726 // TODO(danvk): move this into predraw_. It's insane to do it here.
2727 series = this.rollingAverage(series, this.rollPeriod_);
2728
2729 // Prune down to the desired range, if necessary (for zooming)
2730 // Because there can be lines going to points outside of the visible area,
2731 // we actually prune to visible points, plus one on either side.
2732 var bars = this.attr_("errorBars") || this.attr_("customBars");
2733 if (this.dateWindow_) {
2734 var low = this.dateWindow_[0];
2735 var high= this.dateWindow_[1];
2736 var pruned = [];
2737 // TODO(danvk): do binary search instead of linear search.
2738 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
2739 var firstIdx = null, lastIdx = null;
2740 for (var k = 0; k < series.length; k++) {
2741 if (series[k][0] >= low && firstIdx === null) {
2742 firstIdx = k;
2743 }
2744 if (series[k][0] <= high) {
2745 lastIdx = k;
2746 }
2747 }
2748 if (firstIdx === null) firstIdx = 0;
2749 if (firstIdx > 0) firstIdx--;
2750 if (lastIdx === null) lastIdx = series.length - 1;
2751 if (lastIdx < series.length - 1) lastIdx++;
2752 this.boundaryIds_[i-1] = [firstIdx, lastIdx];
2753 for (var k = firstIdx; k <= lastIdx; k++) {
2754 pruned.push(series[k]);
2755 }
2756 series = pruned;
2757 } else {
2758 this.boundaryIds_[i-1] = [0, series.length-1];
2759 }
2760
2761 var seriesExtremes = this.extremeValues_(series);
2762
2763 if (bars) {
2764 for (var j=0; j<series.length; j++) {
2765 val = [series[j][0], series[j][1][0], series[j][1][1], series[j][1][2]];
2766 series[j] = val;
2767 }
2768 } else if (this.attr_("stackedGraph")) {
2769 var l = series.length;
2770 var actual_y;
2771 for (var j = 0; j < l; j++) {
2772 // If one data set has a NaN, let all subsequent stacked
2773 // sets inherit the NaN -- only start at 0 for the first set.
2774 var x = series[j][0];
2775 if (cumulative_y[x] === undefined) {
2776 cumulative_y[x] = 0;
2777 }
2778
2779 actual_y = series[j][1];
2780 cumulative_y[x] += actual_y;
2781
2782 series[j] = [x, cumulative_y[x]]
2783
2784 if (cumulative_y[x] > seriesExtremes[1]) {
2785 seriesExtremes[1] = cumulative_y[x];
2786 }
2787 if (cumulative_y[x] < seriesExtremes[0]) {
2788 seriesExtremes[0] = cumulative_y[x];
2789 }
2790 }
2791 }
2792 extremes[seriesName] = seriesExtremes;
2793
2794 datasets[i] = series;
2795 }
2796
2797 for (var i = 1; i < datasets.length; i++) {
2798 if (!this.visibility()[i - 1]) continue;
2799 this.layout_.addDataset(this.attr_("labels")[i], datasets[i]);
2800 }
2801
2802 this.computeYAxisRanges_(extremes);
2803 this.layout_.setYAxes(this.axes_);
2804
2805 this.addXTicks_();
2806
2807 // Save the X axis zoomed status as the updateOptions call will tend to set it erroneously
2808 var tmp_zoomed_x = this.zoomed_x_;
2809 // Tell PlotKit to use this new data and render itself
2810 this.layout_.setDateWindow(this.dateWindow_);
2811 this.zoomed_x_ = tmp_zoomed_x;
2812 this.layout_.evaluateWithError();
2813 this.plotter_.clear();
2814 this.plotter_.render();
2815 this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width,
2816 this.canvas_.height);
2817
2818 if (is_initial_draw) {
2819 // Generate a static legend before any particular point is selected.
2820 this.setLegendHTML_();
2821 } else {
2822 if (typeof(this.selPoints_) !== 'undefined' && this.selPoints_.length) {
2823 this.lastx_ = this.selPoints_[0].xval;
2824 this.updateSelection_();
2825 } else {
2826 this.clearSelection();
2827 }
2828 }
2829
2830 if (this.attr_("drawCallback") !== null) {
2831 this.attr_("drawCallback")(this, is_initial_draw);
2832 }
2833 };
2834
2835 /**
2836 * @private
2837 * Determine properties of the y-axes which are independent of the data
2838 * currently being displayed. This includes things like the number of axes and
2839 * the style of the axes. It does not include the range of each axis and its
2840 * tick marks.
2841 * This fills in this.axes_ and this.seriesToAxisMap_.
2842 * axes_ = [ { options } ]
2843 * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... }
2844 * indices are into the axes_ array.
2845 */
2846 Dygraph.prototype.computeYAxes_ = function() {
2847 this.axes_ = [{ yAxisId : 0, g : this }]; // always have at least one y-axis.
2848 this.seriesToAxisMap_ = {};
2849
2850 // Get a list of series names.
2851 var labels = this.attr_("labels");
2852 var series = {};
2853 for (var i = 1; i < labels.length; i++) series[labels[i]] = (i - 1);
2854
2855 // all options which could be applied per-axis:
2856 var axisOptions = [
2857 'includeZero',
2858 'valueRange',
2859 'labelsKMB',
2860 'labelsKMG2',
2861 'pixelsPerYLabel',
2862 'yAxisLabelWidth',
2863 'axisLabelFontSize',
2864 'axisTickSize',
2865 'logscale'
2866 ];
2867
2868 // Copy global axis options over to the first axis.
2869 for (var i = 0; i < axisOptions.length; i++) {
2870 var k = axisOptions[i];
2871 var v = this.attr_(k);
2872 if (v) this.axes_[0][k] = v;
2873 }
2874
2875 // Go through once and add all the axes.
2876 for (var seriesName in series) {
2877 if (!series.hasOwnProperty(seriesName)) continue;
2878 var axis = this.attr_("axis", seriesName);
2879 if (axis == null) {
2880 this.seriesToAxisMap_[seriesName] = 0;
2881 continue;
2882 }
2883 if (typeof(axis) == 'object') {
2884 // Add a new axis, making a copy of its per-axis options.
2885 var opts = {};
2886 Dygraph.update(opts, this.axes_[0]);
2887 Dygraph.update(opts, { valueRange: null }); // shouldn't inherit this.
2888 var yAxisId = this.axes_.length;
2889 opts.yAxisId = yAxisId;
2890 opts.g = this;
2891 Dygraph.update(opts, axis);
2892 this.axes_.push(opts);
2893 this.seriesToAxisMap_[seriesName] = yAxisId;
2894 }
2895 }
2896
2897 // Go through one more time and assign series to an axis defined by another
2898 // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } }
2899 for (var seriesName in series) {
2900 if (!series.hasOwnProperty(seriesName)) continue;
2901 var axis = this.attr_("axis", seriesName);
2902 if (typeof(axis) == 'string') {
2903 if (!this.seriesToAxisMap_.hasOwnProperty(axis)) {
2904 this.error("Series " + seriesName + " wants to share a y-axis with " +
2905 "series " + axis + ", which does not define its own axis.");
2906 return null;
2907 }
2908 var idx = this.seriesToAxisMap_[axis];
2909 this.seriesToAxisMap_[seriesName] = idx;
2910 }
2911 }
2912
2913 // Now we remove series from seriesToAxisMap_ which are not visible. We do
2914 // this last so that hiding the first series doesn't destroy the axis
2915 // properties of the primary axis.
2916 var seriesToAxisFiltered = {};
2917 var vis = this.visibility();
2918 for (var i = 1; i < labels.length; i++) {
2919 var s = labels[i];
2920 if (vis[i - 1]) seriesToAxisFiltered[s] = this.seriesToAxisMap_[s];
2921 }
2922 this.seriesToAxisMap_ = seriesToAxisFiltered;
2923 };
2924
2925 /**
2926 * Returns the number of y-axes on the chart.
2927 * @return {Number} the number of axes.
2928 */
2929 Dygraph.prototype.numAxes = function() {
2930 var last_axis = 0;
2931 for (var series in this.seriesToAxisMap_) {
2932 if (!this.seriesToAxisMap_.hasOwnProperty(series)) continue;
2933 var idx = this.seriesToAxisMap_[series];
2934 if (idx > last_axis) last_axis = idx;
2935 }
2936 return 1 + last_axis;
2937 };
2938
2939 /**
2940 * @private
2941 * Returns axis properties for the given series.
2942 * @param { String } setName The name of the series for which to get axis
2943 * properties, e.g. 'Y1'.
2944 * @return { Object } The axis properties.
2945 */
2946 Dygraph.prototype.axisPropertiesForSeries = function(series) {
2947 // TODO(danvk): handle errors.
2948 return this.axes_[this.seriesToAxisMap_[series]];
2949 };
2950
2951 /**
2952 * @private
2953 * Determine the value range and tick marks for each axis.
2954 * @param {Object} extremes A mapping from seriesName -> [low, high]
2955 * This fills in the valueRange and ticks fields in each entry of this.axes_.
2956 */
2957 Dygraph.prototype.computeYAxisRanges_ = function(extremes) {
2958 // Build a map from axis number -> [list of series names]
2959 var seriesForAxis = [];
2960 for (var series in this.seriesToAxisMap_) {
2961 if (!this.seriesToAxisMap_.hasOwnProperty(series)) continue;
2962 var idx = this.seriesToAxisMap_[series];
2963 while (seriesForAxis.length <= idx) seriesForAxis.push([]);
2964 seriesForAxis[idx].push(series);
2965 }
2966
2967 // Compute extreme values, a span and tick marks for each axis.
2968 for (var i = 0; i < this.axes_.length; i++) {
2969 var axis = this.axes_[i];
2970
2971 if (!seriesForAxis[i]) {
2972 // If no series are defined or visible then use a reasonable default
2973 axis.extremeRange = [0, 1];
2974 } else {
2975 // Calculate the extremes of extremes.
2976 var series = seriesForAxis[i];
2977 var minY = Infinity; // extremes[series[0]][0];
2978 var maxY = -Infinity; // extremes[series[0]][1];
2979 var extremeMinY, extremeMaxY;
2980 for (var j = 0; j < series.length; j++) {
2981 // Only use valid extremes to stop null data series' from corrupting the scale.
2982 extremeMinY = extremes[series[j]][0];
2983 if (extremeMinY != null) {
2984 minY = Math.min(extremeMinY, minY);
2985 }
2986 extremeMaxY = extremes[series[j]][1];
2987 if (extremeMaxY != null) {
2988 maxY = Math.max(extremeMaxY, maxY);
2989 }
2990 }
2991 if (axis.includeZero && minY > 0) minY = 0;
2992
2993 // Ensure we have a valid scale, otherwise defualt to zero for safety.
2994 if (minY == Infinity) minY = 0;
2995 if (maxY == -Infinity) maxY = 0;
2996
2997 // Add some padding and round up to an integer to be human-friendly.
2998 var span = maxY - minY;
2999 // special case: if we have no sense of scale, use +/-10% of the sole value.
3000 if (span == 0) { span = maxY; }
3001
3002 var maxAxisY;
3003 var minAxisY;
3004 if (axis.logscale) {
3005 var maxAxisY = maxY + 0.1 * span;
3006 var minAxisY = minY;
3007 } else {
3008 var maxAxisY = maxY + 0.1 * span;
3009 var minAxisY = minY - 0.1 * span;
3010
3011 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
3012 if (!this.attr_("avoidMinZero")) {
3013 if (minAxisY < 0 && minY >= 0) minAxisY = 0;
3014 if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0;
3015 }
3016
3017 if (this.attr_("includeZero")) {
3018 if (maxY < 0) maxAxisY = 0;
3019 if (minY > 0) minAxisY = 0;
3020 }
3021 }
3022 axis.extremeRange = [minAxisY, maxAxisY];
3023 }
3024 if (axis.valueWindow) {
3025 // This is only set if the user has zoomed on the y-axis. It is never set
3026 // by a user. It takes precedence over axis.valueRange because, if you set
3027 // valueRange, you'd still expect to be able to pan.
3028 axis.computedValueRange = [axis.valueWindow[0], axis.valueWindow[1]];
3029 } else if (axis.valueRange) {
3030 // This is a user-set value range for this axis.
3031 axis.computedValueRange = [axis.valueRange[0], axis.valueRange[1]];
3032 } else {
3033 axis.computedValueRange = axis.extremeRange;
3034 }
3035
3036 // Add ticks. By default, all axes inherit the tick positions of the
3037 // primary axis. However, if an axis is specifically marked as having
3038 // independent ticks, then that is permissible as well.
3039 if (i == 0 || axis.independentTicks) {
3040 axis.ticks =
3041 Dygraph.numericTicks(axis.computedValueRange[0],
3042 axis.computedValueRange[1],
3043 this,
3044 axis);
3045 } else {
3046 var p_axis = this.axes_[0];
3047 var p_ticks = p_axis.ticks;
3048 var p_scale = p_axis.computedValueRange[1] - p_axis.computedValueRange[0];
3049 var scale = axis.computedValueRange[1] - axis.computedValueRange[0];
3050 var tick_values = [];
3051 for (var i = 0; i < p_ticks.length; i++) {
3052 var y_frac = (p_ticks[i].v - p_axis.computedValueRange[0]) / p_scale;
3053 var y_val = axis.computedValueRange[0] + y_frac * scale;
3054 tick_values.push(y_val);
3055 }
3056
3057 axis.ticks =
3058 Dygraph.numericTicks(axis.computedValueRange[0],
3059 axis.computedValueRange[1],
3060 this, axis, tick_values);
3061 }
3062 }
3063 };
3064
3065 /**
3066 * @private
3067 * Calculates the rolling average of a data set.
3068 * If originalData is [label, val], rolls the average of those.
3069 * If originalData is [label, [, it's interpreted as [value, stddev]
3070 * and the roll is returned in the same form, with appropriately reduced
3071 * stddev for each value.
3072 * Note that this is where fractional input (i.e. '5/10') is converted into
3073 * decimal values.
3074 * @param {Array} originalData The data in the appropriate format (see above)
3075 * @param {Number} rollPeriod The number of points over which to average the
3076 * data
3077 */
3078 Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) {
3079 if (originalData.length < 2)
3080 return originalData;
3081 var rollPeriod = Math.min(rollPeriod, originalData.length - 1);
3082 var rollingData = [];
3083 var sigma = this.attr_("sigma");
3084
3085 if (this.fractions_) {
3086 var num = 0;
3087 var den = 0; // numerator/denominator
3088 var mult = 100.0;
3089 for (var i = 0; i < originalData.length; i++) {
3090 num += originalData[i][1][0];
3091 den += originalData[i][1][1];
3092 if (i - rollPeriod >= 0) {
3093 num -= originalData[i - rollPeriod][1][0];
3094 den -= originalData[i - rollPeriod][1][1];
3095 }
3096
3097 var date = originalData[i][0];
3098 var value = den ? num / den : 0.0;
3099 if (this.attr_("errorBars")) {
3100 if (this.wilsonInterval_) {
3101 // For more details on this confidence interval, see:
3102 // http://en.wikipedia.org/wiki/Binomial_confidence_interval
3103 if (den) {
3104 var p = value < 0 ? 0 : value, n = den;
3105 var pm = sigma * Math.sqrt(p*(1-p)/n + sigma*sigma/(4*n*n));
3106 var denom = 1 + sigma * sigma / den;
3107 var low = (p + sigma * sigma / (2 * den) - pm) / denom;
3108 var high = (p + sigma * sigma / (2 * den) + pm) / denom;
3109 rollingData[i] = [date,
3110 [p * mult, (p - low) * mult, (high - p) * mult]];
3111 } else {
3112 rollingData[i] = [date, [0, 0, 0]];
3113 }
3114 } else {
3115 var stddev = den ? sigma * Math.sqrt(value * (1 - value) / den) : 1.0;
3116 rollingData[i] = [date, [mult * value, mult * stddev, mult * stddev]];
3117 }
3118 } else {
3119 rollingData[i] = [date, mult * value];
3120 }
3121 }
3122 } else if (this.attr_("customBars")) {
3123 var low = 0;
3124 var mid = 0;
3125 var high = 0;
3126 var count = 0;
3127 for (var i = 0; i < originalData.length; i++) {
3128 var data = originalData[i][1];
3129 var y = data[1];
3130 rollingData[i] = [originalData[i][0], [y, y - data[0], data[2] - y]];
3131
3132 if (y != null && !isNaN(y)) {
3133 low += data[0];
3134 mid += y;
3135 high += data[2];
3136 count += 1;
3137 }
3138 if (i - rollPeriod >= 0) {
3139 var prev = originalData[i - rollPeriod];
3140 if (prev[1][1] != null && !isNaN(prev[1][1])) {
3141 low -= prev[1][0];
3142 mid -= prev[1][1];
3143 high -= prev[1][2];
3144 count -= 1;
3145 }
3146 }
3147 rollingData[i] = [originalData[i][0], [ 1.0 * mid / count,
3148 1.0 * (mid - low) / count,
3149 1.0 * (high - mid) / count ]];
3150 }
3151 } else {
3152 // Calculate the rolling average for the first rollPeriod - 1 points where
3153 // there is not enough data to roll over the full number of points
3154 var num_init_points = Math.min(rollPeriod - 1, originalData.length - 2);
3155 if (!this.attr_("errorBars")){
3156 if (rollPeriod == 1) {
3157 return originalData;
3158 }
3159
3160 for (var i = 0; i < originalData.length; i++) {
3161 var sum = 0;
3162 var num_ok = 0;
3163 for (var j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
3164 var y = originalData[j][1];
3165 if (y == null || isNaN(y)) continue;
3166 num_ok++;
3167 sum += originalData[j][1];
3168 }
3169 if (num_ok) {
3170 rollingData[i] = [originalData[i][0], sum / num_ok];
3171 } else {
3172 rollingData[i] = [originalData[i][0], null];
3173 }
3174 }
3175
3176 } else {
3177 for (var i = 0; i < originalData.length; i++) {
3178 var sum = 0;
3179 var variance = 0;
3180 var num_ok = 0;
3181 for (var j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
3182 var y = originalData[j][1][0];
3183 if (y == null || isNaN(y)) continue;
3184 num_ok++;
3185 sum += originalData[j][1][0];
3186 variance += Math.pow(originalData[j][1][1], 2);
3187 }
3188 if (num_ok) {
3189 var stddev = Math.sqrt(variance) / num_ok;
3190 rollingData[i] = [originalData[i][0],
3191 [sum / num_ok, sigma * stddev, sigma * stddev]];
3192 } else {
3193 rollingData[i] = [originalData[i][0], [null, null, null]];
3194 }
3195 }
3196 }
3197 }
3198
3199 return rollingData;
3200 };
3201
3202 /**
3203 * @private
3204 * Parses a date, returning the number of milliseconds since epoch. This can be
3205 * passed in as an xValueParser in the Dygraph constructor.
3206 * TODO(danvk): enumerate formats that this understands.
3207 * @param {String} A date in YYYYMMDD format.
3208 * @return {Number} Milliseconds since epoch.
3209 */
3210 Dygraph.dateParser = function(dateStr, self) {
3211 var dateStrSlashed;
3212 var d;
3213 if (dateStr.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
3214 dateStrSlashed = dateStr.replace("-", "/", "g");
3215 while (dateStrSlashed.search("-") != -1) {
3216 dateStrSlashed = dateStrSlashed.replace("-", "/");
3217 }
3218 d = Dygraph.dateStrToMillis(dateStrSlashed);
3219 } else if (dateStr.length == 8) { // e.g. '20090712'
3220 // TODO(danvk): remove support for this format. It's confusing.
3221 dateStrSlashed = dateStr.substr(0,4) + "/" + dateStr.substr(4,2)
3222 + "/" + dateStr.substr(6,2);
3223 d = Dygraph.dateStrToMillis(dateStrSlashed);
3224 } else {
3225 // Any format that Date.parse will accept, e.g. "2009/07/12" or
3226 // "2009/07/12 12:34:56"
3227 d = Dygraph.dateStrToMillis(dateStr);
3228 }
3229
3230 if (!d || isNaN(d)) {
3231 self.error("Couldn't parse " + dateStr + " as a date");
3232 }
3233 return d;
3234 };
3235
3236 /**
3237 * Detects the type of the str (date or numeric) and sets the various
3238 * formatting attributes in this.attrs_ based on this type.
3239 * @param {String} str An x value.
3240 * @private
3241 */
3242 Dygraph.prototype.detectTypeFromString_ = function(str) {
3243 var isDate = false;
3244 if (str.indexOf('-') > 0 ||
3245 str.indexOf('/') >= 0 ||
3246 isNaN(parseFloat(str))) {
3247 isDate = true;
3248 } else if (str.length == 8 && str > '19700101' && str < '20371231') {
3249 // TODO(danvk): remove support for this format.
3250 isDate = true;
3251 }
3252
3253 if (isDate) {
3254 this.attrs_.xValueFormatter = Dygraph.dateString_;
3255 this.attrs_.xValueParser = Dygraph.dateParser;
3256 this.attrs_.xTicker = Dygraph.dateTicker;
3257 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
3258 } else {
3259 // TODO(danvk): use Dygraph.numberFormatter here?
3260 /** @private (shut up, jsdoc!) */
3261 this.attrs_.xValueFormatter = function(x) { return x; };
3262 /** @private (shut up, jsdoc!) */
3263 this.attrs_.xValueParser = function(x) { return parseFloat(x); };
3264 this.attrs_.xTicker = Dygraph.numericTicks;
3265 this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;
3266 }
3267 };
3268
3269 /**
3270 * Parses the value as a floating point number. This is like the parseFloat()
3271 * built-in, but with a few differences:
3272 * - the empty string is parsed as null, rather than NaN.
3273 * - if the string cannot be parsed at all, an error is logged.
3274 * If the string can't be parsed, this method returns null.
3275 * @param {String} x The string to be parsed
3276 * @param {Number} opt_line_no The line number from which the string comes.
3277 * @param {String} opt_line The text of the line from which the string comes.
3278 * @private
3279 */
3280
3281 // Parse the x as a float or return null if it's not a number.
3282 Dygraph.prototype.parseFloat_ = function(x, opt_line_no, opt_line) {
3283 var val = parseFloat(x);
3284 if (!isNaN(val)) return val;
3285
3286 // Try to figure out what happeend.
3287 // If the value is the empty string, parse it as null.
3288 if (/^ *$/.test(x)) return null;
3289
3290 // If it was actually "NaN", return it as NaN.
3291 if (/^ *nan *$/i.test(x)) return NaN;
3292
3293 // Looks like a parsing error.
3294 var msg = "Unable to parse '" + x + "' as a number";
3295 if (opt_line !== null && opt_line_no !== null) {
3296 msg += " on line " + (1+opt_line_no) + " ('" + opt_line + "') of CSV.";
3297 }
3298 this.error(msg);
3299
3300 return null;
3301 };
3302
3303 /**
3304 * @private
3305 * Parses a string in a special csv format. We expect a csv file where each
3306 * line is a date point, and the first field in each line is the date string.
3307 * We also expect that all remaining fields represent series.
3308 * if the errorBars attribute is set, then interpret the fields as:
3309 * date, series1, stddev1, series2, stddev2, ...
3310 * @param {[Object]} data See above.
3311 *
3312 * @return [Object] An array with one entry for each row. These entries
3313 * are an array of cells in that row. The first entry is the parsed x-value for
3314 * the row. The second, third, etc. are the y-values. These can take on one of
3315 * three forms, depending on the CSV and constructor parameters:
3316 * 1. numeric value
3317 * 2. [ value, stddev ]
3318 * 3. [ low value, center value, high value ]
3319 */
3320 Dygraph.prototype.parseCSV_ = function(data) {
3321 var ret = [];
3322 var lines = data.split("\n");
3323
3324 // Use the default delimiter or fall back to a tab if that makes sense.
3325 var delim = this.attr_('delimiter');
3326 if (lines[0].indexOf(delim) == -1 && lines[0].indexOf('\t') >= 0) {
3327 delim = '\t';
3328 }
3329
3330 var start = 0;
3331 if (this.labelsFromCSV_) {
3332 start = 1;
3333 this.attrs_.labels = lines[0].split(delim);
3334 }
3335 var line_no = 0;
3336
3337 var xParser;
3338 var defaultParserSet = false; // attempt to auto-detect x value type
3339 var expectedCols = this.attr_("labels").length;
3340 var outOfOrder = false;
3341 for (var i = start; i < lines.length; i++) {
3342 var line = lines[i];
3343 line_no = i;
3344 if (line.length == 0) continue; // skip blank lines
3345 if (line[0] == '#') continue; // skip comment lines
3346 var inFields = line.split(delim);
3347 if (inFields.length < 2) continue;
3348
3349 var fields = [];
3350 if (!defaultParserSet) {
3351 this.detectTypeFromString_(inFields[0]);
3352 xParser = this.attr_("xValueParser");
3353 defaultParserSet = true;
3354 }
3355 fields[0] = xParser(inFields[0], this);
3356
3357 // If fractions are expected, parse the numbers as "A/B"
3358 if (this.fractions_) {
3359 for (var j = 1; j < inFields.length; j++) {
3360 // TODO(danvk): figure out an appropriate way to flag parse errors.
3361 var vals = inFields[j].split("/");
3362 if (vals.length != 2) {
3363 this.error('Expected fractional "num/den" values in CSV data ' +
3364 "but found a value '" + inFields[j] + "' on line " +
3365 (1 + i) + " ('" + line + "') which is not of this form.");
3366 fields[j] = [0, 0];
3367 } else {
3368 fields[j] = [this.parseFloat_(vals[0], i, line),
3369 this.parseFloat_(vals[1], i, line)];
3370 }
3371 }
3372 } else if (this.attr_("errorBars")) {
3373 // If there are error bars, values are (value, stddev) pairs
3374 if (inFields.length % 2 != 1) {
3375 this.error('Expected alternating (value, stdev.) pairs in CSV data ' +
3376 'but line ' + (1 + i) + ' has an odd number of values (' +
3377 (inFields.length - 1) + "): '" + line + "'");
3378 }
3379 for (var j = 1; j < inFields.length; j += 2) {
3380 fields[(j + 1) / 2] = [this.parseFloat_(inFields[j], i, line),
3381 this.parseFloat_(inFields[j + 1], i, line)];
3382 }
3383 } else if (this.attr_("customBars")) {
3384 // Bars are a low;center;high tuple
3385 for (var j = 1; j < inFields.length; j++) {
3386 var val = inFields[j];
3387 if (/^ *$/.test(val)) {
3388 fields[j] = [null, null, null];
3389 } else {
3390 var vals = val.split(";");
3391 if (vals.length == 3) {
3392 fields[j] = [ this.parseFloat_(vals[0], i, line),
3393 this.parseFloat_(vals[1], i, line),
3394 this.parseFloat_(vals[2], i, line) ];
3395 } else {
3396 this.warning('When using customBars, values must be either blank ' +
3397 'or "low;center;high" tuples (got "' + val +
3398 '" on line ' + (1+i));
3399 }
3400 }
3401 }
3402 } else {
3403 // Values are just numbers
3404 for (var j = 1; j < inFields.length; j++) {
3405 fields[j] = this.parseFloat_(inFields[j], i, line);
3406 }
3407 }
3408 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
3409 outOfOrder = true;
3410 }
3411
3412 if (fields.length != expectedCols) {
3413 this.error("Number of columns in line " + i + " (" + fields.length +
3414 ") does not agree with number of labels (" + expectedCols +
3415 ") " + line);
3416 }
3417
3418 // If the user specified the 'labels' option and none of the cells of the
3419 // first row parsed correctly, then they probably double-specified the
3420 // labels. We go with the values set in the option, discard this row and
3421 // log a warning to the JS console.
3422 if (i == 0 && this.attr_('labels')) {
3423 var all_null = true;
3424 for (var j = 0; all_null && j < fields.length; j++) {
3425 if (fields[j]) all_null = false;
3426 }
3427 if (all_null) {
3428 this.warn("The dygraphs 'labels' option is set, but the first row of " +
3429 "CSV data ('" + line + "') appears to also contain labels. " +
3430 "Will drop the CSV labels and use the option labels.");
3431 continue;
3432 }
3433 }
3434 ret.push(fields);
3435 }
3436
3437 if (outOfOrder) {
3438 this.warn("CSV is out of order; order it correctly to speed loading.");
3439 ret.sort(function(a,b) { return a[0] - b[0] });
3440 }
3441
3442 return ret;
3443 };
3444
3445 /**
3446 * @private
3447 * The user has provided their data as a pre-packaged JS array. If the x values
3448 * are numeric, this is the same as dygraphs' internal format. If the x values
3449 * are dates, we need to convert them from Date objects to ms since epoch.
3450 * @param {[Object]} data
3451 * @return {[Object]} data with numeric x values.
3452 */
3453 Dygraph.prototype.parseArray_ = function(data) {
3454 // Peek at the first x value to see if it's numeric.
3455 if (data.length == 0) {
3456 this.error("Can't plot empty data set");
3457 return null;
3458 }
3459 if (data[0].length == 0) {
3460 this.error("Data set cannot contain an empty row");
3461 return null;
3462 }
3463
3464 if (this.attr_("labels") == null) {
3465 this.warn("Using default labels. Set labels explicitly via 'labels' " +
3466 "in the options parameter");
3467 this.attrs_.labels = [ "X" ];
3468 for (var i = 1; i < data[0].length; i++) {
3469 this.attrs_.labels.push("Y" + i);
3470 }
3471 }
3472
3473 if (Dygraph.isDateLike(data[0][0])) {
3474 // Some intelligent defaults for a date x-axis.
3475 this.attrs_.xValueFormatter = Dygraph.dateString_;
3476 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
3477 this.attrs_.xTicker = Dygraph.dateTicker;
3478
3479 // Assume they're all dates.
3480 var parsedData = Dygraph.clone(data);
3481 for (var i = 0; i < data.length; i++) {
3482 if (parsedData[i].length == 0) {
3483 this.error("Row " + (1 + i) + " of data is empty");
3484 return null;
3485 }
3486 if (parsedData[i][0] == null
3487 || typeof(parsedData[i][0].getTime) != 'function'
3488 || isNaN(parsedData[i][0].getTime())) {
3489 this.error("x value in row " + (1 + i) + " is not a Date");
3490 return null;
3491 }
3492 parsedData[i][0] = parsedData[i][0].getTime();
3493 }
3494 return parsedData;
3495 } else {
3496 // Some intelligent defaults for a numeric x-axis.
3497 /** @private (shut up, jsdoc!) */
3498 this.attrs_.xValueFormatter = function(x) { return x; };
3499 this.attrs_.xTicker = Dygraph.numericTicks;
3500 return data;
3501 }
3502 };
3503
3504 /**
3505 * Parses a DataTable object from gviz.
3506 * The data is expected to have a first column that is either a date or a
3507 * number. All subsequent columns must be numbers. If there is a clear mismatch
3508 * between this.xValueParser_ and the type of the first column, it will be
3509 * fixed. Fills out rawData_.
3510 * @param {[Object]} data See above.
3511 * @private
3512 */
3513 Dygraph.prototype.parseDataTable_ = function(data) {
3514 var cols = data.getNumberOfColumns();
3515 var rows = data.getNumberOfRows();
3516
3517 var indepType = data.getColumnType(0);
3518 if (indepType == 'date' || indepType == 'datetime') {
3519 this.attrs_.xValueFormatter = Dygraph.dateString_;
3520 this.attrs_.xValueParser = Dygraph.dateParser;
3521 this.attrs_.xTicker = Dygraph.dateTicker;
3522 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
3523 } else if (indepType == 'number') {
3524 this.attrs_.xValueFormatter = function(x) { return x; };
3525 this.attrs_.xValueParser = function(x) { return parseFloat(x); };
3526 this.attrs_.xTicker = Dygraph.numericTicks;
3527 this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;
3528 } else {
3529 this.error("only 'date', 'datetime' and 'number' types are supported for " +
3530 "column 1 of DataTable input (Got '" + indepType + "')");
3531 return null;
3532 }
3533
3534 // Array of the column indices which contain data (and not annotations).
3535 var colIdx = [];
3536 var annotationCols = {}; // data index -> [annotation cols]
3537 var hasAnnotations = false;
3538 for (var i = 1; i < cols; i++) {
3539 var type = data.getColumnType(i);
3540 if (type == 'number') {
3541 colIdx.push(i);
3542 } else if (type == 'string' && this.attr_('displayAnnotations')) {
3543 // This is OK -- it's an annotation column.
3544 var dataIdx = colIdx[colIdx.length - 1];
3545 if (!annotationCols.hasOwnProperty(dataIdx)) {
3546 annotationCols[dataIdx] = [i];
3547 } else {
3548 annotationCols[dataIdx].push(i);
3549 }
3550 hasAnnotations = true;
3551 } else {
3552 this.error("Only 'number' is supported as a dependent type with Gviz." +
3553 " 'string' is only supported if displayAnnotations is true");
3554 }
3555 }
3556
3557 // Read column labels
3558 // TODO(danvk): add support back for errorBars
3559 var labels = [data.getColumnLabel(0)];
3560 for (var i = 0; i < colIdx.length; i++) {
3561 labels.push(data.getColumnLabel(colIdx[i]));
3562 if (this.attr_("errorBars")) i += 1;
3563 }
3564 this.attrs_.labels = labels;
3565 cols = labels.length;
3566
3567 var ret = [];
3568 var outOfOrder = false;
3569 var annotations = [];
3570 for (var i = 0; i < rows; i++) {
3571 var row = [];
3572 if (typeof(data.getValue(i, 0)) === 'undefined' ||
3573 data.getValue(i, 0) === null) {
3574 this.warn("Ignoring row " + i +
3575 " of DataTable because of undefined or null first column.");
3576 continue;
3577 }
3578
3579 if (indepType == 'date' || indepType == 'datetime') {
3580 row.push(data.getValue(i, 0).getTime());
3581 } else {
3582 row.push(data.getValue(i, 0));
3583 }
3584 if (!this.attr_("errorBars")) {
3585 for (var j = 0; j < colIdx.length; j++) {
3586 var col = colIdx[j];
3587 row.push(data.getValue(i, col));
3588 if (hasAnnotations &&
3589 annotationCols.hasOwnProperty(col) &&
3590 data.getValue(i, annotationCols[col][0]) != null) {
3591 var ann = {};
3592 ann.series = data.getColumnLabel(col);
3593 ann.xval = row[0];
3594 ann.shortText = String.fromCharCode(65 /* A */ + annotations.length)
3595 ann.text = '';
3596 for (var k = 0; k < annotationCols[col].length; k++) {
3597 if (k) ann.text += "\n";
3598 ann.text += data.getValue(i, annotationCols[col][k]);
3599 }
3600 annotations.push(ann);
3601 }
3602 }
3603
3604 // Strip out infinities, which give dygraphs problems later on.
3605 for (var j = 0; j < row.length; j++) {
3606 if (!isFinite(row[j])) row[j] = null;
3607 }
3608 } else {
3609 for (var j = 0; j < cols - 1; j++) {
3610 row.push([ data.getValue(i, 1 + 2 * j), data.getValue(i, 2 + 2 * j) ]);
3611 }
3612 }
3613 if (ret.length > 0 && row[0] < ret[ret.length - 1][0]) {
3614 outOfOrder = true;
3615 }
3616 ret.push(row);
3617 }
3618
3619 if (outOfOrder) {
3620 this.warn("DataTable is out of order; order it correctly to speed loading.");
3621 ret.sort(function(a,b) { return a[0] - b[0] });
3622 }
3623 this.rawData_ = ret;
3624
3625 if (annotations.length > 0) {
3626 this.setAnnotations(annotations, true);
3627 }
3628 }
3629
3630 /**
3631 * @private
3632 * This is identical to JavaScript's built-in Date.parse() method, except that
3633 * it doesn't get replaced with an incompatible method by aggressive JS
3634 * libraries like MooTools or Joomla.
3635 * @param { String } str The date string, e.g. "2011/05/06"
3636 * @return { Integer } millis since epoch
3637 */
3638 Dygraph.dateStrToMillis = function(str) {
3639 return new Date(str).getTime();
3640 };
3641
3642 // These functions are all based on MochiKit.
3643 /**
3644 * @private
3645 */
3646 Dygraph.update = function (self, o) {
3647 if (typeof(o) != 'undefined' && o !== null) {
3648 for (var k in o) {
3649 if (o.hasOwnProperty(k)) {
3650 self[k] = o[k];
3651 }
3652 }
3653 }
3654 return self;
3655 };
3656
3657 /**
3658 * @private
3659 */
3660 Dygraph.isArrayLike = function (o) {
3661 var typ = typeof(o);
3662 if (
3663 (typ != 'object' && !(typ == 'function' &&
3664 typeof(o.item) == 'function')) ||
3665 o === null ||
3666 typeof(o.length) != 'number' ||
3667 o.nodeType === 3
3668 ) {
3669 return false;
3670 }
3671 return true;
3672 };
3673
3674 /**
3675 * @private
3676 */
3677 Dygraph.isDateLike = function (o) {
3678 if (typeof(o) != "object" || o === null ||
3679 typeof(o.getTime) != 'function') {
3680 return false;
3681 }
3682 return true;
3683 };
3684
3685 /**
3686 * @private
3687 */
3688 Dygraph.clone = function(o) {
3689 // TODO(danvk): figure out how MochiKit's version works
3690 var r = [];
3691 for (var i = 0; i < o.length; i++) {
3692 if (Dygraph.isArrayLike(o[i])) {
3693 r.push(Dygraph.clone(o[i]));
3694 } else {
3695 r.push(o[i]);
3696 }
3697 }
3698 return r;
3699 };
3700
3701
3702 /**
3703 * Get the CSV data. If it's in a function, call that function. If it's in a
3704 * file, do an XMLHttpRequest to get it.
3705 * @private
3706 */
3707 Dygraph.prototype.start_ = function() {
3708 if (typeof this.file_ == 'function') {
3709 // CSV string. Pretend we got it via XHR.
3710 this.loadedEvent_(this.file_());
3711 } else if (Dygraph.isArrayLike(this.file_)) {
3712 this.rawData_ = this.parseArray_(this.file_);
3713 this.predraw_();
3714 } else if (typeof this.file_ == 'object' &&
3715 typeof this.file_.getColumnRange == 'function') {
3716 // must be a DataTable from gviz.
3717 this.parseDataTable_(this.file_);
3718 this.predraw_();
3719 } else if (typeof this.file_ == 'string') {
3720 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
3721 if (this.file_.indexOf('\n') >= 0) {
3722 this.loadedEvent_(this.file_);
3723 } else {
3724 var req = new XMLHttpRequest();
3725 var caller = this;
3726 req.onreadystatechange = function () {
3727 if (req.readyState == 4) {
3728 if (req.status == 200) {
3729 caller.loadedEvent_(req.responseText);
3730 }
3731 }
3732 };
3733
3734 req.open("GET", this.file_, true);
3735 req.send(null);
3736 }
3737 } else {
3738 this.error("Unknown data format: " + (typeof this.file_));
3739 }
3740 };
3741
3742 /**
3743 * Changes various properties of the graph. These can include:
3744 * <ul>
3745 * <li>file: changes the source data for the graph</li>
3746 * <li>errorBars: changes whether the data contains stddev</li>
3747 * </ul>
3748 *
3749 * @param {Object} attrs The new properties and values
3750 */
3751 Dygraph.prototype.updateOptions = function(attrs) {
3752 // TODO(danvk): this is a mess. Rethink this function.
3753 if ('rollPeriod' in attrs) {
3754 this.rollPeriod_ = attrs.rollPeriod;
3755 }
3756 if ('dateWindow' in attrs) {
3757 this.dateWindow_ = attrs.dateWindow;
3758 if (!('isZoomedIgnoreProgrammaticZoom' in attrs)) {
3759 this.zoomed_x_ = attrs.dateWindow != null;
3760 }
3761 }
3762 if ('valueRange' in attrs && !('isZoomedIgnoreProgrammaticZoom' in attrs)) {
3763 this.zoomed_y_ = attrs.valueRange != null;
3764 }
3765
3766 // TODO(danvk): validate per-series options.
3767 // Supported:
3768 // strokeWidth
3769 // pointSize
3770 // drawPoints
3771 // highlightCircleSize
3772
3773 Dygraph.update(this.user_attrs_, attrs);
3774 Dygraph.update(this.renderOptions_, attrs);
3775
3776 this.labelsFromCSV_ = (this.attr_("labels") == null);
3777
3778 if (attrs['file']) {
3779 this.file_ = attrs['file'];
3780 this.start_();
3781 } else {
3782 this.predraw_();
3783 }
3784 };
3785
3786 /**
3787 * Resizes the dygraph. If no parameters are specified, resizes to fill the
3788 * containing div (which has presumably changed size since the dygraph was
3789 * instantiated. If the width/height are specified, the div will be resized.
3790 *
3791 * This is far more efficient than destroying and re-instantiating a
3792 * Dygraph, since it doesn't have to reparse the underlying data.
3793 *
3794 * @param {Number} [width] Width (in pixels)
3795 * @param {Number} [height] Height (in pixels)
3796 */
3797 Dygraph.prototype.resize = function(width, height) {
3798 if (this.resize_lock) {
3799 return;
3800 }
3801 this.resize_lock = true;
3802
3803 if ((width === null) != (height === null)) {
3804 this.warn("Dygraph.resize() should be called with zero parameters or " +
3805 "two non-NULL parameters. Pretending it was zero.");
3806 width = height = null;
3807 }
3808
3809 // TODO(danvk): there should be a clear() method.
3810 this.maindiv_.innerHTML = "";
3811 this.attrs_.labelsDiv = null;
3812
3813 if (width) {
3814 this.maindiv_.style.width = width + "px";
3815 this.maindiv_.style.height = height + "px";
3816 this.width_ = width;
3817 this.height_ = height;
3818 } else {
3819 this.width_ = this.maindiv_.offsetWidth;
3820 this.height_ = this.maindiv_.offsetHeight;
3821 }
3822
3823 this.createInterface_();
3824 this.predraw_();
3825
3826 this.resize_lock = false;
3827 };
3828
3829 /**
3830 * Adjusts the number of points in the rolling average. Updates the graph to
3831 * reflect the new averaging period.
3832 * @param {Number} length Number of points over which to average the data.
3833 */
3834 Dygraph.prototype.adjustRoll = function(length) {
3835 this.rollPeriod_ = length;
3836 this.predraw_();
3837 };
3838
3839 /**
3840 * Returns a boolean array of visibility statuses.
3841 */
3842 Dygraph.prototype.visibility = function() {
3843 // Do lazy-initialization, so that this happens after we know the number of
3844 // data series.
3845 if (!this.attr_("visibility")) {
3846 this.attrs_["visibility"] = [];
3847 }
3848 while (this.attr_("visibility").length < this.rawData_[0].length - 1) {
3849 this.attr_("visibility").push(true);
3850 }
3851 return this.attr_("visibility");
3852 };
3853
3854 /**
3855 * Changes the visiblity of a series.
3856 */
3857 Dygraph.prototype.setVisibility = function(num, value) {
3858 var x = this.visibility();
3859 if (num < 0 || num >= x.length) {
3860 this.warn("invalid series number in setVisibility: " + num);
3861 } else {
3862 x[num] = value;
3863 this.predraw_();
3864 }
3865 };
3866
3867 /**
3868 * Update the list of annotations and redraw the chart.
3869 */
3870 Dygraph.prototype.setAnnotations = function(ann, suppressDraw) {
3871 // Only add the annotation CSS rule once we know it will be used.
3872 Dygraph.addAnnotationRule();
3873 this.annotations_ = ann;
3874 this.layout_.setAnnotations(this.annotations_);
3875 if (!suppressDraw) {
3876 this.predraw_();
3877 }
3878 };
3879
3880 /**
3881 * Return the list of annotations.
3882 */
3883 Dygraph.prototype.annotations = function() {
3884 return this.annotations_;
3885 };
3886
3887 /**
3888 * Get the index of a series (column) given its name. The first column is the
3889 * x-axis, so the data series start with index 1.
3890 */
3891 Dygraph.prototype.indexFromSetName = function(name) {
3892 var labels = this.attr_("labels");
3893 for (var i = 0; i < labels.length; i++) {
3894 if (labels[i] == name) return i;
3895 }
3896 return null;
3897 };
3898
3899 /**
3900 * @private
3901 * Adds a default style for the annotation CSS classes to the document. This is
3902 * only executed when annotations are actually used. It is designed to only be
3903 * called once -- all calls after the first will return immediately.
3904 */
3905 Dygraph.addAnnotationRule = function() {
3906 if (Dygraph.addedAnnotationCSS) return;
3907
3908 var rule = "border: 1px solid black; " +
3909 "background-color: white; " +
3910 "text-align: center;";
3911
3912 var styleSheetElement = document.createElement("style");
3913 styleSheetElement.type = "text/css";
3914 document.getElementsByTagName("head")[0].appendChild(styleSheetElement);
3915
3916 // Find the first style sheet that we can access.
3917 // We may not add a rule to a style sheet from another domain for security
3918 // reasons. This sometimes comes up when using gviz, since the Google gviz JS
3919 // adds its own style sheets from google.com.
3920 for (var i = 0; i < document.styleSheets.length; i++) {
3921 if (document.styleSheets[i].disabled) continue;
3922 var mysheet = document.styleSheets[i];
3923 try {
3924 if (mysheet.insertRule) { // Firefox
3925 var idx = mysheet.cssRules ? mysheet.cssRules.length : 0;
3926 mysheet.insertRule(".dygraphDefaultAnnotation { " + rule + " }", idx);
3927 } else if (mysheet.addRule) { // IE
3928 mysheet.addRule(".dygraphDefaultAnnotation", rule);
3929 }
3930 Dygraph.addedAnnotationCSS = true;
3931 return;
3932 } catch(err) {
3933 // Was likely a security exception.
3934 }
3935 }
3936
3937 this.warn("Unable to add default annotation CSS rule; display may be off.");
3938 }
3939
3940 /**
3941 * @private
3942 * Create a new canvas element. This is more complex than a simple
3943 * document.createElement("canvas") because of IE and excanvas.
3944 */
3945 Dygraph.createCanvas = function() {
3946 var canvas = document.createElement("canvas");
3947
3948 isIE = (/MSIE/.test(navigator.userAgent) && !window.opera);
3949 if (isIE && (typeof(G_vmlCanvasManager) != 'undefined')) {
3950 canvas = G_vmlCanvasManager.initElement(canvas);
3951 }
3952
3953 return canvas;
3954 };
3955
3956
3957 /**
3958 * A wrapper around Dygraph that implements the gviz API.
3959 * @param {Object} container The DOM object the visualization should live in.
3960 */
3961 Dygraph.GVizChart = function(container) {
3962 this.container = container;
3963 }
3964
3965 Dygraph.GVizChart.prototype.draw = function(data, options) {
3966 // Clear out any existing dygraph.
3967 // TODO(danvk): would it make more sense to simply redraw using the current
3968 // date_graph object?
3969 this.container.innerHTML = '';
3970 if (typeof(this.date_graph) != 'undefined') {
3971 this.date_graph.destroy();
3972 }
3973
3974 this.date_graph = new Dygraph(this.container, data, options);
3975 }
3976
3977 /**
3978 * Google charts compatible setSelection
3979 * Only row selection is supported, all points in the row will be highlighted
3980 * @param {Array} array of the selected cells
3981 * @public
3982 */
3983 Dygraph.GVizChart.prototype.setSelection = function(selection_array) {
3984 var row = false;
3985 if (selection_array.length) {
3986 row = selection_array[0].row;
3987 }
3988 this.date_graph.setSelection(row);
3989 }
3990
3991 /**
3992 * Google charts compatible getSelection implementation
3993 * @return {Array} array of the selected cells
3994 * @public
3995 */
3996 Dygraph.GVizChart.prototype.getSelection = function() {
3997 var selection = [];
3998
3999 var row = this.date_graph.getSelection();
4000
4001 if (row < 0) return selection;
4002
4003 col = 1;
4004 for (var i in this.date_graph.layout_.datasets) {
4005 selection.push({row: row, column: col});
4006 col++;
4007 }
4008
4009 return selection;
4010 }
4011
4012 // Older pages may still use this name.
4013 DateGraph = Dygraph;
4014
4015 // <REMOVE_FOR_COMBINED>
4016 Dygraph.OPTIONS_REFERENCE = // <JSON>
4017 {
4018 "xValueParser": {
4019 "default": "parseFloat() or Date.parse()*",
4020 "labels": ["CSV parsing"],
4021 "type": "function(str) -> number",
4022 "description": "A function which parses x-values (i.e. the dependent series). Must return a number, even when the values are dates. In this case, millis since epoch are used. This is used primarily for parsing CSV data. *=Dygraphs is slightly more accepting in the dates which it will parse. See code for details."
4023 },
4024 "stackedGraph": {
4025 "default": "false",
4026 "labels": ["Data Line display"],
4027 "type": "boolean",
4028 "description": "If set, stack series on top of one another rather than drawing them independently."
4029 },
4030 "pointSize": {
4031 "default": "1",
4032 "labels": ["Data Line display"],
4033 "type": "integer",
4034 "description": "The size of the dot to draw on each point in pixels (see drawPoints). A dot is always drawn when a point is \"isolated\", i.e. there is a missing point on either side of it. This also controls the size of those dots."
4035 },
4036 "labelsDivStyles": {
4037 "default": "null",
4038 "labels": ["Legend"],
4039 "type": "{}",
4040 "description": "Additional styles to apply to the currently-highlighted points div. For example, { 'font-weight': 'bold' } will make the labels bold."
4041 },
4042 "drawPoints": {
4043 "default": "false",
4044 "labels": ["Data Line display"],
4045 "type": "boolean",
4046 "description": "Draw a small dot at each point, in addition to a line going through the point. This makes the individual data points easier to see, but can increase visual clutter in the chart."
4047 },
4048 "height": {
4049 "default": "320",
4050 "labels": ["Overall display"],
4051 "type": "integer",
4052 "description": "Height, in pixels, of the chart. If the container div has been explicitly sized, this will be ignored."
4053 },
4054 "zoomCallback": {
4055 "default": "null",
4056 "labels": ["Callbacks"],
4057 "type": "function(minDate, maxDate, yRanges)",
4058 "description": "A function to call when the zoom window is changed (either by zooming in or out). minDate and maxDate are milliseconds since epoch. yRanges is an array of [bottom, top] pairs, one for each y-axis."
4059 },
4060 "pointClickCallback": {
4061 "default": "",
4062 "labels": ["Callbacks", "Interactive Elements"],
4063 "type": "",
4064 "description": ""
4065 },
4066 "colors": {
4067 "default": "(see description)",
4068 "labels": ["Data Series Colors"],
4069 "type": "array<string>",
4070 "example": "['red', '#00FF00']",
4071 "description": "List of colors for the data series. These can be of the form \"#AABBCC\" or \"rgb(255,100,200)\" or \"yellow\", etc. If not specified, equally-spaced points around a color wheel are used."
4072 },
4073 "connectSeparatedPoints": {
4074 "default": "false",
4075 "labels": ["Data Line display"],
4076 "type": "boolean",
4077 "description": "Usually, when Dygraphs encounters a missing value in a data series, it interprets this as a gap and draws it as such. If, instead, the missing values represents an x-value for which only a different series has data, then you'll want to connect the dots by setting this to true. To explicitly include a gap with this option set, use a value of NaN."
4078 },
4079 "highlightCallback": {
4080 "default": "null",
4081 "labels": ["Callbacks"],
4082 "type": "function(event, x, points,row)",
4083 "description": "When set, this callback gets called every time a new point is highlighted. The parameters are the JavaScript mousemove event, the x-coordinate of the highlighted points and an array of highlighted points: <code>[ {name: 'series', yval: y-value}, &hellip; ]</code>"
4084 },
4085 "includeZero": {
4086 "default": "false",
4087 "labels": ["Axis display"],
4088 "type": "boolean",
4089 "description": "Usually, dygraphs will use the range of the data plus some padding to set the range of the y-axis. If this option is set, the y-axis will always include zero, typically as the lowest value. This can be used to avoid exaggerating the variance in the data"
4090 },
4091 "rollPeriod": {
4092 "default": "1",
4093 "labels": ["Error Bars", "Rolling Averages"],
4094 "type": "integer &gt;= 1",
4095 "description": "Number of days over which to average data. Discussed extensively above."
4096 },
4097 "unhighlightCallback": {
4098 "default": "null",
4099 "labels": ["Callbacks"],
4100 "type": "function(event)",
4101 "description": "When set, this callback gets called every time the user stops highlighting any point by mousing out of the graph. The parameter is the mouseout event."
4102 },
4103 "axisTickSize": {
4104 "default": "3.0",
4105 "labels": ["Axis display"],
4106 "type": "number",
4107 "description": "The size of the line to display next to each tick mark on x- or y-axes."
4108 },
4109 "labelsSeparateLines": {
4110 "default": "false",
4111 "labels": ["Legend"],
4112 "type": "boolean",
4113 "description": "Put <code>&lt;br/&gt;</code> between lines in the label string. Often used in conjunction with <strong>labelsDiv</strong>."
4114 },
4115 "xValueFormatter": {
4116 "default": "(Round to 2 decimal places)",
4117 "labels": ["Axis display"],
4118 "type": "function(x)",
4119 "description": "Function to provide a custom display format for the X value for mouseover."
4120 },
4121 "pixelsPerYLabel": {
4122 "default": "30",
4123 "labels": ["Axis display", "Grid"],
4124 "type": "integer",
4125 "description": "Number of pixels to require between each x- and y-label. Larger values will yield a sparser axis with fewer ticks."
4126 },
4127 "annotationMouseOverHandler": {
4128 "default": "null",
4129 "labels": ["Annotations"],
4130 "type": "function(annotation, point, dygraph, event)",
4131 "description": "If provided, this function is called whenever the user mouses over an annotation."
4132 },
4133 "annotationMouseOutHandler": {
4134 "default": "null",
4135 "labels": ["Annotations"],
4136 "type": "function(annotation, point, dygraph, event)",
4137 "description": "If provided, this function is called whenever the user mouses out of an annotation."
4138 },
4139 "annotationClickHandler": {
4140 "default": "null",
4141 "labels": ["Annotations"],
4142 "type": "function(annotation, point, dygraph, event)",
4143 "description": "If provided, this function is called whenever the user clicks on an annotation."
4144 },
4145 "annotationDblClickHandler": {
4146 "default": "null",
4147 "labels": ["Annotations"],
4148 "type": "function(annotation, point, dygraph, event)",
4149 "description": "If provided, this function is called whenever the user double-clicks on an annotation."
4150 },
4151 "drawCallback": {
4152 "default": "null",
4153 "labels": ["Callbacks"],
4154 "type": "function(dygraph, is_initial)",
4155 "description": "When set, this callback gets called every time the dygraph is drawn. This includes the initial draw, after zooming and repeatedly while panning. The first parameter is the dygraph being drawn. The second is a boolean value indicating whether this is the initial draw."
4156 },
4157 "labelsKMG2": {
4158 "default": "false",
4159 "labels": ["Value display/formatting"],
4160 "type": "boolean",
4161 "description": "Show k/M/G for kilo/Mega/Giga on y-axis. This is different than <code>labelsKMB</code> in that it uses base 2, not 10."
4162 },
4163 "delimiter": {
4164 "default": ",",
4165 "labels": ["CSV parsing"],
4166 "type": "string",
4167 "description": "The delimiter to look for when separating fields of a CSV file. Setting this to a tab is not usually necessary, since tab-delimited data is auto-detected."
4168 },
4169 "axisLabelFontSize": {
4170 "default": "14",
4171 "labels": ["Axis display"],
4172 "type": "integer",
4173 "description": "Size of the font (in pixels) to use in the axis labels, both x- and y-axis."
4174 },
4175 "underlayCallback": {
4176 "default": "null",
4177 "labels": ["Callbacks"],
4178 "type": "function(canvas, area, dygraph)",
4179 "description": "When set, this callback gets called before the chart is drawn. It details on how to use this."
4180 },
4181 "width": {
4182 "default": "480",
4183 "labels": ["Overall display"],
4184 "type": "integer",
4185 "description": "Width, in pixels, of the chart. If the container div has been explicitly sized, this will be ignored."
4186 },
4187 "interactionModel": {
4188 "default": "...",
4189 "labels": ["Interactive Elements"],
4190 "type": "Object",
4191 "description": "TODO(konigsberg): document this"
4192 },
4193 "xTicker": {
4194 "default": "Dygraph.dateTicker or Dygraph.numericTicks",
4195 "labels": ["Axis display"],
4196 "type": "function(min, max, dygraph) -> [{v: ..., label: ...}, ...]",
4197 "description": "This lets you specify an arbitrary function to generate tick marks on an axis. The tick marks are an array of (value, label) pairs. The built-in functions go to great lengths to choose good tick marks so, if you set this option, you'll most likely want to call one of them and modify the result."
4198 },
4199 "xAxisLabelWidth": {
4200 "default": "50",
4201 "labels": ["Axis display"],
4202 "type": "integer",
4203 "description": "Width, in pixels, of the x-axis labels."
4204 },
4205 "showLabelsOnHighlight": {
4206 "default": "true",
4207 "labels": ["Interactive Elements", "Legend"],
4208 "type": "boolean",
4209 "description": "Whether to show the legend upon mouseover."
4210 },
4211 "axis": {
4212 "default": "(none)",
4213 "labels": ["Axis display"],
4214 "type": "string or object",
4215 "description": "Set to either an object ({}) filled with options for this axis or to the name of an existing data series with its own axis to re-use that axis. See tests for usage."
4216 },
4217 "pixelsPerXLabel": {
4218 "default": "60",
4219 "labels": ["Axis display", "Grid"],
4220 "type": "integer",
4221 "description": "Number of pixels to require between each x- and y-label. Larger values will yield a sparser axis with fewer ticks."
4222 },
4223 "labelsDiv": {
4224 "default": "null",
4225 "labels": ["Legend"],
4226 "type": "DOM element or string",
4227 "example": "<code style='font-size: small'>document.getElementById('foo')</code>or<code>'foo'",
4228 "description": "Show data labels in an external div, rather than on the graph. This value can either be a div element or a div id."
4229 },
4230 "fractions": {
4231 "default": "false",
4232 "labels": ["CSV parsing", "Error Bars"],
4233 "type": "boolean",
4234 "description": "When set, attempt to parse each cell in the CSV file as \"a/b\", where a and b are integers. The ratio will be plotted. This allows computation of Wilson confidence intervals (see below)."
4235 },
4236 "logscale": {
4237 "default": "false",
4238 "labels": ["Axis display"],
4239 "type": "boolean",
4240 "description": "When set for a y-axis, the graph shows that axis in log scale. Any values less than or equal to zero are not displayed.\n\nNot compatible with showZero, and ignores connectSeparatedPoints. Also, showing log scale with valueRanges that are less than zero will result in an unviewable graph."
4241 },
4242 "strokeWidth": {
4243 "default": "1.0",
4244 "labels": ["Data Line display"],
4245 "type": "integer",
4246 "example": "0.5, 2.0",
4247 "description": "The width of the lines connecting data points. This can be used to increase the contrast or some graphs."
4248 },
4249 "wilsonInterval": {
4250 "default": "true",
4251 "labels": ["Error Bars"],
4252 "type": "boolean",
4253 "description": "Use in conjunction with the \"fractions\" option. Instead of plotting +/- N standard deviations, dygraphs will compute a Wilson confidence interval and plot that. This has more reasonable behavior for ratios close to 0 or 1."
4254 },
4255 "fillGraph": {
4256 "default": "false",
4257 "labels": ["Data Line display"],
4258 "type": "boolean",
4259 "description": "Should the area underneath the graph be filled? This option is not compatible with error bars."
4260 },
4261 "highlightCircleSize": {
4262 "default": "3",
4263 "labels": ["Interactive Elements"],
4264 "type": "integer",
4265 "description": "The size in pixels of the dot drawn over highlighted points."
4266 },
4267 "gridLineColor": {
4268 "default": "rgb(128,128,128)",
4269 "labels": ["Grid"],
4270 "type": "red, blue",
4271 "description": "The color of the gridlines."
4272 },
4273 "visibility": {
4274 "default": "[true, true, ...]",
4275 "labels": ["Data Line display"],
4276 "type": "Array of booleans",
4277 "description": "Which series should initially be visible? Once the Dygraph has been constructed, you can access and modify the visibility of each series using the <code>visibility</code> and <code>setVisibility</code> methods."
4278 },
4279 "valueRange": {
4280 "default": "Full range of the input is shown",
4281 "labels": ["Axis display"],
4282 "type": "Array of two numbers",
4283 "example": "[10, 110]",
4284 "description": "Explicitly set the vertical range of the graph to [low, high]."
4285 },
4286 "labelsDivWidth": {
4287 "default": "250",
4288 "labels": ["Legend"],
4289 "type": "integer",
4290 "description": "Width (in pixels) of the div which shows information on the currently-highlighted points."
4291 },
4292 "colorSaturation": {
4293 "default": "1.0",
4294 "labels": ["Data Series Colors"],
4295 "type": "0.0 - 1.0",
4296 "description": "If <strong>colors</strong> is not specified, saturation of the automatically-generated data series colors."
4297 },
4298 "yAxisLabelWidth": {
4299 "default": "50",
4300 "labels": ["Axis display"],
4301 "type": "integer",
4302 "description": "Width, in pixels, of the y-axis labels."
4303 },
4304 "hideOverlayOnMouseOut": {
4305 "default": "true",
4306 "labels": ["Interactive Elements", "Legend"],
4307 "type": "boolean",
4308 "description": "Whether to hide the legend when the mouse leaves the chart area."
4309 },
4310 "yValueFormatter": {
4311 "default": "(Round to 2 decimal places)",
4312 "labels": ["Axis display"],
4313 "type": "function(x)",
4314 "description": "Function to provide a custom display format for the Y value for mouseover."
4315 },
4316 "legend": {
4317 "default": "onmouseover",
4318 "labels": ["Legend"],
4319 "type": "string",
4320 "description": "When to display the legend. By default, it only appears when a user mouses over the chart. Set it to \"always\" to always display a legend of some sort."
4321 },
4322 "labelsShowZeroValues": {
4323 "default": "true",
4324 "labels": ["Legend"],
4325 "type": "boolean",
4326 "description": "Show zero value labels in the labelsDiv."
4327 },
4328 "stepPlot": {
4329 "default": "false",
4330 "labels": ["Data Line display"],
4331 "type": "boolean",
4332 "description": "When set, display the graph as a step plot instead of a line plot."
4333 },
4334 "labelsKMB": {
4335 "default": "false",
4336 "labels": ["Value display/formatting"],
4337 "type": "boolean",
4338 "description": "Show K/M/B for thousands/millions/billions on y-axis."
4339 },
4340 "rightGap": {
4341 "default": "5",
4342 "labels": ["Overall display"],
4343 "type": "integer",
4344 "description": "Number of pixels to leave blank at the right edge of the Dygraph. This makes it easier to highlight the right-most data point."
4345 },
4346 "avoidMinZero": {
4347 "default": "false",
4348 "labels": ["Axis display"],
4349 "type": "boolean",
4350 "description": "When set, the heuristic that fixes the Y axis at zero for a data set with the minimum Y value of zero is disabled. \nThis is particularly useful for data sets that contain many zero values, especially for step plots which may otherwise have lines not visible running along the bottom axis."
4351 },
4352 "xAxisLabelFormatter": {
4353 "default": "Dygraph.dateAxisFormatter",
4354 "labels": ["Axis display", "Value display/formatting"],
4355 "type": "function(date, granularity)",
4356 "description": "Function to call to format values along the x axis."
4357 },
4358 "clickCallback": {
4359 "snippet": "function(e, date){<br>&nbsp;&nbsp;alert(date);<br>}",
4360 "default": "null",
4361 "labels": ["Callbacks"],
4362 "type": "function(e, date)",
4363 "description": "A function to call when a data point is clicked. The function should take two arguments, the event object for the click and the date that was clicked."
4364 },
4365 "yAxisLabelFormatter": {
4366 "default": "yValueFormatter",
4367 "labels": ["Axis display", "Value display/formatting"],
4368 "type": "function(x)",
4369 "description": "Function used to format values along the Y axis. By default it uses the same as the <code>yValueFormatter</code> unless specified."
4370 },
4371 "labels": {
4372 "default": "[\"X\", \"Y1\", \"Y2\", ...]*",
4373 "labels": ["Legend"],
4374 "type": "array<string>",
4375 "description": "A name for each data series, including the independent (X) series. For CSV files and DataTable objections, this is determined by context. For raw data, this must be specified. If it is not, default values are supplied and a warning is logged."
4376 },
4377 "dateWindow": {
4378 "default": "Full range of the input is shown",
4379 "labels": ["Axis display"],
4380 "type": "Array of two Dates or numbers",
4381 "example": "[<br>&nbsp;&nbsp;Date.parse('2006-01-01'),<br>&nbsp;&nbsp;(new Date()).valueOf()<br>]",
4382 "description": "Initially zoom in on a section of the graph. Is of the form [earliest, latest], where earliest/latest are milliseconds since epoch. If the data for the x-axis is numeric, the values in dateWindow must also be numbers."
4383 },
4384 "showRoller": {
4385 "default": "false",
4386 "labels": ["Interactive Elements", "Rolling Averages"],
4387 "type": "boolean",
4388 "description": "If the rolling average period text box should be shown."
4389 },
4390 "sigma": {
4391 "default": "2.0",
4392 "labels": ["Error Bars"],
4393 "type": "float",
4394 "description": "When errorBars is set, shade this many standard deviations above/below each point."
4395 },
4396 "customBars": {
4397 "default": "false",
4398 "labels": ["CSV parsing", "Error Bars"],
4399 "type": "boolean",
4400 "description": "When set, parse each CSV cell as \"low;middle;high\". Error bars will be drawn for each point between low and high, with the series itself going through middle."
4401 },
4402 "colorValue": {
4403 "default": "1.0",
4404 "labels": ["Data Series Colors"],
4405 "type": "float (0.0 - 1.0)",
4406 "description": "If colors is not specified, value of the data series colors, as in hue/saturation/value. (0.0-1.0, default 0.5)"
4407 },
4408 "errorBars": {
4409 "default": "false",
4410 "labels": ["CSV parsing", "Error Bars"],
4411 "type": "boolean",
4412 "description": "Does the data contain standard deviations? Setting this to true alters the input format (see above)."
4413 },
4414 "displayAnnotations": {
4415 "default": "false",
4416 "labels": ["Annotations"],
4417 "type": "boolean",
4418 "description": "Only applies when Dygraphs is used as a GViz chart. Causes string columns following a data series to be interpreted as annotations on points in that series. This is the same format used by Google's AnnotatedTimeLine chart."
4419 },
4420 "panEdgeFraction": {
4421 "default": "null",
4422 "labels": ["Axis Display", "Interactive Elements"],
4423 "type": "float",
4424 "default": "null",
4425 "description": "A value representing the farthest a graph may be panned, in percent of the display. For example, a value of 0.1 means that the graph can only be panned 10% pased the edges of the displayed values. null means no bounds."
4426 },
4427 "title": {
4428 "labels": ["Chart labels"],
4429 "type": "string",
4430 "default": "null",
4431 "description": "Text to display above the chart. You can supply any HTML for this value, not just text. If you wish to style it using CSS, use the 'dygraph-label' or 'dygraph-title' classes."
4432 },
4433 "titleHeight": {
4434 "default": "18",
4435 "labels": ["Chart labels"],
4436 "type": "integer",
4437 "description": "Height of the chart title, in pixels. This also controls the default font size of the title. If you style the title on your own, this controls how much space is set aside above the chart for the title's div."
4438 },
4439 "xlabel": {
4440 "labels": ["Chart labels"],
4441 "type": "string",
4442 "default": "null",
4443 "description": "Text to display below the chart's x-axis. You can supply any HTML for this value, not just text. If you wish to style it using CSS, use the 'dygraph-label' or 'dygraph-xlabel' classes."
4444 },
4445 "xLabelHeight": {
4446 "labels": ["Chart labels"],
4447 "type": "integer",
4448 "default": "18",
4449 "description": "Height of the x-axis label, in pixels. This also controls the default font size of the x-axis label. If you style the label on your own, this controls how much space is set aside below the chart for the x-axis label's div."
4450 },
4451 "ylabel": {
4452 "labels": ["Chart labels"],
4453 "type": "string",
4454 "default": "null",
4455 "description": "Text to display to the left of the chart's y-axis. You can supply any HTML for this value, not just text. If you wish to style it using CSS, use the 'dygraph-label' or 'dygraph-ylabel' classes. The text will be rotated 90 degrees by default, so CSS rules may behave in unintuitive ways. No additional space is set aside for a y-axis label. If you need more space, increase the width of the y-axis tick labels using the yAxisLabelWidth option. If you need a wider div for the y-axis label, either style it that way with CSS (but remember that it's rotated, so width is controlled by the 'height' property) or set the yLabelWidth option."
4456 },
4457 "yLabelWidth": {
4458 "labels": ["Chart labels"],
4459 "type": "integer",
4460 "default": "18",
4461 "description": "Width of the div which contains the y-axis label. Since the y-axis label appears rotated 90 degrees, this actually affects the height of its div."
4462 },
4463 "isZoomedIgnoreProgrammaticZoom" : {
4464 "default": "false",
4465 "labels": ["Zooming"],
4466 "type": "boolean",
4467 "description" : "When this option is passed to updateOptions() along with either the <code>dateWindow</code> or <code>valueRange</code> options, the zoom flags are not changed to reflect a zoomed state. This is primarily useful for when the display area of a chart is changed programmatically and also where manual zooming is allowed and use is made of the <code>isZoomed</code> method to determine this."
4468 },
4469 "sigFigs" : {
4470 "default": "null",
4471 "labels": ["Value display/formatting"],
4472 "type": "integer",
4473 "description": "By default, dygraphs displays numbers with a fixed number of digits after the decimal point. If you'd prefer to have a fixed number of significant figures, set this option to that number of sig figs. A value of 2, for instance, would cause 1 to be display as 1.0 and 1234 to be displayed as 1.23e+3."
4474 },
4475 "digitsAfterDecimal" : {
4476 "default": "2",
4477 "labels": ["Value display/formatting"],
4478 "type": "integer",
4479 "description": "Unless it's run in scientific mode (see the <code>sigFigs</code> option), dygraphs displays numbers with <code>digitsAfterDecimal</code> digits after the decimal point. Trailing zeros are not displayed, so with a value of 2 you'll get '0', '0.1', '0.12', '123.45' but not '123.456' (it will be rounded to '123.46'). Numbers with absolute value less than 0.1^digitsAfterDecimal (i.e. those which would show up as '0.00') will be displayed in scientific notation."
4480 },
4481 "maxNumberWidth" : {
4482 "default": "6",
4483 "labels": ["Value display/formatting"],
4484 "type": "integer",
4485 "description": "When displaying numbers in normal (not scientific) mode, large numbers will be displayed with many trailing zeros (e.g. 100000000 instead of 1e9). This can lead to unwieldy y-axis labels. If there are more than <code>maxNumberWidth</code> digits to the left of the decimal in a number, dygraphs will switch to scientific notation, even when not operating in scientific mode. If you'd like to see all those digits, set this to something large, like 20 or 30."
4486 }
4487 }
4488 ; // </JSON>
4489 // NOTE: in addition to parsing as JS, this snippet is expected to be valid
4490 // JSON. This assumption cannot be checked in JS, but it will be checked when
4491 // documentation is generated by the generate-documentation.py script. For the
4492 // most part, this just means that you should always use double quotes.
4493
4494 // Do a quick sanity check on the options reference.
4495 (function() {
4496 var warn = function(msg) { if (console) console.warn(msg); };
4497 var flds = ['type', 'default', 'description'];
4498 var valid_cats = [
4499 'Annotations',
4500 'Axis display',
4501 'Chart labels',
4502 'CSV parsing',
4503 'Callbacks',
4504 'Data Line display',
4505 'Data Series Colors',
4506 'Error Bars',
4507 'Grid',
4508 'Interactive Elements',
4509 'Legend',
4510 'Overall display',
4511 'Rolling Averages',
4512 'Value display/formatting',
4513 'Zooming'
4514 ];
4515 var cats = {};
4516 for (var i = 0; i < valid_cats.length; i++) cats[valid_cats[i]] = true;
4517
4518 for (var k in Dygraph.OPTIONS_REFERENCE) {
4519 if (!Dygraph.OPTIONS_REFERENCE.hasOwnProperty(k)) continue;
4520 var op = Dygraph.OPTIONS_REFERENCE[k];
4521 for (var i = 0; i < flds.length; i++) {
4522 if (!op.hasOwnProperty(flds[i])) {
4523 warn('Option ' + k + ' missing "' + flds[i] + '" property');
4524 } else if (typeof(op[flds[i]]) != 'string') {
4525 warn(k + '.' + flds[i] + ' must be of type string');
4526 }
4527 }
4528 var labels = op['labels'];
4529 if (typeof(labels) !== 'object') {
4530 warn('Option "' + k + '" is missing a "labels": [...] option');
4531 for (var i = 0; i < labels.length; i++) {
4532 if (!cats.hasOwnProperty(labels[i])) {
4533 warn('Option "' + k + '" has label "' + labels[i] +
4534 '", which is invalid.');
4535 }
4536 }
4537 }
4538 }
4539 })();
4540 // </REMOVE_FOR_COMBINED>