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