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