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