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