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