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