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