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