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