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