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