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