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