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