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