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