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