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