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