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