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