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