update both linear regression tests; support null as per-series value
[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, series) {
244 if (series &&
245 typeof(this.user_attrs_[series]) != 'undefined' &&
246 this.user_attrs_[series] != null &&
247 typeof(this.user_attrs_[series][name]) != 'undefined') {
248 return this.user_attrs_[series][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 num_series = this.attr_('labels').length;
1030 for (var i = 1; i < num_series; i++) {
1031 var r = this.attr_('highlightCircleSize', 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 setIdx = this.indexFromSetName(this.selPoints_[i].name);
1073 var circleSize = this.attr_('highlightCircleSize', setIdx);
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 this.selPoints_.push(this.layout_.points[pos+row]);
1105 }
1106 pos += this.layout_.datasets[i].length;
1107 }
1108 }
1109
1110 if (this.selPoints_.length) {
1111 this.lastx_ = this.selPoints_[0].xval;
1112 this.updateSelection_();
1113 } else {
1114 this.lastx_ = -1;
1115 this.clearSelection();
1116 }
1117
1118 };
1119
1120 /**
1121 * The mouse has left the canvas. Clear out whatever artifacts remain
1122 * @param {Object} event the mouseout event from the browser.
1123 * @private
1124 */
1125 Dygraph.prototype.mouseOut_ = function(event) {
1126 if (this.attr_("unhighlightCallback")) {
1127 this.attr_("unhighlightCallback")(event);
1128 }
1129
1130 if (this.attr_("hideOverlayOnMouseOut")) {
1131 this.clearSelection();
1132 }
1133 };
1134
1135 /**
1136 * Remove all selection from the canvas
1137 * @public
1138 */
1139 Dygraph.prototype.clearSelection = function() {
1140 // Get rid of the overlay data
1141 var ctx = this.canvas_.getContext("2d");
1142 ctx.clearRect(0, 0, this.width_, this.height_);
1143 this.attr_("labelsDiv").innerHTML = "";
1144 this.selPoints_ = [];
1145 this.lastx_ = -1;
1146 }
1147
1148 /**
1149 * Returns the number of the currently selected row
1150 * @return int row number, of -1 if nothing is selected
1151 * @public
1152 */
1153 Dygraph.prototype.getSelection = function() {
1154 if (!this.selPoints_ || this.selPoints_.length < 1) {
1155 return -1;
1156 }
1157
1158 for (var row=0; row<this.layout_.points.length; row++ ) {
1159 if (this.layout_.points[row].x == this.selPoints_[0].x) {
1160 return row + this.boundaryIds_[0][0];
1161 }
1162 }
1163 return -1;
1164 }
1165
1166 Dygraph.zeropad = function(x) {
1167 if (x < 10) return "0" + x; else return "" + x;
1168 }
1169
1170 /**
1171 * Return a string version of the hours, minutes and seconds portion of a date.
1172 * @param {Number} date The JavaScript date (ms since epoch)
1173 * @return {String} A time of the form "HH:MM:SS"
1174 * @private
1175 */
1176 Dygraph.hmsString_ = function(date) {
1177 var zeropad = Dygraph.zeropad;
1178 var d = new Date(date);
1179 if (d.getSeconds()) {
1180 return zeropad(d.getHours()) + ":" +
1181 zeropad(d.getMinutes()) + ":" +
1182 zeropad(d.getSeconds());
1183 } else {
1184 return zeropad(d.getHours()) + ":" + zeropad(d.getMinutes());
1185 }
1186 }
1187
1188 /**
1189 * Convert a JS date to a string appropriate to display on an axis that
1190 * is displaying values at the stated granularity.
1191 * @param {Date} date The date to format
1192 * @param {Number} granularity One of the Dygraph granularity constants
1193 * @return {String} The formatted date
1194 * @private
1195 */
1196 Dygraph.dateAxisFormatter = function(date, granularity) {
1197 if (granularity >= Dygraph.MONTHLY) {
1198 return date.strftime('%b %y');
1199 } else {
1200 var frac = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds();
1201 if (frac == 0 || granularity >= Dygraph.DAILY) {
1202 return new Date(date.getTime() + 3600*1000).strftime('%d%b');
1203 } else {
1204 return Dygraph.hmsString_(date.getTime());
1205 }
1206 }
1207 }
1208
1209 /**
1210 * Convert a JS date (millis since epoch) to YYYY/MM/DD
1211 * @param {Number} date The JavaScript date (ms since epoch)
1212 * @return {String} A date of the form "YYYY/MM/DD"
1213 * @private
1214 */
1215 Dygraph.dateString_ = function(date, self) {
1216 var zeropad = Dygraph.zeropad;
1217 var d = new Date(date);
1218
1219 // Get the year:
1220 var year = "" + d.getFullYear();
1221 // Get a 0 padded month string
1222 var month = zeropad(d.getMonth() + 1); //months are 0-offset, sigh
1223 // Get a 0 padded day string
1224 var day = zeropad(d.getDate());
1225
1226 var ret = "";
1227 var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
1228 if (frac) ret = " " + Dygraph.hmsString_(date);
1229
1230 return year + "/" + month + "/" + day + ret;
1231 };
1232
1233 /**
1234 * Round a number to the specified number of digits past the decimal point.
1235 * @param {Number} num The number to round
1236 * @param {Number} places The number of decimals to which to round
1237 * @return {Number} The rounded number
1238 * @private
1239 */
1240 Dygraph.round_ = function(num, places) {
1241 var shift = Math.pow(10, places);
1242 return Math.round(num * shift)/shift;
1243 };
1244
1245 /**
1246 * Fires when there's data available to be graphed.
1247 * @param {String} data Raw CSV data to be plotted
1248 * @private
1249 */
1250 Dygraph.prototype.loadedEvent_ = function(data) {
1251 this.rawData_ = this.parseCSV_(data);
1252 this.drawGraph_(this.rawData_);
1253 };
1254
1255 Dygraph.prototype.months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
1256 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1257 Dygraph.prototype.quarters = ["Jan", "Apr", "Jul", "Oct"];
1258
1259 /**
1260 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1261 * @private
1262 */
1263 Dygraph.prototype.addXTicks_ = function() {
1264 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1265 var startDate, endDate;
1266 if (this.dateWindow_) {
1267 startDate = this.dateWindow_[0];
1268 endDate = this.dateWindow_[1];
1269 } else {
1270 startDate = this.rawData_[0][0];
1271 endDate = this.rawData_[this.rawData_.length - 1][0];
1272 }
1273
1274 var xTicks = this.attr_('xTicker')(startDate, endDate, this);
1275 this.layout_.updateOptions({xTicks: xTicks});
1276 };
1277
1278 // Time granularity enumeration
1279 Dygraph.SECONDLY = 0;
1280 Dygraph.TWO_SECONDLY = 1;
1281 Dygraph.FIVE_SECONDLY = 2;
1282 Dygraph.TEN_SECONDLY = 3;
1283 Dygraph.THIRTY_SECONDLY = 4;
1284 Dygraph.MINUTELY = 5;
1285 Dygraph.TWO_MINUTELY = 6;
1286 Dygraph.FIVE_MINUTELY = 7;
1287 Dygraph.TEN_MINUTELY = 8;
1288 Dygraph.THIRTY_MINUTELY = 9;
1289 Dygraph.HOURLY = 10;
1290 Dygraph.TWO_HOURLY = 11;
1291 Dygraph.SIX_HOURLY = 12;
1292 Dygraph.DAILY = 13;
1293 Dygraph.WEEKLY = 14;
1294 Dygraph.MONTHLY = 15;
1295 Dygraph.QUARTERLY = 16;
1296 Dygraph.BIANNUAL = 17;
1297 Dygraph.ANNUAL = 18;
1298 Dygraph.DECADAL = 19;
1299 Dygraph.NUM_GRANULARITIES = 20;
1300
1301 Dygraph.SHORT_SPACINGS = [];
1302 Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY] = 1000 * 1;
1303 Dygraph.SHORT_SPACINGS[Dygraph.TWO_SECONDLY] = 1000 * 2;
1304 Dygraph.SHORT_SPACINGS[Dygraph.FIVE_SECONDLY] = 1000 * 5;
1305 Dygraph.SHORT_SPACINGS[Dygraph.TEN_SECONDLY] = 1000 * 10;
1306 Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_SECONDLY] = 1000 * 30;
1307 Dygraph.SHORT_SPACINGS[Dygraph.MINUTELY] = 1000 * 60;
1308 Dygraph.SHORT_SPACINGS[Dygraph.TWO_MINUTELY] = 1000 * 60 * 2;
1309 Dygraph.SHORT_SPACINGS[Dygraph.FIVE_MINUTELY] = 1000 * 60 * 5;
1310 Dygraph.SHORT_SPACINGS[Dygraph.TEN_MINUTELY] = 1000 * 60 * 10;
1311 Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_MINUTELY] = 1000 * 60 * 30;
1312 Dygraph.SHORT_SPACINGS[Dygraph.HOURLY] = 1000 * 3600;
1313 Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY] = 1000 * 3600 * 2;
1314 Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY] = 1000 * 3600 * 6;
1315 Dygraph.SHORT_SPACINGS[Dygraph.DAILY] = 1000 * 86400;
1316 Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY] = 1000 * 604800;
1317
1318 // NumXTicks()
1319 //
1320 // If we used this time granularity, how many ticks would there be?
1321 // This is only an approximation, but it's generally good enough.
1322 //
1323 Dygraph.prototype.NumXTicks = function(start_time, end_time, granularity) {
1324 if (granularity < Dygraph.MONTHLY) {
1325 // Generate one tick mark for every fixed interval of time.
1326 var spacing = Dygraph.SHORT_SPACINGS[granularity];
1327 return Math.floor(0.5 + 1.0 * (end_time - start_time) / spacing);
1328 } else {
1329 var year_mod = 1; // e.g. to only print one point every 10 years.
1330 var num_months = 12;
1331 if (granularity == Dygraph.QUARTERLY) num_months = 3;
1332 if (granularity == Dygraph.BIANNUAL) num_months = 2;
1333 if (granularity == Dygraph.ANNUAL) num_months = 1;
1334 if (granularity == Dygraph.DECADAL) { num_months = 1; year_mod = 10; }
1335
1336 var msInYear = 365.2524 * 24 * 3600 * 1000;
1337 var num_years = 1.0 * (end_time - start_time) / msInYear;
1338 return Math.floor(0.5 + 1.0 * num_years * num_months / year_mod);
1339 }
1340 };
1341
1342 // GetXAxis()
1343 //
1344 // Construct an x-axis of nicely-formatted times on meaningful boundaries
1345 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1346 //
1347 // Returns an array containing {v: millis, label: label} dictionaries.
1348 //
1349 Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) {
1350 var formatter = this.attr_("xAxisLabelFormatter");
1351 var ticks = [];
1352 if (granularity < Dygraph.MONTHLY) {
1353 // Generate one tick mark for every fixed interval of time.
1354 var spacing = Dygraph.SHORT_SPACINGS[granularity];
1355 var format = '%d%b'; // e.g. "1Jan"
1356
1357 // Find a time less than start_time which occurs on a "nice" time boundary
1358 // for this granularity.
1359 var g = spacing / 1000;
1360 var d = new Date(start_time);
1361 if (g <= 60) { // seconds
1362 var x = d.getSeconds(); d.setSeconds(x - x % g);
1363 } else {
1364 d.setSeconds(0);
1365 g /= 60;
1366 if (g <= 60) { // minutes
1367 var x = d.getMinutes(); d.setMinutes(x - x % g);
1368 } else {
1369 d.setMinutes(0);
1370 g /= 60;
1371
1372 if (g <= 24) { // days
1373 var x = d.getHours(); d.setHours(x - x % g);
1374 } else {
1375 d.setHours(0);
1376 g /= 24;
1377
1378 if (g == 7) { // one week
1379 d.setDate(d.getDate() - d.getDay());
1380 }
1381 }
1382 }
1383 }
1384 start_time = d.getTime();
1385
1386 for (var t = start_time; t <= end_time; t += spacing) {
1387 ticks.push({ v:t, label: formatter(new Date(t), granularity) });
1388 }
1389 } else {
1390 // Display a tick mark on the first of a set of months of each year.
1391 // Years get a tick mark iff y % year_mod == 0. This is useful for
1392 // displaying a tick mark once every 10 years, say, on long time scales.
1393 var months;
1394 var year_mod = 1; // e.g. to only print one point every 10 years.
1395
1396 if (granularity == Dygraph.MONTHLY) {
1397 months = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
1398 } else if (granularity == Dygraph.QUARTERLY) {
1399 months = [ 0, 3, 6, 9 ];
1400 } else if (granularity == Dygraph.BIANNUAL) {
1401 months = [ 0, 6 ];
1402 } else if (granularity == Dygraph.ANNUAL) {
1403 months = [ 0 ];
1404 } else if (granularity == Dygraph.DECADAL) {
1405 months = [ 0 ];
1406 year_mod = 10;
1407 }
1408
1409 var start_year = new Date(start_time).getFullYear();
1410 var end_year = new Date(end_time).getFullYear();
1411 var zeropad = Dygraph.zeropad;
1412 for (var i = start_year; i <= end_year; i++) {
1413 if (i % year_mod != 0) continue;
1414 for (var j = 0; j < months.length; j++) {
1415 var date_str = i + "/" + zeropad(1 + months[j]) + "/01";
1416 var t = Date.parse(date_str);
1417 if (t < start_time || t > end_time) continue;
1418 ticks.push({ v:t, label: formatter(new Date(t), granularity) });
1419 }
1420 }
1421 }
1422
1423 return ticks;
1424 };
1425
1426
1427 /**
1428 * Add ticks to the x-axis based on a date range.
1429 * @param {Number} startDate Start of the date window (millis since epoch)
1430 * @param {Number} endDate End of the date window (millis since epoch)
1431 * @return {Array.<Object>} Array of {label, value} tuples.
1432 * @public
1433 */
1434 Dygraph.dateTicker = function(startDate, endDate, self) {
1435 var chosen = -1;
1436 for (var i = 0; i < Dygraph.NUM_GRANULARITIES; i++) {
1437 var num_ticks = self.NumXTicks(startDate, endDate, i);
1438 if (self.width_ / num_ticks >= self.attr_('pixelsPerXLabel')) {
1439 chosen = i;
1440 break;
1441 }
1442 }
1443
1444 if (chosen >= 0) {
1445 return self.GetXAxis(startDate, endDate, chosen);
1446 } else {
1447 // TODO(danvk): signal error.
1448 }
1449 };
1450
1451 /**
1452 * Add ticks when the x axis has numbers on it (instead of dates)
1453 * @param {Number} startDate Start of the date window (millis since epoch)
1454 * @param {Number} endDate End of the date window (millis since epoch)
1455 * @return {Array.<Object>} Array of {label, value} tuples.
1456 * @public
1457 */
1458 Dygraph.numericTicks = function(minV, maxV, self) {
1459 // Basic idea:
1460 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1461 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks).
1462 // The first spacing greater than pixelsPerYLabel is what we use.
1463 // TODO(danvk): version that works on a log scale.
1464 if (self.attr_("labelsKMG2")) {
1465 var mults = [1, 2, 4, 8];
1466 } else {
1467 var mults = [1, 2, 5];
1468 }
1469 var scale, low_val, high_val, nTicks;
1470 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1471 var pixelsPerTick = self.attr_('pixelsPerYLabel');
1472 for (var i = -10; i < 50; i++) {
1473 if (self.attr_("labelsKMG2")) {
1474 var base_scale = Math.pow(16, i);
1475 } else {
1476 var base_scale = Math.pow(10, i);
1477 }
1478 for (var j = 0; j < mults.length; j++) {
1479 scale = base_scale * mults[j];
1480 low_val = Math.floor(minV / scale) * scale;
1481 high_val = Math.ceil(maxV / scale) * scale;
1482 nTicks = Math.abs(high_val - low_val) / scale;
1483 var spacing = self.height_ / nTicks;
1484 // wish I could break out of both loops at once...
1485 if (spacing > pixelsPerTick) break;
1486 }
1487 if (spacing > pixelsPerTick) break;
1488 }
1489
1490 // Construct labels for the ticks
1491 var ticks = [];
1492 var k;
1493 var k_labels = [];
1494 if (self.attr_("labelsKMB")) {
1495 k = 1000;
1496 k_labels = [ "K", "M", "B", "T" ];
1497 }
1498 if (self.attr_("labelsKMG2")) {
1499 if (k) self.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
1500 k = 1024;
1501 k_labels = [ "k", "M", "G", "T" ];
1502 }
1503
1504 // Allow reverse y-axis if it's explicitly requested.
1505 if (low_val > high_val) scale *= -1;
1506
1507 for (var i = 0; i < nTicks; i++) {
1508 var tickV = low_val + i * scale;
1509 var absTickV = Math.abs(tickV);
1510 var label = Dygraph.round_(tickV, 2);
1511 if (k_labels.length) {
1512 // Round up to an appropriate unit.
1513 var n = k*k*k*k;
1514 for (var j = 3; j >= 0; j--, n /= k) {
1515 if (absTickV >= n) {
1516 label = Dygraph.round_(tickV / n, 1) + k_labels[j];
1517 break;
1518 }
1519 }
1520 }
1521 ticks.push( {label: label, v: tickV} );
1522 }
1523 return ticks;
1524 };
1525
1526 /**
1527 * Adds appropriate ticks on the y-axis
1528 * @param {Number} minY The minimum Y value in the data set
1529 * @param {Number} maxY The maximum Y value in the data set
1530 * @private
1531 */
1532 Dygraph.prototype.addYTicks_ = function(minY, maxY) {
1533 // Set the number of ticks so that the labels are human-friendly.
1534 // TODO(danvk): make this an attribute as well.
1535 var ticks = Dygraph.numericTicks(minY, maxY, this);
1536 this.layout_.updateOptions( { yAxis: [minY, maxY],
1537 yTicks: ticks } );
1538 };
1539
1540 // Computes the range of the data series (including confidence intervals).
1541 // series is either [ [x1, y1], [x2, y2], ... ] or
1542 // [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1543 // Returns [low, high]
1544 Dygraph.prototype.extremeValues_ = function(series) {
1545 var minY = null, maxY = null;
1546
1547 var bars = this.attr_("errorBars") || this.attr_("customBars");
1548 if (bars) {
1549 // With custom bars, maxY is the max of the high values.
1550 for (var j = 0; j < series.length; j++) {
1551 var y = series[j][1][0];
1552 if (!y) continue;
1553 var low = y - series[j][1][1];
1554 var high = y + series[j][1][2];
1555 if (low > y) low = y; // this can happen with custom bars,
1556 if (high < y) high = y; // e.g. in tests/custom-bars.html
1557 if (maxY == null || high > maxY) {
1558 maxY = high;
1559 }
1560 if (minY == null || low < minY) {
1561 minY = low;
1562 }
1563 }
1564 } else {
1565 for (var j = 0; j < series.length; j++) {
1566 var y = series[j][1];
1567 if (y === null || isNaN(y)) continue;
1568 if (maxY == null || y > maxY) {
1569 maxY = y;
1570 }
1571 if (minY == null || y < minY) {
1572 minY = y;
1573 }
1574 }
1575 }
1576
1577 return [minY, maxY];
1578 };
1579
1580 /**
1581 * Update the graph with new data. Data is in the format
1582 * [ [date1, val1, val2, ...], [date2, val1, val2, ...] if errorBars=false
1583 * or, if errorBars=true,
1584 * [ [date1, [val1,stddev1], [val2,stddev2], ...], [date2, ...], ...]
1585 * @param {Array.<Object>} data The data (see above)
1586 * @private
1587 */
1588 Dygraph.prototype.drawGraph_ = function(data) {
1589 // This is used to set the second parameter to drawCallback, below.
1590 var is_initial_draw = this.is_initial_draw_;
1591 this.is_initial_draw_ = false;
1592
1593 var minY = null, maxY = null;
1594 this.layout_.removeAllDatasets();
1595 this.setColors_();
1596 this.attrs_['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
1597
1598 // Loop over the fields (series). Go from the last to the first,
1599 // because if they're stacked that's how we accumulate the values.
1600
1601 var cumulative_y = []; // For stacked series.
1602 var datasets = [];
1603
1604 // Loop over all fields and create datasets
1605 for (var i = data[0].length - 1; i >= 1; i--) {
1606 if (!this.visibility()[i - 1]) continue;
1607
1608 var connectSeparatedPoints = this.attr_('connectSeparatedPoints', i);
1609
1610 var series = [];
1611 for (var j = 0; j < data.length; j++) {
1612 if (data[j][i] != null || !connectSeparatedPoints) {
1613 var date = data[j][0];
1614 series.push([date, data[j][i]]);
1615 }
1616 }
1617 series = this.rollingAverage(series, this.rollPeriod_);
1618
1619 // Prune down to the desired range, if necessary (for zooming)
1620 // Because there can be lines going to points outside of the visible area,
1621 // we actually prune to visible points, plus one on either side.
1622 var bars = this.attr_("errorBars") || this.attr_("customBars");
1623 if (this.dateWindow_) {
1624 var low = this.dateWindow_[0];
1625 var high= this.dateWindow_[1];
1626 var pruned = [];
1627 // TODO(danvk): do binary search instead of linear search.
1628 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
1629 var firstIdx = null, lastIdx = null;
1630 for (var k = 0; k < series.length; k++) {
1631 if (series[k][0] >= low && firstIdx === null) {
1632 firstIdx = k;
1633 }
1634 if (series[k][0] <= high) {
1635 lastIdx = k;
1636 }
1637 }
1638 if (firstIdx === null) firstIdx = 0;
1639 if (firstIdx > 0) firstIdx--;
1640 if (lastIdx === null) lastIdx = series.length - 1;
1641 if (lastIdx < series.length - 1) lastIdx++;
1642 this.boundaryIds_[i-1] = [firstIdx, lastIdx];
1643 for (var k = firstIdx; k <= lastIdx; k++) {
1644 pruned.push(series[k]);
1645 }
1646 series = pruned;
1647 } else {
1648 this.boundaryIds_[i-1] = [0, series.length-1];
1649 }
1650
1651 var extremes = this.extremeValues_(series);
1652 var thisMinY = extremes[0];
1653 var thisMaxY = extremes[1];
1654 if (minY === null || thisMinY < minY) minY = thisMinY;
1655 if (maxY === null || thisMaxY > maxY) maxY = thisMaxY;
1656
1657 if (bars) {
1658 for (var j=0; j<series.length; j++) {
1659 val = [series[j][0], series[j][1][0], series[j][1][1], series[j][1][2]];
1660 series[j] = val;
1661 }
1662 } else if (this.attr_("stackedGraph")) {
1663 var l = series.length;
1664 var actual_y;
1665 for (var j = 0; j < l; j++) {
1666 // If one data set has a NaN, let all subsequent stacked
1667 // sets inherit the NaN -- only start at 0 for the first set.
1668 var x = series[j][0];
1669 if (cumulative_y[x] === undefined)
1670 cumulative_y[x] = 0;
1671
1672 actual_y = series[j][1];
1673 cumulative_y[x] += actual_y;
1674
1675 series[j] = [x, cumulative_y[x]]
1676
1677 if (!maxY || cumulative_y[x] > maxY)
1678 maxY = cumulative_y[x];
1679 }
1680 }
1681
1682 datasets[i] = series;
1683 }
1684
1685 for (var i = 1; i < datasets.length; i++) {
1686 if (!this.visibility()[i - 1]) continue;
1687 this.layout_.addDataset(this.attr_("labels")[i], datasets[i]);
1688 }
1689
1690 // Use some heuristics to come up with a good maxY value, unless it's been
1691 // set explicitly by the user.
1692 if (this.valueRange_ != null) {
1693 this.addYTicks_(this.valueRange_[0], this.valueRange_[1]);
1694 this.displayedYRange_ = this.valueRange_;
1695 } else {
1696 // This affects the calculation of span, below.
1697 if (this.attr_("includeZero") && minY > 0) {
1698 minY = 0;
1699 }
1700
1701 // Add some padding and round up to an integer to be human-friendly.
1702 var span = maxY - minY;
1703 // special case: if we have no sense of scale, use +/-10% of the sole value.
1704 if (span == 0) { span = maxY; }
1705 var maxAxisY = maxY + 0.1 * span;
1706 var minAxisY = minY - 0.1 * span;
1707
1708 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
1709 if (minAxisY < 0 && minY >= 0) minAxisY = 0;
1710 if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0;
1711
1712 if (this.attr_("includeZero")) {
1713 if (maxY < 0) maxAxisY = 0;
1714 if (minY > 0) minAxisY = 0;
1715 }
1716
1717 this.addYTicks_(minAxisY, maxAxisY);
1718 this.displayedYRange_ = [minAxisY, maxAxisY];
1719 }
1720
1721 this.addXTicks_();
1722
1723 // Tell PlotKit to use this new data and render itself
1724 this.layout_.updateOptions({dateWindow: this.dateWindow_});
1725 this.layout_.evaluateWithError();
1726 this.plotter_.clear();
1727 this.plotter_.render();
1728 this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width,
1729 this.canvas_.height);
1730
1731 if (this.attr_("drawCallback") !== null) {
1732 this.attr_("drawCallback")(this, is_initial_draw);
1733 }
1734 };
1735
1736 /**
1737 * Calculates the rolling average of a data set.
1738 * If originalData is [label, val], rolls the average of those.
1739 * If originalData is [label, [, it's interpreted as [value, stddev]
1740 * and the roll is returned in the same form, with appropriately reduced
1741 * stddev for each value.
1742 * Note that this is where fractional input (i.e. '5/10') is converted into
1743 * decimal values.
1744 * @param {Array} originalData The data in the appropriate format (see above)
1745 * @param {Number} rollPeriod The number of days over which to average the data
1746 */
1747 Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) {
1748 if (originalData.length < 2)
1749 return originalData;
1750 var rollPeriod = Math.min(rollPeriod, originalData.length - 1);
1751 var rollingData = [];
1752 var sigma = this.attr_("sigma");
1753
1754 if (this.fractions_) {
1755 var num = 0;
1756 var den = 0; // numerator/denominator
1757 var mult = 100.0;
1758 for (var i = 0; i < originalData.length; i++) {
1759 num += originalData[i][1][0];
1760 den += originalData[i][1][1];
1761 if (i - rollPeriod >= 0) {
1762 num -= originalData[i - rollPeriod][1][0];
1763 den -= originalData[i - rollPeriod][1][1];
1764 }
1765
1766 var date = originalData[i][0];
1767 var value = den ? num / den : 0.0;
1768 if (this.attr_("errorBars")) {
1769 if (this.wilsonInterval_) {
1770 // For more details on this confidence interval, see:
1771 // http://en.wikipedia.org/wiki/Binomial_confidence_interval
1772 if (den) {
1773 var p = value < 0 ? 0 : value, n = den;
1774 var pm = sigma * Math.sqrt(p*(1-p)/n + sigma*sigma/(4*n*n));
1775 var denom = 1 + sigma * sigma / den;
1776 var low = (p + sigma * sigma / (2 * den) - pm) / denom;
1777 var high = (p + sigma * sigma / (2 * den) + pm) / denom;
1778 rollingData[i] = [date,
1779 [p * mult, (p - low) * mult, (high - p) * mult]];
1780 } else {
1781 rollingData[i] = [date, [0, 0, 0]];
1782 }
1783 } else {
1784 var stddev = den ? sigma * Math.sqrt(value * (1 - value) / den) : 1.0;
1785 rollingData[i] = [date, [mult * value, mult * stddev, mult * stddev]];
1786 }
1787 } else {
1788 rollingData[i] = [date, mult * value];
1789 }
1790 }
1791 } else if (this.attr_("customBars")) {
1792 var low = 0;
1793 var mid = 0;
1794 var high = 0;
1795 var count = 0;
1796 for (var i = 0; i < originalData.length; i++) {
1797 var data = originalData[i][1];
1798 var y = data[1];
1799 rollingData[i] = [originalData[i][0], [y, y - data[0], data[2] - y]];
1800
1801 if (y != null && !isNaN(y)) {
1802 low += data[0];
1803 mid += y;
1804 high += data[2];
1805 count += 1;
1806 }
1807 if (i - rollPeriod >= 0) {
1808 var prev = originalData[i - rollPeriod];
1809 if (prev[1][1] != null && !isNaN(prev[1][1])) {
1810 low -= prev[1][0];
1811 mid -= prev[1][1];
1812 high -= prev[1][2];
1813 count -= 1;
1814 }
1815 }
1816 rollingData[i] = [originalData[i][0], [ 1.0 * mid / count,
1817 1.0 * (mid - low) / count,
1818 1.0 * (high - mid) / count ]];
1819 }
1820 } else {
1821 // Calculate the rolling average for the first rollPeriod - 1 points where
1822 // there is not enough data to roll over the full number of days
1823 var num_init_points = Math.min(rollPeriod - 1, originalData.length - 2);
1824 if (!this.attr_("errorBars")){
1825 if (rollPeriod == 1) {
1826 return originalData;
1827 }
1828
1829 for (var i = 0; i < originalData.length; i++) {
1830 var sum = 0;
1831 var num_ok = 0;
1832 for (var j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
1833 var y = originalData[j][1];
1834 if (y == null || isNaN(y)) continue;
1835 num_ok++;
1836 sum += originalData[j][1];
1837 }
1838 if (num_ok) {
1839 rollingData[i] = [originalData[i][0], sum / num_ok];
1840 } else {
1841 rollingData[i] = [originalData[i][0], null];
1842 }
1843 }
1844
1845 } else {
1846 for (var i = 0; i < originalData.length; i++) {
1847 var sum = 0;
1848 var variance = 0;
1849 var num_ok = 0;
1850 for (var j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
1851 var y = originalData[j][1][0];
1852 if (y == null || isNaN(y)) continue;
1853 num_ok++;
1854 sum += originalData[j][1][0];
1855 variance += Math.pow(originalData[j][1][1], 2);
1856 }
1857 if (num_ok) {
1858 var stddev = Math.sqrt(variance) / num_ok;
1859 rollingData[i] = [originalData[i][0],
1860 [sum / num_ok, sigma * stddev, sigma * stddev]];
1861 } else {
1862 rollingData[i] = [originalData[i][0], [null, null, null]];
1863 }
1864 }
1865 }
1866 }
1867
1868 return rollingData;
1869 };
1870
1871 /**
1872 * Parses a date, returning the number of milliseconds since epoch. This can be
1873 * passed in as an xValueParser in the Dygraph constructor.
1874 * TODO(danvk): enumerate formats that this understands.
1875 * @param {String} A date in YYYYMMDD format.
1876 * @return {Number} Milliseconds since epoch.
1877 * @public
1878 */
1879 Dygraph.dateParser = function(dateStr, self) {
1880 var dateStrSlashed;
1881 var d;
1882 if (dateStr.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
1883 dateStrSlashed = dateStr.replace("-", "/", "g");
1884 while (dateStrSlashed.search("-") != -1) {
1885 dateStrSlashed = dateStrSlashed.replace("-", "/");
1886 }
1887 d = Date.parse(dateStrSlashed);
1888 } else if (dateStr.length == 8) { // e.g. '20090712'
1889 // TODO(danvk): remove support for this format. It's confusing.
1890 dateStrSlashed = dateStr.substr(0,4) + "/" + dateStr.substr(4,2)
1891 + "/" + dateStr.substr(6,2);
1892 d = Date.parse(dateStrSlashed);
1893 } else {
1894 // Any format that Date.parse will accept, e.g. "2009/07/12" or
1895 // "2009/07/12 12:34:56"
1896 d = Date.parse(dateStr);
1897 }
1898
1899 if (!d || isNaN(d)) {
1900 self.error("Couldn't parse " + dateStr + " as a date");
1901 }
1902 return d;
1903 };
1904
1905 /**
1906 * Detects the type of the str (date or numeric) and sets the various
1907 * formatting attributes in this.attrs_ based on this type.
1908 * @param {String} str An x value.
1909 * @private
1910 */
1911 Dygraph.prototype.detectTypeFromString_ = function(str) {
1912 var isDate = false;
1913 if (str.indexOf('-') >= 0 ||
1914 str.indexOf('/') >= 0 ||
1915 isNaN(parseFloat(str))) {
1916 isDate = true;
1917 } else if (str.length == 8 && str > '19700101' && str < '20371231') {
1918 // TODO(danvk): remove support for this format.
1919 isDate = true;
1920 }
1921
1922 if (isDate) {
1923 this.attrs_.xValueFormatter = Dygraph.dateString_;
1924 this.attrs_.xValueParser = Dygraph.dateParser;
1925 this.attrs_.xTicker = Dygraph.dateTicker;
1926 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
1927 } else {
1928 this.attrs_.xValueFormatter = function(x) { return x; };
1929 this.attrs_.xValueParser = function(x) { return parseFloat(x); };
1930 this.attrs_.xTicker = Dygraph.numericTicks;
1931 this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;
1932 }
1933 };
1934
1935 /**
1936 * Parses a string in a special csv format. We expect a csv file where each
1937 * line is a date point, and the first field in each line is the date string.
1938 * We also expect that all remaining fields represent series.
1939 * if the errorBars attribute is set, then interpret the fields as:
1940 * date, series1, stddev1, series2, stddev2, ...
1941 * @param {Array.<Object>} data See above.
1942 * @private
1943 *
1944 * @return Array.<Object> An array with one entry for each row. These entries
1945 * are an array of cells in that row. The first entry is the parsed x-value for
1946 * the row. The second, third, etc. are the y-values. These can take on one of
1947 * three forms, depending on the CSV and constructor parameters:
1948 * 1. numeric value
1949 * 2. [ value, stddev ]
1950 * 3. [ low value, center value, high value ]
1951 */
1952 Dygraph.prototype.parseCSV_ = function(data) {
1953 var ret = [];
1954 var lines = data.split("\n");
1955
1956 // Use the default delimiter or fall back to a tab if that makes sense.
1957 var delim = this.attr_('delimiter');
1958 if (lines[0].indexOf(delim) == -1 && lines[0].indexOf('\t') >= 0) {
1959 delim = '\t';
1960 }
1961
1962 var start = 0;
1963 if (this.labelsFromCSV_) {
1964 start = 1;
1965 this.attrs_.labels = lines[0].split(delim);
1966 }
1967
1968 // Parse the x as a float or return null if it's not a number.
1969 var parseFloatOrNull = function(x) {
1970 var val = parseFloat(x);
1971 return isNaN(val) ? null : val;
1972 };
1973
1974 var xParser;
1975 var defaultParserSet = false; // attempt to auto-detect x value type
1976 var expectedCols = this.attr_("labels").length;
1977 var outOfOrder = false;
1978 for (var i = start; i < lines.length; i++) {
1979 var line = lines[i];
1980 if (line.length == 0) continue; // skip blank lines
1981 if (line[0] == '#') continue; // skip comment lines
1982 var inFields = line.split(delim);
1983 if (inFields.length < 2) continue;
1984
1985 var fields = [];
1986 if (!defaultParserSet) {
1987 this.detectTypeFromString_(inFields[0]);
1988 xParser = this.attr_("xValueParser");
1989 defaultParserSet = true;
1990 }
1991 fields[0] = xParser(inFields[0], this);
1992
1993 // If fractions are expected, parse the numbers as "A/B"
1994 if (this.fractions_) {
1995 for (var j = 1; j < inFields.length; j++) {
1996 // TODO(danvk): figure out an appropriate way to flag parse errors.
1997 var vals = inFields[j].split("/");
1998 fields[j] = [parseFloatOrNull(vals[0]), parseFloatOrNull(vals[1])];
1999 }
2000 } else if (this.attr_("errorBars")) {
2001 // If there are error bars, values are (value, stddev) pairs
2002 for (var j = 1; j < inFields.length; j += 2)
2003 fields[(j + 1) / 2] = [parseFloatOrNull(inFields[j]),
2004 parseFloatOrNull(inFields[j + 1])];
2005 } else if (this.attr_("customBars")) {
2006 // Bars are a low;center;high tuple
2007 for (var j = 1; j < inFields.length; j++) {
2008 var vals = inFields[j].split(";");
2009 fields[j] = [ parseFloatOrNull(vals[0]),
2010 parseFloatOrNull(vals[1]),
2011 parseFloatOrNull(vals[2]) ];
2012 }
2013 } else {
2014 // Values are just numbers
2015 for (var j = 1; j < inFields.length; j++) {
2016 fields[j] = parseFloatOrNull(inFields[j]);
2017 }
2018 }
2019 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
2020 outOfOrder = true;
2021 }
2022 ret.push(fields);
2023
2024 if (fields.length != expectedCols) {
2025 this.error("Number of columns in line " + i + " (" + fields.length +
2026 ") does not agree with number of labels (" + expectedCols +
2027 ") " + line);
2028 }
2029 }
2030
2031 if (outOfOrder) {
2032 this.warn("CSV is out of order; order it correctly to speed loading.");
2033 ret.sort(function(a,b) { return a[0] - b[0] });
2034 }
2035
2036 return ret;
2037 };
2038
2039 /**
2040 * The user has provided their data as a pre-packaged JS array. If the x values
2041 * are numeric, this is the same as dygraphs' internal format. If the x values
2042 * are dates, we need to convert them from Date objects to ms since epoch.
2043 * @param {Array.<Object>} data
2044 * @return {Array.<Object>} data with numeric x values.
2045 */
2046 Dygraph.prototype.parseArray_ = function(data) {
2047 // Peek at the first x value to see if it's numeric.
2048 if (data.length == 0) {
2049 this.error("Can't plot empty data set");
2050 return null;
2051 }
2052 if (data[0].length == 0) {
2053 this.error("Data set cannot contain an empty row");
2054 return null;
2055 }
2056
2057 if (this.attr_("labels") == null) {
2058 this.warn("Using default labels. Set labels explicitly via 'labels' " +
2059 "in the options parameter");
2060 this.attrs_.labels = [ "X" ];
2061 for (var i = 1; i < data[0].length; i++) {
2062 this.attrs_.labels.push("Y" + i);
2063 }
2064 }
2065
2066 if (Dygraph.isDateLike(data[0][0])) {
2067 // Some intelligent defaults for a date x-axis.
2068 this.attrs_.xValueFormatter = Dygraph.dateString_;
2069 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
2070 this.attrs_.xTicker = Dygraph.dateTicker;
2071
2072 // Assume they're all dates.
2073 var parsedData = Dygraph.clone(data);
2074 for (var i = 0; i < data.length; i++) {
2075 if (parsedData[i].length == 0) {
2076 this.error("Row " + (1 + i) + " of data is empty");
2077 return null;
2078 }
2079 if (parsedData[i][0] == null
2080 || typeof(parsedData[i][0].getTime) != 'function'
2081 || isNaN(parsedData[i][0].getTime())) {
2082 this.error("x value in row " + (1 + i) + " is not a Date");
2083 return null;
2084 }
2085 parsedData[i][0] = parsedData[i][0].getTime();
2086 }
2087 return parsedData;
2088 } else {
2089 // Some intelligent defaults for a numeric x-axis.
2090 this.attrs_.xValueFormatter = function(x) { return x; };
2091 this.attrs_.xTicker = Dygraph.numericTicks;
2092 return data;
2093 }
2094 };
2095
2096 /**
2097 * Parses a DataTable object from gviz.
2098 * The data is expected to have a first column that is either a date or a
2099 * number. All subsequent columns must be numbers. If there is a clear mismatch
2100 * between this.xValueParser_ and the type of the first column, it will be
2101 * fixed. Fills out rawData_.
2102 * @param {Array.<Object>} data See above.
2103 * @private
2104 */
2105 Dygraph.prototype.parseDataTable_ = function(data) {
2106 var cols = data.getNumberOfColumns();
2107 var rows = data.getNumberOfRows();
2108
2109 var indepType = data.getColumnType(0);
2110 if (indepType == 'date' || indepType == 'datetime') {
2111 this.attrs_.xValueFormatter = Dygraph.dateString_;
2112 this.attrs_.xValueParser = Dygraph.dateParser;
2113 this.attrs_.xTicker = Dygraph.dateTicker;
2114 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
2115 } else if (indepType == 'number') {
2116 this.attrs_.xValueFormatter = function(x) { return x; };
2117 this.attrs_.xValueParser = function(x) { return parseFloat(x); };
2118 this.attrs_.xTicker = Dygraph.numericTicks;
2119 this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;
2120 } else {
2121 this.error("only 'date', 'datetime' and 'number' types are supported for " +
2122 "column 1 of DataTable input (Got '" + indepType + "')");
2123 return null;
2124 }
2125
2126 // Array of the column indices which contain data (and not annotations).
2127 var colIdx = [];
2128 var annotationCols = {}; // data index -> [annotation cols]
2129 var hasAnnotations = false;
2130 for (var i = 1; i < cols; i++) {
2131 var type = data.getColumnType(i);
2132 if (type == 'number') {
2133 colIdx.push(i);
2134 } else if (type == 'string' && this.attr_('displayAnnotations')) {
2135 // This is OK -- it's an annotation column.
2136 var dataIdx = colIdx[colIdx.length - 1];
2137 if (!annotationCols.hasOwnProperty(dataIdx)) {
2138 annotationCols[dataIdx] = [i];
2139 } else {
2140 annotationCols[dataIdx].push(i);
2141 }
2142 hasAnnotations = true;
2143 } else {
2144 this.error("Only 'number' is supported as a dependent type with Gviz." +
2145 " 'string' is only supported if displayAnnotations is true");
2146 }
2147 }
2148
2149 // Read column labels
2150 // TODO(danvk): add support back for errorBars
2151 var labels = [data.getColumnLabel(0)];
2152 for (var i = 0; i < colIdx.length; i++) {
2153 labels.push(data.getColumnLabel(colIdx[i]));
2154 }
2155 this.attrs_.labels = labels;
2156 cols = labels.length;
2157
2158 var ret = [];
2159 var outOfOrder = false;
2160 var annotations = [];
2161 for (var i = 0; i < rows; i++) {
2162 var row = [];
2163 if (typeof(data.getValue(i, 0)) === 'undefined' ||
2164 data.getValue(i, 0) === null) {
2165 this.warn("Ignoring row " + i +
2166 " of DataTable because of undefined or null first column.");
2167 continue;
2168 }
2169
2170 if (indepType == 'date' || indepType == 'datetime') {
2171 row.push(data.getValue(i, 0).getTime());
2172 } else {
2173 row.push(data.getValue(i, 0));
2174 }
2175 if (!this.attr_("errorBars")) {
2176 for (var j = 0; j < colIdx.length; j++) {
2177 var col = colIdx[j];
2178 row.push(data.getValue(i, col));
2179 if (hasAnnotations &&
2180 annotationCols.hasOwnProperty(col) &&
2181 data.getValue(i, annotationCols[col][0]) != null) {
2182 var ann = {};
2183 ann.series = data.getColumnLabel(col);
2184 ann.xval = row[0];
2185 ann.shortText = String.fromCharCode(65 /* A */ + annotations.length)
2186 ann.text = '';
2187 for (var k = 0; k < annotationCols[col].length; k++) {
2188 if (k) ann.text += "\n";
2189 ann.text += data.getValue(i, annotationCols[col][k]);
2190 }
2191 annotations.push(ann);
2192 }
2193 }
2194 } else {
2195 for (var j = 0; j < cols - 1; j++) {
2196 row.push([ data.getValue(i, 1 + 2 * j), data.getValue(i, 2 + 2 * j) ]);
2197 }
2198 }
2199 if (ret.length > 0 && row[0] < ret[ret.length - 1][0]) {
2200 outOfOrder = true;
2201 }
2202 ret.push(row);
2203 }
2204
2205 if (outOfOrder) {
2206 this.warn("DataTable is out of order; order it correctly to speed loading.");
2207 ret.sort(function(a,b) { return a[0] - b[0] });
2208 }
2209 this.rawData_ = ret;
2210
2211 if (annotations.length > 0) {
2212 this.setAnnotations(annotations, true);
2213 }
2214 }
2215
2216 // These functions are all based on MochiKit.
2217 Dygraph.update = function (self, o) {
2218 if (typeof(o) != 'undefined' && o !== null) {
2219 for (var k in o) {
2220 if (o.hasOwnProperty(k)) {
2221 self[k] = o[k];
2222 }
2223 }
2224 }
2225 return self;
2226 };
2227
2228 Dygraph.isArrayLike = function (o) {
2229 var typ = typeof(o);
2230 if (
2231 (typ != 'object' && !(typ == 'function' &&
2232 typeof(o.item) == 'function')) ||
2233 o === null ||
2234 typeof(o.length) != 'number' ||
2235 o.nodeType === 3
2236 ) {
2237 return false;
2238 }
2239 return true;
2240 };
2241
2242 Dygraph.isDateLike = function (o) {
2243 if (typeof(o) != "object" || o === null ||
2244 typeof(o.getTime) != 'function') {
2245 return false;
2246 }
2247 return true;
2248 };
2249
2250 Dygraph.clone = function(o) {
2251 // TODO(danvk): figure out how MochiKit's version works
2252 var r = [];
2253 for (var i = 0; i < o.length; i++) {
2254 if (Dygraph.isArrayLike(o[i])) {
2255 r.push(Dygraph.clone(o[i]));
2256 } else {
2257 r.push(o[i]);
2258 }
2259 }
2260 return r;
2261 };
2262
2263
2264 /**
2265 * Get the CSV data. If it's in a function, call that function. If it's in a
2266 * file, do an XMLHttpRequest to get it.
2267 * @private
2268 */
2269 Dygraph.prototype.start_ = function() {
2270 if (typeof this.file_ == 'function') {
2271 // CSV string. Pretend we got it via XHR.
2272 this.loadedEvent_(this.file_());
2273 } else if (Dygraph.isArrayLike(this.file_)) {
2274 this.rawData_ = this.parseArray_(this.file_);
2275 this.drawGraph_(this.rawData_);
2276 } else if (typeof this.file_ == 'object' &&
2277 typeof this.file_.getColumnRange == 'function') {
2278 // must be a DataTable from gviz.
2279 this.parseDataTable_(this.file_);
2280 this.drawGraph_(this.rawData_);
2281 } else if (typeof this.file_ == 'string') {
2282 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
2283 if (this.file_.indexOf('\n') >= 0) {
2284 this.loadedEvent_(this.file_);
2285 } else {
2286 var req = new XMLHttpRequest();
2287 var caller = this;
2288 req.onreadystatechange = function () {
2289 if (req.readyState == 4) {
2290 if (req.status == 200) {
2291 caller.loadedEvent_(req.responseText);
2292 }
2293 }
2294 };
2295
2296 req.open("GET", this.file_, true);
2297 req.send(null);
2298 }
2299 } else {
2300 this.error("Unknown data format: " + (typeof this.file_));
2301 }
2302 };
2303
2304 /**
2305 * Changes various properties of the graph. These can include:
2306 * <ul>
2307 * <li>file: changes the source data for the graph</li>
2308 * <li>errorBars: changes whether the data contains stddev</li>
2309 * </ul>
2310 * @param {Object} attrs The new properties and values
2311 */
2312 Dygraph.prototype.updateOptions = function(attrs) {
2313 // TODO(danvk): this is a mess. Rethink this function.
2314 if (attrs.rollPeriod) {
2315 this.rollPeriod_ = attrs.rollPeriod;
2316 }
2317 if (attrs.dateWindow) {
2318 this.dateWindow_ = attrs.dateWindow;
2319 }
2320 if (attrs.valueRange) {
2321 this.valueRange_ = attrs.valueRange;
2322 }
2323
2324 // TODO(danvk): validate per-series options.
2325 // Supported:
2326 // strokeWidth
2327 // pointSize
2328 // drawPoints
2329 // highlightCircleSize
2330
2331 Dygraph.update(this.user_attrs_, attrs);
2332 Dygraph.update(this.renderOptions_, attrs);
2333
2334 this.labelsFromCSV_ = (this.attr_("labels") == null);
2335
2336 // TODO(danvk): this doesn't match the constructor logic
2337 this.layout_.updateOptions({ 'errorBars': this.attr_("errorBars") });
2338 if (attrs['file']) {
2339 this.file_ = attrs['file'];
2340 this.start_();
2341 } else {
2342 this.drawGraph_(this.rawData_);
2343 }
2344 };
2345
2346 /**
2347 * Resizes the dygraph. If no parameters are specified, resizes to fill the
2348 * containing div (which has presumably changed size since the dygraph was
2349 * instantiated. If the width/height are specified, the div will be resized.
2350 *
2351 * This is far more efficient than destroying and re-instantiating a
2352 * Dygraph, since it doesn't have to reparse the underlying data.
2353 *
2354 * @param {Number} width Width (in pixels)
2355 * @param {Number} height Height (in pixels)
2356 */
2357 Dygraph.prototype.resize = function(width, height) {
2358 if (this.resize_lock) {
2359 return;
2360 }
2361 this.resize_lock = true;
2362
2363 if ((width === null) != (height === null)) {
2364 this.warn("Dygraph.resize() should be called with zero parameters or " +
2365 "two non-NULL parameters. Pretending it was zero.");
2366 width = height = null;
2367 }
2368
2369 // TODO(danvk): there should be a clear() method.
2370 this.maindiv_.innerHTML = "";
2371 this.attrs_.labelsDiv = null;
2372
2373 if (width) {
2374 this.maindiv_.style.width = width + "px";
2375 this.maindiv_.style.height = height + "px";
2376 this.width_ = width;
2377 this.height_ = height;
2378 } else {
2379 this.width_ = this.maindiv_.offsetWidth;
2380 this.height_ = this.maindiv_.offsetHeight;
2381 }
2382
2383 this.createInterface_();
2384 this.drawGraph_(this.rawData_);
2385
2386 this.resize_lock = false;
2387 };
2388
2389 /**
2390 * Adjusts the number of days in the rolling average. Updates the graph to
2391 * reflect the new averaging period.
2392 * @param {Number} length Number of days over which to average the data.
2393 */
2394 Dygraph.prototype.adjustRoll = function(length) {
2395 this.rollPeriod_ = length;
2396 this.drawGraph_(this.rawData_);
2397 };
2398
2399 /**
2400 * Returns a boolean array of visibility statuses.
2401 */
2402 Dygraph.prototype.visibility = function() {
2403 // Do lazy-initialization, so that this happens after we know the number of
2404 // data series.
2405 if (!this.attr_("visibility")) {
2406 this.attrs_["visibility"] = [];
2407 }
2408 while (this.attr_("visibility").length < this.rawData_[0].length - 1) {
2409 this.attr_("visibility").push(true);
2410 }
2411 return this.attr_("visibility");
2412 };
2413
2414 /**
2415 * Changes the visiblity of a series.
2416 */
2417 Dygraph.prototype.setVisibility = function(num, value) {
2418 var x = this.visibility();
2419 if (num < 0 && num >= x.length) {
2420 this.warn("invalid series number in setVisibility: " + num);
2421 } else {
2422 x[num] = value;
2423 this.drawGraph_(this.rawData_);
2424 }
2425 };
2426
2427 /**
2428 * Update the list of annotations and redraw the chart.
2429 */
2430 Dygraph.prototype.setAnnotations = function(ann, suppressDraw) {
2431 this.annotations_ = ann;
2432 this.layout_.setAnnotations(this.annotations_);
2433 if (!suppressDraw) {
2434 this.drawGraph_(this.rawData_);
2435 }
2436 };
2437
2438 /**
2439 * Return the list of annotations.
2440 */
2441 Dygraph.prototype.annotations = function() {
2442 return this.annotations_;
2443 };
2444
2445 /**
2446 * Get the index of a series (column) given its name. The first column is the
2447 * x-axis, so the data series start with index 1.
2448 */
2449 Dygraph.prototype.indexFromSetName = function(name) {
2450 var labels = this.attr_("labels");
2451 for (var i = 0; i < labels.length; i++) {
2452 if (labels[i] == name) return i;
2453 }
2454 return null;
2455 };
2456
2457 Dygraph.addAnnotationRule = function() {
2458 if (Dygraph.addedAnnotationCSS) return;
2459
2460 var mysheet;
2461 if (document.styleSheets.length > 0) {
2462 mysheet = document.styleSheets[0];
2463 } else {
2464 var styleSheetElement = document.createElement("style");
2465 styleSheetElement.type = "text/css";
2466 document.getElementsByTagName("head")[0].appendChild(styleSheetElement);
2467 for(i = 0; i < document.styleSheets.length; i++) {
2468 if (document.styleSheets[i].disabled) continue;
2469 mysheet = document.styleSheets[i];
2470 }
2471 }
2472
2473 var rule = "border: 1px solid black; " +
2474 "background-color: white; " +
2475 "text-align: center;";
2476 if (mysheet.insertRule) { // Firefox
2477 mysheet.insertRule(".dygraphDefaultAnnotation { " + rule + " }", 0);
2478 } else if (mysheet.addRule) { // IE
2479 mysheet.addRule(".dygraphDefaultAnnotation", rule);
2480 }
2481
2482 Dygraph.addedAnnotationCSS = true;
2483 }
2484
2485 /**
2486 * Create a new canvas element. This is more complex than a simple
2487 * document.createElement("canvas") because of IE and excanvas.
2488 */
2489 Dygraph.createCanvas = function() {
2490 var canvas = document.createElement("canvas");
2491
2492 isIE = (/MSIE/.test(navigator.userAgent) && !window.opera);
2493 if (isIE) {
2494 canvas = G_vmlCanvasManager.initElement(canvas);
2495 }
2496
2497 return canvas;
2498 };
2499
2500
2501 /**
2502 * A wrapper around Dygraph that implements the gviz API.
2503 * @param {Object} container The DOM object the visualization should live in.
2504 */
2505 Dygraph.GVizChart = function(container) {
2506 this.container = container;
2507 }
2508
2509 Dygraph.GVizChart.prototype.draw = function(data, options) {
2510 this.container.innerHTML = '';
2511 this.date_graph = new Dygraph(this.container, data, options);
2512 }
2513
2514 /**
2515 * Google charts compatible setSelection
2516 * Only row selection is supported, all points in the row will be highlighted
2517 * @param {Array} array of the selected cells
2518 * @public
2519 */
2520 Dygraph.GVizChart.prototype.setSelection = function(selection_array) {
2521 var row = false;
2522 if (selection_array.length) {
2523 row = selection_array[0].row;
2524 }
2525 this.date_graph.setSelection(row);
2526 }
2527
2528 /**
2529 * Google charts compatible getSelection implementation
2530 * @return {Array} array of the selected cells
2531 * @public
2532 */
2533 Dygraph.GVizChart.prototype.getSelection = function() {
2534 var selection = [];
2535
2536 var row = this.date_graph.getSelection();
2537
2538 if (row < 0) return selection;
2539
2540 col = 1;
2541 for (var i in this.date_graph.layout_.datasets) {
2542 selection.push({row: row, column: col});
2543 col++;
2544 }
2545
2546 return selection;
2547 }
2548
2549 // Older pages may still use this name.
2550 DateGraph = Dygraph;