can generate jsdoc; private methods marked as such
[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/**
629a09ae
DV
44 * Creates an interactive, zoomable chart.
45 *
46 * @constructor
47 * @param {div | String} div A div or the id of a div into which to construct
48 * the chart.
49 * @param {String | Function} file A file containing CSV data or a function
50 * that returns this data. The most basic expected format for each line is
51 * "YYYY/MM/DD,val1,val2,...". For more information, see
52 * http://dygraphs.com/data.html.
6a1aa64f 53 * @param {Object} attrs Various other attributes, e.g. errorBars determines
629a09ae
DV
54 * whether the input data contains error ranges. For a complete list of
55 * options, see http://dygraphs.com/options.html.
6a1aa64f 56 */
285a6bda
DV
57Dygraph = function(div, data, opts) {
58 if (arguments.length > 0) {
59 if (arguments.length == 4) {
60 // Old versions of dygraphs took in the series labels as a constructor
61 // parameter. This doesn't make sense anymore, but it's easy to continue
62 // to support this usage.
63 this.warn("Using deprecated four-argument dygraph constructor");
64 this.__old_init__(div, data, arguments[2], arguments[3]);
65 } else {
66 this.__init__(div, data, opts);
67 }
68 }
6a1aa64f
DV
69};
70
285a6bda
DV
71Dygraph.NAME = "Dygraph";
72Dygraph.VERSION = "1.2";
73Dygraph.__repr__ = function() {
6a1aa64f
DV
74 return "[" + this.NAME + " " + this.VERSION + "]";
75};
629a09ae
DV
76
77/**
78 * Returns information about the Dygraph class.
79 */
285a6bda 80Dygraph.toString = function() {
6a1aa64f
DV
81 return this.__repr__();
82};
83
84// Various default values
285a6bda
DV
85Dygraph.DEFAULT_ROLL_PERIOD = 1;
86Dygraph.DEFAULT_WIDTH = 480;
87Dygraph.DEFAULT_HEIGHT = 320;
88Dygraph.AXIS_LINE_WIDTH = 0.3;
6a1aa64f 89
d59b6f34 90Dygraph.LOG_SCALE = 10;
0037b2a4 91Dygraph.LN_TEN = Math.log(Dygraph.LOG_SCALE);
629a09ae
DV
92/**
93 * @private
94 */
d59b6f34 95Dygraph.log10 = function(x) {
0037b2a4 96 return Math.log(x) / Dygraph.LN_TEN;
d59b6f34 97}
062ef401 98
8e4a6af3 99// Default attribute values.
285a6bda 100Dygraph.DEFAULT_ATTRS = {
a9fc39ab 101 highlightCircleSize: 3,
8e4a6af3 102 pixelsPerXLabel: 60,
c6336f04 103 pixelsPerYLabel: 30,
285a6bda 104
8e4a6af3
DV
105 labelsDivWidth: 250,
106 labelsDivStyles: {
107 // TODO(danvk): move defaults from createStatusMessage_ here.
285a6bda
DV
108 },
109 labelsSeparateLines: false,
bcd3ebf0 110 labelsShowZeroValues: true,
285a6bda 111 labelsKMB: false,
afefbcdb 112 labelsKMG2: false,
d160cc3b 113 showLabelsOnHighlight: true,
12e4c741 114
2e1fcf1a
DV
115 yValueFormatter: function(a,b) { return Dygraph.numberFormatter(a,b); },
116 digitsAfterDecimal: 2,
117 maxNumberWidth: 6,
19589a3e 118 sigFigs: null,
285a6bda
DV
119
120 strokeWidth: 1.0,
8e4a6af3 121
8846615a
DV
122 axisTickSize: 3,
123 axisLabelFontSize: 14,
124 xAxisLabelWidth: 50,
125 yAxisLabelWidth: 50,
bf640e56 126 xAxisLabelFormatter: Dygraph.dateAxisFormatter,
8846615a 127 rightGap: 5,
285a6bda
DV
128
129 showRoller: false,
130 xValueFormatter: Dygraph.dateString_,
131 xValueParser: Dygraph.dateParser,
132 xTicker: Dygraph.dateTicker,
133
3d67f03b
DV
134 delimiter: ',',
135
285a6bda
DV
136 sigma: 2.0,
137 errorBars: false,
138 fractions: false,
139 wilsonInterval: true, // only relevant if fractions is true
5954ef32 140 customBars: false,
43af96e7
NK
141 fillGraph: false,
142 fillAlpha: 0.15,
f032c51d 143 connectSeparatedPoints: false,
43af96e7
NK
144
145 stackedGraph: false,
afdc483f
NN
146 hideOverlayOnMouseOut: true,
147
2fccd3dc
DV
148 // TODO(danvk): support 'onmouseover' and 'never', and remove synonyms.
149 legend: 'onmouseover', // the only relevant value at the moment is 'always'.
150
00c281d4 151 stepPlot: false,
062ef401
JB
152 avoidMinZero: false,
153
ad1798c2 154 // Sizes of the various chart labels.
b4202b3d 155 titleHeight: 28,
86cce9e8
DV
156 xLabelHeight: 18,
157 yLabelWidth: 18,
ad1798c2 158
062ef401 159 interactionModel: null // will be set to Dygraph.defaultInteractionModel.
285a6bda
DV
160};
161
162// Various logging levels.
163Dygraph.DEBUG = 1;
164Dygraph.INFO = 2;
165Dygraph.WARNING = 3;
166Dygraph.ERROR = 3;
167
39b0e098
RK
168// Directions for panning and zooming. Use bit operations when combined
169// values are possible.
170Dygraph.HORIZONTAL = 1;
171Dygraph.VERTICAL = 2;
172
5c528fa2
DV
173// Used for initializing annotation CSS rules only once.
174Dygraph.addedAnnotationCSS = false;
175
2cf95fff 176/**
629a09ae 177 * @private
2cf95fff
RK
178 * Return the 2d context for a dygraph canvas.
179 *
180 * This method is only exposed for the sake of replacing the function in
181 * automated tests, e.g.
182 *
183 * var oldFunc = Dygraph.getContext();
184 * Dygraph.getContext = function(canvas) {
185 * var realContext = oldFunc(canvas);
186 * return new Proxy(realContext);
187 * };
188 */
189Dygraph.getContext = function(canvas) {
190 return canvas.getContext("2d");
191};
192
285a6bda
DV
193Dygraph.prototype.__old_init__ = function(div, file, labels, attrs) {
194 // Labels is no longer a constructor parameter, since it's typically set
195 // directly from the data source. It also conains a name for the x-axis,
196 // which the previous constructor form did not.
197 if (labels != null) {
198 var new_labels = ["Date"];
199 for (var i = 0; i < labels.length; i++) new_labels.push(labels[i]);
fc80a396 200 Dygraph.update(attrs, { 'labels': new_labels });
285a6bda
DV
201 }
202 this.__init__(div, file, attrs);
8e4a6af3
DV
203};
204
6a1aa64f 205/**
285a6bda 206 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
7aedf6fe 207 * and context &lt;canvas&gt; inside of it. See the constructor for details.
6a1aa64f 208 * on the parameters.
12e4c741 209 * @param {Element} div the Element to render the graph into.
6a1aa64f 210 * @param {String | Function} file Source data
6a1aa64f
DV
211 * @param {Object} attrs Miscellaneous other options
212 * @private
213 */
285a6bda 214Dygraph.prototype.__init__ = function(div, file, attrs) {
a2c8fff4
DV
215 // Hack for IE: if we're using excanvas and the document hasn't finished
216 // loading yet (and hence may not have initialized whatever it needs to
217 // initialize), then keep calling this routine periodically until it has.
218 if (/MSIE/.test(navigator.userAgent) && !window.opera &&
219 typeof(G_vmlCanvasManager) != 'undefined' &&
220 document.readyState != 'complete') {
221 var self = this;
222 setTimeout(function() { self.__init__(div, file, attrs) }, 100);
223 }
224
285a6bda
DV
225 // Support two-argument constructor
226 if (attrs == null) { attrs = {}; }
227
6a1aa64f 228 // Copy the important bits into the object
32988383 229 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
6a1aa64f 230 this.maindiv_ = div;
6a1aa64f 231 this.file_ = file;
285a6bda 232 this.rollPeriod_ = attrs.rollPeriod || Dygraph.DEFAULT_ROLL_PERIOD;
6a1aa64f 233 this.previousVerticalX_ = -1;
6a1aa64f 234 this.fractions_ = attrs.fractions || false;
6a1aa64f 235 this.dateWindow_ = attrs.dateWindow || null;
8b83c6cc 236
6a1aa64f 237 this.wilsonInterval_ = attrs.wilsonInterval || true;
fe0b7c03 238 this.is_initial_draw_ = true;
5c528fa2 239 this.annotations_ = [];
7aedf6fe 240
45f2c689 241 // Zoomed indicators - These indicate when the graph has been zoomed and on what axis.
57baab03
NN
242 this.zoomed_x_ = false;
243 this.zoomed_y_ = false;
45f2c689 244
f7d6278e
DV
245 // Clear the div. This ensure that, if multiple dygraphs are passed the same
246 // div, then only one will be drawn.
247 div.innerHTML = "";
248
c21d2c2d 249 // If the div isn't already sized then inherit from our attrs or
250 // give it a default size.
285a6bda 251 if (div.style.width == '') {
ddd1b11f 252 div.style.width = (attrs.width || Dygraph.DEFAULT_WIDTH) + "px";
285a6bda
DV
253 }
254 if (div.style.height == '') {
ddd1b11f 255 div.style.height = (attrs.height || Dygraph.DEFAULT_HEIGHT) + "px";
32988383 256 }
285a6bda
DV
257 this.width_ = parseInt(div.style.width, 10);
258 this.height_ = parseInt(div.style.height, 10);
c21d2c2d 259 // The div might have been specified as percent of the current window size,
260 // convert that to an appropriate number of pixels.
261 if (div.style.width.indexOf("%") == div.style.width.length - 1) {
c6f45033 262 this.width_ = div.offsetWidth;
c21d2c2d 263 }
264 if (div.style.height.indexOf("%") == div.style.height.length - 1) {
c6f45033 265 this.height_ = div.offsetHeight;
c21d2c2d 266 }
32988383 267
10a6456d
DV
268 if (this.width_ == 0) {
269 this.error("dygraph has zero width. Please specify a width in pixels.");
270 }
271 if (this.height_ == 0) {
272 this.error("dygraph has zero height. Please specify a height in pixels.");
273 }
274
344ba8c0 275 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
43af96e7
NK
276 if (attrs['stackedGraph']) {
277 attrs['fillGraph'] = true;
278 // TODO(nikhilk): Add any other stackedGraph checks here.
279 }
280
285a6bda
DV
281 // Dygraphs has many options, some of which interact with one another.
282 // To keep track of everything, we maintain two sets of options:
283 //
c21d2c2d 284 // this.user_attrs_ only options explicitly set by the user.
285a6bda
DV
285 // this.attrs_ defaults, options derived from user_attrs_, data.
286 //
287 // Options are then accessed this.attr_('attr'), which first looks at
288 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
289 // defaults without overriding behavior that the user specifically asks for.
290 this.user_attrs_ = {};
fc80a396 291 Dygraph.update(this.user_attrs_, attrs);
6a1aa64f 292
285a6bda 293 this.attrs_ = {};
fc80a396 294 Dygraph.update(this.attrs_, Dygraph.DEFAULT_ATTRS);
6a1aa64f 295
16269f6e 296 this.boundaryIds_ = [];
6a1aa64f 297
285a6bda
DV
298 // Make a note of whether labels will be pulled from the CSV file.
299 this.labelsFromCSV_ = (this.attr_("labels") == null);
6a1aa64f
DV
300
301 // Create the containing DIV and other interactive elements
302 this.createInterface_();
303
738fc797 304 this.start_();
6a1aa64f
DV
305};
306
dcb25130
NN
307/**
308 * Returns the zoomed status of the chart for one or both axes.
309 *
310 * Axis is an optional parameter. Can be set to 'x' or 'y'.
311 *
312 * The zoomed status for an axis is set whenever a user zooms using the mouse
e5152598 313 * or when the dateWindow or valueRange are updated (unless the isZoomedIgnoreProgrammaticZoom
dcb25130
NN
314 * option is also specified).
315 */
57baab03
NN
316Dygraph.prototype.isZoomed = function(axis) {
317 if (axis == null) return this.zoomed_x_ || this.zoomed_y_;
318 if (axis == 'x') return this.zoomed_x_;
319 if (axis == 'y') return this.zoomed_y_;
320 throw "axis parameter to Dygraph.isZoomed must be missing, 'x' or 'y'.";
321};
322
629a09ae
DV
323/**
324 * Returns information about the Dygraph object, including its containing ID.
325 */
22bd1dfb
RK
326Dygraph.prototype.toString = function() {
327 var maindiv = this.maindiv_;
328 var id = (maindiv && maindiv.id) ? maindiv.id : maindiv
329 return "[Dygraph " + id + "]";
330}
331
629a09ae
DV
332/**
333 * @private
334 * Returns the value of an option. This may be set by the user (either in the
335 * constructor or by calling updateOptions) or by dygraphs, and may be set to a
336 * per-series value.
337 * @param { String } name The name of the option, e.g. 'rollPeriod'.
338 * @param { String } [seriesName] The name of the series to which the option
339 * will be applied. If no per-series value of this option is available, then
340 * the global value is returned. This is optional.
341 * @return { ... } The value of the option.
342 */
227b93cc 343Dygraph.prototype.attr_ = function(name, seriesName) {
028ddf8a
DV
344// <REMOVE_FOR_COMBINED>
345 if (typeof(Dygraph.OPTIONS_REFERENCE) === 'undefined') {
346 this.error('Must include options reference JS for testing');
347 } else if (!Dygraph.OPTIONS_REFERENCE.hasOwnProperty(name)) {
348 this.error('Dygraphs is using property ' + name + ', which has no entry ' +
349 'in the Dygraphs.OPTIONS_REFERENCE listing.');
350 // Only log this error once.
351 Dygraph.OPTIONS_REFERENCE[name] = true;
352 }
353// </REMOVE_FOR_COMBINED>
227b93cc
DV
354 if (seriesName &&
355 typeof(this.user_attrs_[seriesName]) != 'undefined' &&
356 this.user_attrs_[seriesName] != null &&
357 typeof(this.user_attrs_[seriesName][name]) != 'undefined') {
358 return this.user_attrs_[seriesName][name];
450fe64b 359 } else if (typeof(this.user_attrs_[name]) != 'undefined') {
285a6bda
DV
360 return this.user_attrs_[name];
361 } else if (typeof(this.attrs_[name]) != 'undefined') {
362 return this.attrs_[name];
363 } else {
364 return null;
365 }
366};
367
368// TODO(danvk): any way I can get the line numbers to be this.warn call?
629a09ae
DV
369/**
370 * @private
371 * Log an error on the JS console at the given severity.
372 * @param { Integer } severity One of Dygraph.{DEBUG,INFO,WARNING,ERROR}
373 * @param { String } The message to log.
374 */
285a6bda
DV
375Dygraph.prototype.log = function(severity, message) {
376 if (typeof(console) != 'undefined') {
377 switch (severity) {
378 case Dygraph.DEBUG:
379 console.debug('dygraphs: ' + message);
380 break;
381 case Dygraph.INFO:
382 console.info('dygraphs: ' + message);
383 break;
384 case Dygraph.WARNING:
385 console.warn('dygraphs: ' + message);
386 break;
387 case Dygraph.ERROR:
388 console.error('dygraphs: ' + message);
389 break;
390 }
391 }
629a09ae
DV
392};
393
394/**
395 * @private
396 */
285a6bda
DV
397Dygraph.prototype.info = function(message) {
398 this.log(Dygraph.INFO, message);
629a09ae
DV
399};
400
401/**
402 * @private
403 */
285a6bda
DV
404Dygraph.prototype.warn = function(message) {
405 this.log(Dygraph.WARNING, message);
629a09ae
DV
406};
407
408/**
409 * @private
410 */
285a6bda
DV
411Dygraph.prototype.error = function(message) {
412 this.log(Dygraph.ERROR, message);
629a09ae 413};
285a6bda 414
6a1aa64f
DV
415/**
416 * Returns the current rolling period, as set by the user or an option.
6faebb69 417 * @return {Number} The number of points in the rolling window
6a1aa64f 418 */
285a6bda 419Dygraph.prototype.rollPeriod = function() {
6a1aa64f 420 return this.rollPeriod_;
76171648
DV
421};
422
599fb4ad
DV
423/**
424 * Returns the currently-visible x-range. This can be affected by zooming,
425 * panning or a call to updateOptions.
426 * Returns a two-element array: [left, right].
427 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
428 */
429Dygraph.prototype.xAxisRange = function() {
4cac8c7a
RK
430 return this.dateWindow_ ? this.dateWindow_ : this.xAxisExtremes();
431};
599fb4ad 432
4cac8c7a
RK
433/**
434 * Returns the lower- and upper-bound x-axis values of the
435 * data set.
436 */
437Dygraph.prototype.xAxisExtremes = function() {
599fb4ad
DV
438 var left = this.rawData_[0][0];
439 var right = this.rawData_[this.rawData_.length - 1][0];
440 return [left, right];
441};
442
3230c662 443/**
d58ae307
DV
444 * Returns the currently-visible y-range for an axis. This can be affected by
445 * zooming, panning or a call to updateOptions. Axis indices are zero-based. If
446 * called with no arguments, returns the range of the first axis.
3230c662
DV
447 * Returns a two-element array: [bottom, top].
448 */
d58ae307 449Dygraph.prototype.yAxisRange = function(idx) {
d63e6799 450 if (typeof(idx) == "undefined") idx = 0;
d58ae307
DV
451 if (idx < 0 || idx >= this.axes_.length) return null;
452 return [ this.axes_[idx].computedValueRange[0],
453 this.axes_[idx].computedValueRange[1] ];
454};
455
456/**
457 * Returns the currently-visible y-ranges for each axis. This can be affected by
458 * zooming, panning, calls to updateOptions, etc.
459 * Returns an array of [bottom, top] pairs, one for each y-axis.
460 */
461Dygraph.prototype.yAxisRanges = function() {
462 var ret = [];
463 for (var i = 0; i < this.axes_.length; i++) {
464 ret.push(this.yAxisRange(i));
465 }
466 return ret;
3230c662
DV
467};
468
d58ae307 469// TODO(danvk): use these functions throughout dygraphs.
3230c662
DV
470/**
471 * Convert from data coordinates to canvas/div X/Y coordinates.
d58ae307
DV
472 * If specified, do this conversion for the coordinate system of a particular
473 * axis. Uses the first axis by default.
3230c662 474 * Returns a two-element array: [X, Y]
ff022deb 475 *
0747928a 476 * Note: use toDomXCoord instead of toDomCoords(x, null) and use toDomYCoord
ff022deb 477 * instead of toDomCoords(null, y, axis).
3230c662 478 */
d58ae307 479Dygraph.prototype.toDomCoords = function(x, y, axis) {
ff022deb
RK
480 return [ this.toDomXCoord(x), this.toDomYCoord(y, axis) ];
481};
482
483/**
484 * Convert from data x coordinates to canvas/div X coordinate.
485 * If specified, do this conversion for the coordinate system of a particular
0037b2a4
RK
486 * axis.
487 * Returns a single value or null if x is null.
ff022deb
RK
488 */
489Dygraph.prototype.toDomXCoord = function(x) {
490 if (x == null) {
491 return null;
492 };
493
3230c662 494 var area = this.plotter_.area;
ff022deb
RK
495 var xRange = this.xAxisRange();
496 return area.x + (x - xRange[0]) / (xRange[1] - xRange[0]) * area.w;
497}
3230c662 498
ff022deb
RK
499/**
500 * Convert from data x coordinates to canvas/div Y coordinate and optional
501 * axis. Uses the first axis by default.
502 *
503 * returns a single value or null if y is null.
504 */
505Dygraph.prototype.toDomYCoord = function(y, axis) {
0747928a 506 var pct = this.toPercentYCoord(y, axis);
3230c662 507
ff022deb
RK
508 if (pct == null) {
509 return null;
510 }
e4416fb9 511 var area = this.plotter_.area;
ff022deb
RK
512 return area.y + pct * area.h;
513}
3230c662
DV
514
515/**
516 * Convert from canvas/div coords to data coordinates.
d58ae307
DV
517 * If specified, do this conversion for the coordinate system of a particular
518 * axis. Uses the first axis by default.
ff022deb
RK
519 * Returns a two-element array: [X, Y].
520 *
0747928a 521 * Note: use toDataXCoord instead of toDataCoords(x, null) and use toDataYCoord
ff022deb 522 * instead of toDataCoords(null, y, axis).
3230c662 523 */
d58ae307 524Dygraph.prototype.toDataCoords = function(x, y, axis) {
ff022deb
RK
525 return [ this.toDataXCoord(x), this.toDataYCoord(y, axis) ];
526};
527
528/**
529 * Convert from canvas/div x coordinate to data coordinate.
530 *
531 * If x is null, this returns null.
532 */
533Dygraph.prototype.toDataXCoord = function(x) {
534 if (x == null) {
535 return null;
3230c662
DV
536 }
537
ff022deb
RK
538 var area = this.plotter_.area;
539 var xRange = this.xAxisRange();
540 return xRange[0] + (x - area.x) / area.w * (xRange[1] - xRange[0]);
541};
542
543/**
544 * Convert from canvas/div y coord to value.
545 *
546 * If y is null, this returns null.
547 * if axis is null, this uses the first axis.
548 */
549Dygraph.prototype.toDataYCoord = function(y, axis) {
550 if (y == null) {
551 return null;
3230c662
DV
552 }
553
ff022deb
RK
554 var area = this.plotter_.area;
555 var yRange = this.yAxisRange(axis);
556
b70247dc
RK
557 if (typeof(axis) == "undefined") axis = 0;
558 if (!this.axes_[axis].logscale) {
ff022deb
RK
559 return yRange[0] + (area.h - y) / area.h * (yRange[1] - yRange[0]);
560 } else {
561 // Computing the inverse of toDomCoord.
562 var pct = (y - area.y) / area.h
563
564 // Computing the inverse of toPercentYCoord. The function was arrived at with
565 // the following steps:
566 //
567 // Original calcuation:
d59b6f34 568 // pct = (logr1 - Dygraph.log10(y)) / (logr1 - Dygraph.log10(yRange[0]));
ff022deb
RK
569 //
570 // Move denominator to both sides:
d59b6f34 571 // pct * (logr1 - Dygraph.log10(yRange[0])) = logr1 - Dygraph.log10(y);
ff022deb
RK
572 //
573 // subtract logr1, and take the negative value.
d59b6f34 574 // logr1 - (pct * (logr1 - Dygraph.log10(yRange[0]))) = Dygraph.log10(y);
ff022deb
RK
575 //
576 // Swap both sides of the equation, and we can compute the log of the
577 // return value. Which means we just need to use that as the exponent in
578 // e^exponent.
d59b6f34 579 // Dygraph.log10(y) = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0])));
ff022deb 580
d59b6f34
RK
581 var logr1 = Dygraph.log10(yRange[1]);
582 var exponent = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0])));
583 var value = Math.pow(Dygraph.LOG_SCALE, exponent);
ff022deb
RK
584 return value;
585 }
3230c662
DV
586};
587
e99fde05 588/**
ff022deb 589 * Converts a y for an axis to a percentage from the top to the
4cac8c7a 590 * bottom of the drawing area.
ff022deb
RK
591 *
592 * If the coordinate represents a value visible on the canvas, then
593 * the value will be between 0 and 1, where 0 is the top of the canvas.
594 * However, this method will return values outside the range, as
595 * values can fall outside the canvas.
596 *
597 * If y is null, this returns null.
598 * if axis is null, this uses the first axis.
629a09ae
DV
599 *
600 * @param { Number } y The data y-coordinate.
601 * @param { Number } [axis] The axis number on which the data coordinate lives.
602 * @return { Number } A fraction in [0, 1] where 0 = the top edge.
ff022deb
RK
603 */
604Dygraph.prototype.toPercentYCoord = function(y, axis) {
605 if (y == null) {
606 return null;
607 }
7d0e7a0d 608 if (typeof(axis) == "undefined") axis = 0;
ff022deb
RK
609
610 var area = this.plotter_.area;
611 var yRange = this.yAxisRange(axis);
612
613 var pct;
7d0e7a0d 614 if (!this.axes_[axis].logscale) {
4cac8c7a
RK
615 // yRange[1] - y is unit distance from the bottom.
616 // yRange[1] - yRange[0] is the scale of the range.
ff022deb
RK
617 // (yRange[1] - y) / (yRange[1] - yRange[0]) is the % from the bottom.
618 pct = (yRange[1] - y) / (yRange[1] - yRange[0]);
619 } else {
d59b6f34
RK
620 var logr1 = Dygraph.log10(yRange[1]);
621 pct = (logr1 - Dygraph.log10(y)) / (logr1 - Dygraph.log10(yRange[0]));
ff022deb
RK
622 }
623 return pct;
624}
625
626/**
4cac8c7a
RK
627 * Converts an x value to a percentage from the left to the right of
628 * the drawing area.
629 *
630 * If the coordinate represents a value visible on the canvas, then
631 * the value will be between 0 and 1, where 0 is the left of the canvas.
632 * However, this method will return values outside the range, as
633 * values can fall outside the canvas.
634 *
635 * If x is null, this returns null.
629a09ae
DV
636 * @param { Number } x The data x-coordinate.
637 * @return { Number } A fraction in [0, 1] where 0 = the left edge.
4cac8c7a
RK
638 */
639Dygraph.prototype.toPercentXCoord = function(x) {
640 if (x == null) {
641 return null;
642 }
643
4cac8c7a 644 var xRange = this.xAxisRange();
965a030e 645 return (x - xRange[0]) / (xRange[1] - xRange[0]);
629a09ae 646};
4cac8c7a
RK
647
648/**
e99fde05 649 * Returns the number of columns (including the independent variable).
629a09ae 650 * @return { Integer } The number of columns.
e99fde05
DV
651 */
652Dygraph.prototype.numColumns = function() {
653 return this.rawData_[0].length;
654};
655
656/**
657 * Returns the number of rows (excluding any header/label row).
629a09ae 658 * @return { Integer } The number of rows, less any header.
e99fde05
DV
659 */
660Dygraph.prototype.numRows = function() {
661 return this.rawData_.length;
662};
663
664/**
665 * Returns the value in the given row and column. If the row and column exceed
666 * the bounds on the data, returns null. Also returns null if the value is
667 * missing.
629a09ae
DV
668 * @param { Number} row The row number of the data (0-based). Row 0 is the
669 * first row of data, not a header row.
670 * @param { Number} col The column number of the data (0-based)
671 * @return { Number } The value in the specified cell or null if the row/col
672 * were out of range.
e99fde05
DV
673 */
674Dygraph.prototype.getValue = function(row, col) {
675 if (row < 0 || row > this.rawData_.length) return null;
676 if (col < 0 || col > this.rawData_[row].length) return null;
677
678 return this.rawData_[row][col];
679};
680
629a09ae
DV
681/**
682 * @private
683 * Add an event handler. This smooths a difference between IE and the rest of
684 * the world.
685 * @param { DOM element } el The element to add the event to.
686 * @param { String } evt The name of the event, e.g. 'click' or 'mousemove'.
687 * @param { Function } fn The function to call on the event. The function takes
688 * one parameter: the event object.
689 */
76171648
DV
690Dygraph.addEvent = function(el, evt, fn) {
691 var normed_fn = function(e) {
692 if (!e) var e = window.event;
693 fn(e);
694 };
695 if (window.addEventListener) { // Mozilla, Netscape, Firefox
696 el.addEventListener(evt, normed_fn, false);
697 } else { // IE
698 el.attachEvent('on' + evt, normed_fn);
699 }
700};
6a1aa64f 701
062ef401 702
629a09ae
DV
703/**
704 * @private
705 * Cancels further processing of an event. This is useful to prevent default
706 * browser actions, e.g. highlighting text on a double-click.
707 * Based on the article at
708 * http://www.switchonthecode.com/tutorials/javascript-tutorial-the-scroll-wheel
709 * @param { Event } e The event whose normal behavior should be canceled.
710 */
062ef401
JB
711Dygraph.cancelEvent = function(e) {
712 e = e ? e : window.event;
713 if (e.stopPropagation) {
714 e.stopPropagation();
715 }
716 if (e.preventDefault) {
717 e.preventDefault();
718 }
719 e.cancelBubble = true;
720 e.cancel = true;
721 e.returnValue = false;
722 return false;
629a09ae 723};
062ef401
JB
724
725
6a1aa64f 726/**
285a6bda 727 * Generates interface elements for the Dygraph: a containing div, a div to
6a1aa64f 728 * display the current point, and a textbox to adjust the rolling average
697e70b2 729 * period. Also creates the Renderer/Layout elements.
6a1aa64f
DV
730 * @private
731 */
285a6bda 732Dygraph.prototype.createInterface_ = function() {
6a1aa64f
DV
733 // Create the all-enclosing graph div
734 var enclosing = this.maindiv_;
735
b0c3b730
DV
736 this.graphDiv = document.createElement("div");
737 this.graphDiv.style.width = this.width_ + "px";
738 this.graphDiv.style.height = this.height_ + "px";
739 enclosing.appendChild(this.graphDiv);
740
741 // Create the canvas for interactive parts of the chart.
f8cfec73 742 this.canvas_ = Dygraph.createCanvas();
b0c3b730
DV
743 this.canvas_.style.position = "absolute";
744 this.canvas_.width = this.width_;
745 this.canvas_.height = this.height_;
f8cfec73
DV
746 this.canvas_.style.width = this.width_ + "px"; // for IE
747 this.canvas_.style.height = this.height_ + "px"; // for IE
b0c3b730 748
2cf95fff
RK
749 this.canvas_ctx_ = Dygraph.getContext(this.canvas_);
750
b0c3b730 751 // ... and for static parts of the chart.
6a1aa64f 752 this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
2cf95fff 753 this.hidden_ctx_ = Dygraph.getContext(this.hidden_);
76171648 754
eb7bf005
EC
755 // The interactive parts of the graph are drawn on top of the chart.
756 this.graphDiv.appendChild(this.hidden_);
757 this.graphDiv.appendChild(this.canvas_);
758 this.mouseEventElement_ = this.canvas_;
759
76171648 760 var dygraph = this;
eb7bf005 761 Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(e) {
76171648
DV
762 dygraph.mouseMove_(e);
763 });
eb7bf005 764 Dygraph.addEvent(this.mouseEventElement_, 'mouseout', function(e) {
76171648
DV
765 dygraph.mouseOut_(e);
766 });
697e70b2
DV
767
768 // Create the grapher
769 // TODO(danvk): why does the Layout need its own set of options?
770 this.layoutOptions_ = { 'xOriginIsZero': false };
771 Dygraph.update(this.layoutOptions_, this.attrs_);
772 Dygraph.update(this.layoutOptions_, this.user_attrs_);
773 Dygraph.update(this.layoutOptions_, {
774 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
775
776 this.layout_ = new DygraphLayout(this, this.layoutOptions_);
777
778 // TODO(danvk): why does the Renderer need its own set of options?
779 this.renderOptions_ = { colorScheme: this.colors_,
780 strokeColor: null,
781 axisLineWidth: Dygraph.AXIS_LINE_WIDTH };
782 Dygraph.update(this.renderOptions_, this.attrs_);
783 Dygraph.update(this.renderOptions_, this.user_attrs_);
697e70b2
DV
784
785 this.createStatusMessage_();
697e70b2 786 this.createDragInterface_();
4cfcc38c
DV
787};
788
789/**
790 * Detach DOM elements in the dygraph and null out all data references.
791 * Calling this when you're done with a dygraph can dramatically reduce memory
792 * usage. See, e.g., the tests/perf.html example.
793 */
794Dygraph.prototype.destroy = function() {
795 var removeRecursive = function(node) {
796 while (node.hasChildNodes()) {
797 removeRecursive(node.firstChild);
798 node.removeChild(node.firstChild);
799 }
800 };
801 removeRecursive(this.maindiv_);
802
803 var nullOut = function(obj) {
804 for (var n in obj) {
805 if (typeof(obj[n]) === 'object') {
806 obj[n] = null;
807 }
808 }
809 };
810
811 // These may not all be necessary, but it can't hurt...
812 nullOut(this.layout_);
813 nullOut(this.plotter_);
814 nullOut(this);
815};
6a1aa64f
DV
816
817/**
629a09ae
DV
818 * Creates the canvas on which the chart will be drawn. Only the Renderer ever
819 * draws on this particular canvas. All Dygraph work (i.e. drawing hover dots
820 * or the zoom rectangles) is done on this.canvas_.
8846615a 821 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
6a1aa64f
DV
822 * @return {Object} The newly-created canvas
823 * @private
824 */
285a6bda 825Dygraph.prototype.createPlotKitCanvas_ = function(canvas) {
f8cfec73 826 var h = Dygraph.createCanvas();
6a1aa64f 827 h.style.position = "absolute";
9ac5e4ae
DV
828 // TODO(danvk): h should be offset from canvas. canvas needs to include
829 // some extra area to make it easier to zoom in on the far left and far
830 // right. h needs to be precisely the plot area, so that clipping occurs.
6a1aa64f
DV
831 h.style.top = canvas.style.top;
832 h.style.left = canvas.style.left;
833 h.width = this.width_;
834 h.height = this.height_;
f8cfec73
DV
835 h.style.width = this.width_ + "px"; // for IE
836 h.style.height = this.height_ + "px"; // for IE
6a1aa64f
DV
837 return h;
838};
839
629a09ae
DV
840/**
841 * Convert hsv values to an rgb(r,g,b) string. Taken from MochiKit.Color. This
842 * is used to generate default series colors which are evenly spaced on the
843 * color wheel.
844 * @param { Number } hue Range is 0.0-1.0.
845 * @param { Number } saturation Range is 0.0-1.0.
846 * @param { Number } value Range is 0.0-1.0.
847 * @return { String } "rgb(r,g,b)" where r, g and b range from 0-255.
848 * @private
849 */
f474c2a3
DV
850Dygraph.hsvToRGB = function (hue, saturation, value) {
851 var red;
852 var green;
853 var blue;
854 if (saturation === 0) {
855 red = value;
856 green = value;
857 blue = value;
858 } else {
859 var i = Math.floor(hue * 6);
860 var f = (hue * 6) - i;
861 var p = value * (1 - saturation);
862 var q = value * (1 - (saturation * f));
863 var t = value * (1 - (saturation * (1 - f)));
864 switch (i) {
865 case 1: red = q; green = value; blue = p; break;
866 case 2: red = p; green = value; blue = t; break;
867 case 3: red = p; green = q; blue = value; break;
868 case 4: red = t; green = p; blue = value; break;
869 case 5: red = value; green = p; blue = q; break;
870 case 6: // fall through
871 case 0: red = value; green = t; blue = p; break;
872 }
873 }
874 red = Math.floor(255 * red + 0.5);
875 green = Math.floor(255 * green + 0.5);
876 blue = Math.floor(255 * blue + 0.5);
877 return 'rgb(' + red + ',' + green + ',' + blue + ')';
878};
879
880
6a1aa64f
DV
881/**
882 * Generate a set of distinct colors for the data series. This is done with a
883 * color wheel. Saturation/Value are customizable, and the hue is
884 * equally-spaced around the color wheel. If a custom set of colors is
885 * specified, that is used instead.
6a1aa64f
DV
886 * @private
887 */
285a6bda
DV
888Dygraph.prototype.setColors_ = function() {
889 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
890 // away with this.renderOptions_.
891 var num = this.attr_("labels").length - 1;
6a1aa64f 892 this.colors_ = [];
285a6bda
DV
893 var colors = this.attr_('colors');
894 if (!colors) {
895 var sat = this.attr_('colorSaturation') || 1.0;
896 var val = this.attr_('colorValue') || 0.5;
2aa21213 897 var half = Math.ceil(num / 2);
6a1aa64f 898 for (var i = 1; i <= num; i++) {
ec1959eb 899 if (!this.visibility()[i-1]) continue;
43af96e7 900 // alternate colors for high contrast.
2aa21213 901 var idx = i % 2 ? Math.ceil(i / 2) : (half + i / 2);
43af96e7
NK
902 var hue = (1.0 * idx/ (1 + num));
903 this.colors_.push(Dygraph.hsvToRGB(hue, sat, val));
6a1aa64f
DV
904 }
905 } else {
906 for (var i = 0; i < num; i++) {
ec1959eb 907 if (!this.visibility()[i]) continue;
285a6bda 908 var colorStr = colors[i % colors.length];
f474c2a3 909 this.colors_.push(colorStr);
6a1aa64f
DV
910 }
911 }
285a6bda 912
c21d2c2d 913 // TODO(danvk): update this w/r/t/ the new options system.
285a6bda 914 this.renderOptions_.colorScheme = this.colors_;
fc80a396
DV
915 Dygraph.update(this.plotter_.options, this.renderOptions_);
916 Dygraph.update(this.layoutOptions_, this.user_attrs_);
917 Dygraph.update(this.layoutOptions_, this.attrs_);
629a09ae 918};
6a1aa64f 919
43af96e7
NK
920/**
921 * Return the list of colors. This is either the list of colors passed in the
629a09ae 922 * attributes or the autogenerated list of rgb(r,g,b) strings.
43af96e7
NK
923 * @return {Array<string>} The list of colors.
924 */
925Dygraph.prototype.getColors = function() {
926 return this.colors_;
927};
928
5e60386d
DV
929// The following functions are from quirksmode.org with a modification for Safari from
930// http://blog.firetree.net/2005/07/04/javascript-find-position/
3df0ccf0 931// http://www.quirksmode.org/js/findpos.html
629a09ae
DV
932
933/**
934 * @private
935 */
3df0ccf0
DV
936Dygraph.findPosX = function(obj) {
937 var curleft = 0;
5e60386d 938 if(obj.offsetParent)
50360fd0 939 while(1)
5e60386d 940 {
3df0ccf0 941 curleft += obj.offsetLeft;
5e60386d
DV
942 if(!obj.offsetParent)
943 break;
3df0ccf0
DV
944 obj = obj.offsetParent;
945 }
5e60386d 946 else if(obj.x)
3df0ccf0
DV
947 curleft += obj.x;
948 return curleft;
949};
c21d2c2d 950
629a09ae
DV
951
952/**
953 * @private
954 */
3df0ccf0
DV
955Dygraph.findPosY = function(obj) {
956 var curtop = 0;
5e60386d
DV
957 if(obj.offsetParent)
958 while(1)
959 {
3df0ccf0 960 curtop += obj.offsetTop;
5e60386d
DV
961 if(!obj.offsetParent)
962 break;
3df0ccf0
DV
963 obj = obj.offsetParent;
964 }
5e60386d 965 else if(obj.y)
3df0ccf0
DV
966 curtop += obj.y;
967 return curtop;
968};
969
5e60386d 970
6a1aa64f
DV
971/**
972 * Create the div that contains information on the selected point(s)
973 * This goes in the top right of the canvas, unless an external div has already
974 * been specified.
975 * @private
976 */
fedbd797 977Dygraph.prototype.createStatusMessage_ = function() {
978 var userLabelsDiv = this.user_attrs_["labelsDiv"];
979 if (userLabelsDiv && null != userLabelsDiv
980 && (typeof(userLabelsDiv) == "string" || userLabelsDiv instanceof String)) {
981 this.user_attrs_["labelsDiv"] = document.getElementById(userLabelsDiv);
982 }
285a6bda
DV
983 if (!this.attr_("labelsDiv")) {
984 var divWidth = this.attr_('labelsDivWidth');
b0c3b730 985 var messagestyle = {
6a1aa64f
DV
986 "position": "absolute",
987 "fontSize": "14px",
988 "zIndex": 10,
989 "width": divWidth + "px",
990 "top": "0px",
8846615a 991 "left": (this.width_ - divWidth - 2) + "px",
6a1aa64f
DV
992 "background": "white",
993 "textAlign": "left",
b0c3b730 994 "overflow": "hidden"};
fc80a396 995 Dygraph.update(messagestyle, this.attr_('labelsDivStyles'));
b0c3b730
DV
996 var div = document.createElement("div");
997 for (var name in messagestyle) {
85b99f0b
DV
998 if (messagestyle.hasOwnProperty(name)) {
999 div.style[name] = messagestyle[name];
1000 }
b0c3b730
DV
1001 }
1002 this.graphDiv.appendChild(div);
285a6bda 1003 this.attrs_.labelsDiv = div;
6a1aa64f
DV
1004 }
1005};
1006
1007/**
ad1798c2
DV
1008 * Position the labels div so that:
1009 * - its right edge is flush with the right edge of the charting area
1010 * - its top edge is flush with the top edge of the charting area
629a09ae 1011 * @private
0abfbd7e
DV
1012 */
1013Dygraph.prototype.positionLabelsDiv_ = function() {
1014 // Don't touch a user-specified labelsDiv.
1015 if (this.user_attrs_.hasOwnProperty("labelsDiv")) return;
1016
1017 var area = this.plotter_.area;
1018 var div = this.attr_("labelsDiv");
8c21adcf 1019 div.style.left = area.x + area.w - this.attr_("labelsDivWidth") - 1 + "px";
ad1798c2 1020 div.style.top = area.y + "px";
0abfbd7e
DV
1021};
1022
1023/**
6a1aa64f 1024 * Create the text box to adjust the averaging period
6a1aa64f
DV
1025 * @private
1026 */
285a6bda 1027Dygraph.prototype.createRollInterface_ = function() {
8c69de65
DV
1028 // Create a roller if one doesn't exist already.
1029 if (!this.roller_) {
1030 this.roller_ = document.createElement("input");
1031 this.roller_.type = "text";
1032 this.roller_.style.display = "none";
1033 this.graphDiv.appendChild(this.roller_);
1034 }
1035
1036 var display = this.attr_('showRoller') ? 'block' : 'none';
26ca7938 1037
0c38f187 1038 var area = this.plotter_.area;
b0c3b730
DV
1039 var textAttr = { "position": "absolute",
1040 "zIndex": 10,
0c38f187
DV
1041 "top": (area.y + area.h - 25) + "px",
1042 "left": (area.x + 1) + "px",
b0c3b730 1043 "display": display
6a1aa64f 1044 };
8c69de65
DV
1045 this.roller_.size = "2";
1046 this.roller_.value = this.rollPeriod_;
b0c3b730 1047 for (var name in textAttr) {
85b99f0b 1048 if (textAttr.hasOwnProperty(name)) {
8c69de65 1049 this.roller_.style[name] = textAttr[name];
85b99f0b 1050 }
b0c3b730
DV
1051 }
1052
76171648 1053 var dygraph = this;
8c69de65 1054 this.roller_.onchange = function() { dygraph.adjustRoll(dygraph.roller_.value); };
76171648
DV
1055};
1056
629a09ae
DV
1057/**
1058 * @private
1059 * Returns the x-coordinate of the event in a coordinate system where the
1060 * top-left corner of the page (not the window) is (0,0).
1061 * Taken from MochiKit.Signal
1062 */
76171648
DV
1063Dygraph.pageX = function(e) {
1064 if (e.pageX) {
1065 return (!e.pageX || e.pageX < 0) ? 0 : e.pageX;
1066 } else {
1067 var de = document;
1068 var b = document.body;
1069 return e.clientX +
1070 (de.scrollLeft || b.scrollLeft) -
1071 (de.clientLeft || 0);
1072 }
1073};
1074
629a09ae
DV
1075/**
1076 * @private
1077 * Returns the y-coordinate of the event in a coordinate system where the
1078 * top-left corner of the page (not the window) is (0,0).
1079 * Taken from MochiKit.Signal
1080 */
76171648
DV
1081Dygraph.pageY = function(e) {
1082 if (e.pageY) {
1083 return (!e.pageY || e.pageY < 0) ? 0 : e.pageY;
1084 } else {
1085 var de = document;
1086 var b = document.body;
1087 return e.clientY +
1088 (de.scrollTop || b.scrollTop) -
1089 (de.clientTop || 0);
1090 }
1091};
6a1aa64f 1092
629a09ae
DV
1093/**
1094 * @private
1095 * Converts page the x-coordinate of the event to pixel x-coordinates on the
1096 * canvas (i.e. DOM Coords).
1097 */
062ef401
JB
1098Dygraph.prototype.dragGetX_ = function(e, context) {
1099 return Dygraph.pageX(e) - context.px
1100};
bce01b0f 1101
629a09ae
DV
1102/**
1103 * @private
1104 * Converts page the y-coordinate of the event to pixel y-coordinates on the
1105 * canvas (i.e. DOM Coords).
1106 */
062ef401
JB
1107Dygraph.prototype.dragGetY_ = function(e, context) {
1108 return Dygraph.pageY(e) - context.py
1109};
ee672584 1110
629a09ae
DV
1111/**
1112 * Called in response to an interaction model operation that
1113 * should start the default panning behavior.
1114 *
1115 * It's used in the default callback for "mousedown" operations.
1116 * Custom interaction model builders can use it to provide the default
1117 * panning behavior.
1118 *
1119 * @param { Event } event the event object which led to the startPan call.
1120 * @param { Dygraph} g The dygraph on which to act.
1121 * @param { Object} context The dragging context object (with
1122 * dragStartX/dragStartY/etc. properties). This function modifies the context.
1123 */
062ef401 1124Dygraph.startPan = function(event, g, context) {
062ef401
JB
1125 context.isPanning = true;
1126 var xRange = g.xAxisRange();
1127 context.dateRange = xRange[1] - xRange[0];
ec291cbe
RK
1128 context.initialLeftmostDate = xRange[0];
1129 context.xUnitsPerPixel = context.dateRange / (g.plotter_.area.w - 1);
062ef401 1130
965a030e
RK
1131 if (g.attr_("panEdgeFraction")) {
1132 var maxXPixelsToDraw = g.width_ * g.attr_("panEdgeFraction");
4cac8c7a
RK
1133 var xExtremes = g.xAxisExtremes(); // I REALLY WANT TO CALL THIS xTremes!
1134
1135 var boundedLeftX = g.toDomXCoord(xExtremes[0]) - maxXPixelsToDraw;
1136 var boundedRightX = g.toDomXCoord(xExtremes[1]) + maxXPixelsToDraw;
1137
1138 var boundedLeftDate = g.toDataXCoord(boundedLeftX);
1139 var boundedRightDate = g.toDataXCoord(boundedRightX);
1140 context.boundedDates = [boundedLeftDate, boundedRightDate];
1141
1142 var boundedValues = [];
965a030e 1143 var maxYPixelsToDraw = g.height_ * g.attr_("panEdgeFraction");
4cac8c7a
RK
1144
1145 for (var i = 0; i < g.axes_.length; i++) {
1146 var axis = g.axes_[i];
1147 var yExtremes = axis.extremeRange;
1148
1149 var boundedTopY = g.toDomYCoord(yExtremes[0], i) + maxYPixelsToDraw;
1150 var boundedBottomY = g.toDomYCoord(yExtremes[1], i) - maxYPixelsToDraw;
1151
1152 var boundedTopValue = g.toDataYCoord(boundedTopY);
1153 var boundedBottomValue = g.toDataYCoord(boundedBottomY);
1154
4cac8c7a
RK
1155 boundedValues[i] = [boundedTopValue, boundedBottomValue];
1156 }
1157 context.boundedValues = boundedValues;
1158 }
1159
062ef401
JB
1160 // Record the range of each y-axis at the start of the drag.
1161 // If any axis has a valueRange or valueWindow, then we want a 2D pan.
1162 context.is2DPan = false;
1163 for (var i = 0; i < g.axes_.length; i++) {
1164 var axis = g.axes_[i];
1165 var yRange = g.yAxisRange(i);
ec291cbe 1166 // TODO(konigsberg): These values should be in |context|.
ed898bdd
RK
1167 // In log scale, initialTopValue, dragValueRange and unitsPerPixel are log scale.
1168 if (axis.logscale) {
1169 axis.initialTopValue = Dygraph.log10(yRange[1]);
1170 axis.dragValueRange = Dygraph.log10(yRange[1]) - Dygraph.log10(yRange[0]);
1171 } else {
1172 axis.initialTopValue = yRange[1];
1173 axis.dragValueRange = yRange[1] - yRange[0];
1174 }
ec291cbe 1175 axis.unitsPerPixel = axis.dragValueRange / (g.plotter_.area.h - 1);
ed898bdd
RK
1176
1177 // While calculating axes, set 2dpan.
062ef401
JB
1178 if (axis.valueWindow || axis.valueRange) context.is2DPan = true;
1179 }
062ef401 1180};
6a1aa64f 1181
629a09ae
DV
1182/**
1183 * Called in response to an interaction model operation that
1184 * responds to an event that pans the view.
1185 *
1186 * It's used in the default callback for "mousemove" operations.
1187 * Custom interaction model builders can use it to provide the default
1188 * panning behavior.
1189 *
1190 * @param { Event } event the event object which led to the movePan call.
1191 * @param { Dygraph} g The dygraph on which to act.
1192 * @param { Object} context The dragging context object (with
1193 * dragStartX/dragStartY/etc. properties). This function modifies the context.
1194 */
062ef401
JB
1195Dygraph.movePan = function(event, g, context) {
1196 context.dragEndX = g.dragGetX_(event, context);
1197 context.dragEndY = g.dragGetY_(event, context);
79b3ee42 1198
ec291cbe
RK
1199 var minDate = context.initialLeftmostDate -
1200 (context.dragEndX - context.dragStartX) * context.xUnitsPerPixel;
4cac8c7a
RK
1201 if (context.boundedDates) {
1202 minDate = Math.max(minDate, context.boundedDates[0]);
1203 }
062ef401 1204 var maxDate = minDate + context.dateRange;
4cac8c7a
RK
1205 if (context.boundedDates) {
1206 if (maxDate > context.boundedDates[1]) {
1207 // Adjust minDate, and recompute maxDate.
1208 minDate = minDate - (maxDate - context.boundedDates[1]);
965a030e 1209 maxDate = minDate + context.dateRange;
4cac8c7a
RK
1210 }
1211 }
1212
062ef401
JB
1213 g.dateWindow_ = [minDate, maxDate];
1214
1215 // y-axis scaling is automatic unless this is a full 2D pan.
1216 if (context.is2DPan) {
1217 // Adjust each axis appropriately.
062ef401
JB
1218 for (var i = 0; i < g.axes_.length; i++) {
1219 var axis = g.axes_[i];
ed898bdd
RK
1220
1221 var pixelsDragged = context.dragEndY - context.dragStartY;
1222 var unitsDragged = pixelsDragged * axis.unitsPerPixel;
4cac8c7a
RK
1223
1224 var boundedValue = context.boundedValues ? context.boundedValues[i] : null;
ed898bdd
RK
1225
1226 // In log scale, maxValue and minValue are the logs of those values.
1227 var maxValue = axis.initialTopValue + unitsDragged;
4cac8c7a
RK
1228 if (boundedValue) {
1229 maxValue = Math.min(maxValue, boundedValue[1]);
1230 }
062ef401 1231 var minValue = maxValue - axis.dragValueRange;
4cac8c7a
RK
1232 if (boundedValue) {
1233 if (minValue < boundedValue[0]) {
1234 // Adjust maxValue, and recompute minValue.
1235 maxValue = maxValue - (minValue - boundedValue[0]);
1236 minValue = maxValue - axis.dragValueRange;
1237 }
1238 }
ed898bdd 1239 if (axis.logscale) {
5db0e241
DV
1240 axis.valueWindow = [ Math.pow(Dygraph.LOG_SCALE, minValue),
1241 Math.pow(Dygraph.LOG_SCALE, maxValue) ];
ed898bdd
RK
1242 } else {
1243 axis.valueWindow = [ minValue, maxValue ];
1244 }
6faebb69 1245 }
062ef401 1246 }
bce01b0f 1247
062ef401 1248 g.drawGraph_();
629a09ae 1249};
ee672584 1250
629a09ae
DV
1251/**
1252 * Called in response to an interaction model operation that
1253 * responds to an event that ends panning.
1254 *
1255 * It's used in the default callback for "mouseup" operations.
1256 * Custom interaction model builders can use it to provide the default
1257 * panning behavior.
1258 *
1259 * @param { Event } event the event object which led to the startZoom call.
1260 * @param { Dygraph} g The dygraph on which to act.
1261 * @param { Object} context The dragging context object (with
1262 * dragStartX/dragStartY/etc. properties). This function modifies the context.
1263 */
062ef401 1264Dygraph.endPan = function(event, g, context) {
ec291cbe
RK
1265 // TODO(konigsberg): Clear the context data from the axis.
1266 // TODO(konigsberg): mouseup should just delete the
1267 // context object, and mousedown should create a new one.
062ef401
JB
1268 context.isPanning = false;
1269 context.is2DPan = false;
ec291cbe 1270 context.initialLeftmostDate = null;
062ef401
JB
1271 context.dateRange = null;
1272 context.valueRange = null;
9ec21d0a
RK
1273 context.boundedDates = null;
1274 context.boundedValues = null;
629a09ae 1275};
ee672584 1276
629a09ae
DV
1277/**
1278 * Called in response to an interaction model operation that
1279 * responds to an event that starts zooming.
1280 *
1281 * It's used in the default callback for "mousedown" operations.
1282 * Custom interaction model builders can use it to provide the default
1283 * zooming behavior.
1284 *
1285 * @param { Event } event the event object which led to the startZoom call.
1286 * @param { Dygraph} g The dygraph on which to act.
1287 * @param { Object} context The dragging context object (with
1288 * dragStartX/dragStartY/etc. properties). This function modifies the context.
1289 */
062ef401
JB
1290Dygraph.startZoom = function(event, g, context) {
1291 context.isZooming = true;
629a09ae 1292};
062ef401 1293
629a09ae
DV
1294/**
1295 * Called in response to an interaction model operation that
1296 * responds to an event that defines zoom boundaries.
1297 *
1298 * It's used in the default callback for "mousemove" operations.
1299 * Custom interaction model builders can use it to provide the default
1300 * zooming behavior.
1301 *
1302 * @param { Event } event the event object which led to the moveZoom call.
1303 * @param { Dygraph} g The dygraph on which to act.
1304 * @param { Object} context The dragging context object (with
1305 * dragStartX/dragStartY/etc. properties). This function modifies the context.
1306 */
062ef401
JB
1307Dygraph.moveZoom = function(event, g, context) {
1308 context.dragEndX = g.dragGetX_(event, context);
1309 context.dragEndY = g.dragGetY_(event, context);
1310
1311 var xDelta = Math.abs(context.dragStartX - context.dragEndX);
1312 var yDelta = Math.abs(context.dragStartY - context.dragEndY);
1313
1314 // drag direction threshold for y axis is twice as large as x axis
1315 context.dragDirection = (xDelta < yDelta / 2) ? Dygraph.VERTICAL : Dygraph.HORIZONTAL;
1316
1317 g.drawZoomRect_(
1318 context.dragDirection,
1319 context.dragStartX,
1320 context.dragEndX,
1321 context.dragStartY,
1322 context.dragEndY,
1323 context.prevDragDirection,
1324 context.prevEndX,
1325 context.prevEndY);
1326
1327 context.prevEndX = context.dragEndX;
1328 context.prevEndY = context.dragEndY;
1329 context.prevDragDirection = context.dragDirection;
629a09ae 1330};
062ef401 1331
629a09ae
DV
1332/**
1333 * Called in response to an interaction model operation that
1334 * responds to an event that performs a zoom based on previously defined
1335 * bounds..
1336 *
1337 * It's used in the default callback for "mouseup" operations.
1338 * Custom interaction model builders can use it to provide the default
1339 * zooming behavior.
1340 *
1341 * @param { Event } event the event object which led to the endZoom call.
1342 * @param { Dygraph} g The dygraph on which to end the zoom.
1343 * @param { Object} context The dragging context object (with
1344 * dragStartX/dragStartY/etc. properties). This function modifies the context.
1345 */
062ef401 1346Dygraph.endZoom = function(event, g, context) {
629a09ae 1347 // TODO(konigsberg): Refactor or rename this fn -- it deals with clicks, too.
062ef401
JB
1348 context.isZooming = false;
1349 context.dragEndX = g.dragGetX_(event, context);
1350 context.dragEndY = g.dragGetY_(event, context);
1351 var regionWidth = Math.abs(context.dragEndX - context.dragStartX);
1352 var regionHeight = Math.abs(context.dragEndY - context.dragStartY);
1353
1354 if (regionWidth < 2 && regionHeight < 2 &&
1355 g.lastx_ != undefined && g.lastx_ != -1) {
1356 // TODO(danvk): pass along more info about the points, e.g. 'x'
1357 if (g.attr_('clickCallback') != null) {
1358 g.attr_('clickCallback')(event, g.lastx_, g.selPoints_);
1359 }
1360 if (g.attr_('pointClickCallback')) {
1361 // check if the click was on a particular point.
1362 var closestIdx = -1;
1363 var closestDistance = 0;
1364 for (var i = 0; i < g.selPoints_.length; i++) {
1365 var p = g.selPoints_[i];
1366 var distance = Math.pow(p.canvasx - context.dragEndX, 2) +
1367 Math.pow(p.canvasy - context.dragEndY, 2);
1368 if (closestIdx == -1 || distance < closestDistance) {
1369 closestDistance = distance;
1370 closestIdx = i;
d58ae307
DV
1371 }
1372 }
e3489f4f 1373
062ef401
JB
1374 // Allow any click within two pixels of the dot.
1375 var radius = g.attr_('highlightCircleSize') + 2;
1376 if (closestDistance <= 5 * 5) {
1377 g.attr_('pointClickCallback')(event, g.selPoints_[closestIdx]);
6faebb69 1378 }
062ef401
JB
1379 }
1380 }
0a52ab7a 1381
062ef401
JB
1382 if (regionWidth >= 10 && context.dragDirection == Dygraph.HORIZONTAL) {
1383 g.doZoomX_(Math.min(context.dragStartX, context.dragEndX),
1384 Math.max(context.dragStartX, context.dragEndX));
1385 } else if (regionHeight >= 10 && context.dragDirection == Dygraph.VERTICAL) {
1386 g.doZoomY_(Math.min(context.dragStartY, context.dragEndY),
1387 Math.max(context.dragStartY, context.dragEndY));
1388 } else {
2cf95fff 1389 g.canvas_ctx_.clearRect(0, 0, g.canvas_.width, g.canvas_.height);
062ef401
JB
1390 }
1391 context.dragStartX = null;
1392 context.dragStartY = null;
629a09ae 1393};
062ef401 1394
629a09ae
DV
1395/**
1396 * Default interation model for dygraphs. You can refer to specific elements of
1397 * this when constructing your own interaction model, e.g.:
1398 * g.updateOptions( {
1399 * interactionModel: {
1400 * mousedown: Dygraph.defaultInteractionModel.mousedown
1401 * }
1402 * } );
1403 */
062ef401
JB
1404Dygraph.defaultInteractionModel = {
1405 // Track the beginning of drag events
1406 mousedown: function(event, g, context) {
1407 context.initializeMouseDown(event, g, context);
1408
1409 if (event.altKey || event.shiftKey) {
1410 Dygraph.startPan(event, g, context);
bce01b0f 1411 } else {
062ef401 1412 Dygraph.startZoom(event, g, context);
bce01b0f 1413 }
062ef401 1414 },
6a1aa64f 1415
062ef401
JB
1416 // Draw zoom rectangles when the mouse is down and the user moves around
1417 mousemove: function(event, g, context) {
1418 if (context.isZooming) {
1419 Dygraph.moveZoom(event, g, context);
1420 } else if (context.isPanning) {
1421 Dygraph.movePan(event, g, context);
6a1aa64f 1422 }
062ef401 1423 },
bce01b0f 1424
062ef401
JB
1425 mouseup: function(event, g, context) {
1426 if (context.isZooming) {
1427 Dygraph.endZoom(event, g, context);
1428 } else if (context.isPanning) {
1429 Dygraph.endPan(event, g, context);
bce01b0f 1430 }
062ef401 1431 },
6a1aa64f
DV
1432
1433 // Temporarily cancel the dragging event when the mouse leaves the graph
062ef401
JB
1434 mouseout: function(event, g, context) {
1435 if (context.isZooming) {
1436 context.dragEndX = null;
1437 context.dragEndY = null;
6a1aa64f 1438 }
062ef401 1439 },
6a1aa64f 1440
062ef401
JB
1441 // Disable zooming out if panning.
1442 dblclick: function(event, g, context) {
1443 if (event.altKey || event.shiftKey) {
1444 return;
1445 }
1446 // TODO(konigsberg): replace g.doUnzoom()_ with something that is
1447 // friendlier to public use.
1448 g.doUnzoom_();
1449 }
1450};
1e1bf7df 1451
062ef401 1452Dygraph.DEFAULT_ATTRS.interactionModel = Dygraph.defaultInteractionModel;
6a1aa64f 1453
062ef401
JB
1454/**
1455 * Set up all the mouse handlers needed to capture dragging behavior for zoom
1456 * events.
1457 * @private
1458 */
1459Dygraph.prototype.createDragInterface_ = function() {
1460 var context = {
1461 // Tracks whether the mouse is down right now
1462 isZooming: false,
1463 isPanning: false, // is this drag part of a pan?
1464 is2DPan: false, // if so, is that pan 1- or 2-dimensional?
1465 dragStartX: null,
1466 dragStartY: null,
1467 dragEndX: null,
1468 dragEndY: null,
1469 dragDirection: null,
1470 prevEndX: null,
1471 prevEndY: null,
1472 prevDragDirection: null,
1473
ec291cbe
RK
1474 // The value on the left side of the graph when a pan operation starts.
1475 initialLeftmostDate: null,
1476
1477 // The number of units each pixel spans. (This won't be valid for log
1478 // scales)
1479 xUnitsPerPixel: null,
062ef401
JB
1480
1481 // TODO(danvk): update this comment
1482 // The range in second/value units that the viewport encompasses during a
1483 // panning operation.
1484 dateRange: null,
1485
1486 // Utility function to convert page-wide coordinates to canvas coords
1487 px: 0,
1488 py: 0,
1489
965a030e 1490 // Values for use with panEdgeFraction, which limit how far outside the
4cac8c7a
RK
1491 // graph's data boundaries it can be panned.
1492 boundedDates: null, // [minDate, maxDate]
1493 boundedValues: null, // [[minValue, maxValue] ...]
1494
062ef401
JB
1495 initializeMouseDown: function(event, g, context) {
1496 // prevents mouse drags from selecting page text.
1497 if (event.preventDefault) {
1498 event.preventDefault(); // Firefox, Chrome, etc.
6a1aa64f 1499 } else {
062ef401
JB
1500 event.returnValue = false; // IE
1501 event.cancelBubble = true;
6a1aa64f
DV
1502 }
1503
062ef401
JB
1504 context.px = Dygraph.findPosX(g.canvas_);
1505 context.py = Dygraph.findPosY(g.canvas_);
1506 context.dragStartX = g.dragGetX_(event, context);
1507 context.dragStartY = g.dragGetY_(event, context);
6a1aa64f 1508 }
062ef401 1509 };
2b188b3d 1510
062ef401 1511 var interactionModel = this.attr_("interactionModel");
8b83c6cc 1512
062ef401
JB
1513 // Self is the graph.
1514 var self = this;
6faebb69 1515
062ef401
JB
1516 // Function that binds the graph and context to the handler.
1517 var bindHandler = function(handler) {
1518 return function(event) {
1519 handler(event, self, context);
1520 };
1521 };
1522
1523 for (var eventName in interactionModel) {
1524 if (!interactionModel.hasOwnProperty(eventName)) continue;
1525 Dygraph.addEvent(this.mouseEventElement_, eventName,
1526 bindHandler(interactionModel[eventName]));
1527 }
1528
1529 // If the user releases the mouse button during a drag, but not over the
1530 // canvas, then it doesn't count as a zooming action.
1531 Dygraph.addEvent(document, 'mouseup', function(event) {
1532 if (context.isZooming || context.isPanning) {
1533 context.isZooming = false;
1534 context.dragStartX = null;
1535 context.dragStartY = null;
1536 }
1537
1538 if (context.isPanning) {
1539 context.isPanning = false;
1540 context.draggingDate = null;
1541 context.dateRange = null;
1542 for (var i = 0; i < self.axes_.length; i++) {
1543 delete self.axes_[i].draggingValue;
1544 delete self.axes_[i].dragValueRange;
1545 }
1546 }
6a1aa64f
DV
1547 });
1548};
1549
062ef401 1550
6a1aa64f
DV
1551/**
1552 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
1553 * up any previous zoom rectangles that were drawn. This could be optimized to
1554 * avoid extra redrawing, but it's tricky to avoid interactions with the status
1555 * dots.
8b83c6cc 1556 *
39b0e098
RK
1557 * @param {Number} direction the direction of the zoom rectangle. Acceptable
1558 * values are Dygraph.HORIZONTAL and Dygraph.VERTICAL.
6a1aa64f
DV
1559 * @param {Number} startX The X position where the drag started, in canvas
1560 * coordinates.
1561 * @param {Number} endX The current X position of the drag, in canvas coords.
8b83c6cc
RK
1562 * @param {Number} startY The Y position where the drag started, in canvas
1563 * coordinates.
1564 * @param {Number} endY The current Y position of the drag, in canvas coords.
39b0e098 1565 * @param {Number} prevDirection the value of direction on the previous call to
8b83c6cc 1566 * this function. Used to avoid excess redrawing
6a1aa64f
DV
1567 * @param {Number} prevEndX The value of endX on the previous call to this
1568 * function. Used to avoid excess redrawing
8b83c6cc
RK
1569 * @param {Number} prevEndY The value of endY on the previous call to this
1570 * function. Used to avoid excess redrawing
6a1aa64f
DV
1571 * @private
1572 */
7201b11e
JB
1573Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY,
1574 endY, prevDirection, prevEndX,
1575 prevEndY) {
2cf95fff 1576 var ctx = this.canvas_ctx_;
6a1aa64f
DV
1577
1578 // Clean up from the previous rect if necessary
39b0e098 1579 if (prevDirection == Dygraph.HORIZONTAL) {
6a1aa64f
DV
1580 ctx.clearRect(Math.min(startX, prevEndX), 0,
1581 Math.abs(startX - prevEndX), this.height_);
39b0e098 1582 } else if (prevDirection == Dygraph.VERTICAL){
8b83c6cc
RK
1583 ctx.clearRect(0, Math.min(startY, prevEndY),
1584 this.width_, Math.abs(startY - prevEndY));
6a1aa64f
DV
1585 }
1586
1587 // Draw a light-grey rectangle to show the new viewing area
39b0e098 1588 if (direction == Dygraph.HORIZONTAL) {
8b83c6cc
RK
1589 if (endX && startX) {
1590 ctx.fillStyle = "rgba(128,128,128,0.33)";
1591 ctx.fillRect(Math.min(startX, endX), 0,
1592 Math.abs(endX - startX), this.height_);
1593 }
1594 }
39b0e098 1595 if (direction == Dygraph.VERTICAL) {
8b83c6cc
RK
1596 if (endY && startY) {
1597 ctx.fillStyle = "rgba(128,128,128,0.33)";
1598 ctx.fillRect(0, Math.min(startY, endY),
1599 this.width_, Math.abs(endY - startY));
1600 }
6a1aa64f
DV
1601 }
1602};
1603
1604/**
8b83c6cc
RK
1605 * Zoom to something containing [lowX, highX]. These are pixel coordinates in
1606 * the canvas. The exact zoom window may be slightly larger if there are no data
1607 * points near lowX or highX. Don't confuse this function with doZoomXDates,
1608 * which accepts dates that match the raw data. This function redraws the graph.
d58ae307 1609 *
6a1aa64f
DV
1610 * @param {Number} lowX The leftmost pixel value that should be visible.
1611 * @param {Number} highX The rightmost pixel value that should be visible.
1612 * @private
1613 */
8b83c6cc 1614Dygraph.prototype.doZoomX_ = function(lowX, highX) {
6a1aa64f 1615 // Find the earliest and latest dates contained in this canvasx range.
8b83c6cc 1616 // Convert the call to date ranges of the raw data.
ff022deb
RK
1617 var minDate = this.toDataXCoord(lowX);
1618 var maxDate = this.toDataXCoord(highX);
8b83c6cc
RK
1619 this.doZoomXDates_(minDate, maxDate);
1620};
6a1aa64f 1621
8b83c6cc
RK
1622/**
1623 * Zoom to something containing [minDate, maxDate] values. Don't confuse this
1624 * method with doZoomX which accepts pixel coordinates. This function redraws
1625 * the graph.
d58ae307 1626 *
8b83c6cc
RK
1627 * @param {Number} minDate The minimum date that should be visible.
1628 * @param {Number} maxDate The maximum date that should be visible.
1629 * @private
1630 */
1631Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) {
6a1aa64f 1632 this.dateWindow_ = [minDate, maxDate];
57baab03 1633 this.zoomed_x_ = true;
26ca7938 1634 this.drawGraph_();
285a6bda 1635 if (this.attr_("zoomCallback")) {
ac139d19 1636 this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges());
8b83c6cc
RK
1637 }
1638};
1639
1640/**
1641 * Zoom to something containing [lowY, highY]. These are pixel coordinates in
d58ae307
DV
1642 * the canvas. This function redraws the graph.
1643 *
8b83c6cc
RK
1644 * @param {Number} lowY The topmost pixel value that should be visible.
1645 * @param {Number} highY The lowest pixel value that should be visible.
1646 * @private
1647 */
1648Dygraph.prototype.doZoomY_ = function(lowY, highY) {
d58ae307
DV
1649 // Find the highest and lowest values in pixel range for each axis.
1650 // Note that lowY (in pixels) corresponds to the max Value (in data coords).
1651 // This is because pixels increase as you go down on the screen, whereas data
1652 // coordinates increase as you go up the screen.
1653 var valueRanges = [];
1654 for (var i = 0; i < this.axes_.length; i++) {
ff022deb
RK
1655 var hi = this.toDataYCoord(lowY, i);
1656 var low = this.toDataYCoord(highY, i);
1657 this.axes_[i].valueWindow = [low, hi];
1658 valueRanges.push([low, hi]);
d58ae307 1659 }
8b83c6cc 1660
57baab03 1661 this.zoomed_y_ = true;
66c380c4 1662 this.drawGraph_();
8b83c6cc 1663 if (this.attr_("zoomCallback")) {
d58ae307 1664 var xRange = this.xAxisRange();
45f2c689 1665 var yRange = this.yAxisRange();
d58ae307 1666 this.attr_("zoomCallback")(xRange[0], xRange[1], this.yAxisRanges());
8b83c6cc
RK
1667 }
1668};
1669
1670/**
1671 * Reset the zoom to the original view coordinates. This is the same as
1672 * double-clicking on the graph.
d58ae307 1673 *
8b83c6cc
RK
1674 * @private
1675 */
1676Dygraph.prototype.doUnzoom_ = function() {
d58ae307 1677 var dirty = false;
8b83c6cc 1678 if (this.dateWindow_ != null) {
d58ae307 1679 dirty = true;
8b83c6cc
RK
1680 this.dateWindow_ = null;
1681 }
d58ae307
DV
1682
1683 for (var i = 0; i < this.axes_.length; i++) {
1684 if (this.axes_[i].valueWindow != null) {
1685 dirty = true;
1686 delete this.axes_[i].valueWindow;
1687 }
8b83c6cc
RK
1688 }
1689
1690 if (dirty) {
437c0979
RK
1691 // Putting the drawing operation before the callback because it resets
1692 // yAxisRange.
57baab03
NN
1693 this.zoomed_x_ = false;
1694 this.zoomed_y_ = false;
66c380c4 1695 this.drawGraph_();
8b83c6cc
RK
1696 if (this.attr_("zoomCallback")) {
1697 var minDate = this.rawData_[0][0];
1698 var maxDate = this.rawData_[this.rawData_.length - 1][0];
d58ae307 1699 this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges());
8b83c6cc 1700 }
67e650dc 1701 }
6a1aa64f
DV
1702};
1703
1704/**
1705 * When the mouse moves in the canvas, display information about a nearby data
1706 * point and draw dots over those points in the data series. This function
1707 * takes care of cleanup of previously-drawn dots.
1708 * @param {Object} event The mousemove event from the browser.
1709 * @private
1710 */
285a6bda 1711Dygraph.prototype.mouseMove_ = function(event) {
e863a17d 1712 // This prevents JS errors when mousing over the canvas before data loads.
4cac8c7a 1713 var points = this.layout_.points;
685ebbb3 1714 if (points === undefined) return;
e863a17d 1715
4cac8c7a
RK
1716 var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_);
1717
6a1aa64f
DV
1718 var lastx = -1;
1719 var lasty = -1;
1720
1721 // Loop through all the points and find the date nearest to our current
1722 // location.
1723 var minDist = 1e+100;
1724 var idx = -1;
1725 for (var i = 0; i < points.length; i++) {
8a7cc60e
RK
1726 var point = points[i];
1727 if (point == null) continue;
062ef401 1728 var dist = Math.abs(point.canvasx - canvasx);
f032c51d 1729 if (dist > minDist) continue;
6a1aa64f
DV
1730 minDist = dist;
1731 idx = i;
1732 }
1733 if (idx >= 0) lastx = points[idx].xval;
6a1aa64f
DV
1734
1735 // Extract the points we've selected
b258a3da 1736 this.selPoints_ = [];
50360fd0 1737 var l = points.length;
416b05ad
NK
1738 if (!this.attr_("stackedGraph")) {
1739 for (var i = 0; i < l; i++) {
1740 if (points[i].xval == lastx) {
1741 this.selPoints_.push(points[i]);
1742 }
1743 }
1744 } else {
354e15ab
DE
1745 // Need to 'unstack' points starting from the bottom
1746 var cumulative_sum = 0;
416b05ad
NK
1747 for (var i = l - 1; i >= 0; i--) {
1748 if (points[i].xval == lastx) {
354e15ab 1749 var p = {}; // Clone the point since we modify it
d4139cd8
NK
1750 for (var k in points[i]) {
1751 p[k] = points[i][k];
50360fd0
NK
1752 }
1753 p.yval -= cumulative_sum;
1754 cumulative_sum += p.yval;
d4139cd8 1755 this.selPoints_.push(p);
12e4c741 1756 }
6a1aa64f 1757 }
354e15ab 1758 this.selPoints_.reverse();
6a1aa64f
DV
1759 }
1760
b258a3da 1761 if (this.attr_("highlightCallback")) {
a4c6a67c 1762 var px = this.lastx_;
dd082dda 1763 if (px !== null && lastx != px) {
344ba8c0 1764 // only fire if the selected point has changed.
2ddb1197 1765 this.attr_("highlightCallback")(event, lastx, this.selPoints_, this.idxToRow_(idx));
43af96e7 1766 }
12e4c741 1767 }
43af96e7 1768
239c712d
NAG
1769 // Save last x position for callbacks.
1770 this.lastx_ = lastx;
50360fd0 1771
239c712d
NAG
1772 this.updateSelection_();
1773};
b258a3da 1774
239c712d 1775/**
1903f1e4 1776 * Transforms layout_.points index into data row number.
2ddb1197 1777 * @param int layout_.points index
1903f1e4 1778 * @return int row number, or -1 if none could be found.
2ddb1197
SC
1779 * @private
1780 */
1781Dygraph.prototype.idxToRow_ = function(idx) {
1903f1e4 1782 if (idx < 0) return -1;
2ddb1197 1783
1903f1e4
DV
1784 for (var i in this.layout_.datasets) {
1785 if (idx < this.layout_.datasets[i].length) {
1786 return this.boundaryIds_[0][0]+idx;
1787 }
1788 idx -= this.layout_.datasets[i].length;
1789 }
1790 return -1;
1791};
2ddb1197 1792
629a09ae
DV
1793/**
1794 * @private
1795 * @param { Number } x The number to consider.
1796 * @return { Boolean } Whether the number is zero or NaN.
1797 */
2fccd3dc 1798// TODO(danvk): rename this function to something like 'isNonZeroNan'.
e9fe4a2f
DV
1799Dygraph.isOK = function(x) {
1800 return x && !isNaN(x);
1801};
1802
629a09ae
DV
1803/**
1804 * @private
1805 * Generates HTML for the legend which is displayed when hovering over the
1806 * chart. If no selected points are specified, a default legend is returned
1807 * (this may just be the empty string).
1808 * @param { Number } [x] The x-value of the selected points.
1809 * @param { [Object] } [sel_points] List of selected points for the given
1810 * x-value. Should have properties like 'name', 'yval' and 'canvasy'.
1811 */
e9fe4a2f 1812Dygraph.prototype.generateLegendHTML_ = function(x, sel_points) {
2fccd3dc
DV
1813 // If no points are selected, we display a default legend. Traditionally,
1814 // this has been blank. But a better default would be a conventional legend,
1815 // which provides essential information for a non-interactive chart.
1816 if (typeof(x) === 'undefined') {
1817 if (this.attr_('legend') != 'always') return '';
1818
1819 var sepLines = this.attr_('labelsSeparateLines');
1820 var labels = this.attr_('labels');
1821 var html = '';
1822 for (var i = 1; i < labels.length; i++) {
352c8310 1823 if (!this.visibility()[i - 1]) continue;
bafe040e 1824 var c = this.plotter_.colors[labels[i]];
352c8310 1825 if (html != '') html += (sepLines ? '<br/>' : ' ');
bafe040e
DV
1826 html += "<b><span style='color: " + c + ";'>&mdash;" + labels[i] +
1827 "</span></b>";
2fccd3dc
DV
1828 }
1829 return html;
1830 }
1831
032e4c1d 1832 var html = this.attr_('xValueFormatter')(x) + ":";
e9fe4a2f
DV
1833
1834 var fmtFunc = this.attr_('yValueFormatter');
1835 var showZeros = this.attr_("labelsShowZeroValues");
1836 var sepLines = this.attr_("labelsSeparateLines");
1837 for (var i = 0; i < this.selPoints_.length; i++) {
1838 var pt = this.selPoints_[i];
1839 if (pt.yval == 0 && !showZeros) continue;
1840 if (!Dygraph.isOK(pt.canvasy)) continue;
1841 if (sepLines) html += "<br/>";
1842
bafe040e 1843 var c = this.plotter_.colors[pt.name];
91c10d9c 1844 var yval = fmtFunc(pt.yval, this);
2fccd3dc 1845 // TODO(danvk): use a template string here and make it an attribute.
bafe040e
DV
1846 html += " <b><span style='color: " + c + ";'>"
1847 + pt.name + "</span></b>:"
e9fe4a2f
DV
1848 + yval;
1849 }
1850 return html;
1851};
1852
629a09ae
DV
1853/**
1854 * @private
1855 * Displays information about the selected points in the legend. If there is no
1856 * selection, the legend will be cleared.
1857 * @param { Number } [x] The x-value of the selected points.
1858 * @param { [Object] } [sel_points] List of selected points for the given
1859 * x-value. Should have properties like 'name', 'yval' and 'canvasy'.
1860 */
91c10d9c
DV
1861Dygraph.prototype.setLegendHTML_ = function(x, sel_points) {
1862 var html = this.generateLegendHTML_(x, sel_points);
1863 var labelsDiv = this.attr_("labelsDiv");
1864 if (labelsDiv !== null) {
1865 labelsDiv.innerHTML = html;
1866 } else {
1867 if (typeof(this.shown_legend_error_) == 'undefined') {
1868 this.error('labelsDiv is set to something nonexistent; legend will not be shown.');
1869 this.shown_legend_error_ = true;
1870 }
1871 }
1872};
1873
2ddb1197 1874/**
239c712d
NAG
1875 * Draw dots over the selectied points in the data series. This function
1876 * takes care of cleanup of previously-drawn dots.
1877 * @private
1878 */
1879Dygraph.prototype.updateSelection_ = function() {
6a1aa64f 1880 // Clear the previously drawn vertical, if there is one
2cf95fff 1881 var ctx = this.canvas_ctx_;
6a1aa64f 1882 if (this.previousVerticalX_ >= 0) {
46dde5f9
DV
1883 // Determine the maximum highlight circle size.
1884 var maxCircleSize = 0;
227b93cc
DV
1885 var labels = this.attr_('labels');
1886 for (var i = 1; i < labels.length; i++) {
1887 var r = this.attr_('highlightCircleSize', labels[i]);
46dde5f9
DV
1888 if (r > maxCircleSize) maxCircleSize = r;
1889 }
6a1aa64f 1890 var px = this.previousVerticalX_;
46dde5f9
DV
1891 ctx.clearRect(px - maxCircleSize - 1, 0,
1892 2 * maxCircleSize + 2, this.height_);
6a1aa64f
DV
1893 }
1894
d160cc3b 1895 if (this.selPoints_.length > 0) {
6a1aa64f 1896 // Set the status message to indicate the selected point(s)
d160cc3b 1897 if (this.attr_('showLabelsOnHighlight')) {
91c10d9c 1898 this.setLegendHTML_(this.lastx_, this.selPoints_);
6a1aa64f 1899 }
6a1aa64f 1900
6a1aa64f 1901 // Draw colored circles over the center of each selected point
e9fe4a2f 1902 var canvasx = this.selPoints_[0].canvasx;
43af96e7 1903 ctx.save();
b258a3da 1904 for (var i = 0; i < this.selPoints_.length; i++) {
e9fe4a2f
DV
1905 var pt = this.selPoints_[i];
1906 if (!Dygraph.isOK(pt.canvasy)) continue;
1907
1908 var circleSize = this.attr_('highlightCircleSize', pt.name);
6a1aa64f 1909 ctx.beginPath();
e9fe4a2f
DV
1910 ctx.fillStyle = this.plotter_.colors[pt.name];
1911 ctx.arc(canvasx, pt.canvasy, circleSize, 0, 2 * Math.PI, false);
6a1aa64f
DV
1912 ctx.fill();
1913 }
1914 ctx.restore();
1915
1916 this.previousVerticalX_ = canvasx;
1917 }
1918};
1919
1920/**
629a09ae
DV
1921 * Manually set the selected points and display information about them in the
1922 * legend. The selection can be cleared using clearSelection() and queried
1923 * using getSelection().
1924 * @param { Integer } row number that should be highlighted (i.e. appear with
1925 * hover dots on the chart). Set to false to clear any selection.
239c712d
NAG
1926 */
1927Dygraph.prototype.setSelection = function(row) {
1928 // Extract the points we've selected
1929 this.selPoints_ = [];
1930 var pos = 0;
50360fd0 1931
239c712d 1932 if (row !== false) {
16269f6e
NAG
1933 row = row-this.boundaryIds_[0][0];
1934 }
50360fd0 1935
16269f6e 1936 if (row !== false && row >= 0) {
239c712d 1937 for (var i in this.layout_.datasets) {
16269f6e 1938 if (row < this.layout_.datasets[i].length) {
38f33a44 1939 var point = this.layout_.points[pos+row];
1940
1941 if (this.attr_("stackedGraph")) {
8c03ba63 1942 point = this.layout_.unstackPointAtIndex(pos+row);
38f33a44 1943 }
1944
1945 this.selPoints_.push(point);
16269f6e 1946 }
239c712d
NAG
1947 pos += this.layout_.datasets[i].length;
1948 }
16269f6e 1949 }
50360fd0 1950
16269f6e 1951 if (this.selPoints_.length) {
239c712d
NAG
1952 this.lastx_ = this.selPoints_[0].xval;
1953 this.updateSelection_();
1954 } else {
239c712d
NAG
1955 this.clearSelection();
1956 }
1957
1958};
1959
1960/**
6a1aa64f
DV
1961 * The mouse has left the canvas. Clear out whatever artifacts remain
1962 * @param {Object} event the mouseout event from the browser.
1963 * @private
1964 */
285a6bda 1965Dygraph.prototype.mouseOut_ = function(event) {
a4c6a67c
AV
1966 if (this.attr_("unhighlightCallback")) {
1967 this.attr_("unhighlightCallback")(event);
1968 }
1969
43af96e7 1970 if (this.attr_("hideOverlayOnMouseOut")) {
239c712d 1971 this.clearSelection();
43af96e7 1972 }
6a1aa64f
DV
1973};
1974
239c712d 1975/**
629a09ae
DV
1976 * Clears the current selection (i.e. points that were highlighted by moving
1977 * the mouse over the chart).
239c712d
NAG
1978 */
1979Dygraph.prototype.clearSelection = function() {
1980 // Get rid of the overlay data
2cf95fff 1981 this.canvas_ctx_.clearRect(0, 0, this.width_, this.height_);
91c10d9c 1982 this.setLegendHTML_();
239c712d
NAG
1983 this.selPoints_ = [];
1984 this.lastx_ = -1;
1985}
1986
103b7292 1987/**
629a09ae
DV
1988 * Returns the number of the currently selected row. To get data for this row,
1989 * you can use the getValue method.
1990 * @return { Integer } row number, or -1 if nothing is selected
103b7292
NAG
1991 */
1992Dygraph.prototype.getSelection = function() {
1993 if (!this.selPoints_ || this.selPoints_.length < 1) {
1994 return -1;
1995 }
50360fd0 1996
103b7292
NAG
1997 for (var row=0; row<this.layout_.points.length; row++ ) {
1998 if (this.layout_.points[row].x == this.selPoints_[0].x) {
16269f6e 1999 return row + this.boundaryIds_[0][0];
103b7292
NAG
2000 }
2001 }
2002 return -1;
2e1fcf1a 2003};
103b7292 2004
19589a3e
DV
2005/**
2006 * Number formatting function which mimicks the behavior of %g in printf, i.e.
2007 * either exponential or fixed format (without trailing 0s) is used depending on
2008 * the length of the generated string. The advantage of this format is that
2009 * there is a predictable upper bound on the resulting string length,
2010 * significant figures are not dropped, and normal numbers are not displayed in
2011 * exponential notation.
2012 *
2013 * NOTE: JavaScript's native toPrecision() is NOT a drop-in replacement for %g.
2014 * It creates strings which are too long for absolute values between 10^-4 and
2015 * 10^-6, e.g. '0.00001' instead of '1e-5'. See tests/number-format.html for
2016 * output examples.
2017 *
2018 * @param {Number} x The number to format
2019 * @param {Number} opt_precision The precision to use, default 2.
2020 * @return {String} A string formatted like %g in printf. The max generated
2021 * string length should be precision + 6 (e.g 1.123e+300).
2022 */
2023Dygraph.floatFormat = function(x, opt_precision) {
2024 // Avoid invalid precision values; [1, 21] is the valid range.
2025 var p = Math.min(Math.max(1, opt_precision || 2), 21);
2026
2027 // This is deceptively simple. The actual algorithm comes from:
2028 //
2029 // Max allowed length = p + 4
2030 // where 4 comes from 'e+n' and '.'.
2031 //
2032 // Length of fixed format = 2 + y + p
2033 // where 2 comes from '0.' and y = # of leading zeroes.
2034 //
2035 // Equating the two and solving for y yields y = 2, or 0.00xxxx which is
2036 // 1.0e-3.
2037 //
2038 // Since the behavior of toPrecision() is identical for larger numbers, we
2039 // don't have to worry about the other bound.
2040 //
2041 // Finally, the argument for toExponential() is the number of trailing digits,
2042 // so we take off 1 for the value before the '.'.
2043 return (Math.abs(x) < 1.0e-3 && x != 0.0) ?
2044 x.toExponential(p - 1) : x.toPrecision(p);
2e1fcf1a
DV
2045};
2046
2047/**
629a09ae 2048 * @private
2e1fcf1a
DV
2049 * Return a string version of a number. This respects the digitsAfterDecimal
2050 * and maxNumberWidth options.
2051 * @param {Number} x The number to be formatted
2052 * @param {Dygraph} g The dygraph object
2053 */
2054Dygraph.numberFormatter = function(x, g) {
19589a3e
DV
2055 var sigFigs = g.attr_('sigFigs');
2056
2057 if (sigFigs !== null) {
2058 // User has opted for a fixed number of significant figures.
2059 return Dygraph.floatFormat(x, sigFigs);
2060 }
2061
2e1fcf1a
DV
2062 var digits = g.attr_('digitsAfterDecimal');
2063 var maxNumberWidth = g.attr_('maxNumberWidth');
2064
19589a3e
DV
2065 // switch to scientific notation if we underflow or overflow fixed display.
2066 if (x !== 0.0 &&
2067 (Math.abs(x) >= Math.pow(10, maxNumberWidth) ||
2068 Math.abs(x) < Math.pow(10, -digits))) {
2069 return x.toExponential(digits);
2e1fcf1a
DV
2070 } else {
2071 return '' + Dygraph.round_(x, digits);
2072 }
2073};
32988383 2074
629a09ae
DV
2075/**
2076 * @private
2077 * Converts '9' to '09' (useful for dates)
2078 */
19589a3e
DV
2079Dygraph.zeropad = function(x) {
2080 if (x < 10) return "0" + x; else return "" + x;
2081};
2082
6a1aa64f 2083/**
6b8e33dd
DV
2084 * Return a string version of the hours, minutes and seconds portion of a date.
2085 * @param {Number} date The JavaScript date (ms since epoch)
2086 * @return {String} A time of the form "HH:MM:SS"
2087 * @private
2088 */
bf640e56 2089Dygraph.hmsString_ = function(date) {
285a6bda 2090 var zeropad = Dygraph.zeropad;
6b8e33dd
DV
2091 var d = new Date(date);
2092 if (d.getSeconds()) {
2093 return zeropad(d.getHours()) + ":" +
2094 zeropad(d.getMinutes()) + ":" +
2095 zeropad(d.getSeconds());
6b8e33dd 2096 } else {
054531ca 2097 return zeropad(d.getHours()) + ":" + zeropad(d.getMinutes());
6b8e33dd 2098 }
2e1fcf1a 2099};
6b8e33dd
DV
2100
2101/**
bf640e56
AV
2102 * Convert a JS date to a string appropriate to display on an axis that
2103 * is displaying values at the stated granularity.
2104 * @param {Date} date The date to format
2105 * @param {Number} granularity One of the Dygraph granularity constants
2106 * @return {String} The formatted date
2107 * @private
2108 */
2109Dygraph.dateAxisFormatter = function(date, granularity) {
062ef401
JB
2110 if (granularity >= Dygraph.DECADAL) {
2111 return date.strftime('%Y');
2112 } else if (granularity >= Dygraph.MONTHLY) {
bf640e56
AV
2113 return date.strftime('%b %y');
2114 } else {
31eddad3 2115 var frac = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds();
bf640e56
AV
2116 if (frac == 0 || granularity >= Dygraph.DAILY) {
2117 return new Date(date.getTime() + 3600*1000).strftime('%d%b');
2118 } else {
2119 return Dygraph.hmsString_(date.getTime());
2120 }
2121 }
2e1fcf1a 2122};
bf640e56
AV
2123
2124/**
6a1aa64f
DV
2125 * Convert a JS date (millis since epoch) to YYYY/MM/DD
2126 * @param {Number} date The JavaScript date (ms since epoch)
2127 * @return {String} A date of the form "YYYY/MM/DD"
2128 * @private
2129 */
6be8e54c 2130Dygraph.dateString_ = function(date) {
285a6bda 2131 var zeropad = Dygraph.zeropad;
6a1aa64f
DV
2132 var d = new Date(date);
2133
2134 // Get the year:
2135 var year = "" + d.getFullYear();
2136 // Get a 0 padded month string
6b8e33dd 2137 var month = zeropad(d.getMonth() + 1); //months are 0-offset, sigh
6a1aa64f 2138 // Get a 0 padded day string
6b8e33dd 2139 var day = zeropad(d.getDate());
6a1aa64f 2140
6b8e33dd
DV
2141 var ret = "";
2142 var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
bf640e56 2143 if (frac) ret = " " + Dygraph.hmsString_(date);
6b8e33dd
DV
2144
2145 return year + "/" + month + "/" + day + ret;
6a1aa64f
DV
2146};
2147
2148/**
032e4c1d
DV
2149 * Round a number to the specified number of digits past the decimal point.
2150 * @param {Number} num The number to round
2151 * @param {Number} places The number of decimals to which to round
2152 * @return {Number} The rounded number
2153 * @private
2154 */
2155Dygraph.round_ = function(num, places) {
2156 var shift = Math.pow(10, places);
2157 return Math.round(num * shift)/shift;
2158};
2159
2160/**
6a1aa64f
DV
2161 * Fires when there's data available to be graphed.
2162 * @param {String} data Raw CSV data to be plotted
2163 * @private
2164 */
285a6bda 2165Dygraph.prototype.loadedEvent_ = function(data) {
6a1aa64f 2166 this.rawData_ = this.parseCSV_(data);
26ca7938 2167 this.predraw_();
6a1aa64f
DV
2168};
2169
285a6bda 2170Dygraph.prototype.months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
8846615a 2171 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
285a6bda 2172Dygraph.prototype.quarters = ["Jan", "Apr", "Jul", "Oct"];
6a1aa64f
DV
2173
2174/**
2175 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
2176 * @private
2177 */
285a6bda 2178Dygraph.prototype.addXTicks_ = function() {
6a1aa64f 2179 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
7201b11e 2180 var range;
6a1aa64f 2181 if (this.dateWindow_) {
7201b11e 2182 range = [this.dateWindow_[0], this.dateWindow_[1]];
6a1aa64f 2183 } else {
7201b11e
JB
2184 range = [this.rawData_[0][0], this.rawData_[this.rawData_.length - 1][0]];
2185 }
2186
032e4c1d 2187 var xTicks = this.attr_('xTicker')(range[0], range[1], this);
7201b11e 2188 this.layout_.updateOptions({xTicks: xTicks});
32988383
DV
2189};
2190
2191// Time granularity enumeration
285a6bda 2192Dygraph.SECONDLY = 0;
20a41c17
DV
2193Dygraph.TWO_SECONDLY = 1;
2194Dygraph.FIVE_SECONDLY = 2;
2195Dygraph.TEN_SECONDLY = 3;
2196Dygraph.THIRTY_SECONDLY = 4;
2197Dygraph.MINUTELY = 5;
2198Dygraph.TWO_MINUTELY = 6;
2199Dygraph.FIVE_MINUTELY = 7;
2200Dygraph.TEN_MINUTELY = 8;
2201Dygraph.THIRTY_MINUTELY = 9;
2202Dygraph.HOURLY = 10;
2203Dygraph.TWO_HOURLY = 11;
2204Dygraph.SIX_HOURLY = 12;
2205Dygraph.DAILY = 13;
2206Dygraph.WEEKLY = 14;
2207Dygraph.MONTHLY = 15;
2208Dygraph.QUARTERLY = 16;
2209Dygraph.BIANNUAL = 17;
2210Dygraph.ANNUAL = 18;
2211Dygraph.DECADAL = 19;
062ef401
JB
2212Dygraph.CENTENNIAL = 20;
2213Dygraph.NUM_GRANULARITIES = 21;
285a6bda
DV
2214
2215Dygraph.SHORT_SPACINGS = [];
2216Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY] = 1000 * 1;
20a41c17
DV
2217Dygraph.SHORT_SPACINGS[Dygraph.TWO_SECONDLY] = 1000 * 2;
2218Dygraph.SHORT_SPACINGS[Dygraph.FIVE_SECONDLY] = 1000 * 5;
285a6bda
DV
2219Dygraph.SHORT_SPACINGS[Dygraph.TEN_SECONDLY] = 1000 * 10;
2220Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_SECONDLY] = 1000 * 30;
2221Dygraph.SHORT_SPACINGS[Dygraph.MINUTELY] = 1000 * 60;
20a41c17
DV
2222Dygraph.SHORT_SPACINGS[Dygraph.TWO_MINUTELY] = 1000 * 60 * 2;
2223Dygraph.SHORT_SPACINGS[Dygraph.FIVE_MINUTELY] = 1000 * 60 * 5;
285a6bda
DV
2224Dygraph.SHORT_SPACINGS[Dygraph.TEN_MINUTELY] = 1000 * 60 * 10;
2225Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_MINUTELY] = 1000 * 60 * 30;
2226Dygraph.SHORT_SPACINGS[Dygraph.HOURLY] = 1000 * 3600;
20a41c17 2227Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY] = 1000 * 3600 * 2;
805d5519 2228Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY] = 1000 * 3600 * 6;
285a6bda
DV
2229Dygraph.SHORT_SPACINGS[Dygraph.DAILY] = 1000 * 86400;
2230Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY] = 1000 * 604800;
32988383 2231
629a09ae
DV
2232/**
2233 * @private
2234 * If we used this time granularity, how many ticks would there be?
2235 * This is only an approximation, but it's generally good enough.
2236 */
285a6bda
DV
2237Dygraph.prototype.NumXTicks = function(start_time, end_time, granularity) {
2238 if (granularity < Dygraph.MONTHLY) {
32988383 2239 // Generate one tick mark for every fixed interval of time.
285a6bda 2240 var spacing = Dygraph.SHORT_SPACINGS[granularity];
32988383
DV
2241 return Math.floor(0.5 + 1.0 * (end_time - start_time) / spacing);
2242 } else {
2243 var year_mod = 1; // e.g. to only print one point every 10 years.
2244 var num_months = 12;
285a6bda
DV
2245 if (granularity == Dygraph.QUARTERLY) num_months = 3;
2246 if (granularity == Dygraph.BIANNUAL) num_months = 2;
2247 if (granularity == Dygraph.ANNUAL) num_months = 1;
2248 if (granularity == Dygraph.DECADAL) { num_months = 1; year_mod = 10; }
062ef401 2249 if (granularity == Dygraph.CENTENNIAL) { num_months = 1; year_mod = 100; }
32988383
DV
2250
2251 var msInYear = 365.2524 * 24 * 3600 * 1000;
2252 var num_years = 1.0 * (end_time - start_time) / msInYear;
2253 return Math.floor(0.5 + 1.0 * num_years * num_months / year_mod);
2254 }
2255};
2256
629a09ae
DV
2257/**
2258 * @private
2259 *
2260 * Construct an x-axis of nicely-formatted times on meaningful boundaries
2261 * (e.g. 'Jan 09' rather than 'Jan 22, 2009').
2262 *
2263 * Returns an array containing {v: millis, label: label} dictionaries.
2264 */
285a6bda 2265Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) {
bf640e56 2266 var formatter = this.attr_("xAxisLabelFormatter");
32988383 2267 var ticks = [];
285a6bda 2268 if (granularity < Dygraph.MONTHLY) {
32988383 2269 // Generate one tick mark for every fixed interval of time.
285a6bda 2270 var spacing = Dygraph.SHORT_SPACINGS[granularity];
3d29302c 2271 var format = '%d%b'; // e.g. "1Jan"
076c9622
DV
2272
2273 // Find a time less than start_time which occurs on a "nice" time boundary
2274 // for this granularity.
2275 var g = spacing / 1000;
076c9622
DV
2276 var d = new Date(start_time);
2277 if (g <= 60) { // seconds
2278 var x = d.getSeconds(); d.setSeconds(x - x % g);
2279 } else {
2280 d.setSeconds(0);
2281 g /= 60;
2282 if (g <= 60) { // minutes
2283 var x = d.getMinutes(); d.setMinutes(x - x % g);
2284 } else {
2285 d.setMinutes(0);
2286 g /= 60;
2287
2288 if (g <= 24) { // days
2289 var x = d.getHours(); d.setHours(x - x % g);
2290 } else {
2291 d.setHours(0);
2292 g /= 24;
2293
2294 if (g == 7) { // one week
20a41c17 2295 d.setDate(d.getDate() - d.getDay());
076c9622
DV
2296 }
2297 }
2298 }
328bb812 2299 }
076c9622
DV
2300 start_time = d.getTime();
2301
32988383 2302 for (var t = start_time; t <= end_time; t += spacing) {
bf640e56 2303 ticks.push({ v:t, label: formatter(new Date(t), granularity) });
32988383
DV
2304 }
2305 } else {
2306 // Display a tick mark on the first of a set of months of each year.
2307 // Years get a tick mark iff y % year_mod == 0. This is useful for
2308 // displaying a tick mark once every 10 years, say, on long time scales.
2309 var months;
2310 var year_mod = 1; // e.g. to only print one point every 10 years.
2311
285a6bda 2312 if (granularity == Dygraph.MONTHLY) {
32988383 2313 months = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
285a6bda 2314 } else if (granularity == Dygraph.QUARTERLY) {
32988383 2315 months = [ 0, 3, 6, 9 ];
285a6bda 2316 } else if (granularity == Dygraph.BIANNUAL) {
32988383 2317 months = [ 0, 6 ];
285a6bda 2318 } else if (granularity == Dygraph.ANNUAL) {
32988383 2319 months = [ 0 ];
285a6bda 2320 } else if (granularity == Dygraph.DECADAL) {
32988383
DV
2321 months = [ 0 ];
2322 year_mod = 10;
062ef401
JB
2323 } else if (granularity == Dygraph.CENTENNIAL) {
2324 months = [ 0 ];
2325 year_mod = 100;
2326 } else {
2327 this.warn("Span of dates is too long");
32988383
DV
2328 }
2329
2330 var start_year = new Date(start_time).getFullYear();
2331 var end_year = new Date(end_time).getFullYear();
285a6bda 2332 var zeropad = Dygraph.zeropad;
32988383
DV
2333 for (var i = start_year; i <= end_year; i++) {
2334 if (i % year_mod != 0) continue;
2335 for (var j = 0; j < months.length; j++) {
2336 var date_str = i + "/" + zeropad(1 + months[j]) + "/01";
d96b7d1a 2337 var t = Dygraph.dateStrToMillis(date_str);
32988383 2338 if (t < start_time || t > end_time) continue;
bf640e56 2339 ticks.push({ v:t, label: formatter(new Date(t), granularity) });
32988383
DV
2340 }
2341 }
2342 }
2343
2344 return ticks;
2345};
2346
6a1aa64f
DV
2347
2348/**
2349 * Add ticks to the x-axis based on a date range.
2350 * @param {Number} startDate Start of the date window (millis since epoch)
2351 * @param {Number} endDate End of the date window (millis since epoch)
629a09ae
DV
2352 * @param {Dygraph} self The dygraph object
2353 * @return { [Object] } Array of {label, value} tuples.
6a1aa64f
DV
2354 * @public
2355 */
285a6bda 2356Dygraph.dateTicker = function(startDate, endDate, self) {
629a09ae 2357 // TODO(danvk): why does this take 'self' as a param?
32988383 2358 var chosen = -1;
285a6bda
DV
2359 for (var i = 0; i < Dygraph.NUM_GRANULARITIES; i++) {
2360 var num_ticks = self.NumXTicks(startDate, endDate, i);
2361 if (self.width_ / num_ticks >= self.attr_('pixelsPerXLabel')) {
32988383
DV
2362 chosen = i;
2363 break;
2769de62 2364 }
6a1aa64f
DV
2365 }
2366
32988383 2367 if (chosen >= 0) {
285a6bda 2368 return self.GetXAxis(startDate, endDate, chosen);
6a1aa64f 2369 } else {
32988383 2370 // TODO(danvk): signal error.
6a1aa64f 2371 }
6a1aa64f
DV
2372};
2373
629a09ae
DV
2374/**
2375 * @private
2376 * This is a list of human-friendly values at which to show tick marks on a log
2377 * scale. It is k * 10^n, where k=1..9 and n=-39..+39, so:
2378 * ..., 1, 2, 3, 4, 5, ..., 9, 10, 20, 30, ..., 90, 100, 200, 300, ...
2379 * NOTE: this assumes that Dygraph.LOG_SCALE = 10.
2380 */
0cfa06d1 2381Dygraph.PREFERRED_LOG_TICK_VALUES = function() {
6821efbe
RK
2382 var vals = [];
2383 for (var power = -39; power <= 39; power++) {
2384 var range = Math.pow(10, power);
4b467120
RK
2385 for (var mult = 1; mult <= 9; mult++) {
2386 var val = range * mult;
6821efbe
RK
2387 vals.push(val);
2388 }
2389 }
2390 return vals;
2391}();
2392
629a09ae
DV
2393/**
2394 * @private
2395 * Implementation of binary search over an array.
2396 * Currently does not work when val is outside the range of arry's values.
2397 * @param { Integer } val the value to search for
2398 * @param { Integer[] } arry is the value over which to search
2399 * @param { Integer } abs If abs > 0, find the lowest entry greater than val
2400 * If abs < 0, find the highest entry less than val.
2401 * if abs == 0, find the entry that equals val.
2402 * @param { Integer } [low] The first index in arry to consider (optional)
2403 * @param { Integer } [high] The last index in arry to consider (optional)
2404 */
0cfa06d1
RK
2405Dygraph.binarySearch = function(val, arry, abs, low, high) {
2406 if (low == null || high == null) {
2407 low = 0;
2408 high = arry.length - 1;
2409 }
2410 if (low > high) {
2411 return -1;
2412 }
2413 if (abs == null) {
2414 abs = 0;
2415 }
2416 var validIndex = function(idx) {
2417 return idx >= 0 && idx < arry.length;
2418 }
2419 var mid = parseInt((low + high) / 2);
2420 var element = arry[mid];
2421 if (element == val) {
2422 return mid;
2423 }
2424 if (element > val) {
2425 if (abs > 0) {
2426 // Accept if element > val, but also if prior element < val.
2427 var idx = mid - 1;
2428 if (validIndex(idx) && arry[idx] < val) {
2429 return mid;
2430 }
2431 }
c1bc242a 2432 return Dygraph.binarySearch(val, arry, abs, low, mid - 1);
0cfa06d1
RK
2433 }
2434 if (element < val) {
2435 if (abs < 0) {
2436 // Accept if element < val, but also if prior element > val.
2437 var idx = mid + 1;
2438 if (validIndex(idx) && arry[idx] > val) {
2439 return mid;
2440 }
2441 }
2442 return Dygraph.binarySearch(val, arry, abs, mid + 1, high);
2443 }
60a19014 2444};
0cfa06d1 2445
629a09ae 2446// TODO(konigsberg): Update comment.
6a1aa64f
DV
2447/**
2448 * Add ticks when the x axis has numbers on it (instead of dates)
ff022deb 2449 *
7d0e7a0d
RK
2450 * @param {Number} minV minimum value
2451 * @param {Number} maxV maximum value
84fc6aa7 2452 * @param self
f30cf740 2453 * @param {function} attribute accessor function.
629a09ae 2454 * @return {[Object]} Array of {label, value} tuples.
6a1aa64f 2455 */
0d64e596 2456Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) {
70c80071
DV
2457 var attr = function(k) {
2458 if (axis_props && axis_props.hasOwnProperty(k)) return axis_props[k];
2459 return self.attr_(k);
2460 };
f09fc545 2461
0d64e596
DV
2462 var ticks = [];
2463 if (vals) {
2464 for (var i = 0; i < vals.length; i++) {
e863a17d 2465 ticks.push({v: vals[i]});
0d64e596 2466 }
f09e46d4 2467 } else {
7d0e7a0d 2468 if (axis_props && attr("logscale")) {
ff022deb 2469 var pixelsPerTick = attr('pixelsPerYLabel');
7d0e7a0d 2470 // NOTE(konigsberg): Dan, should self.height_ be self.plotter_.area.h?
ff022deb 2471 var nTicks = Math.floor(self.height_ / pixelsPerTick);
0cfa06d1
RK
2472 var minIdx = Dygraph.binarySearch(minV, Dygraph.PREFERRED_LOG_TICK_VALUES, 1);
2473 var maxIdx = Dygraph.binarySearch(maxV, Dygraph.PREFERRED_LOG_TICK_VALUES, -1);
2474 if (minIdx == -1) {
6821efbe
RK
2475 minIdx = 0;
2476 }
0cfa06d1
RK
2477 if (maxIdx == -1) {
2478 maxIdx = Dygraph.PREFERRED_LOG_TICK_VALUES.length - 1;
6821efbe 2479 }
0cfa06d1
RK
2480 // Count the number of tick values would appear, if we can get at least
2481 // nTicks / 4 accept them.
00aa7f61 2482 var lastDisplayed = null;
0cfa06d1 2483 if (maxIdx - minIdx >= nTicks / 4) {
00aa7f61 2484 var axisId = axis_props.yAxisId;
0cfa06d1
RK
2485 for (var idx = maxIdx; idx >= minIdx; idx--) {
2486 var tickValue = Dygraph.PREFERRED_LOG_TICK_VALUES[idx];
00aa7f61
RK
2487 var domCoord = axis_props.g.toDomYCoord(tickValue, axisId);
2488 var tick = { v: tickValue };
2489 if (lastDisplayed == null) {
2490 lastDisplayed = {
2491 tickValue : tickValue,
2492 domCoord : domCoord
2493 };
2494 } else {
2495 if (domCoord - lastDisplayed.domCoord >= pixelsPerTick) {
2496 lastDisplayed = {
2497 tickValue : tickValue,
2498 domCoord : domCoord
2499 };
2500 } else {
c1bc242a 2501 tick.label = "";
00aa7f61
RK
2502 }
2503 }
2504 ticks.push(tick);
6821efbe 2505 }
0cfa06d1
RK
2506 // Since we went in backwards order.
2507 ticks.reverse();
6821efbe 2508 }
f09e46d4 2509 }
c1bc242a 2510
6821efbe
RK
2511 // ticks.length won't be 0 if the log scale function finds values to insert.
2512 if (ticks.length == 0) {
ff022deb
RK
2513 // Basic idea:
2514 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
2515 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks).
2516 // The first spacing greater than pixelsPerYLabel is what we use.
2517 // TODO(danvk): version that works on a log scale.
0d64e596 2518 if (attr("labelsKMG2")) {
ff022deb 2519 var mults = [1, 2, 4, 8];
0d64e596 2520 } else {
ff022deb 2521 var mults = [1, 2, 5];
0d64e596 2522 }
ff022deb
RK
2523 var scale, low_val, high_val, nTicks;
2524 // TODO(danvk): make it possible to set this for x- and y-axes independently.
2525 var pixelsPerTick = attr('pixelsPerYLabel');
2526 for (var i = -10; i < 50; i++) {
2527 if (attr("labelsKMG2")) {
2528 var base_scale = Math.pow(16, i);
2529 } else {
2530 var base_scale = Math.pow(10, i);
2531 }
2532 for (var j = 0; j < mults.length; j++) {
2533 scale = base_scale * mults[j];
2534 low_val = Math.floor(minV / scale) * scale;
2535 high_val = Math.ceil(maxV / scale) * scale;
2536 nTicks = Math.abs(high_val - low_val) / scale;
2537 var spacing = self.height_ / nTicks;
2538 // wish I could break out of both loops at once...
2539 if (spacing > pixelsPerTick) break;
2540 }
0d64e596
DV
2541 if (spacing > pixelsPerTick) break;
2542 }
0d64e596 2543
ff022deb
RK
2544 // Construct the set of ticks.
2545 // Allow reverse y-axis if it's explicitly requested.
2546 if (low_val > high_val) scale *= -1;
2547 for (var i = 0; i < nTicks; i++) {
2548 var tickV = low_val + i * scale;
2549 ticks.push( {v: tickV} );
2550 }
0d64e596 2551 }
6a1aa64f
DV
2552 }
2553
0d64e596 2554 // Add formatted labels to the ticks.
ed11be50
DV
2555 var k;
2556 var k_labels = [];
f09fc545 2557 if (attr("labelsKMB")) {
ed11be50
DV
2558 k = 1000;
2559 k_labels = [ "K", "M", "B", "T" ];
2560 }
f09fc545 2561 if (attr("labelsKMG2")) {
ed11be50
DV
2562 if (k) self.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
2563 k = 1024;
2564 k_labels = [ "k", "M", "G", "T" ];
2565 }
3c1d225b
JB
2566 var formatter = attr('yAxisLabelFormatter') ?
2567 attr('yAxisLabelFormatter') : attr('yValueFormatter');
2568
0cfa06d1 2569 // Add labels to the ticks.
0d64e596 2570 for (var i = 0; i < ticks.length; i++) {
e863a17d 2571 if (ticks[i].label !== undefined) continue; // Use current label.
0d64e596 2572 var tickV = ticks[i].v;
0af6e346 2573 var absTickV = Math.abs(tickV);
2e1fcf1a 2574 var label = formatter(tickV, self);
3c1d225b 2575 if (k_labels.length > 0) {
ed11be50
DV
2576 // Round up to an appropriate unit.
2577 var n = k*k*k*k;
2578 for (var j = 3; j >= 0; j--, n /= k) {
2579 if (absTickV >= n) {
17d0210c 2580 label = Dygraph.round_(tickV / n, attr('digitsAfterDecimal')) + k_labels[j];
ed11be50
DV
2581 break;
2582 }
afefbcdb 2583 }
6a1aa64f 2584 }
d916677a 2585 ticks[i].label = label;
6a1aa64f 2586 }
d916677a 2587
032e4c1d 2588 return ticks;
6a1aa64f
DV
2589};
2590
629a09ae
DV
2591/**
2592 * @private
2593 * Computes the range of the data series (including confidence intervals).
2594 * @param { [Array] } series either [ [x1, y1], [x2, y2], ... ] or
2595 * [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
2596 * @return [low, high]
2597 */
5011e7a1
DV
2598Dygraph.prototype.extremeValues_ = function(series) {
2599 var minY = null, maxY = null;
2600
9922b78b 2601 var bars = this.attr_("errorBars") || this.attr_("customBars");
5011e7a1
DV
2602 if (bars) {
2603 // With custom bars, maxY is the max of the high values.
2604 for (var j = 0; j < series.length; j++) {
2605 var y = series[j][1][0];
2606 if (!y) continue;
2607 var low = y - series[j][1][1];
2608 var high = y + series[j][1][2];
2609 if (low > y) low = y; // this can happen with custom bars,
2610 if (high < y) high = y; // e.g. in tests/custom-bars.html
2611 if (maxY == null || high > maxY) {
2612 maxY = high;
2613 }
2614 if (minY == null || low < minY) {
2615 minY = low;
2616 }
2617 }
2618 } else {
2619 for (var j = 0; j < series.length; j++) {
2620 var y = series[j][1];
d12999d3 2621 if (y === null || isNaN(y)) continue;
5011e7a1
DV
2622 if (maxY == null || y > maxY) {
2623 maxY = y;
2624 }
2625 if (minY == null || y < minY) {
2626 minY = y;
2627 }
2628 }
2629 }
2630
2631 return [minY, maxY];
2632};
2633
6a1aa64f 2634/**
629a09ae 2635 * @private
26ca7938
DV
2636 * This function is called once when the chart's data is changed or the options
2637 * dictionary is updated. It is _not_ called when the user pans or zooms. The
2638 * idea is that values derived from the chart's data can be computed here,
2639 * rather than every time the chart is drawn. This includes things like the
2640 * number of axes, rolling averages, etc.
2641 */
2642Dygraph.prototype.predraw_ = function() {
2643 // TODO(danvk): move more computations out of drawGraph_ and into here.
2644 this.computeYAxes_();
2645
2646 // Create a new plotter.
70c80071 2647 if (this.plotter_) this.plotter_.clear();
26ca7938 2648 this.plotter_ = new DygraphCanvasRenderer(this,
2cf95fff
RK
2649 this.hidden_,
2650 this.hidden_ctx_,
2651 this.layout_,
26ca7938
DV
2652 this.renderOptions_);
2653
0abfbd7e
DV
2654 // The roller sits in the bottom left corner of the chart. We don't know where
2655 // this will be until the options are available, so it's positioned here.
8c69de65 2656 this.createRollInterface_();
26ca7938 2657
0abfbd7e
DV
2658 // Same thing applies for the labelsDiv. It's right edge should be flush with
2659 // the right edge of the charting area (which may not be the same as the right
2660 // edge of the div, if we have two y-axes.
2661 this.positionLabelsDiv_();
2662
26ca7938
DV
2663 // If the data or options have changed, then we'd better redraw.
2664 this.drawGraph_();
2665};
2666
2667/**
2668 * Update the graph with new data. This method is called when the viewing area
2669 * has changed. If the underlying data or options have changed, predraw_ will
2670 * be called before drawGraph_ is called.
6a1aa64f
DV
2671 * @private
2672 */
26ca7938
DV
2673Dygraph.prototype.drawGraph_ = function() {
2674 var data = this.rawData_;
2675
fe0b7c03
DV
2676 // This is used to set the second parameter to drawCallback, below.
2677 var is_initial_draw = this.is_initial_draw_;
2678 this.is_initial_draw_ = false;
2679
3bd9c228 2680 var minY = null, maxY = null;
6a1aa64f 2681 this.layout_.removeAllDatasets();
285a6bda 2682 this.setColors_();
9317362d 2683 this.attrs_['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
285a6bda 2684
354e15ab
DE
2685 // Loop over the fields (series). Go from the last to the first,
2686 // because if they're stacked that's how we accumulate the values.
43af96e7 2687
354e15ab
DE
2688 var cumulative_y = []; // For stacked series.
2689 var datasets = [];
2690
f09fc545
DV
2691 var extremes = {}; // series name -> [low, high]
2692
354e15ab
DE
2693 // Loop over all fields and create datasets
2694 for (var i = data[0].length - 1; i >= 1; i--) {
1cf11047
DV
2695 if (!this.visibility()[i - 1]) continue;
2696
f09fc545 2697 var seriesName = this.attr_("labels")[i];
450fe64b 2698 var connectSeparatedPoints = this.attr_('connectSeparatedPoints', i);
6e6a2b0a 2699 var logScale = this.attr_('logscale', i);
450fe64b 2700
6a1aa64f
DV
2701 var series = [];
2702 for (var j = 0; j < data.length; j++) {
6e6a2b0a
RK
2703 var date = data[j][0];
2704 var point = data[j][i];
2705 if (logScale) {
2706 // On the log scale, points less than zero do not exist.
2707 // This will create a gap in the chart. Note that this ignores
2708 // connectSeparatedPoints.
e863a17d 2709 if (point <= 0) {
6e6a2b0a
RK
2710 point = null;
2711 }
2712 series.push([date, point]);
2713 } else {
2714 if (point != null || !connectSeparatedPoints) {
2715 series.push([date, point]);
2716 }
f032c51d 2717 }
6a1aa64f 2718 }
2f5e7e1a
DV
2719
2720 // TODO(danvk): move this into predraw_. It's insane to do it here.
6a1aa64f
DV
2721 series = this.rollingAverage(series, this.rollPeriod_);
2722
2723 // Prune down to the desired range, if necessary (for zooming)
1a26f3fb
DV
2724 // Because there can be lines going to points outside of the visible area,
2725 // we actually prune to visible points, plus one on either side.
9922b78b 2726 var bars = this.attr_("errorBars") || this.attr_("customBars");
6a1aa64f
DV
2727 if (this.dateWindow_) {
2728 var low = this.dateWindow_[0];
2729 var high= this.dateWindow_[1];
2730 var pruned = [];
1a26f3fb
DV
2731 // TODO(danvk): do binary search instead of linear search.
2732 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
2733 var firstIdx = null, lastIdx = null;
6a1aa64f 2734 for (var k = 0; k < series.length; k++) {
1a26f3fb
DV
2735 if (series[k][0] >= low && firstIdx === null) {
2736 firstIdx = k;
2737 }
2738 if (series[k][0] <= high) {
2739 lastIdx = k;
6a1aa64f
DV
2740 }
2741 }
1a26f3fb
DV
2742 if (firstIdx === null) firstIdx = 0;
2743 if (firstIdx > 0) firstIdx--;
2744 if (lastIdx === null) lastIdx = series.length - 1;
2745 if (lastIdx < series.length - 1) lastIdx++;
16269f6e 2746 this.boundaryIds_[i-1] = [firstIdx, lastIdx];
1a26f3fb
DV
2747 for (var k = firstIdx; k <= lastIdx; k++) {
2748 pruned.push(series[k]);
6a1aa64f
DV
2749 }
2750 series = pruned;
16269f6e
NAG
2751 } else {
2752 this.boundaryIds_[i-1] = [0, series.length-1];
6a1aa64f
DV
2753 }
2754
f09fc545 2755 var seriesExtremes = this.extremeValues_(series);
5011e7a1 2756
6a1aa64f 2757 if (bars) {
354e15ab
DE
2758 for (var j=0; j<series.length; j++) {
2759 val = [series[j][0], series[j][1][0], series[j][1][1], series[j][1][2]];
2760 series[j] = val;
2761 }
43af96e7 2762 } else if (this.attr_("stackedGraph")) {
43af96e7
NK
2763 var l = series.length;
2764 var actual_y;
2765 for (var j = 0; j < l; j++) {
354e15ab
DE
2766 // If one data set has a NaN, let all subsequent stacked
2767 // sets inherit the NaN -- only start at 0 for the first set.
2768 var x = series[j][0];
41b0f691 2769 if (cumulative_y[x] === undefined) {
354e15ab 2770 cumulative_y[x] = 0;
41b0f691 2771 }
43af96e7
NK
2772
2773 actual_y = series[j][1];
354e15ab 2774 cumulative_y[x] += actual_y;
43af96e7 2775
354e15ab 2776 series[j] = [x, cumulative_y[x]]
43af96e7 2777
41b0f691
DV
2778 if (cumulative_y[x] > seriesExtremes[1]) {
2779 seriesExtremes[1] = cumulative_y[x];
2780 }
2781 if (cumulative_y[x] < seriesExtremes[0]) {
2782 seriesExtremes[0] = cumulative_y[x];
2783 }
43af96e7 2784 }
6a1aa64f 2785 }
41b0f691 2786 extremes[seriesName] = seriesExtremes;
354e15ab
DE
2787
2788 datasets[i] = series;
6a1aa64f
DV
2789 }
2790
354e15ab 2791 for (var i = 1; i < datasets.length; i++) {
4523c1f6 2792 if (!this.visibility()[i - 1]) continue;
354e15ab 2793 this.layout_.addDataset(this.attr_("labels")[i], datasets[i]);
43af96e7
NK
2794 }
2795
6faebb69
JB
2796 this.computeYAxisRanges_(extremes);
2797 this.layout_.updateOptions( { yAxes: this.axes_,
2798 seriesToAxisMap: this.seriesToAxisMap_
9012dd21 2799 } );
6a1aa64f
DV
2800 this.addXTicks_();
2801
81856f70
NN
2802 // Save the X axis zoomed status as the updateOptions call will tend to set it errorneously
2803 var tmp_zoomed_x = this.zoomed_x_;
6a1aa64f 2804 // Tell PlotKit to use this new data and render itself
d033ae1c 2805 this.layout_.updateOptions({dateWindow: this.dateWindow_});
81856f70 2806 this.zoomed_x_ = tmp_zoomed_x;
6a1aa64f
DV
2807 this.layout_.evaluateWithError();
2808 this.plotter_.clear();
2809 this.plotter_.render();
f6401bf6 2810 this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width,
2f5e7e1a 2811 this.canvas_.height);
599fb4ad 2812
2fccd3dc
DV
2813 if (is_initial_draw) {
2814 // Generate a static legend before any particular point is selected.
91c10d9c 2815 this.setLegendHTML_();
06303c32
DV
2816 } else {
2817 if (typeof(this.selPoints_) !== 'undefined' && this.selPoints_.length) {
2818 this.lastx_ = this.selPoints_[0].xval;
2819 this.updateSelection_();
2820 } else {
2821 this.clearSelection();
2822 }
2fccd3dc
DV
2823 }
2824
599fb4ad 2825 if (this.attr_("drawCallback") !== null) {
fe0b7c03 2826 this.attr_("drawCallback")(this, is_initial_draw);
599fb4ad 2827 }
6a1aa64f
DV
2828};
2829
2830/**
629a09ae 2831 * @private
26ca7938
DV
2832 * Determine properties of the y-axes which are independent of the data
2833 * currently being displayed. This includes things like the number of axes and
2834 * the style of the axes. It does not include the range of each axis and its
2835 * tick marks.
2836 * This fills in this.axes_ and this.seriesToAxisMap_.
2837 * axes_ = [ { options } ]
2838 * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... }
2839 * indices are into the axes_ array.
f09fc545 2840 */
26ca7938 2841Dygraph.prototype.computeYAxes_ = function() {
00aa7f61 2842 this.axes_ = [{ yAxisId : 0, g : this }]; // always have at least one y-axis.
26ca7938
DV
2843 this.seriesToAxisMap_ = {};
2844
2845 // Get a list of series names.
2846 var labels = this.attr_("labels");
1c77a3a1 2847 var series = {};
26ca7938 2848 for (var i = 1; i < labels.length; i++) series[labels[i]] = (i - 1);
f09fc545
DV
2849
2850 // all options which could be applied per-axis:
2851 var axisOptions = [
2852 'includeZero',
2853 'valueRange',
2854 'labelsKMB',
2855 'labelsKMG2',
2856 'pixelsPerYLabel',
2857 'yAxisLabelWidth',
2858 'axisLabelFontSize',
7d0e7a0d
RK
2859 'axisTickSize',
2860 'logscale'
f09fc545
DV
2861 ];
2862
2863 // Copy global axis options over to the first axis.
2864 for (var i = 0; i < axisOptions.length; i++) {
2865 var k = axisOptions[i];
2866 var v = this.attr_(k);
26ca7938 2867 if (v) this.axes_[0][k] = v;
f09fc545
DV
2868 }
2869
2870 // Go through once and add all the axes.
26ca7938
DV
2871 for (var seriesName in series) {
2872 if (!series.hasOwnProperty(seriesName)) continue;
f09fc545
DV
2873 var axis = this.attr_("axis", seriesName);
2874 if (axis == null) {
26ca7938 2875 this.seriesToAxisMap_[seriesName] = 0;
f09fc545
DV
2876 continue;
2877 }
2878 if (typeof(axis) == 'object') {
2879 // Add a new axis, making a copy of its per-axis options.
2880 var opts = {};
26ca7938 2881 Dygraph.update(opts, this.axes_[0]);
f09fc545 2882 Dygraph.update(opts, { valueRange: null }); // shouldn't inherit this.
00aa7f61
RK
2883 var yAxisId = this.axes_.length;
2884 opts.yAxisId = yAxisId;
2885 opts.g = this;
f09fc545 2886 Dygraph.update(opts, axis);
26ca7938 2887 this.axes_.push(opts);
00aa7f61 2888 this.seriesToAxisMap_[seriesName] = yAxisId;
f09fc545
DV
2889 }
2890 }
2891
2892 // Go through one more time and assign series to an axis defined by another
2893 // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } }
26ca7938
DV
2894 for (var seriesName in series) {
2895 if (!series.hasOwnProperty(seriesName)) continue;
f09fc545
DV
2896 var axis = this.attr_("axis", seriesName);
2897 if (typeof(axis) == 'string') {
26ca7938 2898 if (!this.seriesToAxisMap_.hasOwnProperty(axis)) {
f09fc545
DV
2899 this.error("Series " + seriesName + " wants to share a y-axis with " +
2900 "series " + axis + ", which does not define its own axis.");
2901 return null;
2902 }
26ca7938
DV
2903 var idx = this.seriesToAxisMap_[axis];
2904 this.seriesToAxisMap_[seriesName] = idx;
f09fc545
DV
2905 }
2906 }
1c77a3a1
DV
2907
2908 // Now we remove series from seriesToAxisMap_ which are not visible. We do
2909 // this last so that hiding the first series doesn't destroy the axis
2910 // properties of the primary axis.
2911 var seriesToAxisFiltered = {};
2912 var vis = this.visibility();
2913 for (var i = 1; i < labels.length; i++) {
2914 var s = labels[i];
2915 if (vis[i - 1]) seriesToAxisFiltered[s] = this.seriesToAxisMap_[s];
2916 }
2917 this.seriesToAxisMap_ = seriesToAxisFiltered;
26ca7938
DV
2918};
2919
2920/**
2921 * Returns the number of y-axes on the chart.
2922 * @return {Number} the number of axes.
2923 */
2924Dygraph.prototype.numAxes = function() {
2925 var last_axis = 0;
2926 for (var series in this.seriesToAxisMap_) {
2927 if (!this.seriesToAxisMap_.hasOwnProperty(series)) continue;
2928 var idx = this.seriesToAxisMap_[series];
2929 if (idx > last_axis) last_axis = idx;
2930 }
2931 return 1 + last_axis;
2932};
2933
2934/**
629a09ae 2935 * @private
26ca7938
DV
2936 * Determine the value range and tick marks for each axis.
2937 * @param {Object} extremes A mapping from seriesName -> [low, high]
2938 * This fills in the valueRange and ticks fields in each entry of this.axes_.
2939 */
2940Dygraph.prototype.computeYAxisRanges_ = function(extremes) {
2941 // Build a map from axis number -> [list of series names]
2942 var seriesForAxis = [];
2943 for (var series in this.seriesToAxisMap_) {
2944 if (!this.seriesToAxisMap_.hasOwnProperty(series)) continue;
2945 var idx = this.seriesToAxisMap_[series];
2946 while (seriesForAxis.length <= idx) seriesForAxis.push([]);
2947 seriesForAxis[idx].push(series);
2948 }
f09fc545
DV
2949
2950 // Compute extreme values, a span and tick marks for each axis.
26ca7938
DV
2951 for (var i = 0; i < this.axes_.length; i++) {
2952 var axis = this.axes_[i];
4cac8c7a 2953
06fc69b6
AV
2954 if (!seriesForAxis[i]) {
2955 // If no series are defined or visible then use a reasonable default
2956 axis.extremeRange = [0, 1];
2957 } else {
1c77a3a1 2958 // Calculate the extremes of extremes.
f09fc545
DV
2959 var series = seriesForAxis[i];
2960 var minY = Infinity; // extremes[series[0]][0];
2961 var maxY = -Infinity; // extremes[series[0]][1];
ba049b89 2962 var extremeMinY, extremeMaxY;
f09fc545 2963 for (var j = 0; j < series.length; j++) {
ba049b89
NN
2964 // Only use valid extremes to stop null data series' from corrupting the scale.
2965 extremeMinY = extremes[series[j]][0];
2966 if (extremeMinY != null) {
36dfa958 2967 minY = Math.min(extremeMinY, minY);
ba049b89
NN
2968 }
2969 extremeMaxY = extremes[series[j]][1];
2970 if (extremeMaxY != null) {
36dfa958 2971 maxY = Math.max(extremeMaxY, maxY);
ba049b89 2972 }
f09fc545
DV
2973 }
2974 if (axis.includeZero && minY > 0) minY = 0;
2975
ba049b89 2976 // Ensure we have a valid scale, otherwise defualt to zero for safety.
36dfa958
DV
2977 if (minY == Infinity) minY = 0;
2978 if (maxY == -Infinity) maxY = 0;
ba049b89 2979
f09fc545
DV
2980 // Add some padding and round up to an integer to be human-friendly.
2981 var span = maxY - minY;
2982 // special case: if we have no sense of scale, use +/-10% of the sole value.
2983 if (span == 0) { span = maxY; }
f09fc545 2984
ff022deb
RK
2985 var maxAxisY;
2986 var minAxisY;
7d0e7a0d 2987 if (axis.logscale) {
ff022deb
RK
2988 var maxAxisY = maxY + 0.1 * span;
2989 var minAxisY = minY;
2990 } else {
2991 var maxAxisY = maxY + 0.1 * span;
2992 var minAxisY = minY - 0.1 * span;
f09fc545 2993
ff022deb
RK
2994 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
2995 if (!this.attr_("avoidMinZero")) {
2996 if (minAxisY < 0 && minY >= 0) minAxisY = 0;
2997 if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0;
2998 }
f09fc545 2999
ff022deb
RK
3000 if (this.attr_("includeZero")) {
3001 if (maxY < 0) maxAxisY = 0;
3002 if (minY > 0) minAxisY = 0;
3003 }
f09fc545 3004 }
4cac8c7a
RK
3005 axis.extremeRange = [minAxisY, maxAxisY];
3006 }
3007 if (axis.valueWindow) {
3008 // This is only set if the user has zoomed on the y-axis. It is never set
3009 // by a user. It takes precedence over axis.valueRange because, if you set
3010 // valueRange, you'd still expect to be able to pan.
3011 axis.computedValueRange = [axis.valueWindow[0], axis.valueWindow[1]];
3012 } else if (axis.valueRange) {
3013 // This is a user-set value range for this axis.
3014 axis.computedValueRange = [axis.valueRange[0], axis.valueRange[1]];
3015 } else {
3016 axis.computedValueRange = axis.extremeRange;
f09fc545
DV
3017 }
3018
0d64e596
DV
3019 // Add ticks. By default, all axes inherit the tick positions of the
3020 // primary axis. However, if an axis is specifically marked as having
3021 // independent ticks, then that is permissible as well.
3022 if (i == 0 || axis.independentTicks) {
032e4c1d 3023 axis.ticks =
0d64e596
DV
3024 Dygraph.numericTicks(axis.computedValueRange[0],
3025 axis.computedValueRange[1],
3026 this,
3027 axis);
3028 } else {
3029 var p_axis = this.axes_[0];
3030 var p_ticks = p_axis.ticks;
3031 var p_scale = p_axis.computedValueRange[1] - p_axis.computedValueRange[0];
3032 var scale = axis.computedValueRange[1] - axis.computedValueRange[0];
3033 var tick_values = [];
3034 for (var i = 0; i < p_ticks.length; i++) {
3035 var y_frac = (p_ticks[i].v - p_axis.computedValueRange[0]) / p_scale;
3036 var y_val = axis.computedValueRange[0] + y_frac * scale;
3037 tick_values.push(y_val);
3038 }
3039
032e4c1d 3040 axis.ticks =
0d64e596
DV
3041 Dygraph.numericTicks(axis.computedValueRange[0],
3042 axis.computedValueRange[1],
3043 this, axis, tick_values);
3044 }
f09fc545 3045 }
f09fc545
DV
3046};
3047
3048/**
629a09ae 3049 * @private
6a1aa64f
DV
3050 * Calculates the rolling average of a data set.
3051 * If originalData is [label, val], rolls the average of those.
3052 * If originalData is [label, [, it's interpreted as [value, stddev]
3053 * and the roll is returned in the same form, with appropriately reduced
3054 * stddev for each value.
3055 * Note that this is where fractional input (i.e. '5/10') is converted into
3056 * decimal values.
3057 * @param {Array} originalData The data in the appropriate format (see above)
6faebb69
JB
3058 * @param {Number} rollPeriod The number of points over which to average the
3059 * data
6a1aa64f 3060 */
285a6bda 3061Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) {
6a1aa64f
DV
3062 if (originalData.length < 2)
3063 return originalData;
3064 var rollPeriod = Math.min(rollPeriod, originalData.length - 1);
3065 var rollingData = [];
285a6bda 3066 var sigma = this.attr_("sigma");
6a1aa64f
DV
3067
3068 if (this.fractions_) {
3069 var num = 0;
3070 var den = 0; // numerator/denominator
3071 var mult = 100.0;
3072 for (var i = 0; i < originalData.length; i++) {
3073 num += originalData[i][1][0];
3074 den += originalData[i][1][1];
3075 if (i - rollPeriod >= 0) {
3076 num -= originalData[i - rollPeriod][1][0];
3077 den -= originalData[i - rollPeriod][1][1];
3078 }
3079
3080 var date = originalData[i][0];
3081 var value = den ? num / den : 0.0;
285a6bda 3082 if (this.attr_("errorBars")) {
6a1aa64f
DV
3083 if (this.wilsonInterval_) {
3084 // For more details on this confidence interval, see:
3085 // http://en.wikipedia.org/wiki/Binomial_confidence_interval
3086 if (den) {
3087 var p = value < 0 ? 0 : value, n = den;
3088 var pm = sigma * Math.sqrt(p*(1-p)/n + sigma*sigma/(4*n*n));
3089 var denom = 1 + sigma * sigma / den;
3090 var low = (p + sigma * sigma / (2 * den) - pm) / denom;
3091 var high = (p + sigma * sigma / (2 * den) + pm) / denom;
3092 rollingData[i] = [date,
3093 [p * mult, (p - low) * mult, (high - p) * mult]];
3094 } else {
3095 rollingData[i] = [date, [0, 0, 0]];
3096 }
3097 } else {
3098 var stddev = den ? sigma * Math.sqrt(value * (1 - value) / den) : 1.0;
3099 rollingData[i] = [date, [mult * value, mult * stddev, mult * stddev]];
3100 }
3101 } else {
3102 rollingData[i] = [date, mult * value];
3103 }
3104 }
9922b78b 3105 } else if (this.attr_("customBars")) {
f6885d6a
DV
3106 var low = 0;
3107 var mid = 0;
3108 var high = 0;
3109 var count = 0;
6a1aa64f
DV
3110 for (var i = 0; i < originalData.length; i++) {
3111 var data = originalData[i][1];
3112 var y = data[1];
3113 rollingData[i] = [originalData[i][0], [y, y - data[0], data[2] - y]];
f6885d6a 3114
8b91c51f 3115 if (y != null && !isNaN(y)) {
49a7d0d5
DV
3116 low += data[0];
3117 mid += y;
3118 high += data[2];
3119 count += 1;
3120 }
f6885d6a
DV
3121 if (i - rollPeriod >= 0) {
3122 var prev = originalData[i - rollPeriod];
8b91c51f 3123 if (prev[1][1] != null && !isNaN(prev[1][1])) {
49a7d0d5
DV
3124 low -= prev[1][0];
3125 mid -= prev[1][1];
3126 high -= prev[1][2];
3127 count -= 1;
3128 }
f6885d6a
DV
3129 }
3130 rollingData[i] = [originalData[i][0], [ 1.0 * mid / count,
3131 1.0 * (mid - low) / count,
3132 1.0 * (high - mid) / count ]];
2769de62 3133 }
6a1aa64f
DV
3134 } else {
3135 // Calculate the rolling average for the first rollPeriod - 1 points where
6faebb69 3136 // there is not enough data to roll over the full number of points
6a1aa64f 3137 var num_init_points = Math.min(rollPeriod - 1, originalData.length - 2);
285a6bda 3138 if (!this.attr_("errorBars")){
5011e7a1
DV
3139 if (rollPeriod == 1) {
3140 return originalData;
3141 }
3142
2847c1cf 3143 for (var i = 0; i < originalData.length; i++) {
6a1aa64f 3144 var sum = 0;
5011e7a1 3145 var num_ok = 0;
2847c1cf
DV
3146 for (var j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
3147 var y = originalData[j][1];
8b91c51f 3148 if (y == null || isNaN(y)) continue;
5011e7a1 3149 num_ok++;
2847c1cf 3150 sum += originalData[j][1];
6a1aa64f 3151 }
5011e7a1 3152 if (num_ok) {
2847c1cf 3153 rollingData[i] = [originalData[i][0], sum / num_ok];
5011e7a1 3154 } else {
2847c1cf 3155 rollingData[i] = [originalData[i][0], null];
5011e7a1 3156 }
6a1aa64f 3157 }
2847c1cf
DV
3158
3159 } else {
3160 for (var i = 0; i < originalData.length; i++) {
6a1aa64f
DV
3161 var sum = 0;
3162 var variance = 0;
5011e7a1 3163 var num_ok = 0;
2847c1cf 3164 for (var j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
5011e7a1 3165 var y = originalData[j][1][0];
8b91c51f 3166 if (y == null || isNaN(y)) continue;
5011e7a1 3167 num_ok++;
6a1aa64f
DV
3168 sum += originalData[j][1][0];
3169 variance += Math.pow(originalData[j][1][1], 2);
3170 }
5011e7a1
DV
3171 if (num_ok) {
3172 var stddev = Math.sqrt(variance) / num_ok;
3173 rollingData[i] = [originalData[i][0],
3174 [sum / num_ok, sigma * stddev, sigma * stddev]];
3175 } else {
3176 rollingData[i] = [originalData[i][0], [null, null, null]];
3177 }
6a1aa64f
DV
3178 }
3179 }
3180 }
3181
3182 return rollingData;
3183};
3184
3185/**
629a09ae 3186 * @private
6a1aa64f 3187 * Parses a date, returning the number of milliseconds since epoch. This can be
285a6bda
DV
3188 * passed in as an xValueParser in the Dygraph constructor.
3189 * TODO(danvk): enumerate formats that this understands.
6a1aa64f
DV
3190 * @param {String} A date in YYYYMMDD format.
3191 * @return {Number} Milliseconds since epoch.
6a1aa64f 3192 */
285a6bda 3193Dygraph.dateParser = function(dateStr, self) {
6a1aa64f 3194 var dateStrSlashed;
285a6bda 3195 var d;
986a5026 3196 if (dateStr.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
6a1aa64f 3197 dateStrSlashed = dateStr.replace("-", "/", "g");
353a0294
DV
3198 while (dateStrSlashed.search("-") != -1) {
3199 dateStrSlashed = dateStrSlashed.replace("-", "/");
3200 }
d96b7d1a 3201 d = Dygraph.dateStrToMillis(dateStrSlashed);
2769de62 3202 } else if (dateStr.length == 8) { // e.g. '20090712'
285a6bda 3203 // TODO(danvk): remove support for this format. It's confusing.
6a1aa64f
DV
3204 dateStrSlashed = dateStr.substr(0,4) + "/" + dateStr.substr(4,2)
3205 + "/" + dateStr.substr(6,2);
d96b7d1a 3206 d = Dygraph.dateStrToMillis(dateStrSlashed);
2769de62
DV
3207 } else {
3208 // Any format that Date.parse will accept, e.g. "2009/07/12" or
3209 // "2009/07/12 12:34:56"
d96b7d1a 3210 d = Dygraph.dateStrToMillis(dateStr);
285a6bda
DV
3211 }
3212
3213 if (!d || isNaN(d)) {
3214 self.error("Couldn't parse " + dateStr + " as a date");
3215 }
3216 return d;
3217};
3218
3219/**
3220 * Detects the type of the str (date or numeric) and sets the various
3221 * formatting attributes in this.attrs_ based on this type.
3222 * @param {String} str An x value.
3223 * @private
3224 */
3225Dygraph.prototype.detectTypeFromString_ = function(str) {
3226 var isDate = false;
ea62df82 3227 if (str.indexOf('-') > 0 ||
285a6bda
DV
3228 str.indexOf('/') >= 0 ||
3229 isNaN(parseFloat(str))) {
3230 isDate = true;
3231 } else if (str.length == 8 && str > '19700101' && str < '20371231') {
3232 // TODO(danvk): remove support for this format.
3233 isDate = true;
3234 }
3235
3236 if (isDate) {
3237 this.attrs_.xValueFormatter = Dygraph.dateString_;
3238 this.attrs_.xValueParser = Dygraph.dateParser;
3239 this.attrs_.xTicker = Dygraph.dateTicker;
bf640e56 3240 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
285a6bda 3241 } else {
17d0210c 3242 // TODO(danvk): use Dygraph.numberFormatter here?
032e4c1d 3243 this.attrs_.xValueFormatter = function(x) { return x; };
285a6bda
DV
3244 this.attrs_.xValueParser = function(x) { return parseFloat(x); };
3245 this.attrs_.xTicker = Dygraph.numericTicks;
bf640e56 3246 this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;
6a1aa64f 3247 }
6a1aa64f
DV
3248};
3249
3250/**
5cd7ac68
DV
3251 * Parses the value as a floating point number. This is like the parseFloat()
3252 * built-in, but with a few differences:
3253 * - the empty string is parsed as null, rather than NaN.
3254 * - if the string cannot be parsed at all, an error is logged.
3255 * If the string can't be parsed, this method returns null.
3256 * @param {String} x The string to be parsed
3257 * @param {Number} opt_line_no The line number from which the string comes.
3258 * @param {String} opt_line The text of the line from which the string comes.
3259 * @private
3260 */
3261
3262// Parse the x as a float or return null if it's not a number.
3263Dygraph.prototype.parseFloat_ = function(x, opt_line_no, opt_line) {
3264 var val = parseFloat(x);
3265 if (!isNaN(val)) return val;
3266
3267 // Try to figure out what happeend.
3268 // If the value is the empty string, parse it as null.
3269 if (/^ *$/.test(x)) return null;
3270
3271 // If it was actually "NaN", return it as NaN.
3272 if (/^ *nan *$/i.test(x)) return NaN;
3273
3274 // Looks like a parsing error.
3275 var msg = "Unable to parse '" + x + "' as a number";
3276 if (opt_line !== null && opt_line_no !== null) {
3277 msg += " on line " + (1+opt_line_no) + " ('" + opt_line + "') of CSV.";
3278 }
3279 this.error(msg);
3280
3281 return null;
3282};
3283
3284/**
629a09ae 3285 * @private
6a1aa64f
DV
3286 * Parses a string in a special csv format. We expect a csv file where each
3287 * line is a date point, and the first field in each line is the date string.
3288 * We also expect that all remaining fields represent series.
285a6bda 3289 * if the errorBars attribute is set, then interpret the fields as:
6a1aa64f 3290 * date, series1, stddev1, series2, stddev2, ...
629a09ae 3291 * @param {[Object]} data See above.
285a6bda 3292 *
629a09ae 3293 * @return [Object] An array with one entry for each row. These entries
285a6bda
DV
3294 * are an array of cells in that row. The first entry is the parsed x-value for
3295 * the row. The second, third, etc. are the y-values. These can take on one of
3296 * three forms, depending on the CSV and constructor parameters:
3297 * 1. numeric value
3298 * 2. [ value, stddev ]
3299 * 3. [ low value, center value, high value ]
6a1aa64f 3300 */
285a6bda 3301Dygraph.prototype.parseCSV_ = function(data) {
6a1aa64f
DV
3302 var ret = [];
3303 var lines = data.split("\n");
3d67f03b
DV
3304
3305 // Use the default delimiter or fall back to a tab if that makes sense.
3306 var delim = this.attr_('delimiter');
3307 if (lines[0].indexOf(delim) == -1 && lines[0].indexOf('\t') >= 0) {
3308 delim = '\t';
3309 }
3310
285a6bda 3311 var start = 0;
6a1aa64f 3312 if (this.labelsFromCSV_) {
285a6bda 3313 start = 1;
3d67f03b 3314 this.attrs_.labels = lines[0].split(delim);
6a1aa64f 3315 }
5cd7ac68 3316 var line_no = 0;
03b522a4 3317
285a6bda
DV
3318 var xParser;
3319 var defaultParserSet = false; // attempt to auto-detect x value type
3320 var expectedCols = this.attr_("labels").length;
987840a2 3321 var outOfOrder = false;
6a1aa64f
DV
3322 for (var i = start; i < lines.length; i++) {
3323 var line = lines[i];
5cd7ac68 3324 line_no = i;
6a1aa64f 3325 if (line.length == 0) continue; // skip blank lines
3d67f03b
DV
3326 if (line[0] == '#') continue; // skip comment lines
3327 var inFields = line.split(delim);
285a6bda 3328 if (inFields.length < 2) continue;
6a1aa64f
DV
3329
3330 var fields = [];
285a6bda
DV
3331 if (!defaultParserSet) {
3332 this.detectTypeFromString_(inFields[0]);
3333 xParser = this.attr_("xValueParser");
3334 defaultParserSet = true;
3335 }
3336 fields[0] = xParser(inFields[0], this);
6a1aa64f
DV
3337
3338 // If fractions are expected, parse the numbers as "A/B"
3339 if (this.fractions_) {
3340 for (var j = 1; j < inFields.length; j++) {
3341 // TODO(danvk): figure out an appropriate way to flag parse errors.
3342 var vals = inFields[j].split("/");
7219edb3
DV
3343 if (vals.length != 2) {
3344 this.error('Expected fractional "num/den" values in CSV data ' +
3345 "but found a value '" + inFields[j] + "' on line " +
3346 (1 + i) + " ('" + line + "') which is not of this form.");
3347 fields[j] = [0, 0];
3348 } else {
3349 fields[j] = [this.parseFloat_(vals[0], i, line),
3350 this.parseFloat_(vals[1], i, line)];
3351 }
6a1aa64f 3352 }
285a6bda 3353 } else if (this.attr_("errorBars")) {
6a1aa64f 3354 // If there are error bars, values are (value, stddev) pairs
7219edb3
DV
3355 if (inFields.length % 2 != 1) {
3356 this.error('Expected alternating (value, stdev.) pairs in CSV data ' +
3357 'but line ' + (1 + i) + ' has an odd number of values (' +
3358 (inFields.length - 1) + "): '" + line + "'");
3359 }
3360 for (var j = 1; j < inFields.length; j += 2) {
5cd7ac68
DV
3361 fields[(j + 1) / 2] = [this.parseFloat_(inFields[j], i, line),
3362 this.parseFloat_(inFields[j + 1], i, line)];
7219edb3 3363 }
9922b78b 3364 } else if (this.attr_("customBars")) {
6a1aa64f
DV
3365 // Bars are a low;center;high tuple
3366 for (var j = 1; j < inFields.length; j++) {
327a9279
DV
3367 var val = inFields[j];
3368 if (/^ *$/.test(val)) {
3369 fields[j] = [null, null, null];
3370 } else {
3371 var vals = val.split(";");
3372 if (vals.length == 3) {
3373 fields[j] = [ this.parseFloat_(vals[0], i, line),
3374 this.parseFloat_(vals[1], i, line),
3375 this.parseFloat_(vals[2], i, line) ];
3376 } else {
3377 this.warning('When using customBars, values must be either blank ' +
3378 'or "low;center;high" tuples (got "' + val +
3379 '" on line ' + (1+i));
3380 }
3381 }
6a1aa64f
DV
3382 }
3383 } else {
3384 // Values are just numbers
285a6bda 3385 for (var j = 1; j < inFields.length; j++) {
5cd7ac68 3386 fields[j] = this.parseFloat_(inFields[j], i, line);
285a6bda 3387 }
6a1aa64f 3388 }
987840a2
DV
3389 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
3390 outOfOrder = true;
3391 }
285a6bda
DV
3392
3393 if (fields.length != expectedCols) {
3394 this.error("Number of columns in line " + i + " (" + fields.length +
3395 ") does not agree with number of labels (" + expectedCols +
3396 ") " + line);
3397 }
6d0aaa09
DV
3398
3399 // If the user specified the 'labels' option and none of the cells of the
3400 // first row parsed correctly, then they probably double-specified the
3401 // labels. We go with the values set in the option, discard this row and
3402 // log a warning to the JS console.
3403 if (i == 0 && this.attr_('labels')) {
3404 var all_null = true;
3405 for (var j = 0; all_null && j < fields.length; j++) {
3406 if (fields[j]) all_null = false;
3407 }
3408 if (all_null) {
3409 this.warn("The dygraphs 'labels' option is set, but the first row of " +
3410 "CSV data ('" + line + "') appears to also contain labels. " +
3411 "Will drop the CSV labels and use the option labels.");
3412 continue;
3413 }
3414 }
3415 ret.push(fields);
6a1aa64f 3416 }
987840a2
DV
3417
3418 if (outOfOrder) {
3419 this.warn("CSV is out of order; order it correctly to speed loading.");
3420 ret.sort(function(a,b) { return a[0] - b[0] });
3421 }
3422
6a1aa64f
DV
3423 return ret;
3424};
3425
3426/**
629a09ae 3427 * @private
285a6bda
DV
3428 * The user has provided their data as a pre-packaged JS array. If the x values
3429 * are numeric, this is the same as dygraphs' internal format. If the x values
3430 * are dates, we need to convert them from Date objects to ms since epoch.
629a09ae
DV
3431 * @param {[Object]} data
3432 * @return {[Object]} data with numeric x values.
285a6bda
DV
3433 */
3434Dygraph.prototype.parseArray_ = function(data) {
3435 // Peek at the first x value to see if it's numeric.
3436 if (data.length == 0) {
3437 this.error("Can't plot empty data set");
3438 return null;
3439 }
3440 if (data[0].length == 0) {
3441 this.error("Data set cannot contain an empty row");
3442 return null;
3443 }
3444
3445 if (this.attr_("labels") == null) {
3446 this.warn("Using default labels. Set labels explicitly via 'labels' " +
3447 "in the options parameter");
3448 this.attrs_.labels = [ "X" ];
3449 for (var i = 1; i < data[0].length; i++) {
3450 this.attrs_.labels.push("Y" + i);
3451 }
3452 }
3453
2dda3850 3454 if (Dygraph.isDateLike(data[0][0])) {
285a6bda
DV
3455 // Some intelligent defaults for a date x-axis.
3456 this.attrs_.xValueFormatter = Dygraph.dateString_;
bf640e56 3457 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
285a6bda
DV
3458 this.attrs_.xTicker = Dygraph.dateTicker;
3459
3460 // Assume they're all dates.
e3ab7b40 3461 var parsedData = Dygraph.clone(data);
285a6bda
DV
3462 for (var i = 0; i < data.length; i++) {
3463 if (parsedData[i].length == 0) {
a323ff4a 3464 this.error("Row " + (1 + i) + " of data is empty");
285a6bda
DV
3465 return null;
3466 }
3467 if (parsedData[i][0] == null
3a909ec5
DV
3468 || typeof(parsedData[i][0].getTime) != 'function'
3469 || isNaN(parsedData[i][0].getTime())) {
be96a1f5 3470 this.error("x value in row " + (1 + i) + " is not a Date");
285a6bda
DV
3471 return null;
3472 }
3473 parsedData[i][0] = parsedData[i][0].getTime();
3474 }
3475 return parsedData;
3476 } else {
3477 // Some intelligent defaults for a numeric x-axis.
032e4c1d 3478 this.attrs_.xValueFormatter = function(x) { return x; };
285a6bda
DV
3479 this.attrs_.xTicker = Dygraph.numericTicks;
3480 return data;
3481 }
3482};
3483
3484/**
79420a1e
DV
3485 * Parses a DataTable object from gviz.
3486 * The data is expected to have a first column that is either a date or a
3487 * number. All subsequent columns must be numbers. If there is a clear mismatch
3488 * between this.xValueParser_ and the type of the first column, it will be
a685723c 3489 * fixed. Fills out rawData_.
629a09ae 3490 * @param {[Object]} data See above.
79420a1e
DV
3491 * @private
3492 */
285a6bda 3493Dygraph.prototype.parseDataTable_ = function(data) {
79420a1e
DV
3494 var cols = data.getNumberOfColumns();
3495 var rows = data.getNumberOfRows();
3496
d955e223 3497 var indepType = data.getColumnType(0);
4440f6c8 3498 if (indepType == 'date' || indepType == 'datetime') {
285a6bda
DV
3499 this.attrs_.xValueFormatter = Dygraph.dateString_;
3500 this.attrs_.xValueParser = Dygraph.dateParser;
3501 this.attrs_.xTicker = Dygraph.dateTicker;
bf640e56 3502 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
33127159 3503 } else if (indepType == 'number') {
032e4c1d 3504 this.attrs_.xValueFormatter = function(x) { return x; };
285a6bda
DV
3505 this.attrs_.xValueParser = function(x) { return parseFloat(x); };
3506 this.attrs_.xTicker = Dygraph.numericTicks;
bf640e56 3507 this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;
285a6bda 3508 } else {
987840a2
DV
3509 this.error("only 'date', 'datetime' and 'number' types are supported for " +
3510 "column 1 of DataTable input (Got '" + indepType + "')");
79420a1e
DV
3511 return null;
3512 }
3513
a685723c
DV
3514 // Array of the column indices which contain data (and not annotations).
3515 var colIdx = [];
3516 var annotationCols = {}; // data index -> [annotation cols]
3517 var hasAnnotations = false;
3518 for (var i = 1; i < cols; i++) {
3519 var type = data.getColumnType(i);
3520 if (type == 'number') {
3521 colIdx.push(i);
3522 } else if (type == 'string' && this.attr_('displayAnnotations')) {
3523 // This is OK -- it's an annotation column.
3524 var dataIdx = colIdx[colIdx.length - 1];
3525 if (!annotationCols.hasOwnProperty(dataIdx)) {
3526 annotationCols[dataIdx] = [i];
3527 } else {
3528 annotationCols[dataIdx].push(i);
3529 }
3530 hasAnnotations = true;
3531 } else {
3532 this.error("Only 'number' is supported as a dependent type with Gviz." +
3533 " 'string' is only supported if displayAnnotations is true");
3534 }
3535 }
3536
3537 // Read column labels
3538 // TODO(danvk): add support back for errorBars
3539 var labels = [data.getColumnLabel(0)];
3540 for (var i = 0; i < colIdx.length; i++) {
3541 labels.push(data.getColumnLabel(colIdx[i]));
f9348814 3542 if (this.attr_("errorBars")) i += 1;
a685723c
DV
3543 }
3544 this.attrs_.labels = labels;
3545 cols = labels.length;
3546
79420a1e 3547 var ret = [];
987840a2 3548 var outOfOrder = false;
a685723c 3549 var annotations = [];
79420a1e
DV
3550 for (var i = 0; i < rows; i++) {
3551 var row = [];
debe4434
DV
3552 if (typeof(data.getValue(i, 0)) === 'undefined' ||
3553 data.getValue(i, 0) === null) {
129569a5
FD
3554 this.warn("Ignoring row " + i +
3555 " of DataTable because of undefined or null first column.");
debe4434
DV
3556 continue;
3557 }
3558
c21d2c2d 3559 if (indepType == 'date' || indepType == 'datetime') {
d955e223
DV
3560 row.push(data.getValue(i, 0).getTime());
3561 } else {
3562 row.push(data.getValue(i, 0));
3563 }
3e3f84e4 3564 if (!this.attr_("errorBars")) {
a685723c
DV
3565 for (var j = 0; j < colIdx.length; j++) {
3566 var col = colIdx[j];
3567 row.push(data.getValue(i, col));
3568 if (hasAnnotations &&
3569 annotationCols.hasOwnProperty(col) &&
3570 data.getValue(i, annotationCols[col][0]) != null) {
3571 var ann = {};
3572 ann.series = data.getColumnLabel(col);
3573 ann.xval = row[0];
3574 ann.shortText = String.fromCharCode(65 /* A */ + annotations.length)
3575 ann.text = '';
3576 for (var k = 0; k < annotationCols[col].length; k++) {
3577 if (k) ann.text += "\n";
3578 ann.text += data.getValue(i, annotationCols[col][k]);
3579 }
3580 annotations.push(ann);
3581 }
3e3f84e4 3582 }
92fd68d8
DV
3583
3584 // Strip out infinities, which give dygraphs problems later on.
3585 for (var j = 0; j < row.length; j++) {
3586 if (!isFinite(row[j])) row[j] = null;
3587 }
3e3f84e4
DV
3588 } else {
3589 for (var j = 0; j < cols - 1; j++) {
3590 row.push([ data.getValue(i, 1 + 2 * j), data.getValue(i, 2 + 2 * j) ]);
3591 }
79420a1e 3592 }
987840a2
DV
3593 if (ret.length > 0 && row[0] < ret[ret.length - 1][0]) {
3594 outOfOrder = true;
3595 }
243d96e8 3596 ret.push(row);
79420a1e 3597 }
987840a2
DV
3598
3599 if (outOfOrder) {
3600 this.warn("DataTable is out of order; order it correctly to speed loading.");
3601 ret.sort(function(a,b) { return a[0] - b[0] });
3602 }
a685723c
DV
3603 this.rawData_ = ret;
3604
3605 if (annotations.length > 0) {
3606 this.setAnnotations(annotations, true);
3607 }
79420a1e
DV
3608}
3609
629a09ae
DV
3610/**
3611 * @private
3612 * This is identical to JavaScript's built-in Date.parse() method, except that
3613 * it doesn't get replaced with an incompatible method by aggressive JS
3614 * libraries like MooTools or Joomla.
3615 * @param { String } str The date string, e.g. "2011/05/06"
3616 * @return { Integer } millis since epoch
3617 */
d96b7d1a
DV
3618Dygraph.dateStrToMillis = function(str) {
3619 return new Date(str).getTime();
3620};
3621
24e5350c 3622// These functions are all based on MochiKit.
629a09ae
DV
3623/**
3624 * @private
3625 */
fc80a396
DV
3626Dygraph.update = function (self, o) {
3627 if (typeof(o) != 'undefined' && o !== null) {
3628 for (var k in o) {
85b99f0b
DV
3629 if (o.hasOwnProperty(k)) {
3630 self[k] = o[k];
3631 }
fc80a396
DV
3632 }
3633 }
3634 return self;
3635};
3636
629a09ae
DV
3637/**
3638 * @private
3639 */
2dda3850
DV
3640Dygraph.isArrayLike = function (o) {
3641 var typ = typeof(o);
3642 if (
c21d2c2d 3643 (typ != 'object' && !(typ == 'function' &&
2dda3850
DV
3644 typeof(o.item) == 'function')) ||
3645 o === null ||
3646 typeof(o.length) != 'number' ||
3647 o.nodeType === 3
3648 ) {
3649 return false;
3650 }
3651 return true;
3652};
3653
629a09ae
DV
3654/**
3655 * @private
3656 */
2dda3850
DV
3657Dygraph.isDateLike = function (o) {
3658 if (typeof(o) != "object" || o === null ||
3659 typeof(o.getTime) != 'function') {
3660 return false;
3661 }
3662 return true;
3663};
3664
629a09ae
DV
3665/**
3666 * @private
3667 */
e3ab7b40
DV
3668Dygraph.clone = function(o) {
3669 // TODO(danvk): figure out how MochiKit's version works
3670 var r = [];
3671 for (var i = 0; i < o.length; i++) {
3672 if (Dygraph.isArrayLike(o[i])) {
3673 r.push(Dygraph.clone(o[i]));
3674 } else {
3675 r.push(o[i]);
3676 }
3677 }
3678 return r;
24e5350c
DV
3679};
3680
2dda3850 3681
79420a1e 3682/**
6a1aa64f
DV
3683 * Get the CSV data. If it's in a function, call that function. If it's in a
3684 * file, do an XMLHttpRequest to get it.
3685 * @private
3686 */
285a6bda 3687Dygraph.prototype.start_ = function() {
6a1aa64f 3688 if (typeof this.file_ == 'function') {
285a6bda 3689 // CSV string. Pretend we got it via XHR.
6a1aa64f 3690 this.loadedEvent_(this.file_());
2dda3850 3691 } else if (Dygraph.isArrayLike(this.file_)) {
285a6bda 3692 this.rawData_ = this.parseArray_(this.file_);
26ca7938 3693 this.predraw_();
79420a1e
DV
3694 } else if (typeof this.file_ == 'object' &&
3695 typeof this.file_.getColumnRange == 'function') {
3696 // must be a DataTable from gviz.
a685723c 3697 this.parseDataTable_(this.file_);
26ca7938 3698 this.predraw_();
285a6bda
DV
3699 } else if (typeof this.file_ == 'string') {
3700 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
3701 if (this.file_.indexOf('\n') >= 0) {
3702 this.loadedEvent_(this.file_);
3703 } else {
3704 var req = new XMLHttpRequest();
3705 var caller = this;
3706 req.onreadystatechange = function () {
3707 if (req.readyState == 4) {
3708 if (req.status == 200) {
3709 caller.loadedEvent_(req.responseText);
3710 }
6a1aa64f 3711 }
285a6bda 3712 };
6a1aa64f 3713
285a6bda
DV
3714 req.open("GET", this.file_, true);
3715 req.send(null);
3716 }
3717 } else {
3718 this.error("Unknown data format: " + (typeof this.file_));
6a1aa64f
DV
3719 }
3720};
3721
3722/**
3723 * Changes various properties of the graph. These can include:
3724 * <ul>
3725 * <li>file: changes the source data for the graph</li>
3726 * <li>errorBars: changes whether the data contains stddev</li>
3727 * </ul>
dcb25130 3728 *
6a1aa64f
DV
3729 * @param {Object} attrs The new properties and values
3730 */
285a6bda
DV
3731Dygraph.prototype.updateOptions = function(attrs) {
3732 // TODO(danvk): this is a mess. Rethink this function.
c65f2303 3733 if ('rollPeriod' in attrs) {
6a1aa64f
DV
3734 this.rollPeriod_ = attrs.rollPeriod;
3735 }
c65f2303 3736 if ('dateWindow' in attrs) {
6a1aa64f 3737 this.dateWindow_ = attrs.dateWindow;
e5152598 3738 if (!('isZoomedIgnoreProgrammaticZoom' in attrs)) {
81856f70
NN
3739 this.zoomed_x_ = attrs.dateWindow != null;
3740 }
b7e5862d 3741 }
e5152598 3742 if ('valueRange' in attrs && !('isZoomedIgnoreProgrammaticZoom' in attrs)) {
b7e5862d 3743 this.zoomed_y_ = attrs.valueRange != null;
6a1aa64f 3744 }
450fe64b
DV
3745
3746 // TODO(danvk): validate per-series options.
46dde5f9
DV
3747 // Supported:
3748 // strokeWidth
3749 // pointSize
3750 // drawPoints
3751 // highlightCircleSize
450fe64b 3752
fc80a396 3753 Dygraph.update(this.user_attrs_, attrs);
87bb7958 3754 Dygraph.update(this.renderOptions_, attrs);
285a6bda
DV
3755
3756 this.labelsFromCSV_ = (this.attr_("labels") == null);
3757
3758 // TODO(danvk): this doesn't match the constructor logic
3759 this.layout_.updateOptions({ 'errorBars': this.attr_("errorBars") });
5e50289f 3760 if (attrs['file']) {
6a1aa64f
DV
3761 this.file_ = attrs['file'];
3762 this.start_();
3763 } else {
26ca7938 3764 this.predraw_();
6a1aa64f
DV
3765 }
3766};
3767
3768/**
697e70b2
DV
3769 * Resizes the dygraph. If no parameters are specified, resizes to fill the
3770 * containing div (which has presumably changed size since the dygraph was
3771 * instantiated. If the width/height are specified, the div will be resized.
964f30c6
DV
3772 *
3773 * This is far more efficient than destroying and re-instantiating a
3774 * Dygraph, since it doesn't have to reparse the underlying data.
3775 *
629a09ae
DV
3776 * @param {Number} [width] Width (in pixels)
3777 * @param {Number} [height] Height (in pixels)
697e70b2
DV
3778 */
3779Dygraph.prototype.resize = function(width, height) {
e8c7ef86
DV
3780 if (this.resize_lock) {
3781 return;
3782 }
3783 this.resize_lock = true;
3784
697e70b2
DV
3785 if ((width === null) != (height === null)) {
3786 this.warn("Dygraph.resize() should be called with zero parameters or " +
3787 "two non-NULL parameters. Pretending it was zero.");
3788 width = height = null;
3789 }
3790
b16e6369 3791 // TODO(danvk): there should be a clear() method.
697e70b2 3792 this.maindiv_.innerHTML = "";
b16e6369
DV
3793 this.attrs_.labelsDiv = null;
3794
697e70b2
DV
3795 if (width) {
3796 this.maindiv_.style.width = width + "px";
3797 this.maindiv_.style.height = height + "px";
3798 this.width_ = width;
3799 this.height_ = height;
3800 } else {
3801 this.width_ = this.maindiv_.offsetWidth;
3802 this.height_ = this.maindiv_.offsetHeight;
3803 }
3804
3805 this.createInterface_();
26ca7938 3806 this.predraw_();
e8c7ef86
DV
3807
3808 this.resize_lock = false;
697e70b2
DV
3809};
3810
3811/**
6faebb69 3812 * Adjusts the number of points in the rolling average. Updates the graph to
6a1aa64f 3813 * reflect the new averaging period.
6faebb69 3814 * @param {Number} length Number of points over which to average the data.
6a1aa64f 3815 */
285a6bda 3816Dygraph.prototype.adjustRoll = function(length) {
6a1aa64f 3817 this.rollPeriod_ = length;
26ca7938 3818 this.predraw_();
6a1aa64f 3819};
540d00f1 3820
f8cfec73 3821/**
1cf11047
DV
3822 * Returns a boolean array of visibility statuses.
3823 */
3824Dygraph.prototype.visibility = function() {
3825 // Do lazy-initialization, so that this happens after we know the number of
3826 // data series.
3827 if (!this.attr_("visibility")) {
f38dec01 3828 this.attrs_["visibility"] = [];
1cf11047
DV
3829 }
3830 while (this.attr_("visibility").length < this.rawData_[0].length - 1) {
f38dec01 3831 this.attr_("visibility").push(true);
1cf11047
DV
3832 }
3833 return this.attr_("visibility");
3834};
3835
3836/**
3837 * Changes the visiblity of a series.
3838 */
3839Dygraph.prototype.setVisibility = function(num, value) {
3840 var x = this.visibility();
a6c109c1 3841 if (num < 0 || num >= x.length) {
1cf11047
DV
3842 this.warn("invalid series number in setVisibility: " + num);
3843 } else {
3844 x[num] = value;
26ca7938 3845 this.predraw_();
1cf11047
DV
3846 }
3847};
3848
3849/**
5c528fa2
DV
3850 * Update the list of annotations and redraw the chart.
3851 */
a685723c 3852Dygraph.prototype.setAnnotations = function(ann, suppressDraw) {
3c51ab74
DV
3853 // Only add the annotation CSS rule once we know it will be used.
3854 Dygraph.addAnnotationRule();
5c528fa2
DV
3855 this.annotations_ = ann;
3856 this.layout_.setAnnotations(this.annotations_);
a685723c 3857 if (!suppressDraw) {
26ca7938 3858 this.predraw_();
a685723c 3859 }
5c528fa2
DV
3860};
3861
3862/**
3863 * Return the list of annotations.
3864 */
3865Dygraph.prototype.annotations = function() {
3866 return this.annotations_;
3867};
3868
46dde5f9
DV
3869/**
3870 * Get the index of a series (column) given its name. The first column is the
3871 * x-axis, so the data series start with index 1.
3872 */
3873Dygraph.prototype.indexFromSetName = function(name) {
3874 var labels = this.attr_("labels");
3875 for (var i = 0; i < labels.length; i++) {
3876 if (labels[i] == name) return i;
3877 }
3878 return null;
3879};
3880
629a09ae
DV
3881/**
3882 * @private
3883 * Adds a default style for the annotation CSS classes to the document. This is
3884 * only executed when annotations are actually used. It is designed to only be
3885 * called once -- all calls after the first will return immediately.
3886 */
5c528fa2
DV
3887Dygraph.addAnnotationRule = function() {
3888 if (Dygraph.addedAnnotationCSS) return;
3889
5c528fa2
DV
3890 var rule = "border: 1px solid black; " +
3891 "background-color: white; " +
3892 "text-align: center;";
22186871
DV
3893
3894 var styleSheetElement = document.createElement("style");
3895 styleSheetElement.type = "text/css";
3896 document.getElementsByTagName("head")[0].appendChild(styleSheetElement);
3897
3898 // Find the first style sheet that we can access.
3899 // We may not add a rule to a style sheet from another domain for security
3900 // reasons. This sometimes comes up when using gviz, since the Google gviz JS
3901 // adds its own style sheets from google.com.
3902 for (var i = 0; i < document.styleSheets.length; i++) {
3903 if (document.styleSheets[i].disabled) continue;
3904 var mysheet = document.styleSheets[i];
3905 try {
3906 if (mysheet.insertRule) { // Firefox
3907 var idx = mysheet.cssRules ? mysheet.cssRules.length : 0;
3908 mysheet.insertRule(".dygraphDefaultAnnotation { " + rule + " }", idx);
3909 } else if (mysheet.addRule) { // IE
3910 mysheet.addRule(".dygraphDefaultAnnotation", rule);
3911 }
3912 Dygraph.addedAnnotationCSS = true;
3913 return;
3914 } catch(err) {
3915 // Was likely a security exception.
3916 }
5c528fa2
DV
3917 }
3918
22186871 3919 this.warn("Unable to add default annotation CSS rule; display may be off.");
5c528fa2
DV
3920}
3921
3922/**
629a09ae 3923 * @private
f8cfec73
DV
3924 * Create a new canvas element. This is more complex than a simple
3925 * document.createElement("canvas") because of IE and excanvas.
3926 */
3927Dygraph.createCanvas = function() {
3928 var canvas = document.createElement("canvas");
3929
3930 isIE = (/MSIE/.test(navigator.userAgent) && !window.opera);
8b8f2d59 3931 if (isIE && (typeof(G_vmlCanvasManager) != 'undefined')) {
f8cfec73
DV
3932 canvas = G_vmlCanvasManager.initElement(canvas);
3933 }
3934
3935 return canvas;
3936};
3937
540d00f1
DV
3938
3939/**
285a6bda 3940 * A wrapper around Dygraph that implements the gviz API.
540d00f1
DV
3941 * @param {Object} container The DOM object the visualization should live in.
3942 */
285a6bda 3943Dygraph.GVizChart = function(container) {
540d00f1
DV
3944 this.container = container;
3945}
3946
285a6bda 3947Dygraph.GVizChart.prototype.draw = function(data, options) {
c91f4ae8
DV
3948 // Clear out any existing dygraph.
3949 // TODO(danvk): would it make more sense to simply redraw using the current
3950 // date_graph object?
540d00f1 3951 this.container.innerHTML = '';
c91f4ae8
DV
3952 if (typeof(this.date_graph) != 'undefined') {
3953 this.date_graph.destroy();
3954 }
3955
285a6bda 3956 this.date_graph = new Dygraph(this.container, data, options);
540d00f1 3957}
285a6bda 3958
239c712d
NAG
3959/**
3960 * Google charts compatible setSelection
50360fd0 3961 * Only row selection is supported, all points in the row will be highlighted
239c712d
NAG
3962 * @param {Array} array of the selected cells
3963 * @public
3964 */
3965Dygraph.GVizChart.prototype.setSelection = function(selection_array) {
3966 var row = false;
3967 if (selection_array.length) {
3968 row = selection_array[0].row;
3969 }
3970 this.date_graph.setSelection(row);
3971}
3972
103b7292
NAG
3973/**
3974 * Google charts compatible getSelection implementation
3975 * @return {Array} array of the selected cells
3976 * @public
3977 */
3978Dygraph.GVizChart.prototype.getSelection = function() {
3979 var selection = [];
50360fd0 3980
103b7292 3981 var row = this.date_graph.getSelection();
50360fd0 3982
103b7292 3983 if (row < 0) return selection;
50360fd0 3984
103b7292
NAG
3985 col = 1;
3986 for (var i in this.date_graph.layout_.datasets) {
3987 selection.push({row: row, column: col});
3988 col++;
3989 }
3990
3991 return selection;
3992}
3993
285a6bda
DV
3994// Older pages may still use this name.
3995DateGraph = Dygraph;
028ddf8a
DV
3996
3997// <REMOVE_FOR_COMBINED>
0addac07
DV
3998Dygraph.OPTIONS_REFERENCE = // <JSON>
3999{
a38e9336
DV
4000 "xValueParser": {
4001 "default": "parseFloat() or Date.parse()*",
4002 "labels": ["CSV parsing"],
4003 "type": "function(str) -> number",
4004 "description": "A function which parses x-values (i.e. the dependent series). Must return a number, even when the values are dates. In this case, millis since epoch are used. This is used primarily for parsing CSV data. *=Dygraphs is slightly more accepting in the dates which it will parse. See code for details."
4005 },
4006 "stackedGraph": {
a96f59f4 4007 "default": "false",
a38e9336
DV
4008 "labels": ["Data Line display"],
4009 "type": "boolean",
4010 "description": "If set, stack series on top of one another rather than drawing them independently."
a96f59f4 4011 },
a38e9336 4012 "pointSize": {
a96f59f4 4013 "default": "1",
a38e9336
DV
4014 "labels": ["Data Line display"],
4015 "type": "integer",
4016 "description": "The size of the dot to draw on each point in pixels (see drawPoints). A dot is always drawn when a point is \"isolated\", i.e. there is a missing point on either side of it. This also controls the size of those dots."
a96f59f4 4017 },
a38e9336
DV
4018 "labelsDivStyles": {
4019 "default": "null",
4020 "labels": ["Legend"],
4021 "type": "{}",
4022 "description": "Additional styles to apply to the currently-highlighted points div. For example, { 'font-weight': 'bold' } will make the labels bold."
a96f59f4 4023 },
a38e9336 4024 "drawPoints": {
a96f59f4 4025 "default": "false",
a38e9336
DV
4026 "labels": ["Data Line display"],
4027 "type": "boolean",
4028 "description": "Draw a small dot at each point, in addition to a line going through the point. This makes the individual data points easier to see, but can increase visual clutter in the chart."
a96f59f4 4029 },
a38e9336
DV
4030 "height": {
4031 "default": "320",
4032 "labels": ["Overall display"],
4033 "type": "integer",
4034 "description": "Height, in pixels, of the chart. If the container div has been explicitly sized, this will be ignored."
a96f59f4
DV
4035 },
4036 "zoomCallback": {
a96f59f4 4037 "default": "null",
a38e9336
DV
4038 "labels": ["Callbacks"],
4039 "type": "function(minDate, maxDate, yRanges)",
a96f59f4
DV
4040 "description": "A function to call when the zoom window is changed (either by zooming in or out). minDate and maxDate are milliseconds since epoch. yRanges is an array of [bottom, top] pairs, one for each y-axis."
4041 },
a38e9336
DV
4042 "pointClickCallback": {
4043 "default": "",
4044 "labels": ["Callbacks", "Interactive Elements"],
4045 "type": "",
4046 "description": ""
a96f59f4 4047 },
a38e9336
DV
4048 "colors": {
4049 "default": "(see description)",
4050 "labels": ["Data Series Colors"],
4051 "type": "array<string>",
4052 "example": "['red', '#00FF00']",
4053 "description": "List of colors for the data series. These can be of the form \"#AABBCC\" or \"rgb(255,100,200)\" or \"yellow\", etc. If not specified, equally-spaced points around a color wheel are used."
a96f59f4 4054 },
a38e9336 4055 "connectSeparatedPoints": {
a96f59f4 4056 "default": "false",
a38e9336
DV
4057 "labels": ["Data Line display"],
4058 "type": "boolean",
4059 "description": "Usually, when Dygraphs encounters a missing value in a data series, it interprets this as a gap and draws it as such. If, instead, the missing values represents an x-value for which only a different series has data, then you'll want to connect the dots by setting this to true. To explicitly include a gap with this option set, use a value of NaN."
a96f59f4 4060 },
a38e9336 4061 "highlightCallback": {
a96f59f4 4062 "default": "null",
a38e9336
DV
4063 "labels": ["Callbacks"],
4064 "type": "function(event, x, points,row)",
4065 "description": "When set, this callback gets called every time a new point is highlighted. The parameters are the JavaScript mousemove event, the x-coordinate of the highlighted points and an array of highlighted points: <code>[ {name: 'series', yval: y-value}, &hellip; ]</code>"
a96f59f4 4066 },
a38e9336 4067 "includeZero": {
a96f59f4 4068 "default": "false",
a38e9336 4069 "labels": ["Axis display"],
a96f59f4 4070 "type": "boolean",
a38e9336 4071 "description": "Usually, dygraphs will use the range of the data plus some padding to set the range of the y-axis. If this option is set, the y-axis will always include zero, typically as the lowest value. This can be used to avoid exaggerating the variance in the data"
a96f59f4 4072 },
a38e9336
DV
4073 "rollPeriod": {
4074 "default": "1",
4075 "labels": ["Error Bars", "Rolling Averages"],
4076 "type": "integer &gt;= 1",
4077 "description": "Number of days over which to average data. Discussed extensively above."
a96f59f4 4078 },
a38e9336 4079 "unhighlightCallback": {
a96f59f4 4080 "default": "null",
a38e9336
DV
4081 "labels": ["Callbacks"],
4082 "type": "function(event)",
4083 "description": "When set, this callback gets called every time the user stops highlighting any point by mousing out of the graph. The parameter is the mouseout event."
a96f59f4 4084 },
a38e9336
DV
4085 "axisTickSize": {
4086 "default": "3.0",
4087 "labels": ["Axis display"],
4088 "type": "number",
4089 "description": "The size of the line to display next to each tick mark on x- or y-axes."
a96f59f4 4090 },
a38e9336 4091 "labelsSeparateLines": {
a96f59f4 4092 "default": "false",
a38e9336
DV
4093 "labels": ["Legend"],
4094 "type": "boolean",
4095 "description": "Put <code>&lt;br/&gt;</code> between lines in the label string. Often used in conjunction with <strong>labelsDiv</strong>."
a96f59f4 4096 },
a38e9336
DV
4097 "xValueFormatter": {
4098 "default": "(Round to 2 decimal places)",
4099 "labels": ["Axis display"],
4100 "type": "function(x)",
4101 "description": "Function to provide a custom display format for the X value for mouseover."
a96f59f4
DV
4102 },
4103 "pixelsPerYLabel": {
a96f59f4 4104 "default": "30",
a38e9336
DV
4105 "labels": ["Axis display", "Grid"],
4106 "type": "integer",
a96f59f4
DV
4107 "description": "Number of pixels to require between each x- and y-label. Larger values will yield a sparser axis with fewer ticks."
4108 },
a38e9336 4109 "annotationMouseOverHandler": {
5cc5f631 4110 "default": "null",
a38e9336 4111 "labels": ["Annotations"],
5cc5f631
DV
4112 "type": "function(annotation, point, dygraph, event)",
4113 "description": "If provided, this function is called whenever the user mouses over an annotation."
4114 },
4115 "annotationMouseOutHandler": {
4116 "default": "null",
4117 "labels": ["Annotations"],
4118 "type": "function(annotation, point, dygraph, event)",
4119 "description": "If provided, this function is called whenever the user mouses out of an annotation."
a96f59f4 4120 },
8165189e 4121 "annotationClickHandler": {
5cc5f631 4122 "default": "null",
8165189e 4123 "labels": ["Annotations"],
5cc5f631
DV
4124 "type": "function(annotation, point, dygraph, event)",
4125 "description": "If provided, this function is called whenever the user clicks on an annotation."
8165189e
DV
4126 },
4127 "annotationDblClickHandler": {
5cc5f631 4128 "default": "null",
8165189e 4129 "labels": ["Annotations"],
5cc5f631
DV
4130 "type": "function(annotation, point, dygraph, event)",
4131 "description": "If provided, this function is called whenever the user double-clicks on an annotation."
8165189e 4132 },
a38e9336
DV
4133 "drawCallback": {
4134 "default": "null",
4135 "labels": ["Callbacks"],
4136 "type": "function(dygraph, is_initial)",
4137 "description": "When set, this callback gets called every time the dygraph is drawn. This includes the initial draw, after zooming and repeatedly while panning. The first parameter is the dygraph being drawn. The second is a boolean value indicating whether this is the initial draw."
4138 },
4139 "labelsKMG2": {
4140 "default": "false",
4141 "labels": ["Value display/formatting"],
4142 "type": "boolean",
4143 "description": "Show k/M/G for kilo/Mega/Giga on y-axis. This is different than <code>labelsKMB</code> in that it uses base 2, not 10."
4144 },
4145 "delimiter": {
4146 "default": ",",
4147 "labels": ["CSV parsing"],
4148 "type": "string",
4149 "description": "The delimiter to look for when separating fields of a CSV file. Setting this to a tab is not usually necessary, since tab-delimited data is auto-detected."
a96f59f4
DV
4150 },
4151 "axisLabelFontSize": {
a96f59f4 4152 "default": "14",
a38e9336
DV
4153 "labels": ["Axis display"],
4154 "type": "integer",
a96f59f4
DV
4155 "description": "Size of the font (in pixels) to use in the axis labels, both x- and y-axis."
4156 },
a38e9336
DV
4157 "underlayCallback": {
4158 "default": "null",
4159 "labels": ["Callbacks"],
4160 "type": "function(canvas, area, dygraph)",
4161 "description": "When set, this callback gets called before the chart is drawn. It details on how to use this."
a96f59f4 4162 },
a38e9336
DV
4163 "width": {
4164 "default": "480",
4165 "labels": ["Overall display"],
4166 "type": "integer",
4167 "description": "Width, in pixels, of the chart. If the container div has been explicitly sized, this will be ignored."
a96f59f4 4168 },
a38e9336
DV
4169 "interactionModel": {
4170 "default": "...",
4171 "labels": ["Interactive Elements"],
4172 "type": "Object",
4173 "description": "TODO(konigsberg): document this"
4174 },
4175 "xTicker": {
4176 "default": "Dygraph.dateTicker or Dygraph.numericTicks",
4177 "labels": ["Axis display"],
4178 "type": "function(min, max, dygraph) -> [{v: ..., label: ...}, ...]",
4179 "description": "This lets you specify an arbitrary function to generate tick marks on an axis. The tick marks are an array of (value, label) pairs. The built-in functions go to great lengths to choose good tick marks so, if you set this option, you'll most likely want to call one of them and modify the result."
4180 },
4181 "xAxisLabelWidth": {
4182 "default": "50",
4183 "labels": ["Axis display"],
a96f59f4 4184 "type": "integer",
a38e9336 4185 "description": "Width, in pixels, of the x-axis labels."
a96f59f4 4186 },
a38e9336
DV
4187 "showLabelsOnHighlight": {
4188 "default": "true",
4189 "labels": ["Interactive Elements", "Legend"],
a96f59f4 4190 "type": "boolean",
a38e9336 4191 "description": "Whether to show the legend upon mouseover."
a96f59f4 4192 },
a38e9336
DV
4193 "axis": {
4194 "default": "(none)",
4195 "labels": ["Axis display"],
4196 "type": "string or object",
4197 "description": "Set to either an object ({}) filled with options for this axis or to the name of an existing data series with its own axis to re-use that axis. See tests for usage."
4198 },
4199 "pixelsPerXLabel": {
4200 "default": "60",
4201 "labels": ["Axis display", "Grid"],
a96f59f4 4202 "type": "integer",
a38e9336
DV
4203 "description": "Number of pixels to require between each x- and y-label. Larger values will yield a sparser axis with fewer ticks."
4204 },
4205 "labelsDiv": {
4206 "default": "null",
4207 "labels": ["Legend"],
4208 "type": "DOM element or string",
4209 "example": "<code style='font-size: small'>document.getElementById('foo')</code>or<code>'foo'",
4210 "description": "Show data labels in an external div, rather than on the graph. This value can either be a div element or a div id."
a96f59f4
DV
4211 },
4212 "fractions": {
a96f59f4 4213 "default": "false",
a38e9336
DV
4214 "labels": ["CSV parsing", "Error Bars"],
4215 "type": "boolean",
a96f59f4
DV
4216 "description": "When set, attempt to parse each cell in the CSV file as \"a/b\", where a and b are integers. The ratio will be plotted. This allows computation of Wilson confidence intervals (see below)."
4217 },
a38e9336
DV
4218 "logscale": {
4219 "default": "false",
4220 "labels": ["Axis display"],
a96f59f4 4221 "type": "boolean",
a109b711 4222 "description": "When set for a y-axis, the graph shows that axis in log scale. Any values less than or equal to zero are not displayed.\n\nNot compatible with showZero, and ignores connectSeparatedPoints. Also, showing log scale with valueRanges that are less than zero will result in an unviewable graph."
a38e9336
DV
4223 },
4224 "strokeWidth": {
4225 "default": "1.0",
4226 "labels": ["Data Line display"],
4227 "type": "integer",
4228 "example": "0.5, 2.0",
4229 "description": "The width of the lines connecting data points. This can be used to increase the contrast or some graphs."
4230 },
4231 "wilsonInterval": {
a96f59f4 4232 "default": "true",
a38e9336
DV
4233 "labels": ["Error Bars"],
4234 "type": "boolean",
a96f59f4
DV
4235 "description": "Use in conjunction with the \"fractions\" option. Instead of plotting +/- N standard deviations, dygraphs will compute a Wilson confidence interval and plot that. This has more reasonable behavior for ratios close to 0 or 1."
4236 },
a38e9336 4237 "fillGraph": {
a96f59f4 4238 "default": "false",
a38e9336
DV
4239 "labels": ["Data Line display"],
4240 "type": "boolean",
4241 "description": "Should the area underneath the graph be filled? This option is not compatible with error bars."
a96f59f4 4242 },
a38e9336
DV
4243 "highlightCircleSize": {
4244 "default": "3",
4245 "labels": ["Interactive Elements"],
4246 "type": "integer",
4247 "description": "The size in pixels of the dot drawn over highlighted points."
a96f59f4
DV
4248 },
4249 "gridLineColor": {
a96f59f4 4250 "default": "rgb(128,128,128)",
a38e9336
DV
4251 "labels": ["Grid"],
4252 "type": "red, blue",
a96f59f4
DV
4253 "description": "The color of the gridlines."
4254 },
a38e9336
DV
4255 "visibility": {
4256 "default": "[true, true, ...]",
4257 "labels": ["Data Line display"],
4258 "type": "Array of booleans",
4259 "description": "Which series should initially be visible? Once the Dygraph has been constructed, you can access and modify the visibility of each series using the <code>visibility</code> and <code>setVisibility</code> methods."
a96f59f4 4260 },
a38e9336
DV
4261 "valueRange": {
4262 "default": "Full range of the input is shown",
4263 "labels": ["Axis display"],
4264 "type": "Array of two numbers",
4265 "example": "[10, 110]",
4266 "description": "Explicitly set the vertical range of the graph to [low, high]."
a96f59f4 4267 },
a38e9336
DV
4268 "labelsDivWidth": {
4269 "default": "250",
4270 "labels": ["Legend"],
a96f59f4 4271 "type": "integer",
a38e9336 4272 "description": "Width (in pixels) of the div which shows information on the currently-highlighted points."
a96f59f4 4273 },
a38e9336
DV
4274 "colorSaturation": {
4275 "default": "1.0",
4276 "labels": ["Data Series Colors"],
4277 "type": "0.0 - 1.0",
4278 "description": "If <strong>colors</strong> is not specified, saturation of the automatically-generated data series colors."
4279 },
4280 "yAxisLabelWidth": {
4281 "default": "50",
4282 "labels": ["Axis display"],
a96f59f4 4283 "type": "integer",
a38e9336 4284 "description": "Width, in pixels, of the y-axis labels."
a96f59f4 4285 },
a38e9336
DV
4286 "hideOverlayOnMouseOut": {
4287 "default": "true",
4288 "labels": ["Interactive Elements", "Legend"],
a96f59f4 4289 "type": "boolean",
a38e9336 4290 "description": "Whether to hide the legend when the mouse leaves the chart area."
a96f59f4
DV
4291 },
4292 "yValueFormatter": {
a96f59f4 4293 "default": "(Round to 2 decimal places)",
a38e9336
DV
4294 "labels": ["Axis display"],
4295 "type": "function(x)",
a96f59f4
DV
4296 "description": "Function to provide a custom display format for the Y value for mouseover."
4297 },
a38e9336
DV
4298 "legend": {
4299 "default": "onmouseover",
4300 "labels": ["Legend"],
4301 "type": "string",
4302 "description": "When to display the legend. By default, it only appears when a user mouses over the chart. Set it to \"always\" to always display a legend of some sort."
4303 },
4304 "labelsShowZeroValues": {
4305 "default": "true",
4306 "labels": ["Legend"],
a96f59f4 4307 "type": "boolean",
a38e9336
DV
4308 "description": "Show zero value labels in the labelsDiv."
4309 },
4310 "stepPlot": {
a96f59f4 4311 "default": "false",
a38e9336
DV
4312 "labels": ["Data Line display"],
4313 "type": "boolean",
4314 "description": "When set, display the graph as a step plot instead of a line plot."
a96f59f4 4315 },
a38e9336
DV
4316 "labelsKMB": {
4317 "default": "false",
4318 "labels": ["Value display/formatting"],
a96f59f4 4319 "type": "boolean",
a38e9336
DV
4320 "description": "Show K/M/B for thousands/millions/billions on y-axis."
4321 },
4322 "rightGap": {
4323 "default": "5",
4324 "labels": ["Overall display"],
4325 "type": "integer",
4326 "description": "Number of pixels to leave blank at the right edge of the Dygraph. This makes it easier to highlight the right-most data point."
4327 },
4328 "avoidMinZero": {
a96f59f4 4329 "default": "false",
a38e9336
DV
4330 "labels": ["Axis display"],
4331 "type": "boolean",
4332 "description": "When set, the heuristic that fixes the Y axis at zero for a data set with the minimum Y value of zero is disabled. \nThis is particularly useful for data sets that contain many zero values, especially for step plots which may otherwise have lines not visible running along the bottom axis."
4333 },
4334 "xAxisLabelFormatter": {
4335 "default": "Dygraph.dateAxisFormatter",
4336 "labels": ["Axis display", "Value display/formatting"],
4337 "type": "function(date, granularity)",
4338 "description": "Function to call to format values along the x axis."
4339 },
4340 "clickCallback": {
4341 "snippet": "function(e, date){<br>&nbsp;&nbsp;alert(date);<br>}",
4342 "default": "null",
4343 "labels": ["Callbacks"],
4344 "type": "function(e, date)",
4345 "description": "A function to call when a data point is clicked. The function should take two arguments, the event object for the click and the date that was clicked."
4346 },
4347 "yAxisLabelFormatter": {
4348 "default": "yValueFormatter",
4349 "labels": ["Axis display", "Value display/formatting"],
4350 "type": "function(x)",
4351 "description": "Function used to format values along the Y axis. By default it uses the same as the <code>yValueFormatter</code> unless specified."
a96f59f4
DV
4352 },
4353 "labels": {
a96f59f4 4354 "default": "[\"X\", \"Y1\", \"Y2\", ...]*",
a38e9336
DV
4355 "labels": ["Legend"],
4356 "type": "array<string>",
a96f59f4
DV
4357 "description": "A name for each data series, including the independent (X) series. For CSV files and DataTable objections, this is determined by context. For raw data, this must be specified. If it is not, default values are supplied and a warning is logged."
4358 },
a38e9336
DV
4359 "dateWindow": {
4360 "default": "Full range of the input is shown",
4361 "labels": ["Axis display"],
4362 "type": "Array of two Dates or numbers",
4363 "example": "[<br>&nbsp;&nbsp;Date.parse('2006-01-01'),<br>&nbsp;&nbsp;(new Date()).valueOf()<br>]",
4364 "description": "Initially zoom in on a section of the graph. Is of the form [earliest, latest], where earliest/latest are milliseconds since epoch. If the data for the x-axis is numeric, the values in dateWindow must also be numbers."
a96f59f4 4365 },
a38e9336
DV
4366 "showRoller": {
4367 "default": "false",
4368 "labels": ["Interactive Elements", "Rolling Averages"],
4369 "type": "boolean",
4370 "description": "If the rolling average period text box should be shown."
a96f59f4 4371 },
a38e9336
DV
4372 "sigma": {
4373 "default": "2.0",
4374 "labels": ["Error Bars"],
ca49434a 4375 "type": "float",
a38e9336 4376 "description": "When errorBars is set, shade this many standard deviations above/below each point."
a96f59f4 4377 },
a38e9336 4378 "customBars": {
a96f59f4 4379 "default": "false",
a38e9336 4380 "labels": ["CSV parsing", "Error Bars"],
a96f59f4 4381 "type": "boolean",
a38e9336 4382 "description": "When set, parse each CSV cell as \"low;middle;high\". Error bars will be drawn for each point between low and high, with the series itself going through middle."
a96f59f4 4383 },
a38e9336
DV
4384 "colorValue": {
4385 "default": "1.0",
4386 "labels": ["Data Series Colors"],
4387 "type": "float (0.0 - 1.0)",
4388 "description": "If colors is not specified, value of the data series colors, as in hue/saturation/value. (0.0-1.0, default 0.5)"
a96f59f4 4389 },
a38e9336
DV
4390 "errorBars": {
4391 "default": "false",
4392 "labels": ["CSV parsing", "Error Bars"],
a96f59f4 4393 "type": "boolean",
a38e9336 4394 "description": "Does the data contain standard deviations? Setting this to true alters the input format (see above)."
a96f59f4 4395 },
a38e9336
DV
4396 "displayAnnotations": {
4397 "default": "false",
4398 "labels": ["Annotations"],
a96f59f4 4399 "type": "boolean",
a38e9336 4400 "description": "Only applies when Dygraphs is used as a GViz chart. Causes string columns following a data series to be interpreted as annotations on points in that series. This is the same format used by Google's AnnotatedTimeLine chart."
4cac8c7a 4401 },
965a030e 4402 "panEdgeFraction": {
4cac8c7a 4403 "default": "null",
965a030e 4404 "labels": ["Axis Display", "Interactive Elements"],
4cac8c7a
RK
4405 "type": "float",
4406 "default": "null",
4407 "description": "A value representing the farthest a graph may be panned, in percent of the display. For example, a value of 0.1 means that the graph can only be panned 10% pased the edges of the displayed values. null means no bounds."
ca49434a
DV
4408 },
4409 "title": {
4410 "labels": ["Chart labels"],
4411 "type": "string",
4412 "default": "null",
4413 "description": "Text to display above the chart. You can supply any HTML for this value, not just text. If you wish to style it using CSS, use the 'dygraph-label' or 'dygraph-title' classes."
4414 },
4415 "titleHeight": {
4416 "default": "18",
4417 "labels": ["Chart labels"],
4418 "type": "integer",
4419 "description": "Height of the chart title, in pixels. This also controls the default font size of the title. If you style the title on your own, this controls how much space is set aside above the chart for the title's div."
4420 },
4421 "xlabel": {
4422 "labels": ["Chart labels"],
4423 "type": "string",
4424 "default": "null",
4425 "description": "Text to display below the chart's x-axis. You can supply any HTML for this value, not just text. If you wish to style it using CSS, use the 'dygraph-label' or 'dygraph-xlabel' classes."
4426 },
4427 "xLabelHeight": {
4428 "labels": ["Chart labels"],
4429 "type": "integer",
4430 "default": "18",
4431 "description": "Height of the x-axis label, in pixels. This also controls the default font size of the x-axis label. If you style the label on your own, this controls how much space is set aside below the chart for the x-axis label's div."
4432 },
4433 "ylabel": {
4434 "labels": ["Chart labels"],
4435 "type": "string",
4436 "default": "null",
4437 "description": "Text to display to the left of the chart's y-axis. You can supply any HTML for this value, not just text. If you wish to style it using CSS, use the 'dygraph-label' or 'dygraph-ylabel' classes. The text will be rotated 90 degrees by default, so CSS rules may behave in unintuitive ways. No additional space is set aside for a y-axis label. If you need more space, increase the width of the y-axis tick labels using the yAxisLabelWidth option. If you need a wider div for the y-axis label, either style it that way with CSS (but remember that it's rotated, so width is controlled by the 'height' property) or set the yLabelWidth option."
4438 },
4439 "yLabelWidth": {
4440 "labels": ["Chart labels"],
4441 "type": "integer",
4442 "default": "18",
4443 "description": "Width of the div which contains the y-axis label. Since the y-axis label appears rotated 90 degrees, this actually affects the height of its div."
5bc3e265 4444 },
e5152598
NN
4445 "isZoomedIgnoreProgrammaticZoom" : {
4446 "default": "false",
4447 "labels": ["Zooming"],
4448 "type": "boolean",
36dfa958 4449 "description" : "When this option is passed to updateOptions() along with either the <code>dateWindow</code> or <code>valueRange</code> options, the zoom flags are not changed to reflect a zoomed state. This is primarily useful for when the display area of a chart is changed programmatically and also where manual zooming is allowed and use is made of the <code>isZoomed</code> method to determine this."
17d0210c
DV
4450 },
4451 "sigFigs" : {
4452 "default": "null",
4453 "labels": ["Value display/formatting"],
4454 "type": "integer",
4455 "description": "By default, dygraphs displays numbers with a fixed number of digits after the decimal point. If you'd prefer to have a fixed number of significant figures, set this option to that number of sig figs. A value of 2, for instance, would cause 1 to be display as 1.0 and 1234 to be displayed as 1.23e+3."
4456 },
4457 "digitsAfterDecimal" : {
4458 "default": "2",
4459 "labels": ["Value display/formatting"],
4460 "type": "integer",
4461 "description": "Unless it's run in scientific mode (see the <code>sigFigs</code> option), dygraphs displays numbers with <code>digitsAfterDecimal</code> digits after the decimal point. Trailing zeros are not displayed, so with a value of 2 you'll get '0', '0.1', '0.12', '123.45' but not '123.456' (it will be rounded to '123.46'). Numbers with absolute value less than 0.1^digitsAfterDecimal (i.e. those which would show up as '0.00') will be displayed in scientific notation."
4462 },
4463 "maxNumberWidth" : {
4464 "default": "6",
4465 "labels": ["Value display/formatting"],
4466 "type": "integer",
4467 "description": "When displaying numbers in normal (not scientific) mode, large numbers will be displayed with many trailing zeros (e.g. 100000000 instead of 1e9). This can lead to unwieldy y-axis labels. If there are more than <code>maxNumberWidth</code> digits to the left of the decimal in a number, dygraphs will switch to scientific notation, even when not operating in scientific mode. If you'd like to see all those digits, set this to something large, like 20 or 30."
028ddf8a 4468 }
0addac07
DV
4469}
4470; // </JSON>
4471// NOTE: in addition to parsing as JS, this snippet is expected to be valid
4472// JSON. This assumption cannot be checked in JS, but it will be checked when
ca49434a
DV
4473// documentation is generated by the generate-documentation.py script. For the
4474// most part, this just means that you should always use double quotes.
028ddf8a
DV
4475
4476// Do a quick sanity check on the options reference.
4477(function() {
4478 var warn = function(msg) { if (console) console.warn(msg); };
4479 var flds = ['type', 'default', 'description'];
a38e9336
DV
4480 var valid_cats = [
4481 'Annotations',
4482 'Axis display',
ca49434a 4483 'Chart labels',
a38e9336
DV
4484 'CSV parsing',
4485 'Callbacks',
4486 'Data Line display',
4487 'Data Series Colors',
4488 'Error Bars',
4489 'Grid',
4490 'Interactive Elements',
4491 'Legend',
4492 'Overall display',
4493 'Rolling Averages',
36dfa958
DV
4494 'Value display/formatting',
4495 'Zooming'
a38e9336
DV
4496 ];
4497 var cats = {};
4498 for (var i = 0; i < valid_cats.length; i++) cats[valid_cats[i]] = true;
4499
028ddf8a
DV
4500 for (var k in Dygraph.OPTIONS_REFERENCE) {
4501 if (!Dygraph.OPTIONS_REFERENCE.hasOwnProperty(k)) continue;
4502 var op = Dygraph.OPTIONS_REFERENCE[k];
4503 for (var i = 0; i < flds.length; i++) {
4504 if (!op.hasOwnProperty(flds[i])) {
4505 warn('Option ' + k + ' missing "' + flds[i] + '" property');
4506 } else if (typeof(op[flds[i]]) != 'string') {
4507 warn(k + '.' + flds[i] + ' must be of type string');
4508 }
4509 }
a38e9336
DV
4510 var labels = op['labels'];
4511 if (typeof(labels) !== 'object') {
4512 warn('Option "' + k + '" is missing a "labels": [...] option');
4513 for (var i = 0; i < labels.length; i++) {
4514 if (!cats.hasOwnProperty(labels[i])) {
4515 warn('Option "' + k + '" has label "' + labels[i] +
4516 '", which is invalid.');
4517 }
4518 }
4519 }
028ddf8a
DV
4520 }
4521})();
4522// </REMOVE_FOR_COMBINED>