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