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