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