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