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