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