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