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