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