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