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