f83e36d6b101ba886196f4dc6c89c0521bcb8b77
[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
28 Date,SeriesA,SeriesB,...
29 YYYYMMDD,A1,sigmaA1,B1,sigmaB1,...
30 YYYYMMDD,A2,sigmaA2,B2,sigmaB2,...
31
32 If the 'fractions' option is set, the input should be of the form:
33
34 Date,SeriesA,SeriesB,...
35 YYYYMMDD,A1/B1,A2/B2,...
36 YYYYMMDD,A1/B1,A2/B2,...
37
38 And error bars will be calculated automatically using a binomial distribution.
39
40 For further documentation and examples, see http://www.danvk.org/dygraphs
41
42 */
43
44 /**
45 * An interactive, zoomable graph
46 * @param {String | Function} file A file containing CSV data or a function that
47 * returns this data. The expected format for each line is
48 * YYYYMMDD,val1,val2,... or, if attrs.errorBars is set,
49 * YYYYMMDD,val1,stddev1,val2,stddev2,...
50 * @param {Object} attrs Various other attributes, e.g. errorBars determines
51 * whether the input data contains error ranges.
52 */
53 Dygraph = function(div, data, opts) {
54 if (arguments.length > 0) {
55 if (arguments.length == 4) {
56 // Old versions of dygraphs took in the series labels as a constructor
57 // parameter. This doesn't make sense anymore, but it's easy to continue
58 // to support this usage.
59 this.warn("Using deprecated four-argument dygraph constructor");
60 this.__old_init__(div, data, arguments[2], arguments[3]);
61 } else {
62 this.__init__(div, data, opts);
63 }
64 }
65 };
66
67 Dygraph.NAME = "Dygraph";
68 Dygraph.VERSION = "1.2";
69 Dygraph.__repr__ = function() {
70 return "[" + this.NAME + " " + this.VERSION + "]";
71 };
72 Dygraph.toString = function() {
73 return this.__repr__();
74 };
75
76 // Various default values
77 Dygraph.DEFAULT_ROLL_PERIOD = 1;
78 Dygraph.DEFAULT_WIDTH = 480;
79 Dygraph.DEFAULT_HEIGHT = 320;
80 Dygraph.AXIS_LINE_WIDTH = 0.3;
81
82 // Default attribute values.
83 Dygraph.DEFAULT_ATTRS = {
84 highlightCircleSize: 3,
85 pixelsPerXLabel: 60,
86 pixelsPerYLabel: 30,
87
88 labelsDivWidth: 250,
89 labelsDivStyles: {
90 // TODO(danvk): move defaults from createStatusMessage_ here.
91 },
92 labelsSeparateLines: false,
93 labelsShowZeroValues: true,
94 labelsKMB: false,
95 labelsKMG2: false,
96 showLabelsOnHighlight: true,
97
98 yValueFormatter: function(x) { return Dygraph.round_(x, 2); },
99
100 strokeWidth: 1.0,
101
102 axisTickSize: 3,
103 axisLabelFontSize: 14,
104 xAxisLabelWidth: 50,
105 yAxisLabelWidth: 50,
106 xAxisLabelFormatter: Dygraph.dateAxisFormatter,
107 rightGap: 5,
108
109 showRoller: false,
110 xValueFormatter: Dygraph.dateString_,
111 xValueParser: Dygraph.dateParser,
112 xTicker: Dygraph.dateTicker,
113
114 delimiter: ',',
115
116 logScale: false,
117 sigma: 2.0,
118 errorBars: false,
119 fractions: false,
120 wilsonInterval: true, // only relevant if fractions is true
121 customBars: false,
122 fillGraph: false,
123 fillAlpha: 0.15,
124 connectSeparatedPoints: false,
125
126 stackedGraph: false,
127 hideOverlayOnMouseOut: true,
128
129 stepPlot: false
130 };
131
132 // Various logging levels.
133 Dygraph.DEBUG = 1;
134 Dygraph.INFO = 2;
135 Dygraph.WARNING = 3;
136 Dygraph.ERROR = 3;
137
138 // Directions for panning and zooming. Use bit operations when combined
139 // values are possible.
140 Dygraph.HORIZONTAL = 1;
141 Dygraph.VERTICAL = 2;
142
143 // Used for initializing annotation CSS rules only once.
144 Dygraph.addedAnnotationCSS = false;
145
146 Dygraph.prototype.__old_init__ = function(div, file, labels, attrs) {
147 // Labels is no longer a constructor parameter, since it's typically set
148 // directly from the data source. It also conains a name for the x-axis,
149 // which the previous constructor form did not.
150 if (labels != null) {
151 var new_labels = ["Date"];
152 for (var i = 0; i < labels.length; i++) new_labels.push(labels[i]);
153 Dygraph.update(attrs, { 'labels': new_labels });
154 }
155 this.__init__(div, file, attrs);
156 };
157
158 /**
159 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
160 * and interaction &lt;canvas&gt; inside of it. See the constructor for details
161 * on the parameters.
162 * @param {Element} div the Element to render the graph into.
163 * @param {String | Function} file Source data
164 * @param {Object} attrs Miscellaneous other options
165 * @private
166 */
167 Dygraph.prototype.__init__ = function(div, file, attrs) {
168 // Support two-argument constructor
169 if (attrs == null) { attrs = {}; }
170
171 // Copy the important bits into the object
172 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
173 this.maindiv_ = div;
174 this.file_ = file;
175 this.rollPeriod_ = attrs.rollPeriod || Dygraph.DEFAULT_ROLL_PERIOD;
176 this.previousVerticalX_ = -1;
177 this.fractions_ = attrs.fractions || false;
178 this.dateWindow_ = attrs.dateWindow || null;
179 // valueRange and valueWindow are similar, but not the same. valueRange is a
180 // locally-stored copy of the attribute. valueWindow starts off the same as
181 // valueRange but is impacted by zoom or pan effects. valueRange is kept
182 // around to restore the original value back to valueRange.
183 this.valueRange_ = attrs.valueRange || null;
184 this.valueWindow_ = this.valueRange_;
185
186 this.wilsonInterval_ = attrs.wilsonInterval || true;
187 this.is_initial_draw_ = true;
188 this.annotations_ = [];
189
190 // Clear the div. This ensure that, if multiple dygraphs are passed the same
191 // div, then only one will be drawn.
192 div.innerHTML = "";
193
194 // If the div isn't already sized then inherit from our attrs or
195 // give it a default size.
196 if (div.style.width == '') {
197 div.style.width = attrs.width || Dygraph.DEFAULT_WIDTH + "px";
198 }
199 if (div.style.height == '') {
200 div.style.height = attrs.height || Dygraph.DEFAULT_HEIGHT + "px";
201 }
202 this.width_ = parseInt(div.style.width, 10);
203 this.height_ = parseInt(div.style.height, 10);
204 // The div might have been specified as percent of the current window size,
205 // convert that to an appropriate number of pixels.
206 if (div.style.width.indexOf("%") == div.style.width.length - 1) {
207 this.width_ = div.offsetWidth;
208 }
209 if (div.style.height.indexOf("%") == div.style.height.length - 1) {
210 this.height_ = div.offsetHeight;
211 }
212
213 if (this.width_ == 0) {
214 this.error("dygraph has zero width. Please specify a width in pixels.");
215 }
216 if (this.height_ == 0) {
217 this.error("dygraph has zero height. Please specify a height in pixels.");
218 }
219
220 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
221 if (attrs['stackedGraph']) {
222 attrs['fillGraph'] = true;
223 // TODO(nikhilk): Add any other stackedGraph checks here.
224 }
225
226 // Dygraphs has many options, some of which interact with one another.
227 // To keep track of everything, we maintain two sets of options:
228 //
229 // this.user_attrs_ only options explicitly set by the user.
230 // this.attrs_ defaults, options derived from user_attrs_, data.
231 //
232 // Options are then accessed this.attr_('attr'), which first looks at
233 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
234 // defaults without overriding behavior that the user specifically asks for.
235 this.user_attrs_ = {};
236 Dygraph.update(this.user_attrs_, attrs);
237
238 this.attrs_ = {};
239 Dygraph.update(this.attrs_, Dygraph.DEFAULT_ATTRS);
240
241 this.boundaryIds_ = [];
242
243 // Make a note of whether labels will be pulled from the CSV file.
244 this.labelsFromCSV_ = (this.attr_("labels") == null);
245
246 Dygraph.addAnnotationRule();
247
248 // Create the containing DIV and other interactive elements
249 this.createInterface_();
250
251 this.start_();
252 };
253
254 Dygraph.prototype.attr_ = function(name) {
255 if (typeof(this.user_attrs_[name]) != 'undefined') {
256 return this.user_attrs_[name];
257 } else if (typeof(this.attrs_[name]) != 'undefined') {
258 return this.attrs_[name];
259 } else {
260 return null;
261 }
262 };
263
264 // TODO(danvk): any way I can get the line numbers to be this.warn call?
265 Dygraph.prototype.log = function(severity, message) {
266 if (typeof(console) != 'undefined') {
267 switch (severity) {
268 case Dygraph.DEBUG:
269 console.debug('dygraphs: ' + message);
270 break;
271 case Dygraph.INFO:
272 console.info('dygraphs: ' + message);
273 break;
274 case Dygraph.WARNING:
275 console.warn('dygraphs: ' + message);
276 break;
277 case Dygraph.ERROR:
278 console.error('dygraphs: ' + message);
279 break;
280 }
281 }
282 }
283 Dygraph.prototype.info = function(message) {
284 this.log(Dygraph.INFO, message);
285 }
286 Dygraph.prototype.warn = function(message) {
287 this.log(Dygraph.WARNING, message);
288 }
289 Dygraph.prototype.error = function(message) {
290 this.log(Dygraph.ERROR, message);
291 }
292
293 /**
294 * Returns the current rolling period, as set by the user or an option.
295 * @return {Number} The number of days in the rolling window
296 */
297 Dygraph.prototype.rollPeriod = function() {
298 return this.rollPeriod_;
299 };
300
301 /**
302 * Returns the currently-visible x-range. This can be affected by zooming,
303 * panning or a call to updateOptions.
304 * Returns a two-element array: [left, right].
305 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
306 */
307 Dygraph.prototype.xAxisRange = function() {
308 if (this.dateWindow_) return this.dateWindow_;
309
310 // The entire chart is visible.
311 var left = this.rawData_[0][0];
312 var right = this.rawData_[this.rawData_.length - 1][0];
313 return [left, right];
314 };
315
316 /**
317 * Returns the currently-visible y-range. This can be affected by zooming,
318 * panning or a call to updateOptions.
319 * Returns a two-element array: [bottom, top].
320 */
321 Dygraph.prototype.yAxisRange = function() {
322 return this.displayedYRange_;
323 };
324
325 /**
326 * Convert from data coordinates to canvas/div X/Y coordinates.
327 * Returns a two-element array: [X, Y]
328 */
329 Dygraph.prototype.toDomCoords = function(x, y) {
330 var ret = [null, null];
331 var area = this.plotter_.area;
332 if (x !== null) {
333 var xRange = this.xAxisRange();
334 ret[0] = area.x + (x - xRange[0]) / (xRange[1] - xRange[0]) * area.w;
335 }
336
337 if (y !== null) {
338 var yRange = this.yAxisRange();
339 ret[1] = area.y + (yRange[1] - y) / (yRange[1] - yRange[0]) * area.h;
340 }
341
342 return ret;
343 };
344
345 // TODO(danvk): use these functions throughout dygraphs.
346 /**
347 * Convert from canvas/div coords to data coordinates.
348 * Returns a two-element array: [X, Y]
349 */
350 Dygraph.prototype.toDataCoords = function(x, y) {
351 var ret = [null, null];
352 var area = this.plotter_.area;
353 if (x !== null) {
354 var xRange = this.xAxisRange();
355 ret[0] = xRange[0] + (x - area.x) / area.w * (xRange[1] - xRange[0]);
356 }
357
358 if (y !== null) {
359 var yRange = this.yAxisRange();
360 ret[1] = yRange[0] + (area.h - y) / area.h * (yRange[1] - yRange[0]);
361 }
362
363 return ret;
364 };
365
366 /**
367 * Returns the number of columns (including the independent variable).
368 */
369 Dygraph.prototype.numColumns = function() {
370 return this.rawData_[0].length;
371 };
372
373 /**
374 * Returns the number of rows (excluding any header/label row).
375 */
376 Dygraph.prototype.numRows = function() {
377 return this.rawData_.length;
378 };
379
380 /**
381 * Returns the value in the given row and column. If the row and column exceed
382 * the bounds on the data, returns null. Also returns null if the value is
383 * missing.
384 */
385 Dygraph.prototype.getValue = function(row, col) {
386 if (row < 0 || row > this.rawData_.length) return null;
387 if (col < 0 || col > this.rawData_[row].length) return null;
388
389 return this.rawData_[row][col];
390 };
391
392 Dygraph.addEvent = function(el, evt, fn) {
393 var normed_fn = function(e) {
394 if (!e) var e = window.event;
395 fn(e);
396 };
397 if (window.addEventListener) { // Mozilla, Netscape, Firefox
398 el.addEventListener(evt, normed_fn, false);
399 } else { // IE
400 el.attachEvent('on' + evt, normed_fn);
401 }
402 };
403
404 Dygraph.clipCanvas_ = function(cnv, clip) {
405 var ctx = cnv.getContext("2d");
406 ctx.beginPath();
407 ctx.rect(clip.left, clip.top, clip.width, clip.height);
408 ctx.clip();
409 };
410
411 /**
412 * Generates interface elements for the Dygraph: a containing div, a div to
413 * display the current point, and a textbox to adjust the rolling average
414 * period. Also creates the Renderer/Layout elements.
415 * @private
416 */
417 Dygraph.prototype.createInterface_ = function() {
418 // Create the all-enclosing graph div
419 var enclosing = this.maindiv_;
420
421 this.graphDiv = document.createElement("div");
422 this.graphDiv.style.width = this.width_ + "px";
423 this.graphDiv.style.height = this.height_ + "px";
424 enclosing.appendChild(this.graphDiv);
425
426 var clip = {
427 top: 0,
428 left: this.attr_("yAxisLabelWidth") + 2 * this.attr_("axisTickSize")
429 };
430 clip.width = this.width_ - clip.left - this.attr_("rightGap");
431 clip.height = this.height_ - this.attr_("axisLabelFontSize")
432 - 2 * this.attr_("axisTickSize");
433 this.clippingArea_ = clip;
434
435 // Create the canvas for interactive parts of the chart.
436 this.canvas_ = Dygraph.createCanvas();
437 this.canvas_.style.position = "absolute";
438 this.canvas_.width = this.width_;
439 this.canvas_.height = this.height_;
440 this.canvas_.style.width = this.width_ + "px"; // for IE
441 this.canvas_.style.height = this.height_ + "px"; // for IE
442
443 // ... and for static parts of the chart.
444 this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
445
446 // The interactive parts of the graph are drawn on top of the chart.
447 this.graphDiv.appendChild(this.hidden_);
448 this.graphDiv.appendChild(this.canvas_);
449 this.mouseEventElement_ = this.canvas_;
450
451 // Make sure we don't overdraw.
452 Dygraph.clipCanvas_(this.hidden_, this.clippingArea_);
453 Dygraph.clipCanvas_(this.canvas_, this.clippingArea_);
454
455 var dygraph = this;
456 Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(e) {
457 dygraph.mouseMove_(e);
458 });
459 Dygraph.addEvent(this.mouseEventElement_, 'mouseout', function(e) {
460 dygraph.mouseOut_(e);
461 });
462
463 // Create the grapher
464 // TODO(danvk): why does the Layout need its own set of options?
465 this.layoutOptions_ = { 'xOriginIsZero': false };
466 Dygraph.update(this.layoutOptions_, this.attrs_);
467 Dygraph.update(this.layoutOptions_, this.user_attrs_);
468 Dygraph.update(this.layoutOptions_, {
469 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
470
471 this.layout_ = new DygraphLayout(this, this.layoutOptions_);
472
473 // TODO(danvk): why does the Renderer need its own set of options?
474 this.renderOptions_ = { colorScheme: this.colors_,
475 strokeColor: null,
476 axisLineWidth: Dygraph.AXIS_LINE_WIDTH };
477 Dygraph.update(this.renderOptions_, this.attrs_);
478 Dygraph.update(this.renderOptions_, this.user_attrs_);
479 this.plotter_ = new DygraphCanvasRenderer(this,
480 this.hidden_, this.layout_,
481 this.renderOptions_);
482
483 this.createStatusMessage_();
484 this.createRollInterface_();
485 this.createDragInterface_();
486 };
487
488 /**
489 * Detach DOM elements in the dygraph and null out all data references.
490 * Calling this when you're done with a dygraph can dramatically reduce memory
491 * usage. See, e.g., the tests/perf.html example.
492 */
493 Dygraph.prototype.destroy = function() {
494 var removeRecursive = function(node) {
495 while (node.hasChildNodes()) {
496 removeRecursive(node.firstChild);
497 node.removeChild(node.firstChild);
498 }
499 };
500 removeRecursive(this.maindiv_);
501
502 var nullOut = function(obj) {
503 for (var n in obj) {
504 if (typeof(obj[n]) === 'object') {
505 obj[n] = null;
506 }
507 }
508 };
509
510 // These may not all be necessary, but it can't hurt...
511 nullOut(this.layout_);
512 nullOut(this.plotter_);
513 nullOut(this);
514 };
515
516 /**
517 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
518 * this particular canvas. All Dygraph work is done on this.canvas_.
519 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
520 * @return {Object} The newly-created canvas
521 * @private
522 */
523 Dygraph.prototype.createPlotKitCanvas_ = function(canvas) {
524 var h = Dygraph.createCanvas();
525 h.style.position = "absolute";
526 // TODO(danvk): h should be offset from canvas. canvas needs to include
527 // some extra area to make it easier to zoom in on the far left and far
528 // right. h needs to be precisely the plot area, so that clipping occurs.
529 h.style.top = canvas.style.top;
530 h.style.left = canvas.style.left;
531 h.width = this.width_;
532 h.height = this.height_;
533 h.style.width = this.width_ + "px"; // for IE
534 h.style.height = this.height_ + "px"; // for IE
535 return h;
536 };
537
538 // Taken from MochiKit.Color
539 Dygraph.hsvToRGB = function (hue, saturation, value) {
540 var red;
541 var green;
542 var blue;
543 if (saturation === 0) {
544 red = value;
545 green = value;
546 blue = value;
547 } else {
548 var i = Math.floor(hue * 6);
549 var f = (hue * 6) - i;
550 var p = value * (1 - saturation);
551 var q = value * (1 - (saturation * f));
552 var t = value * (1 - (saturation * (1 - f)));
553 switch (i) {
554 case 1: red = q; green = value; blue = p; break;
555 case 2: red = p; green = value; blue = t; break;
556 case 3: red = p; green = q; blue = value; break;
557 case 4: red = t; green = p; blue = value; break;
558 case 5: red = value; green = p; blue = q; break;
559 case 6: // fall through
560 case 0: red = value; green = t; blue = p; break;
561 }
562 }
563 red = Math.floor(255 * red + 0.5);
564 green = Math.floor(255 * green + 0.5);
565 blue = Math.floor(255 * blue + 0.5);
566 return 'rgb(' + red + ',' + green + ',' + blue + ')';
567 };
568
569
570 /**
571 * Generate a set of distinct colors for the data series. This is done with a
572 * color wheel. Saturation/Value are customizable, and the hue is
573 * equally-spaced around the color wheel. If a custom set of colors is
574 * specified, that is used instead.
575 * @private
576 */
577 Dygraph.prototype.setColors_ = function() {
578 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
579 // away with this.renderOptions_.
580 var num = this.attr_("labels").length - 1;
581 this.colors_ = [];
582 var colors = this.attr_('colors');
583 if (!colors) {
584 var sat = this.attr_('colorSaturation') || 1.0;
585 var val = this.attr_('colorValue') || 0.5;
586 var half = Math.ceil(num / 2);
587 for (var i = 1; i <= num; i++) {
588 if (!this.visibility()[i-1]) continue;
589 // alternate colors for high contrast.
590 var idx = i % 2 ? Math.ceil(i / 2) : (half + i / 2);
591 var hue = (1.0 * idx/ (1 + num));
592 this.colors_.push(Dygraph.hsvToRGB(hue, sat, val));
593 }
594 } else {
595 for (var i = 0; i < num; i++) {
596 if (!this.visibility()[i]) continue;
597 var colorStr = colors[i % colors.length];
598 this.colors_.push(colorStr);
599 }
600 }
601
602 // TODO(danvk): update this w/r/t/ the new options system.
603 this.renderOptions_.colorScheme = this.colors_;
604 Dygraph.update(this.plotter_.options, this.renderOptions_);
605 Dygraph.update(this.layoutOptions_, this.user_attrs_);
606 Dygraph.update(this.layoutOptions_, this.attrs_);
607 }
608
609 /**
610 * Return the list of colors. This is either the list of colors passed in the
611 * attributes, or the autogenerated list of rgb(r,g,b) strings.
612 * @return {Array<string>} The list of colors.
613 */
614 Dygraph.prototype.getColors = function() {
615 return this.colors_;
616 };
617
618 // The following functions are from quirksmode.org with a modification for Safari from
619 // http://blog.firetree.net/2005/07/04/javascript-find-position/
620 // http://www.quirksmode.org/js/findpos.html
621 Dygraph.findPosX = function(obj) {
622 var curleft = 0;
623 if(obj.offsetParent)
624 while(1)
625 {
626 curleft += obj.offsetLeft;
627 if(!obj.offsetParent)
628 break;
629 obj = obj.offsetParent;
630 }
631 else if(obj.x)
632 curleft += obj.x;
633 return curleft;
634 };
635
636 Dygraph.findPosY = function(obj) {
637 var curtop = 0;
638 if(obj.offsetParent)
639 while(1)
640 {
641 curtop += obj.offsetTop;
642 if(!obj.offsetParent)
643 break;
644 obj = obj.offsetParent;
645 }
646 else if(obj.y)
647 curtop += obj.y;
648 return curtop;
649 };
650
651
652
653 /**
654 * Create the div that contains information on the selected point(s)
655 * This goes in the top right of the canvas, unless an external div has already
656 * been specified.
657 * @private
658 */
659 Dygraph.prototype.createStatusMessage_ = function() {
660 var userLabelsDiv = this.user_attrs_["labelsDiv"];
661 if (userLabelsDiv && null != userLabelsDiv
662 && (typeof(userLabelsDiv) == "string" || userLabelsDiv instanceof String)) {
663 this.user_attrs_["labelsDiv"] = document.getElementById(userLabelsDiv);
664 }
665 if (!this.attr_("labelsDiv")) {
666 var divWidth = this.attr_('labelsDivWidth');
667 var messagestyle = {
668 "position": "absolute",
669 "fontSize": "14px",
670 "zIndex": 10,
671 "width": divWidth + "px",
672 "top": "0px",
673 "left": (this.width_ - divWidth - 2) + "px",
674 "background": "white",
675 "textAlign": "left",
676 "overflow": "hidden"};
677 Dygraph.update(messagestyle, this.attr_('labelsDivStyles'));
678 var div = document.createElement("div");
679 for (var name in messagestyle) {
680 if (messagestyle.hasOwnProperty(name)) {
681 div.style[name] = messagestyle[name];
682 }
683 }
684 this.graphDiv.appendChild(div);
685 this.attrs_.labelsDiv = div;
686 }
687 };
688
689 /**
690 * Create the text box to adjust the averaging period
691 * @return {Object} The newly-created text box
692 * @private
693 */
694 Dygraph.prototype.createRollInterface_ = function() {
695 var display = this.attr_('showRoller') ? "block" : "none";
696 var textAttr = { "position": "absolute",
697 "zIndex": 10,
698 "top": (this.plotter_.area.h - 25) + "px",
699 "left": (this.plotter_.area.x + 1) + "px",
700 "display": display
701 };
702 var roller = document.createElement("input");
703 roller.type = "text";
704 roller.size = "2";
705 roller.value = this.rollPeriod_;
706 for (var name in textAttr) {
707 if (textAttr.hasOwnProperty(name)) {
708 roller.style[name] = textAttr[name];
709 }
710 }
711
712 var pa = this.graphDiv;
713 pa.appendChild(roller);
714 var dygraph = this;
715 roller.onchange = function() { dygraph.adjustRoll(roller.value); };
716 return roller;
717 };
718
719 // These functions are taken from MochiKit.Signal
720 Dygraph.pageX = function(e) {
721 if (e.pageX) {
722 return (!e.pageX || e.pageX < 0) ? 0 : e.pageX;
723 } else {
724 var de = document;
725 var b = document.body;
726 return e.clientX +
727 (de.scrollLeft || b.scrollLeft) -
728 (de.clientLeft || 0);
729 }
730 };
731
732 Dygraph.pageY = function(e) {
733 if (e.pageY) {
734 return (!e.pageY || e.pageY < 0) ? 0 : e.pageY;
735 } else {
736 var de = document;
737 var b = document.body;
738 return e.clientY +
739 (de.scrollTop || b.scrollTop) -
740 (de.clientTop || 0);
741 }
742 };
743
744 /**
745 * Set up all the mouse handlers needed to capture dragging behavior for zoom
746 * events.
747 * @private
748 */
749 Dygraph.prototype.createDragInterface_ = function() {
750 var self = this;
751
752 // Tracks whether the mouse is down right now
753 var isZooming = false;
754 var isPanning = false;
755 var dragStartX = null;
756 var dragStartY = null;
757 var dragEndX = null;
758 var dragEndY = null;
759 var prevEndX = null;
760 var prevEndY = null;
761 var prevDragDirection = null;
762
763 // draggingDate and draggingValue represent the [date,value] point on the
764 // graph at which the mouse was pressed. As the mouse moves while panning,
765 // the viewport must pan so that the mouse position points to
766 // [draggingDate, draggingValue]
767 var draggingDate = null;
768 var draggingValue = null;
769
770 // The range in second/value units that the viewport encompasses during a
771 // panning operation.
772 var dateRange = null;
773 var valueRange = null;
774
775 // Utility function to convert page-wide coordinates to canvas coords
776 var px = 0;
777 var py = 0;
778 var getX = function(e) { return Dygraph.pageX(e) - px };
779 var getY = function(e) { return Dygraph.pageY(e) - py };
780
781 // Draw zoom rectangles when the mouse is down and the user moves around
782 Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(event) {
783 if (isZooming) {
784 dragEndX = getX(event);
785 dragEndY = getY(event);
786
787 var xDelta = Math.abs(dragStartX - dragEndX);
788 var yDelta = Math.abs(dragStartY - dragEndY);
789 var dragDirection = (xDelta < yDelta) ? Dygraph.VERTICAL : Dygraph.HORIZONTAL;
790
791 self.drawZoomRect_(dragDirection, dragStartX, dragEndX, dragStartY, dragEndY,
792 prevDragDirection, prevEndX, prevEndY);
793
794 prevEndX = dragEndX;
795 prevEndY = dragEndY;
796 prevDragDirection = dragDirection;
797 } else if (isPanning) {
798 dragEndX = getX(event);
799 dragEndY = getY(event);
800
801 // Want to have it so that:
802 // 1. draggingDate appears at dragEndX, draggingValue appears at dragEndY.
803 // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
804 // 3. draggingValue appears at dragEndY.
805 // 4. valueRange is unaltered.
806
807 var minDate = draggingDate - (dragEndX / self.width_) * dateRange;
808 var maxDate = minDate + dateRange;
809 self.dateWindow_ = [minDate, maxDate];
810
811 var maxValue = draggingValue + (dragEndY / self.height_) * valueRange;
812 var minValue = maxValue - valueRange;
813 self.valueWindow_ = [ minValue, maxValue ];
814 self.drawGraph_(self.rawData_);
815 }
816 });
817
818 // Track the beginning of drag events
819 Dygraph.addEvent(this.mouseEventElement_, 'mousedown', function(event) {
820 px = Dygraph.findPosX(self.canvas_);
821 py = Dygraph.findPosY(self.canvas_);
822 dragStartX = getX(event);
823 dragStartY = getY(event);
824
825 if (event.altKey || event.shiftKey) {
826 // have to be zoomed in to pan.
827 if (!self.dateWindow_ && !self.valueWindow_) return;
828
829 isPanning = true;
830 var xRange = self.xAxisRange();
831 dateRange = xRange[1] - xRange[0];
832 var yRange = self.yAxisRange();
833 valueRange = yRange[1] - yRange[0];
834
835 // TODO(konigsberg): Switch from all this math to toDataCoords?
836 // Seems to work for the dragging value.
837 draggingDate = (dragStartX / self.width_) * dateRange +
838 xRange[0];
839 var r = self.toDataCoords(null, dragStartY);
840 draggingValue = r[1];
841 } else {
842 isZooming = true;
843 }
844 });
845
846 // If the user releases the mouse button during a drag, but not over the
847 // canvas, then it doesn't count as a zooming action.
848 Dygraph.addEvent(document, 'mouseup', function(event) {
849 if (isZooming || isPanning) {
850 isZooming = false;
851 dragStartX = null;
852 dragStartY = null;
853 }
854
855 if (isPanning) {
856 isPanning = false;
857 draggingDate = null;
858 draggingValue = null;
859 dateRange = null;
860 valueRange = null;
861 }
862 });
863
864 // Temporarily cancel the dragging event when the mouse leaves the graph
865 Dygraph.addEvent(this.mouseEventElement_, 'mouseout', function(event) {
866 if (isZooming) {
867 dragEndX = null;
868 dragEndY = null;
869 }
870 });
871
872 // If the mouse is released on the canvas during a drag event, then it's a
873 // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
874 Dygraph.addEvent(this.mouseEventElement_, 'mouseup', function(event) {
875 if (isZooming) {
876 isZooming = false;
877 dragEndX = getX(event);
878 dragEndY = getY(event);
879 var regionWidth = Math.abs(dragEndX - dragStartX);
880 var regionHeight = Math.abs(dragEndY - dragStartY);
881
882 if (regionWidth < 2 && regionHeight < 2 &&
883 self.lastx_ != undefined && self.lastx_ != -1) {
884 // TODO(danvk): pass along more info about the points, e.g. 'x'
885 if (self.attr_('clickCallback') != null) {
886 self.attr_('clickCallback')(event, self.lastx_, self.selPoints_);
887 }
888 if (self.attr_('pointClickCallback')) {
889 // check if the click was on a particular point.
890 var closestIdx = -1;
891 var closestDistance = 0;
892 for (var i = 0; i < self.selPoints_.length; i++) {
893 var p = self.selPoints_[i];
894 var distance = Math.pow(p.canvasx - dragEndX, 2) +
895 Math.pow(p.canvasy - dragEndY, 2);
896 if (closestIdx == -1 || distance < closestDistance) {
897 closestDistance = distance;
898 closestIdx = i;
899 }
900 }
901
902 // Allow any click within two pixels of the dot.
903 var radius = self.attr_('highlightCircleSize') + 2;
904 if (closestDistance <= 5 * 5) {
905 self.attr_('pointClickCallback')(event, self.selPoints_[closestIdx]);
906 }
907 }
908 }
909
910 if (regionWidth >= 10 && regionWidth > regionHeight) {
911 self.doZoomX_(Math.min(dragStartX, dragEndX),
912 Math.max(dragStartX, dragEndX));
913 } else if (regionHeight >= 10 && regionHeight > regionWidth){
914 self.doZoomY_(Math.min(dragStartY, dragEndY),
915 Math.max(dragStartY, dragEndY));
916 } else {
917 self.canvas_.getContext("2d").clearRect(0, 0,
918 self.canvas_.width,
919 self.canvas_.height);
920 }
921
922 dragStartX = null;
923 dragStartY = null;
924 }
925
926 if (isPanning) {
927 isPanning = false;
928 draggingDate = null;
929 draggingValue = null;
930 dateRange = null;
931 valueRange = null;
932 }
933 });
934
935 // Double-clicking zooms back out
936 Dygraph.addEvent(this.mouseEventElement_, 'dblclick', function(event) {
937 // Disable zooming out if panning.
938 if (event.altKey || event.shiftKey) return;
939
940 self.doUnzoom_();
941 });
942 };
943
944 /**
945 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
946 * up any previous zoom rectangles that were drawn. This could be optimized to
947 * avoid extra redrawing, but it's tricky to avoid interactions with the status
948 * dots.
949 *
950 * @param {Number} direction the direction of the zoom rectangle. Acceptable
951 * values are Dygraph.HORIZONTAL and Dygraph.VERTICAL.
952 * @param {Number} startX The X position where the drag started, in canvas
953 * coordinates.
954 * @param {Number} endX The current X position of the drag, in canvas coords.
955 * @param {Number} startY The Y position where the drag started, in canvas
956 * coordinates.
957 * @param {Number} endY The current Y position of the drag, in canvas coords.
958 * @param {Number} prevDirection the value of direction on the previous call to
959 * this function. Used to avoid excess redrawing
960 * @param {Number} prevEndX The value of endX on the previous call to this
961 * function. Used to avoid excess redrawing
962 * @param {Number} prevEndY The value of endY on the previous call to this
963 * function. Used to avoid excess redrawing
964 * @private
965 */
966 Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY, endY,
967 prevDirection, prevEndX, prevEndY) {
968 var ctx = this.canvas_.getContext("2d");
969
970 // Clean up from the previous rect if necessary
971 if (prevDirection == Dygraph.HORIZONTAL) {
972 ctx.clearRect(Math.min(startX, prevEndX), 0,
973 Math.abs(startX - prevEndX), this.height_);
974 } else if (prevDirection == Dygraph.VERTICAL){
975 ctx.clearRect(0, Math.min(startY, prevEndY),
976 this.width_, Math.abs(startY - prevEndY));
977 }
978
979 // Draw a light-grey rectangle to show the new viewing area
980 if (direction == Dygraph.HORIZONTAL) {
981 if (endX && startX) {
982 ctx.fillStyle = "rgba(128,128,128,0.33)";
983 ctx.fillRect(Math.min(startX, endX), 0,
984 Math.abs(endX - startX), this.height_);
985 }
986 }
987 if (direction == Dygraph.VERTICAL) {
988 if (endY && startY) {
989 ctx.fillStyle = "rgba(128,128,128,0.33)";
990 ctx.fillRect(0, Math.min(startY, endY),
991 this.width_, Math.abs(endY - startY));
992 }
993 }
994 };
995
996 /**
997 * Zoom to something containing [lowX, highX]. These are pixel coordinates in
998 * the canvas. The exact zoom window may be slightly larger if there are no data
999 * points near lowX or highX. Don't confuse this function with doZoomXDates,
1000 * which accepts dates that match the raw data. This function redraws the graph.
1001 *
1002 * @param {Number} lowX The leftmost pixel value that should be visible.
1003 * @param {Number} highX The rightmost pixel value that should be visible.
1004 * @private
1005 */
1006 Dygraph.prototype.doZoomX_ = function(lowX, highX) {
1007 // Find the earliest and latest dates contained in this canvasx range.
1008 // Convert the call to date ranges of the raw data.
1009 var r = this.toDataCoords(lowX, null);
1010 var minDate = r[0];
1011 r = this.toDataCoords(highX, null);
1012 var maxDate = r[0];
1013 this.doZoomXDates_(minDate, maxDate);
1014 };
1015
1016 /**
1017 * Zoom to something containing [minDate, maxDate] values. Don't confuse this
1018 * method with doZoomX which accepts pixel coordinates. This function redraws
1019 * the graph.
1020 *
1021 * @param {Number} minDate The minimum date that should be visible.
1022 * @param {Number} maxDate The maximum date that should be visible.
1023 * @private
1024 */
1025 Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) {
1026 this.dateWindow_ = [minDate, maxDate];
1027 this.drawGraph_(this.rawData_);
1028 if (this.attr_("zoomCallback")) {
1029 var yRange = this.yAxisRange();
1030 this.attr_("zoomCallback")(minDate, maxDate, yRange[0], yRange[1]);
1031 }
1032 };
1033
1034 /**
1035 * Zoom to something containing [lowY, highY]. These are pixel coordinates in
1036 * the canvas. The exact zoom window may be slightly larger if there are no
1037 * data points near lowY or highY. Don't confuse this function with
1038 * doZoomYValues, which accepts parameters that match the raw data. This
1039 * function redraws the graph.
1040 *
1041 * @param {Number} lowY The topmost pixel value that should be visible.
1042 * @param {Number} highY The lowest pixel value that should be visible.
1043 * @private
1044 */
1045 Dygraph.prototype.doZoomY_ = function(lowY, highY) {
1046 // Find the highest and lowest values in pixel range.
1047 var r = this.toDataCoords(null, lowY);
1048 var maxValue = r[1];
1049 r = this.toDataCoords(null, highY);
1050 var minValue = r[1];
1051
1052 this.doZoomYValues_(minValue, maxValue);
1053 };
1054
1055 /**
1056 * Zoom to something containing [minValue, maxValue] values. Don't confuse this
1057 * method with doZoomY which accepts pixel coordinates. This function redraws
1058 * the graph.
1059 *
1060 * @param {Number} minValue The minimum Value that should be visible.
1061 * @param {Number} maxValue The maximum value that should be visible.
1062 * @private
1063 */
1064 Dygraph.prototype.doZoomYValues_ = function(minValue, maxValue) {
1065 this.valueWindow_ = [minValue, maxValue];
1066 this.drawGraph_(this.rawData_);
1067 if (this.attr_("zoomCallback")) {
1068 var xRange = this.xAxisRange();
1069 this.attr_("zoomCallback")(xRange[0], xRange[1], minValue, maxValue);
1070 }
1071 };
1072
1073 /**
1074 * Reset the zoom to the original view coordinates. This is the same as
1075 * double-clicking on the graph.
1076 *
1077 * @private
1078 */
1079 Dygraph.prototype.doUnzoom_ = function() {
1080 var dirty = null;
1081 if (this.dateWindow_ != null) {
1082 dirty = 1;
1083 this.dateWindow_ = null;
1084 }
1085 if (this.valueWindow_ != null) {
1086 dirty = 1;
1087 this.valueWindow_ = this.valueRange_;
1088 }
1089
1090 if (dirty) {
1091 // Putting the drawing operation before the callback because it resets
1092 // yAxisRange.
1093 this.drawGraph_(this.rawData_);
1094 if (this.attr_("zoomCallback")) {
1095 var minDate = this.rawData_[0][0];
1096 var maxDate = this.rawData_[this.rawData_.length - 1][0];
1097 var minValue = this.yAxisRange()[0];
1098 var maxValue = this.yAxisRange()[1];
1099 this.attr_("zoomCallback")(minDate, maxDate, minValue, maxValue);
1100 }
1101 }
1102 };
1103
1104 /**
1105 * When the mouse moves in the canvas, display information about a nearby data
1106 * point and draw dots over those points in the data series. This function
1107 * takes care of cleanup of previously-drawn dots.
1108 * @param {Object} event The mousemove event from the browser.
1109 * @private
1110 */
1111 Dygraph.prototype.mouseMove_ = function(event) {
1112 var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_);
1113 var points = this.layout_.points;
1114
1115 var lastx = -1;
1116 var lasty = -1;
1117
1118 // Loop through all the points and find the date nearest to our current
1119 // location.
1120 var minDist = 1e+100;
1121 var idx = -1;
1122 for (var i = 0; i < points.length; i++) {
1123 var dist = Math.abs(points[i].canvasx - canvasx);
1124 if (dist > minDist) continue;
1125 minDist = dist;
1126 idx = i;
1127 }
1128 if (idx >= 0) lastx = points[idx].xval;
1129 // Check that you can really highlight the last day's data
1130 if (canvasx > points[points.length-1].canvasx)
1131 lastx = points[points.length-1].xval;
1132
1133 // Extract the points we've selected
1134 this.selPoints_ = [];
1135 var l = points.length;
1136 if (!this.attr_("stackedGraph")) {
1137 for (var i = 0; i < l; i++) {
1138 if (points[i].xval == lastx) {
1139 this.selPoints_.push(points[i]);
1140 }
1141 }
1142 } else {
1143 // Need to 'unstack' points starting from the bottom
1144 var cumulative_sum = 0;
1145 for (var i = l - 1; i >= 0; i--) {
1146 if (points[i].xval == lastx) {
1147 var p = {}; // Clone the point since we modify it
1148 for (var k in points[i]) {
1149 p[k] = points[i][k];
1150 }
1151 p.yval -= cumulative_sum;
1152 cumulative_sum += p.yval;
1153 this.selPoints_.push(p);
1154 }
1155 }
1156 this.selPoints_.reverse();
1157 }
1158
1159 if (this.attr_("highlightCallback")) {
1160 var px = this.lastx_;
1161 if (px !== null && lastx != px) {
1162 // only fire if the selected point has changed.
1163 this.attr_("highlightCallback")(event, lastx, this.selPoints_);
1164 }
1165 }
1166
1167 // Save last x position for callbacks.
1168 this.lastx_ = lastx;
1169
1170 this.updateSelection_();
1171 };
1172
1173 /**
1174 * Draw dots over the selectied points in the data series. This function
1175 * takes care of cleanup of previously-drawn dots.
1176 * @private
1177 */
1178 Dygraph.prototype.updateSelection_ = function() {
1179 // Clear the previously drawn vertical, if there is one
1180 var circleSize = this.attr_('highlightCircleSize');
1181 var ctx = this.canvas_.getContext("2d");
1182 if (this.previousVerticalX_ >= 0) {
1183 var px = this.previousVerticalX_;
1184 ctx.clearRect(px - circleSize - 1, 0, 2 * circleSize + 2, this.height_);
1185 }
1186
1187 var isOK = function(x) { return x && !isNaN(x); };
1188
1189 if (this.selPoints_.length > 0) {
1190 var canvasx = this.selPoints_[0].canvasx;
1191
1192 // Set the status message to indicate the selected point(s)
1193 var replace = this.attr_('xValueFormatter')(this.lastx_, this) + ":";
1194 var fmtFunc = this.attr_('yValueFormatter');
1195 var clen = this.colors_.length;
1196
1197 if (this.attr_('showLabelsOnHighlight')) {
1198 // Set the status message to indicate the selected point(s)
1199 for (var i = 0; i < this.selPoints_.length; i++) {
1200 if (!this.attr_("labelsShowZeroValues") && this.selPoints_[i].yval == 0) continue;
1201 if (!isOK(this.selPoints_[i].canvasy)) continue;
1202 if (this.attr_("labelsSeparateLines")) {
1203 replace += "<br/>";
1204 }
1205 var point = this.selPoints_[i];
1206 var c = new RGBColor(this.colors_[i%clen]);
1207 var yval = fmtFunc(point.yval);
1208 replace += " <b><font color='" + c.toHex() + "'>"
1209 + point.name + "</font></b>:"
1210 + yval;
1211 }
1212
1213 this.attr_("labelsDiv").innerHTML = replace;
1214 }
1215
1216 // Draw colored circles over the center of each selected point
1217 ctx.save();
1218 for (var i = 0; i < this.selPoints_.length; i++) {
1219 if (!isOK(this.selPoints_[i].canvasy)) continue;
1220 ctx.beginPath();
1221 ctx.fillStyle = this.plotter_.colors[this.selPoints_[i].name];
1222 ctx.arc(canvasx, this.selPoints_[i].canvasy, circleSize,
1223 0, 2 * Math.PI, false);
1224 ctx.fill();
1225 }
1226 ctx.restore();
1227
1228 this.previousVerticalX_ = canvasx;
1229 }
1230 };
1231
1232 /**
1233 * Set manually set selected dots, and display information about them
1234 * @param int row number that should by highlighted
1235 * false value clears the selection
1236 * @public
1237 */
1238 Dygraph.prototype.setSelection = function(row) {
1239 // Extract the points we've selected
1240 this.selPoints_ = [];
1241 var pos = 0;
1242
1243 if (row !== false) {
1244 row = row-this.boundaryIds_[0][0];
1245 }
1246
1247 if (row !== false && row >= 0) {
1248 for (var i in this.layout_.datasets) {
1249 if (row < this.layout_.datasets[i].length) {
1250 this.selPoints_.push(this.layout_.points[pos+row]);
1251 }
1252 pos += this.layout_.datasets[i].length;
1253 }
1254 }
1255
1256 if (this.selPoints_.length) {
1257 this.lastx_ = this.selPoints_[0].xval;
1258 this.updateSelection_();
1259 } else {
1260 this.lastx_ = -1;
1261 this.clearSelection();
1262 }
1263
1264 };
1265
1266 /**
1267 * The mouse has left the canvas. Clear out whatever artifacts remain
1268 * @param {Object} event the mouseout event from the browser.
1269 * @private
1270 */
1271 Dygraph.prototype.mouseOut_ = function(event) {
1272 if (this.attr_("unhighlightCallback")) {
1273 this.attr_("unhighlightCallback")(event);
1274 }
1275
1276 if (this.attr_("hideOverlayOnMouseOut")) {
1277 this.clearSelection();
1278 }
1279 };
1280
1281 /**
1282 * Remove all selection from the canvas
1283 * @public
1284 */
1285 Dygraph.prototype.clearSelection = function() {
1286 // Get rid of the overlay data
1287 var ctx = this.canvas_.getContext("2d");
1288 ctx.clearRect(0, 0, this.width_, this.height_);
1289 this.attr_("labelsDiv").innerHTML = "";
1290 this.selPoints_ = [];
1291 this.lastx_ = -1;
1292 }
1293
1294 /**
1295 * Returns the number of the currently selected row
1296 * @return int row number, of -1 if nothing is selected
1297 * @public
1298 */
1299 Dygraph.prototype.getSelection = function() {
1300 if (!this.selPoints_ || this.selPoints_.length < 1) {
1301 return -1;
1302 }
1303
1304 for (var row=0; row<this.layout_.points.length; row++ ) {
1305 if (this.layout_.points[row].x == this.selPoints_[0].x) {
1306 return row + this.boundaryIds_[0][0];
1307 }
1308 }
1309 return -1;
1310 }
1311
1312 Dygraph.zeropad = function(x) {
1313 if (x < 10) return "0" + x; else return "" + x;
1314 }
1315
1316 /**
1317 * Return a string version of the hours, minutes and seconds portion of a date.
1318 * @param {Number} date The JavaScript date (ms since epoch)
1319 * @return {String} A time of the form "HH:MM:SS"
1320 * @private
1321 */
1322 Dygraph.hmsString_ = function(date) {
1323 var zeropad = Dygraph.zeropad;
1324 var d = new Date(date);
1325 if (d.getSeconds()) {
1326 return zeropad(d.getHours()) + ":" +
1327 zeropad(d.getMinutes()) + ":" +
1328 zeropad(d.getSeconds());
1329 } else {
1330 return zeropad(d.getHours()) + ":" + zeropad(d.getMinutes());
1331 }
1332 }
1333
1334 /**
1335 * Convert a JS date to a string appropriate to display on an axis that
1336 * is displaying values at the stated granularity.
1337 * @param {Date} date The date to format
1338 * @param {Number} granularity One of the Dygraph granularity constants
1339 * @return {String} The formatted date
1340 * @private
1341 */
1342 Dygraph.dateAxisFormatter = function(date, granularity) {
1343 if (granularity >= Dygraph.MONTHLY) {
1344 return date.strftime('%b %y');
1345 } else {
1346 var frac = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds();
1347 if (frac == 0 || granularity >= Dygraph.DAILY) {
1348 return new Date(date.getTime() + 3600*1000).strftime('%d%b');
1349 } else {
1350 return Dygraph.hmsString_(date.getTime());
1351 }
1352 }
1353 }
1354
1355 /**
1356 * Convert a JS date (millis since epoch) to YYYY/MM/DD
1357 * @param {Number} date The JavaScript date (ms since epoch)
1358 * @return {String} A date of the form "YYYY/MM/DD"
1359 * @private
1360 */
1361 Dygraph.dateString_ = function(date, self) {
1362 var zeropad = Dygraph.zeropad;
1363 var d = new Date(date);
1364
1365 // Get the year:
1366 var year = "" + d.getFullYear();
1367 // Get a 0 padded month string
1368 var month = zeropad(d.getMonth() + 1); //months are 0-offset, sigh
1369 // Get a 0 padded day string
1370 var day = zeropad(d.getDate());
1371
1372 var ret = "";
1373 var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
1374 if (frac) ret = " " + Dygraph.hmsString_(date);
1375
1376 return year + "/" + month + "/" + day + ret;
1377 };
1378
1379 /**
1380 * Round a number to the specified number of digits past the decimal point.
1381 * @param {Number} num The number to round
1382 * @param {Number} places The number of decimals to which to round
1383 * @return {Number} The rounded number
1384 * @private
1385 */
1386 Dygraph.round_ = function(num, places) {
1387 var shift = Math.pow(10, places);
1388 return Math.round(num * shift)/shift;
1389 };
1390
1391 /**
1392 * Fires when there's data available to be graphed.
1393 * @param {String} data Raw CSV data to be plotted
1394 * @private
1395 */
1396 Dygraph.prototype.loadedEvent_ = function(data) {
1397 this.rawData_ = this.parseCSV_(data);
1398 this.drawGraph_(this.rawData_);
1399 };
1400
1401 Dygraph.prototype.months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
1402 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1403 Dygraph.prototype.quarters = ["Jan", "Apr", "Jul", "Oct"];
1404
1405 /**
1406 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1407 * @private
1408 */
1409 Dygraph.prototype.addXTicks_ = function() {
1410 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1411 var startDate, endDate;
1412 if (this.dateWindow_) {
1413 startDate = this.dateWindow_[0];
1414 endDate = this.dateWindow_[1];
1415 } else {
1416 startDate = this.rawData_[0][0];
1417 endDate = this.rawData_[this.rawData_.length - 1][0];
1418 }
1419
1420 var xTicks = this.attr_('xTicker')(startDate, endDate, this);
1421 this.layout_.updateOptions({xTicks: xTicks});
1422 };
1423
1424 // Time granularity enumeration
1425 Dygraph.SECONDLY = 0;
1426 Dygraph.TWO_SECONDLY = 1;
1427 Dygraph.FIVE_SECONDLY = 2;
1428 Dygraph.TEN_SECONDLY = 3;
1429 Dygraph.THIRTY_SECONDLY = 4;
1430 Dygraph.MINUTELY = 5;
1431 Dygraph.TWO_MINUTELY = 6;
1432 Dygraph.FIVE_MINUTELY = 7;
1433 Dygraph.TEN_MINUTELY = 8;
1434 Dygraph.THIRTY_MINUTELY = 9;
1435 Dygraph.HOURLY = 10;
1436 Dygraph.TWO_HOURLY = 11;
1437 Dygraph.SIX_HOURLY = 12;
1438 Dygraph.DAILY = 13;
1439 Dygraph.WEEKLY = 14;
1440 Dygraph.MONTHLY = 15;
1441 Dygraph.QUARTERLY = 16;
1442 Dygraph.BIANNUAL = 17;
1443 Dygraph.ANNUAL = 18;
1444 Dygraph.DECADAL = 19;
1445 Dygraph.NUM_GRANULARITIES = 20;
1446
1447 Dygraph.SHORT_SPACINGS = [];
1448 Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY] = 1000 * 1;
1449 Dygraph.SHORT_SPACINGS[Dygraph.TWO_SECONDLY] = 1000 * 2;
1450 Dygraph.SHORT_SPACINGS[Dygraph.FIVE_SECONDLY] = 1000 * 5;
1451 Dygraph.SHORT_SPACINGS[Dygraph.TEN_SECONDLY] = 1000 * 10;
1452 Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_SECONDLY] = 1000 * 30;
1453 Dygraph.SHORT_SPACINGS[Dygraph.MINUTELY] = 1000 * 60;
1454 Dygraph.SHORT_SPACINGS[Dygraph.TWO_MINUTELY] = 1000 * 60 * 2;
1455 Dygraph.SHORT_SPACINGS[Dygraph.FIVE_MINUTELY] = 1000 * 60 * 5;
1456 Dygraph.SHORT_SPACINGS[Dygraph.TEN_MINUTELY] = 1000 * 60 * 10;
1457 Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_MINUTELY] = 1000 * 60 * 30;
1458 Dygraph.SHORT_SPACINGS[Dygraph.HOURLY] = 1000 * 3600;
1459 Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY] = 1000 * 3600 * 2;
1460 Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY] = 1000 * 3600 * 6;
1461 Dygraph.SHORT_SPACINGS[Dygraph.DAILY] = 1000 * 86400;
1462 Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY] = 1000 * 604800;
1463
1464 // NumXTicks()
1465 //
1466 // If we used this time granularity, how many ticks would there be?
1467 // This is only an approximation, but it's generally good enough.
1468 //
1469 Dygraph.prototype.NumXTicks = function(start_time, end_time, granularity) {
1470 if (granularity < Dygraph.MONTHLY) {
1471 // Generate one tick mark for every fixed interval of time.
1472 var spacing = Dygraph.SHORT_SPACINGS[granularity];
1473 return Math.floor(0.5 + 1.0 * (end_time - start_time) / spacing);
1474 } else {
1475 var year_mod = 1; // e.g. to only print one point every 10 years.
1476 var num_months = 12;
1477 if (granularity == Dygraph.QUARTERLY) num_months = 3;
1478 if (granularity == Dygraph.BIANNUAL) num_months = 2;
1479 if (granularity == Dygraph.ANNUAL) num_months = 1;
1480 if (granularity == Dygraph.DECADAL) { num_months = 1; year_mod = 10; }
1481
1482 var msInYear = 365.2524 * 24 * 3600 * 1000;
1483 var num_years = 1.0 * (end_time - start_time) / msInYear;
1484 return Math.floor(0.5 + 1.0 * num_years * num_months / year_mod);
1485 }
1486 };
1487
1488 // GetXAxis()
1489 //
1490 // Construct an x-axis of nicely-formatted times on meaningful boundaries
1491 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1492 //
1493 // Returns an array containing {v: millis, label: label} dictionaries.
1494 //
1495 Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) {
1496 var formatter = this.attr_("xAxisLabelFormatter");
1497 var ticks = [];
1498 if (granularity < Dygraph.MONTHLY) {
1499 // Generate one tick mark for every fixed interval of time.
1500 var spacing = Dygraph.SHORT_SPACINGS[granularity];
1501 var format = '%d%b'; // e.g. "1Jan"
1502
1503 // Find a time less than start_time which occurs on a "nice" time boundary
1504 // for this granularity.
1505 var g = spacing / 1000;
1506 var d = new Date(start_time);
1507 if (g <= 60) { // seconds
1508 var x = d.getSeconds(); d.setSeconds(x - x % g);
1509 } else {
1510 d.setSeconds(0);
1511 g /= 60;
1512 if (g <= 60) { // minutes
1513 var x = d.getMinutes(); d.setMinutes(x - x % g);
1514 } else {
1515 d.setMinutes(0);
1516 g /= 60;
1517
1518 if (g <= 24) { // days
1519 var x = d.getHours(); d.setHours(x - x % g);
1520 } else {
1521 d.setHours(0);
1522 g /= 24;
1523
1524 if (g == 7) { // one week
1525 d.setDate(d.getDate() - d.getDay());
1526 }
1527 }
1528 }
1529 }
1530 start_time = d.getTime();
1531
1532 for (var t = start_time; t <= end_time; t += spacing) {
1533 ticks.push({ v:t, label: formatter(new Date(t), granularity) });
1534 }
1535 } else {
1536 // Display a tick mark on the first of a set of months of each year.
1537 // Years get a tick mark iff y % year_mod == 0. This is useful for
1538 // displaying a tick mark once every 10 years, say, on long time scales.
1539 var months;
1540 var year_mod = 1; // e.g. to only print one point every 10 years.
1541
1542 if (granularity == Dygraph.MONTHLY) {
1543 months = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
1544 } else if (granularity == Dygraph.QUARTERLY) {
1545 months = [ 0, 3, 6, 9 ];
1546 } else if (granularity == Dygraph.BIANNUAL) {
1547 months = [ 0, 6 ];
1548 } else if (granularity == Dygraph.ANNUAL) {
1549 months = [ 0 ];
1550 } else if (granularity == Dygraph.DECADAL) {
1551 months = [ 0 ];
1552 year_mod = 10;
1553 }
1554
1555 var start_year = new Date(start_time).getFullYear();
1556 var end_year = new Date(end_time).getFullYear();
1557 var zeropad = Dygraph.zeropad;
1558 for (var i = start_year; i <= end_year; i++) {
1559 if (i % year_mod != 0) continue;
1560 for (var j = 0; j < months.length; j++) {
1561 var date_str = i + "/" + zeropad(1 + months[j]) + "/01";
1562 var t = Date.parse(date_str);
1563 if (t < start_time || t > end_time) continue;
1564 ticks.push({ v:t, label: formatter(new Date(t), granularity) });
1565 }
1566 }
1567 }
1568
1569 return ticks;
1570 };
1571
1572
1573 /**
1574 * Add ticks to the x-axis based on a date range.
1575 * @param {Number} startDate Start of the date window (millis since epoch)
1576 * @param {Number} endDate End of the date window (millis since epoch)
1577 * @return {Array.<Object>} Array of {label, value} tuples.
1578 * @public
1579 */
1580 Dygraph.dateTicker = function(startDate, endDate, self) {
1581 var chosen = -1;
1582 for (var i = 0; i < Dygraph.NUM_GRANULARITIES; i++) {
1583 var num_ticks = self.NumXTicks(startDate, endDate, i);
1584 if (self.width_ / num_ticks >= self.attr_('pixelsPerXLabel')) {
1585 chosen = i;
1586 break;
1587 }
1588 }
1589
1590 if (chosen >= 0) {
1591 return self.GetXAxis(startDate, endDate, chosen);
1592 } else {
1593 // TODO(danvk): signal error.
1594 }
1595 };
1596
1597 /**
1598 * Add ticks when the x axis has numbers on it (instead of dates)
1599 * @param {Number} startDate Start of the date window (millis since epoch)
1600 * @param {Number} endDate End of the date window (millis since epoch)
1601 * @return {Array.<Object>} Array of {label, value} tuples.
1602 * @public
1603 */
1604 Dygraph.numericTicks = function(minV, maxV, self) {
1605 // Basic idea:
1606 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1607 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks).
1608 // The first spacing greater than pixelsPerYLabel is what we use.
1609 // TODO(danvk): version that works on a log scale.
1610 if (self.attr_("labelsKMG2")) {
1611 var mults = [1, 2, 4, 8];
1612 } else {
1613 var mults = [1, 2, 5];
1614 }
1615 var scale, low_val, high_val, nTicks;
1616 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1617 var pixelsPerTick = self.attr_('pixelsPerYLabel');
1618 for (var i = -10; i < 50; i++) {
1619 if (self.attr_("labelsKMG2")) {
1620 var base_scale = Math.pow(16, i);
1621 } else {
1622 var base_scale = Math.pow(10, i);
1623 }
1624 for (var j = 0; j < mults.length; j++) {
1625 scale = base_scale * mults[j];
1626 low_val = Math.floor(minV / scale) * scale;
1627 high_val = Math.ceil(maxV / scale) * scale;
1628 nTicks = Math.abs(high_val - low_val) / scale;
1629 var spacing = self.height_ / nTicks;
1630 // wish I could break out of both loops at once...
1631 if (spacing > pixelsPerTick) break;
1632 }
1633 if (spacing > pixelsPerTick) break;
1634 }
1635
1636 // Construct labels for the ticks
1637 var ticks = [];
1638 var k;
1639 var k_labels = [];
1640 if (self.attr_("labelsKMB")) {
1641 k = 1000;
1642 k_labels = [ "K", "M", "B", "T" ];
1643 }
1644 if (self.attr_("labelsKMG2")) {
1645 if (k) self.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
1646 k = 1024;
1647 k_labels = [ "k", "M", "G", "T" ];
1648 }
1649
1650 // Allow reverse y-axis if it's explicitly requested.
1651 if (low_val > high_val) scale *= -1;
1652
1653 for (var i = 0; i < nTicks; i++) {
1654 var tickV = low_val + i * scale;
1655 var absTickV = Math.abs(tickV);
1656 var label = Dygraph.round_(tickV, 2);
1657 if (k_labels.length) {
1658 // Round up to an appropriate unit.
1659 var n = k*k*k*k;
1660 for (var j = 3; j >= 0; j--, n /= k) {
1661 if (absTickV >= n) {
1662 label = Dygraph.round_(tickV / n, 1) + k_labels[j];
1663 break;
1664 }
1665 }
1666 }
1667 ticks.push( {label: label, v: tickV} );
1668 }
1669 return ticks;
1670 };
1671
1672 /**
1673 * Adds appropriate ticks on the y-axis
1674 * @param {Number} minY The minimum Y value in the data set
1675 * @param {Number} maxY The maximum Y value in the data set
1676 * @private
1677 */
1678 Dygraph.prototype.addYTicks_ = function(minY, maxY) {
1679 // Set the number of ticks so that the labels are human-friendly.
1680 // TODO(danvk): make this an attribute as well.
1681 var ticks = Dygraph.numericTicks(minY, maxY, this);
1682 this.layout_.updateOptions( { yAxis: [minY, maxY],
1683 yTicks: ticks } );
1684 };
1685
1686 // Computes the range of the data series (including confidence intervals).
1687 // series is either [ [x1, y1], [x2, y2], ... ] or
1688 // [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1689 // Returns [low, high]
1690 Dygraph.prototype.extremeValues_ = function(series) {
1691 var minY = null, maxY = null;
1692
1693 var bars = this.attr_("errorBars") || this.attr_("customBars");
1694 if (bars) {
1695 // With custom bars, maxY is the max of the high values.
1696 for (var j = 0; j < series.length; j++) {
1697 var y = series[j][1][0];
1698 if (!y) continue;
1699 var low = y - series[j][1][1];
1700 var high = y + series[j][1][2];
1701 if (low > y) low = y; // this can happen with custom bars,
1702 if (high < y) high = y; // e.g. in tests/custom-bars.html
1703 if (maxY == null || high > maxY) {
1704 maxY = high;
1705 }
1706 if (minY == null || low < minY) {
1707 minY = low;
1708 }
1709 }
1710 } else {
1711 for (var j = 0; j < series.length; j++) {
1712 var y = series[j][1];
1713 if (y === null || isNaN(y)) continue;
1714 if (maxY == null || y > maxY) {
1715 maxY = y;
1716 }
1717 if (minY == null || y < minY) {
1718 minY = y;
1719 }
1720 }
1721 }
1722
1723 return [minY, maxY];
1724 };
1725
1726 /**
1727 * Update the graph with new data. Data is in the format
1728 * [ [date1, val1, val2, ...], [date2, val1, val2, ...] if errorBars=false
1729 * or, if errorBars=true,
1730 * [ [date1, [val1,stddev1], [val2,stddev2], ...], [date2, ...], ...]
1731 * @param {Array.<Object>} data The data (see above)
1732 * @private
1733 */
1734 Dygraph.prototype.drawGraph_ = function(data) {
1735 // This is used to set the second parameter to drawCallback, below.
1736 var is_initial_draw = this.is_initial_draw_;
1737 this.is_initial_draw_ = false;
1738
1739 var minY = null, maxY = null;
1740 this.layout_.removeAllDatasets();
1741 this.setColors_();
1742 this.attrs_['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
1743
1744 var connectSeparatedPoints = this.attr_('connectSeparatedPoints');
1745
1746 // Loop over the fields (series). Go from the last to the first,
1747 // because if they're stacked that's how we accumulate the values.
1748
1749 var cumulative_y = []; // For stacked series.
1750 var datasets = [];
1751
1752 // Loop over all fields and create datasets
1753 for (var i = data[0].length - 1; i >= 1; i--) {
1754 if (!this.visibility()[i - 1]) continue;
1755
1756 var series = [];
1757 for (var j = 0; j < data.length; j++) {
1758 if (data[j][i] != null || !connectSeparatedPoints) {
1759 var date = data[j][0];
1760 series.push([date, data[j][i]]);
1761 }
1762 }
1763 series = this.rollingAverage(series, this.rollPeriod_);
1764
1765 // Prune down to the desired range, if necessary (for zooming)
1766 // Because there can be lines going to points outside of the visible area,
1767 // we actually prune to visible points, plus one on either side.
1768 var bars = this.attr_("errorBars") || this.attr_("customBars");
1769 if (this.dateWindow_) {
1770 var low = this.dateWindow_[0];
1771 var high= this.dateWindow_[1];
1772 var pruned = [];
1773 // TODO(danvk): do binary search instead of linear search.
1774 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
1775 var firstIdx = null, lastIdx = null;
1776 for (var k = 0; k < series.length; k++) {
1777 if (series[k][0] >= low && firstIdx === null) {
1778 firstIdx = k;
1779 }
1780 if (series[k][0] <= high) {
1781 lastIdx = k;
1782 }
1783 }
1784 if (firstIdx === null) firstIdx = 0;
1785 if (firstIdx > 0) firstIdx--;
1786 if (lastIdx === null) lastIdx = series.length - 1;
1787 if (lastIdx < series.length - 1) lastIdx++;
1788 this.boundaryIds_[i-1] = [firstIdx, lastIdx];
1789 for (var k = firstIdx; k <= lastIdx; k++) {
1790 pruned.push(series[k]);
1791 }
1792 series = pruned;
1793 } else {
1794 this.boundaryIds_[i-1] = [0, series.length-1];
1795 }
1796
1797 var extremes = this.extremeValues_(series);
1798 var thisMinY = extremes[0];
1799 var thisMaxY = extremes[1];
1800 if (minY === null || thisMinY < minY) minY = thisMinY;
1801 if (maxY === null || thisMaxY > maxY) maxY = thisMaxY;
1802
1803 if (bars) {
1804 for (var j=0; j<series.length; j++) {
1805 val = [series[j][0], series[j][1][0], series[j][1][1], series[j][1][2]];
1806 series[j] = val;
1807 }
1808 } else if (this.attr_("stackedGraph")) {
1809 var l = series.length;
1810 var actual_y;
1811 for (var j = 0; j < l; j++) {
1812 // If one data set has a NaN, let all subsequent stacked
1813 // sets inherit the NaN -- only start at 0 for the first set.
1814 var x = series[j][0];
1815 if (cumulative_y[x] === undefined)
1816 cumulative_y[x] = 0;
1817
1818 actual_y = series[j][1];
1819 cumulative_y[x] += actual_y;
1820
1821 series[j] = [x, cumulative_y[x]]
1822
1823 if (!maxY || cumulative_y[x] > maxY)
1824 maxY = cumulative_y[x];
1825 }
1826 }
1827
1828 datasets[i] = series;
1829 }
1830
1831 for (var i = 1; i < datasets.length; i++) {
1832 if (!this.visibility()[i - 1]) continue;
1833 this.layout_.addDataset(this.attr_("labels")[i], datasets[i]);
1834 }
1835
1836 // Use some heuristics to come up with a good maxY value, unless it's been
1837 // set explicitly by the developer or end-user (via drag)
1838 if (this.valueWindow_ != null) {
1839 this.addYTicks_(this.valueWindow_[0], this.valueWindow_[1]);
1840 this.displayedYRange_ = this.valueWindow_;
1841 } else {
1842 // This affects the calculation of span, below.
1843 if (this.attr_("includeZero") && minY > 0) {
1844 minY = 0;
1845 }
1846
1847 // Add some padding and round up to an integer to be human-friendly.
1848 var span = maxY - minY;
1849 // special case: if we have no sense of scale, use +/-10% of the sole value.
1850 if (span == 0) { span = maxY; }
1851 var maxAxisY = maxY + 0.1 * span;
1852 var minAxisY = minY - 0.1 * span;
1853
1854 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
1855 if (minAxisY < 0 && minY >= 0) minAxisY = 0;
1856 if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0;
1857
1858 if (this.attr_("includeZero")) {
1859 if (maxY < 0) maxAxisY = 0;
1860 if (minY > 0) minAxisY = 0;
1861 }
1862
1863 this.addYTicks_(minAxisY, maxAxisY);
1864 this.displayedYRange_ = [minAxisY, maxAxisY];
1865 }
1866
1867 this.addXTicks_();
1868
1869 // Tell PlotKit to use this new data and render itself
1870 this.layout_.updateOptions({dateWindow: this.dateWindow_});
1871 this.layout_.evaluateWithError();
1872 this.plotter_.clear();
1873 this.plotter_.render();
1874 this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width,
1875 this.canvas_.height);
1876
1877 if (this.attr_("drawCallback") !== null) {
1878 this.attr_("drawCallback")(this, is_initial_draw);
1879 }
1880 };
1881
1882 /**
1883 * Calculates the rolling average of a data set.
1884 * If originalData is [label, val], rolls the average of those.
1885 * If originalData is [label, [, it's interpreted as [value, stddev]
1886 * and the roll is returned in the same form, with appropriately reduced
1887 * stddev for each value.
1888 * Note that this is where fractional input (i.e. '5/10') is converted into
1889 * decimal values.
1890 * @param {Array} originalData The data in the appropriate format (see above)
1891 * @param {Number} rollPeriod The number of days over which to average the data
1892 */
1893 Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) {
1894 if (originalData.length < 2)
1895 return originalData;
1896 var rollPeriod = Math.min(rollPeriod, originalData.length - 1);
1897 var rollingData = [];
1898 var sigma = this.attr_("sigma");
1899
1900 if (this.fractions_) {
1901 var num = 0;
1902 var den = 0; // numerator/denominator
1903 var mult = 100.0;
1904 for (var i = 0; i < originalData.length; i++) {
1905 num += originalData[i][1][0];
1906 den += originalData[i][1][1];
1907 if (i - rollPeriod >= 0) {
1908 num -= originalData[i - rollPeriod][1][0];
1909 den -= originalData[i - rollPeriod][1][1];
1910 }
1911
1912 var date = originalData[i][0];
1913 var value = den ? num / den : 0.0;
1914 if (this.attr_("errorBars")) {
1915 if (this.wilsonInterval_) {
1916 // For more details on this confidence interval, see:
1917 // http://en.wikipedia.org/wiki/Binomial_confidence_interval
1918 if (den) {
1919 var p = value < 0 ? 0 : value, n = den;
1920 var pm = sigma * Math.sqrt(p*(1-p)/n + sigma*sigma/(4*n*n));
1921 var denom = 1 + sigma * sigma / den;
1922 var low = (p + sigma * sigma / (2 * den) - pm) / denom;
1923 var high = (p + sigma * sigma / (2 * den) + pm) / denom;
1924 rollingData[i] = [date,
1925 [p * mult, (p - low) * mult, (high - p) * mult]];
1926 } else {
1927 rollingData[i] = [date, [0, 0, 0]];
1928 }
1929 } else {
1930 var stddev = den ? sigma * Math.sqrt(value * (1 - value) / den) : 1.0;
1931 rollingData[i] = [date, [mult * value, mult * stddev, mult * stddev]];
1932 }
1933 } else {
1934 rollingData[i] = [date, mult * value];
1935 }
1936 }
1937 } else if (this.attr_("customBars")) {
1938 var low = 0;
1939 var mid = 0;
1940 var high = 0;
1941 var count = 0;
1942 for (var i = 0; i < originalData.length; i++) {
1943 var data = originalData[i][1];
1944 var y = data[1];
1945 rollingData[i] = [originalData[i][0], [y, y - data[0], data[2] - y]];
1946
1947 if (y != null && !isNaN(y)) {
1948 low += data[0];
1949 mid += y;
1950 high += data[2];
1951 count += 1;
1952 }
1953 if (i - rollPeriod >= 0) {
1954 var prev = originalData[i - rollPeriod];
1955 if (prev[1][1] != null && !isNaN(prev[1][1])) {
1956 low -= prev[1][0];
1957 mid -= prev[1][1];
1958 high -= prev[1][2];
1959 count -= 1;
1960 }
1961 }
1962 rollingData[i] = [originalData[i][0], [ 1.0 * mid / count,
1963 1.0 * (mid - low) / count,
1964 1.0 * (high - mid) / count ]];
1965 }
1966 } else {
1967 // Calculate the rolling average for the first rollPeriod - 1 points where
1968 // there is not enough data to roll over the full number of days
1969 var num_init_points = Math.min(rollPeriod - 1, originalData.length - 2);
1970 if (!this.attr_("errorBars")){
1971 if (rollPeriod == 1) {
1972 return originalData;
1973 }
1974
1975 for (var i = 0; i < originalData.length; i++) {
1976 var sum = 0;
1977 var num_ok = 0;
1978 for (var j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
1979 var y = originalData[j][1];
1980 if (y == null || isNaN(y)) continue;
1981 num_ok++;
1982 sum += originalData[j][1];
1983 }
1984 if (num_ok) {
1985 rollingData[i] = [originalData[i][0], sum / num_ok];
1986 } else {
1987 rollingData[i] = [originalData[i][0], null];
1988 }
1989 }
1990
1991 } else {
1992 for (var i = 0; i < originalData.length; i++) {
1993 var sum = 0;
1994 var variance = 0;
1995 var num_ok = 0;
1996 for (var j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
1997 var y = originalData[j][1][0];
1998 if (y == null || isNaN(y)) continue;
1999 num_ok++;
2000 sum += originalData[j][1][0];
2001 variance += Math.pow(originalData[j][1][1], 2);
2002 }
2003 if (num_ok) {
2004 var stddev = Math.sqrt(variance) / num_ok;
2005 rollingData[i] = [originalData[i][0],
2006 [sum / num_ok, sigma * stddev, sigma * stddev]];
2007 } else {
2008 rollingData[i] = [originalData[i][0], [null, null, null]];
2009 }
2010 }
2011 }
2012 }
2013
2014 return rollingData;
2015 };
2016
2017 /**
2018 * Parses a date, returning the number of milliseconds since epoch. This can be
2019 * passed in as an xValueParser in the Dygraph constructor.
2020 * TODO(danvk): enumerate formats that this understands.
2021 * @param {String} A date in YYYYMMDD format.
2022 * @return {Number} Milliseconds since epoch.
2023 * @public
2024 */
2025 Dygraph.dateParser = function(dateStr, self) {
2026 var dateStrSlashed;
2027 var d;
2028 if (dateStr.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
2029 dateStrSlashed = dateStr.replace("-", "/", "g");
2030 while (dateStrSlashed.search("-") != -1) {
2031 dateStrSlashed = dateStrSlashed.replace("-", "/");
2032 }
2033 d = Date.parse(dateStrSlashed);
2034 } else if (dateStr.length == 8) { // e.g. '20090712'
2035 // TODO(danvk): remove support for this format. It's confusing.
2036 dateStrSlashed = dateStr.substr(0,4) + "/" + dateStr.substr(4,2)
2037 + "/" + dateStr.substr(6,2);
2038 d = Date.parse(dateStrSlashed);
2039 } else {
2040 // Any format that Date.parse will accept, e.g. "2009/07/12" or
2041 // "2009/07/12 12:34:56"
2042 d = Date.parse(dateStr);
2043 }
2044
2045 if (!d || isNaN(d)) {
2046 self.error("Couldn't parse " + dateStr + " as a date");
2047 }
2048 return d;
2049 };
2050
2051 /**
2052 * Detects the type of the str (date or numeric) and sets the various
2053 * formatting attributes in this.attrs_ based on this type.
2054 * @param {String} str An x value.
2055 * @private
2056 */
2057 Dygraph.prototype.detectTypeFromString_ = function(str) {
2058 var isDate = false;
2059 if (str.indexOf('-') >= 0 ||
2060 str.indexOf('/') >= 0 ||
2061 isNaN(parseFloat(str))) {
2062 isDate = true;
2063 } else if (str.length == 8 && str > '19700101' && str < '20371231') {
2064 // TODO(danvk): remove support for this format.
2065 isDate = true;
2066 }
2067
2068 if (isDate) {
2069 this.attrs_.xValueFormatter = Dygraph.dateString_;
2070 this.attrs_.xValueParser = Dygraph.dateParser;
2071 this.attrs_.xTicker = Dygraph.dateTicker;
2072 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
2073 } else {
2074 this.attrs_.xValueFormatter = function(x) { return x; };
2075 this.attrs_.xValueParser = function(x) { return parseFloat(x); };
2076 this.attrs_.xTicker = Dygraph.numericTicks;
2077 this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;
2078 }
2079 };
2080
2081 /**
2082 * Parses a string in a special csv format. We expect a csv file where each
2083 * line is a date point, and the first field in each line is the date string.
2084 * We also expect that all remaining fields represent series.
2085 * if the errorBars attribute is set, then interpret the fields as:
2086 * date, series1, stddev1, series2, stddev2, ...
2087 * @param {Array.<Object>} data See above.
2088 * @private
2089 *
2090 * @return Array.<Object> An array with one entry for each row. These entries
2091 * are an array of cells in that row. The first entry is the parsed x-value for
2092 * the row. The second, third, etc. are the y-values. These can take on one of
2093 * three forms, depending on the CSV and constructor parameters:
2094 * 1. numeric value
2095 * 2. [ value, stddev ]
2096 * 3. [ low value, center value, high value ]
2097 */
2098 Dygraph.prototype.parseCSV_ = function(data) {
2099 var ret = [];
2100 var lines = data.split("\n");
2101
2102 // Use the default delimiter or fall back to a tab if that makes sense.
2103 var delim = this.attr_('delimiter');
2104 if (lines[0].indexOf(delim) == -1 && lines[0].indexOf('\t') >= 0) {
2105 delim = '\t';
2106 }
2107
2108 var start = 0;
2109 if (this.labelsFromCSV_) {
2110 start = 1;
2111 this.attrs_.labels = lines[0].split(delim);
2112 }
2113
2114 // Parse the x as a float or return null if it's not a number.
2115 var parseFloatOrNull = function(x) {
2116 var val = parseFloat(x);
2117 return isNaN(val) ? null : val;
2118 };
2119
2120 var xParser;
2121 var defaultParserSet = false; // attempt to auto-detect x value type
2122 var expectedCols = this.attr_("labels").length;
2123 var outOfOrder = false;
2124 for (var i = start; i < lines.length; i++) {
2125 var line = lines[i];
2126 if (line.length == 0) continue; // skip blank lines
2127 if (line[0] == '#') continue; // skip comment lines
2128 var inFields = line.split(delim);
2129 if (inFields.length < 2) continue;
2130
2131 var fields = [];
2132 if (!defaultParserSet) {
2133 this.detectTypeFromString_(inFields[0]);
2134 xParser = this.attr_("xValueParser");
2135 defaultParserSet = true;
2136 }
2137 fields[0] = xParser(inFields[0], this);
2138
2139 // If fractions are expected, parse the numbers as "A/B"
2140 if (this.fractions_) {
2141 for (var j = 1; j < inFields.length; j++) {
2142 // TODO(danvk): figure out an appropriate way to flag parse errors.
2143 var vals = inFields[j].split("/");
2144 fields[j] = [parseFloatOrNull(vals[0]), parseFloatOrNull(vals[1])];
2145 }
2146 } else if (this.attr_("errorBars")) {
2147 // If there are error bars, values are (value, stddev) pairs
2148 for (var j = 1; j < inFields.length; j += 2)
2149 fields[(j + 1) / 2] = [parseFloatOrNull(inFields[j]),
2150 parseFloatOrNull(inFields[j + 1])];
2151 } else if (this.attr_("customBars")) {
2152 // Bars are a low;center;high tuple
2153 for (var j = 1; j < inFields.length; j++) {
2154 var vals = inFields[j].split(";");
2155 fields[j] = [ parseFloatOrNull(vals[0]),
2156 parseFloatOrNull(vals[1]),
2157 parseFloatOrNull(vals[2]) ];
2158 }
2159 } else {
2160 // Values are just numbers
2161 for (var j = 1; j < inFields.length; j++) {
2162 fields[j] = parseFloatOrNull(inFields[j]);
2163 }
2164 }
2165 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
2166 outOfOrder = true;
2167 }
2168 ret.push(fields);
2169
2170 if (fields.length != expectedCols) {
2171 this.error("Number of columns in line " + i + " (" + fields.length +
2172 ") does not agree with number of labels (" + expectedCols +
2173 ") " + line);
2174 }
2175 }
2176
2177 if (outOfOrder) {
2178 this.warn("CSV is out of order; order it correctly to speed loading.");
2179 ret.sort(function(a,b) { return a[0] - b[0] });
2180 }
2181
2182 return ret;
2183 };
2184
2185 /**
2186 * The user has provided their data as a pre-packaged JS array. If the x values
2187 * are numeric, this is the same as dygraphs' internal format. If the x values
2188 * are dates, we need to convert them from Date objects to ms since epoch.
2189 * @param {Array.<Object>} data
2190 * @return {Array.<Object>} data with numeric x values.
2191 */
2192 Dygraph.prototype.parseArray_ = function(data) {
2193 // Peek at the first x value to see if it's numeric.
2194 if (data.length == 0) {
2195 this.error("Can't plot empty data set");
2196 return null;
2197 }
2198 if (data[0].length == 0) {
2199 this.error("Data set cannot contain an empty row");
2200 return null;
2201 }
2202
2203 if (this.attr_("labels") == null) {
2204 this.warn("Using default labels. Set labels explicitly via 'labels' " +
2205 "in the options parameter");
2206 this.attrs_.labels = [ "X" ];
2207 for (var i = 1; i < data[0].length; i++) {
2208 this.attrs_.labels.push("Y" + i);
2209 }
2210 }
2211
2212 if (Dygraph.isDateLike(data[0][0])) {
2213 // Some intelligent defaults for a date x-axis.
2214 this.attrs_.xValueFormatter = Dygraph.dateString_;
2215 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
2216 this.attrs_.xTicker = Dygraph.dateTicker;
2217
2218 // Assume they're all dates.
2219 var parsedData = Dygraph.clone(data);
2220 for (var i = 0; i < data.length; i++) {
2221 if (parsedData[i].length == 0) {
2222 this.error("Row " + (1 + i) + " of data is empty");
2223 return null;
2224 }
2225 if (parsedData[i][0] == null
2226 || typeof(parsedData[i][0].getTime) != 'function'
2227 || isNaN(parsedData[i][0].getTime())) {
2228 this.error("x value in row " + (1 + i) + " is not a Date");
2229 return null;
2230 }
2231 parsedData[i][0] = parsedData[i][0].getTime();
2232 }
2233 return parsedData;
2234 } else {
2235 // Some intelligent defaults for a numeric x-axis.
2236 this.attrs_.xValueFormatter = function(x) { return x; };
2237 this.attrs_.xTicker = Dygraph.numericTicks;
2238 return data;
2239 }
2240 };
2241
2242 /**
2243 * Parses a DataTable object from gviz.
2244 * The data is expected to have a first column that is either a date or a
2245 * number. All subsequent columns must be numbers. If there is a clear mismatch
2246 * between this.xValueParser_ and the type of the first column, it will be
2247 * fixed. Fills out rawData_.
2248 * @param {Array.<Object>} data See above.
2249 * @private
2250 */
2251 Dygraph.prototype.parseDataTable_ = function(data) {
2252 var cols = data.getNumberOfColumns();
2253 var rows = data.getNumberOfRows();
2254
2255 var indepType = data.getColumnType(0);
2256 if (indepType == 'date' || indepType == 'datetime') {
2257 this.attrs_.xValueFormatter = Dygraph.dateString_;
2258 this.attrs_.xValueParser = Dygraph.dateParser;
2259 this.attrs_.xTicker = Dygraph.dateTicker;
2260 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
2261 } else if (indepType == 'number') {
2262 this.attrs_.xValueFormatter = function(x) { return x; };
2263 this.attrs_.xValueParser = function(x) { return parseFloat(x); };
2264 this.attrs_.xTicker = Dygraph.numericTicks;
2265 this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;
2266 } else {
2267 this.error("only 'date', 'datetime' and 'number' types are supported for " +
2268 "column 1 of DataTable input (Got '" + indepType + "')");
2269 return null;
2270 }
2271
2272 // Array of the column indices which contain data (and not annotations).
2273 var colIdx = [];
2274 var annotationCols = {}; // data index -> [annotation cols]
2275 var hasAnnotations = false;
2276 for (var i = 1; i < cols; i++) {
2277 var type = data.getColumnType(i);
2278 if (type == 'number') {
2279 colIdx.push(i);
2280 } else if (type == 'string' && this.attr_('displayAnnotations')) {
2281 // This is OK -- it's an annotation column.
2282 var dataIdx = colIdx[colIdx.length - 1];
2283 if (!annotationCols.hasOwnProperty(dataIdx)) {
2284 annotationCols[dataIdx] = [i];
2285 } else {
2286 annotationCols[dataIdx].push(i);
2287 }
2288 hasAnnotations = true;
2289 } else {
2290 this.error("Only 'number' is supported as a dependent type with Gviz." +
2291 " 'string' is only supported if displayAnnotations is true");
2292 }
2293 }
2294
2295 // Read column labels
2296 // TODO(danvk): add support back for errorBars
2297 var labels = [data.getColumnLabel(0)];
2298 for (var i = 0; i < colIdx.length; i++) {
2299 labels.push(data.getColumnLabel(colIdx[i]));
2300 }
2301 this.attrs_.labels = labels;
2302 cols = labels.length;
2303
2304 var ret = [];
2305 var outOfOrder = false;
2306 var annotations = [];
2307 for (var i = 0; i < rows; i++) {
2308 var row = [];
2309 if (typeof(data.getValue(i, 0)) === 'undefined' ||
2310 data.getValue(i, 0) === null) {
2311 this.warning("Ignoring row " + i +
2312 " of DataTable because of undefined or null first column.");
2313 continue;
2314 }
2315
2316 if (indepType == 'date' || indepType == 'datetime') {
2317 row.push(data.getValue(i, 0).getTime());
2318 } else {
2319 row.push(data.getValue(i, 0));
2320 }
2321 if (!this.attr_("errorBars")) {
2322 for (var j = 0; j < colIdx.length; j++) {
2323 var col = colIdx[j];
2324 row.push(data.getValue(i, col));
2325 if (hasAnnotations &&
2326 annotationCols.hasOwnProperty(col) &&
2327 data.getValue(i, annotationCols[col][0]) != null) {
2328 var ann = {};
2329 ann.series = data.getColumnLabel(col);
2330 ann.xval = row[0];
2331 ann.shortText = String.fromCharCode(65 /* A */ + annotations.length)
2332 ann.text = '';
2333 for (var k = 0; k < annotationCols[col].length; k++) {
2334 if (k) ann.text += "\n";
2335 ann.text += data.getValue(i, annotationCols[col][k]);
2336 }
2337 annotations.push(ann);
2338 }
2339 }
2340 } else {
2341 for (var j = 0; j < cols - 1; j++) {
2342 row.push([ data.getValue(i, 1 + 2 * j), data.getValue(i, 2 + 2 * j) ]);
2343 }
2344 }
2345 if (ret.length > 0 && row[0] < ret[ret.length - 1][0]) {
2346 outOfOrder = true;
2347 }
2348 ret.push(row);
2349 }
2350
2351 if (outOfOrder) {
2352 this.warn("DataTable is out of order; order it correctly to speed loading.");
2353 ret.sort(function(a,b) { return a[0] - b[0] });
2354 }
2355 this.rawData_ = ret;
2356
2357 if (annotations.length > 0) {
2358 this.setAnnotations(annotations, true);
2359 }
2360 }
2361
2362 // These functions are all based on MochiKit.
2363 Dygraph.update = function (self, o) {
2364 if (typeof(o) != 'undefined' && o !== null) {
2365 for (var k in o) {
2366 if (o.hasOwnProperty(k)) {
2367 self[k] = o[k];
2368 }
2369 }
2370 }
2371 return self;
2372 };
2373
2374 Dygraph.isArrayLike = function (o) {
2375 var typ = typeof(o);
2376 if (
2377 (typ != 'object' && !(typ == 'function' &&
2378 typeof(o.item) == 'function')) ||
2379 o === null ||
2380 typeof(o.length) != 'number' ||
2381 o.nodeType === 3
2382 ) {
2383 return false;
2384 }
2385 return true;
2386 };
2387
2388 Dygraph.isDateLike = function (o) {
2389 if (typeof(o) != "object" || o === null ||
2390 typeof(o.getTime) != 'function') {
2391 return false;
2392 }
2393 return true;
2394 };
2395
2396 Dygraph.clone = function(o) {
2397 // TODO(danvk): figure out how MochiKit's version works
2398 var r = [];
2399 for (var i = 0; i < o.length; i++) {
2400 if (Dygraph.isArrayLike(o[i])) {
2401 r.push(Dygraph.clone(o[i]));
2402 } else {
2403 r.push(o[i]);
2404 }
2405 }
2406 return r;
2407 };
2408
2409
2410 /**
2411 * Get the CSV data. If it's in a function, call that function. If it's in a
2412 * file, do an XMLHttpRequest to get it.
2413 * @private
2414 */
2415 Dygraph.prototype.start_ = function() {
2416 if (typeof this.file_ == 'function') {
2417 // CSV string. Pretend we got it via XHR.
2418 this.loadedEvent_(this.file_());
2419 } else if (Dygraph.isArrayLike(this.file_)) {
2420 this.rawData_ = this.parseArray_(this.file_);
2421 this.drawGraph_(this.rawData_);
2422 } else if (typeof this.file_ == 'object' &&
2423 typeof this.file_.getColumnRange == 'function') {
2424 // must be a DataTable from gviz.
2425 this.parseDataTable_(this.file_);
2426 this.drawGraph_(this.rawData_);
2427 } else if (typeof this.file_ == 'string') {
2428 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
2429 if (this.file_.indexOf('\n') >= 0) {
2430 this.loadedEvent_(this.file_);
2431 } else {
2432 var req = new XMLHttpRequest();
2433 var caller = this;
2434 req.onreadystatechange = function () {
2435 if (req.readyState == 4) {
2436 if (req.status == 200) {
2437 caller.loadedEvent_(req.responseText);
2438 }
2439 }
2440 };
2441
2442 req.open("GET", this.file_, true);
2443 req.send(null);
2444 }
2445 } else {
2446 this.error("Unknown data format: " + (typeof this.file_));
2447 }
2448 };
2449
2450 /**
2451 * Changes various properties of the graph. These can include:
2452 * <ul>
2453 * <li>file: changes the source data for the graph</li>
2454 * <li>errorBars: changes whether the data contains stddev</li>
2455 * </ul>
2456 * @param {Object} attrs The new properties and values
2457 */
2458 Dygraph.prototype.updateOptions = function(attrs) {
2459 // TODO(danvk): this is a mess. Rethink this function.
2460 if (attrs.rollPeriod) {
2461 this.rollPeriod_ = attrs.rollPeriod;
2462 }
2463 if (attrs.dateWindow) {
2464 this.dateWindow_ = attrs.dateWindow;
2465 }
2466 if (attrs.valueRange) {
2467 this.valueRange_ = attrs.valueRange;
2468 }
2469 Dygraph.update(this.user_attrs_, attrs);
2470 Dygraph.update(this.renderOptions_, attrs);
2471
2472 this.labelsFromCSV_ = (this.attr_("labels") == null);
2473
2474 // TODO(danvk): this doesn't match the constructor logic
2475 this.layout_.updateOptions({ 'errorBars': this.attr_("errorBars") });
2476 if (attrs['file']) {
2477 this.file_ = attrs['file'];
2478 this.start_();
2479 } else {
2480 this.drawGraph_(this.rawData_);
2481 }
2482 };
2483
2484 /**
2485 * Resizes the dygraph. If no parameters are specified, resizes to fill the
2486 * containing div (which has presumably changed size since the dygraph was
2487 * instantiated. If the width/height are specified, the div will be resized.
2488 *
2489 * This is far more efficient than destroying and re-instantiating a
2490 * Dygraph, since it doesn't have to reparse the underlying data.
2491 *
2492 * @param {Number} width Width (in pixels)
2493 * @param {Number} height Height (in pixels)
2494 */
2495 Dygraph.prototype.resize = function(width, height) {
2496 if (this.resize_lock) {
2497 return;
2498 }
2499 this.resize_lock = true;
2500
2501 if ((width === null) != (height === null)) {
2502 this.warn("Dygraph.resize() should be called with zero parameters or " +
2503 "two non-NULL parameters. Pretending it was zero.");
2504 width = height = null;
2505 }
2506
2507 // TODO(danvk): there should be a clear() method.
2508 this.maindiv_.innerHTML = "";
2509 this.attrs_.labelsDiv = null;
2510
2511 if (width) {
2512 this.maindiv_.style.width = width + "px";
2513 this.maindiv_.style.height = height + "px";
2514 this.width_ = width;
2515 this.height_ = height;
2516 } else {
2517 this.width_ = this.maindiv_.offsetWidth;
2518 this.height_ = this.maindiv_.offsetHeight;
2519 }
2520
2521 this.createInterface_();
2522 this.drawGraph_(this.rawData_);
2523
2524 this.resize_lock = false;
2525 };
2526
2527 /**
2528 * Adjusts the number of days in the rolling average. Updates the graph to
2529 * reflect the new averaging period.
2530 * @param {Number} length Number of days over which to average the data.
2531 */
2532 Dygraph.prototype.adjustRoll = function(length) {
2533 this.rollPeriod_ = length;
2534 this.drawGraph_(this.rawData_);
2535 };
2536
2537 /**
2538 * Returns a boolean array of visibility statuses.
2539 */
2540 Dygraph.prototype.visibility = function() {
2541 // Do lazy-initialization, so that this happens after we know the number of
2542 // data series.
2543 if (!this.attr_("visibility")) {
2544 this.attrs_["visibility"] = [];
2545 }
2546 while (this.attr_("visibility").length < this.rawData_[0].length - 1) {
2547 this.attr_("visibility").push(true);
2548 }
2549 return this.attr_("visibility");
2550 };
2551
2552 /**
2553 * Changes the visiblity of a series.
2554 */
2555 Dygraph.prototype.setVisibility = function(num, value) {
2556 var x = this.visibility();
2557 if (num < 0 && num >= x.length) {
2558 this.warn("invalid series number in setVisibility: " + num);
2559 } else {
2560 x[num] = value;
2561 this.drawGraph_(this.rawData_);
2562 }
2563 };
2564
2565 /**
2566 * Update the list of annotations and redraw the chart.
2567 */
2568 Dygraph.prototype.setAnnotations = function(ann, suppressDraw) {
2569 this.annotations_ = ann;
2570 this.layout_.setAnnotations(this.annotations_);
2571 if (!suppressDraw) {
2572 this.drawGraph_(this.rawData_);
2573 }
2574 };
2575
2576 /**
2577 * Return the list of annotations.
2578 */
2579 Dygraph.prototype.annotations = function() {
2580 return this.annotations_;
2581 };
2582
2583 Dygraph.addAnnotationRule = function() {
2584 if (Dygraph.addedAnnotationCSS) return;
2585
2586 var mysheet;
2587 if (document.styleSheets.length > 0) {
2588 mysheet = document.styleSheets[0];
2589 } else {
2590 var styleSheetElement = document.createElement("style");
2591 styleSheetElement.type = "text/css";
2592 document.getElementsByTagName("head")[0].appendChild(styleSheetElement);
2593 for(i = 0; i < document.styleSheets.length; i++) {
2594 if (document.styleSheets[i].disabled) continue;
2595 mysheet = document.styleSheets[i];
2596 }
2597 }
2598
2599 var rule = "border: 1px solid black; " +
2600 "background-color: white; " +
2601 "text-align: center;";
2602 if (mysheet.insertRule) { // Firefox
2603 mysheet.insertRule(".dygraphDefaultAnnotation { " + rule + " }", 0);
2604 } else if (mysheet.addRule) { // IE
2605 mysheet.addRule(".dygraphDefaultAnnotation", rule);
2606 }
2607
2608 Dygraph.addedAnnotationCSS = true;
2609 }
2610
2611 /**
2612 * Create a new canvas element. This is more complex than a simple
2613 * document.createElement("canvas") because of IE and excanvas.
2614 */
2615 Dygraph.createCanvas = function() {
2616 var canvas = document.createElement("canvas");
2617
2618 isIE = (/MSIE/.test(navigator.userAgent) && !window.opera);
2619 if (isIE) {
2620 canvas = G_vmlCanvasManager.initElement(canvas);
2621 }
2622
2623 return canvas;
2624 };
2625
2626
2627 /**
2628 * A wrapper around Dygraph that implements the gviz API.
2629 * @param {Object} container The DOM object the visualization should live in.
2630 */
2631 Dygraph.GVizChart = function(container) {
2632 this.container = container;
2633 }
2634
2635 Dygraph.GVizChart.prototype.draw = function(data, options) {
2636 this.container.innerHTML = '';
2637 this.date_graph = new Dygraph(this.container, data, options);
2638 }
2639
2640 /**
2641 * Google charts compatible setSelection
2642 * Only row selection is supported, all points in the row will be highlighted
2643 * @param {Array} array of the selected cells
2644 * @public
2645 */
2646 Dygraph.GVizChart.prototype.setSelection = function(selection_array) {
2647 var row = false;
2648 if (selection_array.length) {
2649 row = selection_array[0].row;
2650 }
2651 this.date_graph.setSelection(row);
2652 }
2653
2654 /**
2655 * Google charts compatible getSelection implementation
2656 * @return {Array} array of the selected cells
2657 * @public
2658 */
2659 Dygraph.GVizChart.prototype.getSelection = function() {
2660 var selection = [];
2661
2662 var row = this.date_graph.getSelection();
2663
2664 if (row < 0) return selection;
2665
2666 col = 1;
2667 for (var i in this.date_graph.layout_.datasets) {
2668 selection.push({row: row, column: col});
2669 col++;
2670 }
2671
2672 return selection;
2673 }
2674
2675 // Older pages may still use this name.
2676 DateGraph = Dygraph;