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