Cleanup of previous commit which included some extraneous lefovers from
[dygraphs.git] / dygraph.js
CommitLineData
6a1aa64f
DV
1// Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
2// All Rights Reserved.
3
4/**
5 * @fileoverview Creates an interactive, zoomable graph based on a CSV file or
285a6bda
DV
6 * string. Dygraph can handle multiple series with or without error bars. The
7 * date/value ranges will be automatically set. Dygraph uses the
6a1aa64f
DV
8 * <canvas> tag, so it only works in FF1.5+.
9 * @author danvdk@gmail.com (Dan Vanderkam)
10
11 Usage:
12 <div id="graphdiv" style="width:800px; height:500px;"></div>
13 <script type="text/javascript">
285a6bda
DV
14 new Dygraph(document.getElementById("graphdiv"),
15 "datafile.csv", // CSV file with headers
16 { }); // options
6a1aa64f
DV
17 </script>
18
19 The CSV file is of the form
20
285a6bda 21 Date,SeriesA,SeriesB,SeriesC
6a1aa64f
DV
22 YYYYMMDD,A1,B1,C1
23 YYYYMMDD,A2,B2,C2
24
6a1aa64f
DV
25 If the 'errorBars' option is set in the constructor, the input should be of
26 the form
27
285a6bda 28 Date,SeriesA,SeriesB,...
6a1aa64f
DV
29 YYYYMMDD,A1,sigmaA1,B1,sigmaB1,...
30 YYYYMMDD,A2,sigmaA2,B2,sigmaB2,...
31
32 If the 'fractions' option is set, the input should be of the form:
33
285a6bda 34 Date,SeriesA,SeriesB,...
6a1aa64f
DV
35 YYYYMMDD,A1/B1,A2/B2,...
36 YYYYMMDD,A1/B1,A2/B2,...
37
38 And error bars will be calculated automatically using a binomial distribution.
39
727439b4 40 For further documentation and examples, see http://dygraphs.com/
6a1aa64f
DV
41
42 */
43
44/**
45 * An interactive, zoomable graph
46 * @param {String | Function} file A file containing CSV data or a function that
47 * returns this data. The expected format for each line is
48 * YYYYMMDD,val1,val2,... or, if attrs.errorBars is set,
49 * YYYYMMDD,val1,stddev1,val2,stddev2,...
6a1aa64f
DV
50 * @param {Object} attrs Various other attributes, e.g. errorBars determines
51 * whether the input data contains error ranges.
52 */
285a6bda
DV
53Dygraph = function(div, data, opts) {
54 if (arguments.length > 0) {
55 if (arguments.length == 4) {
56 // Old versions of dygraphs took in the series labels as a constructor
57 // parameter. This doesn't make sense anymore, but it's easy to continue
58 // to support this usage.
59 this.warn("Using deprecated four-argument dygraph constructor");
60 this.__old_init__(div, data, arguments[2], arguments[3]);
61 } else {
62 this.__init__(div, data, opts);
63 }
64 }
6a1aa64f
DV
65};
66
285a6bda
DV
67Dygraph.NAME = "Dygraph";
68Dygraph.VERSION = "1.2";
69Dygraph.__repr__ = function() {
6a1aa64f
DV
70 return "[" + this.NAME + " " + this.VERSION + "]";
71};
285a6bda 72Dygraph.toString = function() {
6a1aa64f
DV
73 return this.__repr__();
74};
75
15b00ba8
JB
76/**
77 * Number formatting function which mimicks the behavior of %g in printf, i.e.
78 * either exponential or fixed format (without trailing 0s) is used depending on
79 * the length of the generated string. The advantage of this format is that
80 * there is a predictable upper bound on the resulting string length and
81 * significant figures are not dropped.
82 *
83 * NOTE: JavaScript's native toPrecision() is NOT a drop-in replacement for %g.
84 * It creates strings which are too long for absolute values between 10^-4 and
85 * 10^-6. See tests/number-format.html for examples.
86 *
87 * @param {Number} x The number to format
88 * @param {Number} opt_precision The precision to use, default 2.
89 * @return {String} A string formatted like %g in printf. The max generated
90 * string length should be precision +
91 */
92Dygraph.defaultFormat = function(x, opt_precision) {
93 // Avoid invalid precision values; [1, 21] is the valid range.
94 var p = Math.min(Math.max(1, opt_precision || 2), 21);
95
96 // This is deceptively simple. The actual algorithm comes from:
97 //
98 // Max allowed length = p + 4
99 // where 4 comes from 'e+n' and '.'.
100 //
101 // Length of fixed format = 2 + y + p
102 // where 2 comes from '0.' and y = # of leading zeroes.
103 //
104 // Equating the two and solving for y yields y = 2, or 0.00xxxx which is
105 // 1.0e-3.
106 //
107 // Since the behavior of toPrecision() is identical for larger numbers, we
108 // don't have to worry about the other bound.
109 //
110 // Finally, the argument for toExponential() is the number of trailing digits,
111 // so we take off 1 for the value before the '.'.
112 return (Math.abs(x) < 1.0e-3 && x != 0.0) ?
113 x.toExponential(p - 1) : x.toPrecision(p);
114};
115
6a1aa64f 116// Various default values
285a6bda
DV
117Dygraph.DEFAULT_ROLL_PERIOD = 1;
118Dygraph.DEFAULT_WIDTH = 480;
119Dygraph.DEFAULT_HEIGHT = 320;
120Dygraph.AXIS_LINE_WIDTH = 0.3;
6a1aa64f 121
8e4a6af3 122// Default attribute values.
285a6bda 123Dygraph.DEFAULT_ATTRS = {
a9fc39ab 124 highlightCircleSize: 3,
8e4a6af3 125 pixelsPerXLabel: 60,
c6336f04 126 pixelsPerYLabel: 30,
285a6bda 127
8e4a6af3
DV
128 labelsDivWidth: 250,
129 labelsDivStyles: {
130 // TODO(danvk): move defaults from createStatusMessage_ here.
285a6bda
DV
131 },
132 labelsSeparateLines: false,
bcd3ebf0 133 labelsShowZeroValues: true,
285a6bda 134 labelsKMB: false,
afefbcdb 135 labelsKMG2: false,
d160cc3b 136 showLabelsOnHighlight: true,
12e4c741 137
15b00ba8 138 yValueFormatter: Dygraph.defaultFormat,
285a6bda
DV
139
140 strokeWidth: 1.0,
8e4a6af3 141
8846615a
DV
142 axisTickSize: 3,
143 axisLabelFontSize: 14,
144 xAxisLabelWidth: 50,
145 yAxisLabelWidth: 50,
bf640e56 146 xAxisLabelFormatter: Dygraph.dateAxisFormatter,
8846615a 147 rightGap: 5,
285a6bda
DV
148
149 showRoller: false,
150 xValueFormatter: Dygraph.dateString_,
151 xValueParser: Dygraph.dateParser,
152 xTicker: Dygraph.dateTicker,
153
3d67f03b
DV
154 delimiter: ',',
155
ff00d3e2 156 logScale: false,
285a6bda
DV
157 sigma: 2.0,
158 errorBars: false,
159 fractions: false,
160 wilsonInterval: true, // only relevant if fractions is true
5954ef32 161 customBars: false,
43af96e7
NK
162 fillGraph: false,
163 fillAlpha: 0.15,
f032c51d 164 connectSeparatedPoints: false,
43af96e7
NK
165
166 stackedGraph: false,
afdc483f
NN
167 hideOverlayOnMouseOut: true,
168
00c281d4 169 stepPlot: false,
6faebb69 170 avoidMinZero: false
285a6bda
DV
171};
172
173// Various logging levels.
174Dygraph.DEBUG = 1;
175Dygraph.INFO = 2;
176Dygraph.WARNING = 3;
177Dygraph.ERROR = 3;
178
39b0e098
RK
179// Directions for panning and zooming. Use bit operations when combined
180// values are possible.
181Dygraph.HORIZONTAL = 1;
182Dygraph.VERTICAL = 2;
183
5c528fa2
DV
184// Used for initializing annotation CSS rules only once.
185Dygraph.addedAnnotationCSS = false;
186
285a6bda
DV
187Dygraph.prototype.__old_init__ = function(div, file, labels, attrs) {
188 // Labels is no longer a constructor parameter, since it's typically set
189 // directly from the data source. It also conains a name for the x-axis,
190 // which the previous constructor form did not.
191 if (labels != null) {
192 var new_labels = ["Date"];
193 for (var i = 0; i < labels.length; i++) new_labels.push(labels[i]);
fc80a396 194 Dygraph.update(attrs, { 'labels': new_labels });
285a6bda
DV
195 }
196 this.__init__(div, file, attrs);
8e4a6af3
DV
197};
198
6a1aa64f 199/**
285a6bda 200 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
6faebb69 201 * and interaction &lt;canvas&gt; inside of it. See the constructor for details
6a1aa64f 202 * on the parameters.
12e4c741 203 * @param {Element} div the Element to render the graph into.
6a1aa64f 204 * @param {String | Function} file Source data
6a1aa64f
DV
205 * @param {Object} attrs Miscellaneous other options
206 * @private
207 */
285a6bda 208Dygraph.prototype.__init__ = function(div, file, attrs) {
a2c8fff4
DV
209 // Hack for IE: if we're using excanvas and the document hasn't finished
210 // loading yet (and hence may not have initialized whatever it needs to
211 // initialize), then keep calling this routine periodically until it has.
212 if (/MSIE/.test(navigator.userAgent) && !window.opera &&
213 typeof(G_vmlCanvasManager) != 'undefined' &&
214 document.readyState != 'complete') {
215 var self = this;
216 setTimeout(function() { self.__init__(div, file, attrs) }, 100);
217 }
218
285a6bda
DV
219 // Support two-argument constructor
220 if (attrs == null) { attrs = {}; }
221
6a1aa64f 222 // Copy the important bits into the object
32988383 223 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
6a1aa64f 224 this.maindiv_ = div;
6a1aa64f 225 this.file_ = file;
285a6bda 226 this.rollPeriod_ = attrs.rollPeriod || Dygraph.DEFAULT_ROLL_PERIOD;
6a1aa64f 227 this.previousVerticalX_ = -1;
6a1aa64f 228 this.fractions_ = attrs.fractions || false;
6a1aa64f 229 this.dateWindow_ = attrs.dateWindow || null;
8b83c6cc 230
6a1aa64f 231 this.wilsonInterval_ = attrs.wilsonInterval || true;
fe0b7c03 232 this.is_initial_draw_ = true;
5c528fa2 233 this.annotations_ = [];
6be8e54c
JB
234
235 // Number of digits to use when labeling the x (if numeric) and y axis
236 // ticks.
237 this.numXDigits_ = 2;
238 this.numYDigits_ = 2;
239
240 // When labeling x (if numeric) or y values in the legend, there are
241 // numDigits + numExtraDigits of precision used. For axes labels with N
242 // digits of precision, the data should be displayed with at least N+1 digits
243 // of precision. The reason for this is to divide each interval between
244 // successive ticks into tenths (for 1) or hundredths (for 2), etc. For
245 // example, if the labels are [0, 1, 2], we want data to be displayed as
246 // 0.1, 1.3, etc.
247 this.numExtraDigits_ = 1;
8e4a6af3 248
f7d6278e
DV
249 // Clear the div. This ensure that, if multiple dygraphs are passed the same
250 // div, then only one will be drawn.
251 div.innerHTML = "";
252
c21d2c2d 253 // If the div isn't already sized then inherit from our attrs or
254 // give it a default size.
285a6bda 255 if (div.style.width == '') {
ddd1b11f 256 div.style.width = (attrs.width || Dygraph.DEFAULT_WIDTH) + "px";
285a6bda
DV
257 }
258 if (div.style.height == '') {
ddd1b11f 259 div.style.height = (attrs.height || Dygraph.DEFAULT_HEIGHT) + "px";
32988383 260 }
285a6bda
DV
261 this.width_ = parseInt(div.style.width, 10);
262 this.height_ = parseInt(div.style.height, 10);
c21d2c2d 263 // The div might have been specified as percent of the current window size,
264 // convert that to an appropriate number of pixels.
265 if (div.style.width.indexOf("%") == div.style.width.length - 1) {
c6f45033 266 this.width_ = div.offsetWidth;
c21d2c2d 267 }
268 if (div.style.height.indexOf("%") == div.style.height.length - 1) {
c6f45033 269 this.height_ = div.offsetHeight;
c21d2c2d 270 }
32988383 271
10a6456d
DV
272 if (this.width_ == 0) {
273 this.error("dygraph has zero width. Please specify a width in pixels.");
274 }
275 if (this.height_ == 0) {
276 this.error("dygraph has zero height. Please specify a height in pixels.");
277 }
278
344ba8c0 279 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
43af96e7
NK
280 if (attrs['stackedGraph']) {
281 attrs['fillGraph'] = true;
282 // TODO(nikhilk): Add any other stackedGraph checks here.
283 }
284
285a6bda
DV
285 // Dygraphs has many options, some of which interact with one another.
286 // To keep track of everything, we maintain two sets of options:
287 //
c21d2c2d 288 // this.user_attrs_ only options explicitly set by the user.
285a6bda
DV
289 // this.attrs_ defaults, options derived from user_attrs_, data.
290 //
291 // Options are then accessed this.attr_('attr'), which first looks at
292 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
293 // defaults without overriding behavior that the user specifically asks for.
294 this.user_attrs_ = {};
fc80a396 295 Dygraph.update(this.user_attrs_, attrs);
6a1aa64f 296
285a6bda 297 this.attrs_ = {};
fc80a396 298 Dygraph.update(this.attrs_, Dygraph.DEFAULT_ATTRS);
6a1aa64f 299
16269f6e 300 this.boundaryIds_ = [];
6a1aa64f 301
285a6bda
DV
302 // Make a note of whether labels will be pulled from the CSV file.
303 this.labelsFromCSV_ = (this.attr_("labels") == null);
6a1aa64f
DV
304
305 // Create the containing DIV and other interactive elements
306 this.createInterface_();
307
738fc797 308 this.start_();
6a1aa64f
DV
309};
310
227b93cc
DV
311Dygraph.prototype.attr_ = function(name, seriesName) {
312 if (seriesName &&
313 typeof(this.user_attrs_[seriesName]) != 'undefined' &&
314 this.user_attrs_[seriesName] != null &&
315 typeof(this.user_attrs_[seriesName][name]) != 'undefined') {
316 return this.user_attrs_[seriesName][name];
450fe64b 317 } else if (typeof(this.user_attrs_[name]) != 'undefined') {
285a6bda
DV
318 return this.user_attrs_[name];
319 } else if (typeof(this.attrs_[name]) != 'undefined') {
320 return this.attrs_[name];
321 } else {
322 return null;
323 }
324};
325
326// TODO(danvk): any way I can get the line numbers to be this.warn call?
327Dygraph.prototype.log = function(severity, message) {
328 if (typeof(console) != 'undefined') {
329 switch (severity) {
330 case Dygraph.DEBUG:
331 console.debug('dygraphs: ' + message);
332 break;
333 case Dygraph.INFO:
334 console.info('dygraphs: ' + message);
335 break;
336 case Dygraph.WARNING:
337 console.warn('dygraphs: ' + message);
338 break;
339 case Dygraph.ERROR:
340 console.error('dygraphs: ' + message);
341 break;
342 }
343 }
344}
345Dygraph.prototype.info = function(message) {
346 this.log(Dygraph.INFO, message);
347}
348Dygraph.prototype.warn = function(message) {
349 this.log(Dygraph.WARNING, message);
350}
351Dygraph.prototype.error = function(message) {
352 this.log(Dygraph.ERROR, message);
353}
354
6a1aa64f
DV
355/**
356 * Returns the current rolling period, as set by the user or an option.
6faebb69 357 * @return {Number} The number of points in the rolling window
6a1aa64f 358 */
285a6bda 359Dygraph.prototype.rollPeriod = function() {
6a1aa64f 360 return this.rollPeriod_;
76171648
DV
361};
362
599fb4ad
DV
363/**
364 * Returns the currently-visible x-range. This can be affected by zooming,
365 * panning or a call to updateOptions.
366 * Returns a two-element array: [left, right].
367 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
368 */
369Dygraph.prototype.xAxisRange = function() {
370 if (this.dateWindow_) return this.dateWindow_;
371
372 // The entire chart is visible.
373 var left = this.rawData_[0][0];
374 var right = this.rawData_[this.rawData_.length - 1][0];
375 return [left, right];
376};
377
3230c662 378/**
d58ae307
DV
379 * Returns the currently-visible y-range for an axis. This can be affected by
380 * zooming, panning or a call to updateOptions. Axis indices are zero-based. If
381 * called with no arguments, returns the range of the first axis.
3230c662
DV
382 * Returns a two-element array: [bottom, top].
383 */
d58ae307 384Dygraph.prototype.yAxisRange = function(idx) {
d63e6799 385 if (typeof(idx) == "undefined") idx = 0;
d58ae307
DV
386 if (idx < 0 || idx >= this.axes_.length) return null;
387 return [ this.axes_[idx].computedValueRange[0],
388 this.axes_[idx].computedValueRange[1] ];
389};
390
391/**
392 * Returns the currently-visible y-ranges for each axis. This can be affected by
393 * zooming, panning, calls to updateOptions, etc.
394 * Returns an array of [bottom, top] pairs, one for each y-axis.
395 */
396Dygraph.prototype.yAxisRanges = function() {
397 var ret = [];
398 for (var i = 0; i < this.axes_.length; i++) {
399 ret.push(this.yAxisRange(i));
400 }
401 return ret;
3230c662
DV
402};
403
d58ae307 404// TODO(danvk): use these functions throughout dygraphs.
3230c662
DV
405/**
406 * Convert from data coordinates to canvas/div X/Y coordinates.
d58ae307
DV
407 * If specified, do this conversion for the coordinate system of a particular
408 * axis. Uses the first axis by default.
3230c662
DV
409 * Returns a two-element array: [X, Y]
410 */
d58ae307 411Dygraph.prototype.toDomCoords = function(x, y, axis) {
3230c662
DV
412 var ret = [null, null];
413 var area = this.plotter_.area;
414 if (x !== null) {
415 var xRange = this.xAxisRange();
416 ret[0] = area.x + (x - xRange[0]) / (xRange[1] - xRange[0]) * area.w;
417 }
418
419 if (y !== null) {
d58ae307 420 var yRange = this.yAxisRange(axis);
891ad846 421 ret[1] = area.y + (yRange[1] - y) / (yRange[1] - yRange[0]) * area.h;
3230c662
DV
422 }
423
424 return ret;
425};
426
427/**
428 * Convert from canvas/div coords to data coordinates.
d58ae307
DV
429 * If specified, do this conversion for the coordinate system of a particular
430 * axis. Uses the first axis by default.
3230c662
DV
431 * Returns a two-element array: [X, Y]
432 */
d58ae307 433Dygraph.prototype.toDataCoords = function(x, y, axis) {
3230c662
DV
434 var ret = [null, null];
435 var area = this.plotter_.area;
436 if (x !== null) {
437 var xRange = this.xAxisRange();
438 ret[0] = xRange[0] + (x - area.x) / area.w * (xRange[1] - xRange[0]);
439 }
440
441 if (y !== null) {
d58ae307 442 var yRange = this.yAxisRange(axis);
3230c662
DV
443 ret[1] = yRange[0] + (area.h - y) / area.h * (yRange[1] - yRange[0]);
444 }
445
446 return ret;
447};
448
e99fde05
DV
449/**
450 * Returns the number of columns (including the independent variable).
451 */
452Dygraph.prototype.numColumns = function() {
453 return this.rawData_[0].length;
454};
455
456/**
457 * Returns the number of rows (excluding any header/label row).
458 */
459Dygraph.prototype.numRows = function() {
460 return this.rawData_.length;
461};
462
463/**
464 * Returns the value in the given row and column. If the row and column exceed
465 * the bounds on the data, returns null. Also returns null if the value is
466 * missing.
467 */
468Dygraph.prototype.getValue = function(row, col) {
469 if (row < 0 || row > this.rawData_.length) return null;
470 if (col < 0 || col > this.rawData_[row].length) return null;
471
472 return this.rawData_[row][col];
473};
474
76171648
DV
475Dygraph.addEvent = function(el, evt, fn) {
476 var normed_fn = function(e) {
477 if (!e) var e = window.event;
478 fn(e);
479 };
480 if (window.addEventListener) { // Mozilla, Netscape, Firefox
481 el.addEventListener(evt, normed_fn, false);
482 } else { // IE
483 el.attachEvent('on' + evt, normed_fn);
484 }
485};
6a1aa64f
DV
486
487/**
285a6bda 488 * Generates interface elements for the Dygraph: a containing div, a div to
6a1aa64f 489 * display the current point, and a textbox to adjust the rolling average
697e70b2 490 * period. Also creates the Renderer/Layout elements.
6a1aa64f
DV
491 * @private
492 */
285a6bda 493Dygraph.prototype.createInterface_ = function() {
6a1aa64f
DV
494 // Create the all-enclosing graph div
495 var enclosing = this.maindiv_;
496
b0c3b730
DV
497 this.graphDiv = document.createElement("div");
498 this.graphDiv.style.width = this.width_ + "px";
499 this.graphDiv.style.height = this.height_ + "px";
500 enclosing.appendChild(this.graphDiv);
501
502 // Create the canvas for interactive parts of the chart.
f8cfec73 503 this.canvas_ = Dygraph.createCanvas();
b0c3b730
DV
504 this.canvas_.style.position = "absolute";
505 this.canvas_.width = this.width_;
506 this.canvas_.height = this.height_;
f8cfec73
DV
507 this.canvas_.style.width = this.width_ + "px"; // for IE
508 this.canvas_.style.height = this.height_ + "px"; // for IE
b0c3b730
DV
509
510 // ... and for static parts of the chart.
6a1aa64f 511 this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
76171648 512
eb7bf005
EC
513 // The interactive parts of the graph are drawn on top of the chart.
514 this.graphDiv.appendChild(this.hidden_);
515 this.graphDiv.appendChild(this.canvas_);
516 this.mouseEventElement_ = this.canvas_;
517
76171648 518 var dygraph = this;
eb7bf005 519 Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(e) {
76171648
DV
520 dygraph.mouseMove_(e);
521 });
eb7bf005 522 Dygraph.addEvent(this.mouseEventElement_, 'mouseout', function(e) {
76171648
DV
523 dygraph.mouseOut_(e);
524 });
697e70b2
DV
525
526 // Create the grapher
527 // TODO(danvk): why does the Layout need its own set of options?
528 this.layoutOptions_ = { 'xOriginIsZero': false };
529 Dygraph.update(this.layoutOptions_, this.attrs_);
530 Dygraph.update(this.layoutOptions_, this.user_attrs_);
531 Dygraph.update(this.layoutOptions_, {
532 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
533
534 this.layout_ = new DygraphLayout(this, this.layoutOptions_);
535
536 // TODO(danvk): why does the Renderer need its own set of options?
537 this.renderOptions_ = { colorScheme: this.colors_,
538 strokeColor: null,
539 axisLineWidth: Dygraph.AXIS_LINE_WIDTH };
540 Dygraph.update(this.renderOptions_, this.attrs_);
541 Dygraph.update(this.renderOptions_, this.user_attrs_);
697e70b2
DV
542
543 this.createStatusMessage_();
697e70b2 544 this.createDragInterface_();
4cfcc38c
DV
545};
546
547/**
548 * Detach DOM elements in the dygraph and null out all data references.
549 * Calling this when you're done with a dygraph can dramatically reduce memory
550 * usage. See, e.g., the tests/perf.html example.
551 */
552Dygraph.prototype.destroy = function() {
553 var removeRecursive = function(node) {
554 while (node.hasChildNodes()) {
555 removeRecursive(node.firstChild);
556 node.removeChild(node.firstChild);
557 }
558 };
559 removeRecursive(this.maindiv_);
560
561 var nullOut = function(obj) {
562 for (var n in obj) {
563 if (typeof(obj[n]) === 'object') {
564 obj[n] = null;
565 }
566 }
567 };
568
569 // These may not all be necessary, but it can't hurt...
570 nullOut(this.layout_);
571 nullOut(this.plotter_);
572 nullOut(this);
573};
6a1aa64f
DV
574
575/**
576 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
285a6bda 577 * this particular canvas. All Dygraph work is done on this.canvas_.
8846615a 578 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
6a1aa64f
DV
579 * @return {Object} The newly-created canvas
580 * @private
581 */
285a6bda 582Dygraph.prototype.createPlotKitCanvas_ = function(canvas) {
f8cfec73 583 var h = Dygraph.createCanvas();
6a1aa64f 584 h.style.position = "absolute";
9ac5e4ae
DV
585 // TODO(danvk): h should be offset from canvas. canvas needs to include
586 // some extra area to make it easier to zoom in on the far left and far
587 // right. h needs to be precisely the plot area, so that clipping occurs.
6a1aa64f
DV
588 h.style.top = canvas.style.top;
589 h.style.left = canvas.style.left;
590 h.width = this.width_;
591 h.height = this.height_;
f8cfec73
DV
592 h.style.width = this.width_ + "px"; // for IE
593 h.style.height = this.height_ + "px"; // for IE
6a1aa64f
DV
594 return h;
595};
596
f474c2a3
DV
597// Taken from MochiKit.Color
598Dygraph.hsvToRGB = function (hue, saturation, value) {
599 var red;
600 var green;
601 var blue;
602 if (saturation === 0) {
603 red = value;
604 green = value;
605 blue = value;
606 } else {
607 var i = Math.floor(hue * 6);
608 var f = (hue * 6) - i;
609 var p = value * (1 - saturation);
610 var q = value * (1 - (saturation * f));
611 var t = value * (1 - (saturation * (1 - f)));
612 switch (i) {
613 case 1: red = q; green = value; blue = p; break;
614 case 2: red = p; green = value; blue = t; break;
615 case 3: red = p; green = q; blue = value; break;
616 case 4: red = t; green = p; blue = value; break;
617 case 5: red = value; green = p; blue = q; break;
618 case 6: // fall through
619 case 0: red = value; green = t; blue = p; break;
620 }
621 }
622 red = Math.floor(255 * red + 0.5);
623 green = Math.floor(255 * green + 0.5);
624 blue = Math.floor(255 * blue + 0.5);
625 return 'rgb(' + red + ',' + green + ',' + blue + ')';
626};
627
628
6a1aa64f
DV
629/**
630 * Generate a set of distinct colors for the data series. This is done with a
631 * color wheel. Saturation/Value are customizable, and the hue is
632 * equally-spaced around the color wheel. If a custom set of colors is
633 * specified, that is used instead.
6a1aa64f
DV
634 * @private
635 */
285a6bda
DV
636Dygraph.prototype.setColors_ = function() {
637 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
638 // away with this.renderOptions_.
639 var num = this.attr_("labels").length - 1;
6a1aa64f 640 this.colors_ = [];
285a6bda
DV
641 var colors = this.attr_('colors');
642 if (!colors) {
643 var sat = this.attr_('colorSaturation') || 1.0;
644 var val = this.attr_('colorValue') || 0.5;
2aa21213 645 var half = Math.ceil(num / 2);
6a1aa64f 646 for (var i = 1; i <= num; i++) {
ec1959eb 647 if (!this.visibility()[i-1]) continue;
43af96e7 648 // alternate colors for high contrast.
2aa21213 649 var idx = i % 2 ? Math.ceil(i / 2) : (half + i / 2);
43af96e7
NK
650 var hue = (1.0 * idx/ (1 + num));
651 this.colors_.push(Dygraph.hsvToRGB(hue, sat, val));
6a1aa64f
DV
652 }
653 } else {
654 for (var i = 0; i < num; i++) {
ec1959eb 655 if (!this.visibility()[i]) continue;
285a6bda 656 var colorStr = colors[i % colors.length];
f474c2a3 657 this.colors_.push(colorStr);
6a1aa64f
DV
658 }
659 }
285a6bda 660
c21d2c2d 661 // TODO(danvk): update this w/r/t/ the new options system.
285a6bda 662 this.renderOptions_.colorScheme = this.colors_;
fc80a396
DV
663 Dygraph.update(this.plotter_.options, this.renderOptions_);
664 Dygraph.update(this.layoutOptions_, this.user_attrs_);
665 Dygraph.update(this.layoutOptions_, this.attrs_);
6a1aa64f
DV
666}
667
43af96e7
NK
668/**
669 * Return the list of colors. This is either the list of colors passed in the
670 * attributes, or the autogenerated list of rgb(r,g,b) strings.
671 * @return {Array<string>} The list of colors.
672 */
673Dygraph.prototype.getColors = function() {
674 return this.colors_;
675};
676
5e60386d
DV
677// The following functions are from quirksmode.org with a modification for Safari from
678// http://blog.firetree.net/2005/07/04/javascript-find-position/
3df0ccf0
DV
679// http://www.quirksmode.org/js/findpos.html
680Dygraph.findPosX = function(obj) {
681 var curleft = 0;
5e60386d 682 if(obj.offsetParent)
50360fd0 683 while(1)
5e60386d 684 {
3df0ccf0 685 curleft += obj.offsetLeft;
5e60386d
DV
686 if(!obj.offsetParent)
687 break;
3df0ccf0
DV
688 obj = obj.offsetParent;
689 }
5e60386d 690 else if(obj.x)
3df0ccf0
DV
691 curleft += obj.x;
692 return curleft;
693};
c21d2c2d 694
3df0ccf0
DV
695Dygraph.findPosY = function(obj) {
696 var curtop = 0;
5e60386d
DV
697 if(obj.offsetParent)
698 while(1)
699 {
3df0ccf0 700 curtop += obj.offsetTop;
5e60386d
DV
701 if(!obj.offsetParent)
702 break;
3df0ccf0
DV
703 obj = obj.offsetParent;
704 }
5e60386d 705 else if(obj.y)
3df0ccf0
DV
706 curtop += obj.y;
707 return curtop;
708};
709
5e60386d 710
71a11a8e 711
6a1aa64f
DV
712/**
713 * Create the div that contains information on the selected point(s)
714 * This goes in the top right of the canvas, unless an external div has already
715 * been specified.
716 * @private
717 */
fedbd797 718Dygraph.prototype.createStatusMessage_ = function() {
719 var userLabelsDiv = this.user_attrs_["labelsDiv"];
720 if (userLabelsDiv && null != userLabelsDiv
721 && (typeof(userLabelsDiv) == "string" || userLabelsDiv instanceof String)) {
722 this.user_attrs_["labelsDiv"] = document.getElementById(userLabelsDiv);
723 }
285a6bda
DV
724 if (!this.attr_("labelsDiv")) {
725 var divWidth = this.attr_('labelsDivWidth');
b0c3b730 726 var messagestyle = {
6a1aa64f
DV
727 "position": "absolute",
728 "fontSize": "14px",
729 "zIndex": 10,
730 "width": divWidth + "px",
731 "top": "0px",
8846615a 732 "left": (this.width_ - divWidth - 2) + "px",
6a1aa64f
DV
733 "background": "white",
734 "textAlign": "left",
b0c3b730 735 "overflow": "hidden"};
fc80a396 736 Dygraph.update(messagestyle, this.attr_('labelsDivStyles'));
b0c3b730
DV
737 var div = document.createElement("div");
738 for (var name in messagestyle) {
85b99f0b
DV
739 if (messagestyle.hasOwnProperty(name)) {
740 div.style[name] = messagestyle[name];
741 }
b0c3b730
DV
742 }
743 this.graphDiv.appendChild(div);
285a6bda 744 this.attrs_.labelsDiv = div;
6a1aa64f
DV
745 }
746};
747
748/**
0abfbd7e
DV
749 * Position the labels div so that its right edge is flush with the right edge
750 * of the charting area.
751 */
752Dygraph.prototype.positionLabelsDiv_ = function() {
753 // Don't touch a user-specified labelsDiv.
754 if (this.user_attrs_.hasOwnProperty("labelsDiv")) return;
755
756 var area = this.plotter_.area;
757 var div = this.attr_("labelsDiv");
8c21adcf 758 div.style.left = area.x + area.w - this.attr_("labelsDivWidth") - 1 + "px";
0abfbd7e
DV
759};
760
761/**
6a1aa64f 762 * Create the text box to adjust the averaging period
6a1aa64f
DV
763 * @private
764 */
285a6bda 765Dygraph.prototype.createRollInterface_ = function() {
8c69de65
DV
766 // Create a roller if one doesn't exist already.
767 if (!this.roller_) {
768 this.roller_ = document.createElement("input");
769 this.roller_.type = "text";
770 this.roller_.style.display = "none";
771 this.graphDiv.appendChild(this.roller_);
772 }
773
774 var display = this.attr_('showRoller') ? 'block' : 'none';
26ca7938 775
b0c3b730
DV
776 var textAttr = { "position": "absolute",
777 "zIndex": 10,
778 "top": (this.plotter_.area.h - 25) + "px",
779 "left": (this.plotter_.area.x + 1) + "px",
780 "display": display
6a1aa64f 781 };
8c69de65
DV
782 this.roller_.size = "2";
783 this.roller_.value = this.rollPeriod_;
b0c3b730 784 for (var name in textAttr) {
85b99f0b 785 if (textAttr.hasOwnProperty(name)) {
8c69de65 786 this.roller_.style[name] = textAttr[name];
85b99f0b 787 }
b0c3b730
DV
788 }
789
76171648 790 var dygraph = this;
8c69de65 791 this.roller_.onchange = function() { dygraph.adjustRoll(dygraph.roller_.value); };
76171648
DV
792};
793
794// These functions are taken from MochiKit.Signal
795Dygraph.pageX = function(e) {
796 if (e.pageX) {
797 return (!e.pageX || e.pageX < 0) ? 0 : e.pageX;
798 } else {
799 var de = document;
800 var b = document.body;
801 return e.clientX +
802 (de.scrollLeft || b.scrollLeft) -
803 (de.clientLeft || 0);
804 }
805};
806
807Dygraph.pageY = function(e) {
808 if (e.pageY) {
809 return (!e.pageY || e.pageY < 0) ? 0 : e.pageY;
810 } else {
811 var de = document;
812 var b = document.body;
813 return e.clientY +
814 (de.scrollTop || b.scrollTop) -
815 (de.clientTop || 0);
816 }
817};
6a1aa64f 818
6faebb69
JB
819/**
820 * Set up all the mouse handlers needed to capture dragging behavior for zoom
821 * events.
822 * @private
823 */
824Dygraph.prototype.createDragInterface_ = function() {
825 var self = this;
6a1aa64f 826
6faebb69
JB
827 // Tracks whether the mouse is down right now
828 var isZooming = false;
829 var isPanning = false; // is this drag part of a pan?
830 var is2DPan = false; // if so, is that pan 1- or 2-dimensional?
831 var dragStartX = null;
832 var dragStartY = null;
833 var dragEndX = null;
834 var dragEndY = null;
835 var dragDirection = null;
836 var prevEndX = null;
837 var prevEndY = null;
838 var prevDragDirection = null;
bce01b0f 839
d58ae307 840 // TODO(danvk): update this comment
6faebb69
JB
841 // draggingDate and draggingValue represent the [date,value] point on the
842 // graph at which the mouse was pressed. As the mouse moves while panning,
843 // the viewport must pan so that the mouse position points to
844 // [draggingDate, draggingValue]
845 var draggingDate = null;
ee672584 846
6faebb69
JB
847 // TODO(danvk): update this comment
848 // The range in second/value units that the viewport encompasses during a
849 // panning operation.
850 var dateRange = null;
ee672584 851
6faebb69
JB
852 // Utility function to convert page-wide coordinates to canvas coords
853 var px = 0;
854 var py = 0;
855 var getX = function(e) { return Dygraph.pageX(e) - px };
856 var getY = function(e) { return Dygraph.pageY(e) - py };
6a1aa64f 857
6faebb69
JB
858 // Draw zoom rectangles when the mouse is down and the user moves around
859 Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(event) {
860 if (isZooming) {
861 dragEndX = getX(event);
862 dragEndY = getY(event);
863
864 var xDelta = Math.abs(dragStartX - dragEndX);
865 var yDelta = Math.abs(dragStartY - dragEndY);
866
867 // drag direction threshold for y axis is twice as large as x axis
868 dragDirection = (xDelta < yDelta / 2) ? Dygraph.VERTICAL : Dygraph.HORIZONTAL;
869
870 self.drawZoomRect_(dragDirection, dragStartX, dragEndX, dragStartY, dragEndY,
871 prevDragDirection, prevEndX, prevEndY);
872
873 prevEndX = dragEndX;
874 prevEndY = dragEndY;
875 prevDragDirection = dragDirection;
876 } else if (isPanning) {
877 dragEndX = getX(event);
878 dragEndY = getY(event);
879
880 // TODO(danvk): update this comment
881 // Want to have it so that:
882 // 1. draggingDate appears at dragEndX, draggingValue appears at dragEndY.
883 // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
884 // 3. draggingValue appears at dragEndY.
885 // 4. valueRange is unaltered.
886
887 var minDate = draggingDate - (dragEndX / self.width_) * dateRange;
888 var maxDate = minDate + dateRange;
889 self.dateWindow_ = [minDate, maxDate];
890
891
892 // y-axis scaling is automatic unless this is a full 2D pan.
893 if (is2DPan) {
894 // Adjust each axis appropriately.
895 var y_frac = dragEndY / self.height_;
896 for (var i = 0; i < self.axes_.length; i++) {
897 var axis = self.axes_[i];
898 var maxValue = axis.draggingValue + y_frac * axis.dragValueRange;
899 var minValue = maxValue - axis.dragValueRange;
900 axis.valueWindow = [ minValue, maxValue ];
901 }
902 }
79b3ee42 903
6faebb69
JB
904 self.drawGraph_();
905 }
906 });
bce01b0f 907
6faebb69
JB
908 // Track the beginning of drag events
909 Dygraph.addEvent(this.mouseEventElement_, 'mousedown', function(event) {
910 // prevents mouse drags from selecting page text.
911 if (event.preventDefault) {
912 event.preventDefault(); // Firefox, Chrome, etc.
913 } else {
914 event.returnValue = false; // IE
915 event.cancelBubble = true;
916 }
ee672584 917
6faebb69
JB
918 px = Dygraph.findPosX(self.canvas_);
919 py = Dygraph.findPosY(self.canvas_);
920 dragStartX = getX(event);
921 dragStartY = getY(event);
ee672584 922
6faebb69
JB
923 if (event.altKey || event.shiftKey) {
924 // have to be zoomed in to pan.
925 var zoomedY = false;
926 for (var i = 0; i < self.axes_.length; i++) {
927 if (self.axes_[i].valueWindow || self.axes_[i].valueRange) {
928 zoomedY = true;
929 break;
d58ae307
DV
930 }
931 }
6faebb69 932 if (!self.dateWindow_ && !zoomedY) return;
d58ae307 933
6faebb69
JB
934 isPanning = true;
935 var xRange = self.xAxisRange();
936 dateRange = xRange[1] - xRange[0];
e3489f4f 937
6faebb69
JB
938 // Record the range of each y-axis at the start of the drag.
939 // If any axis has a valueRange or valueWindow, then we want a 2D pan.
940 is2DPan = false;
941 for (var i = 0; i < self.axes_.length; i++) {
942 var axis = self.axes_[i];
943 var yRange = self.yAxisRange(i);
944 axis.dragValueRange = yRange[1] - yRange[0];
945 var r = self.toDataCoords(null, dragStartY, i);
946 axis.draggingValue = r[1];
947 if (axis.valueWindow || axis.valueRange) is2DPan = true;
948 }
0a52ab7a 949
6faebb69
JB
950 // TODO(konigsberg): Switch from all this math to toDataCoords?
951 // Seems to work for the dragging value.
952 draggingDate = (dragStartX / self.width_) * dateRange + xRange[0];
bce01b0f 953 } else {
6faebb69 954 isZooming = true;
bce01b0f 955 }
6faebb69 956 });
6a1aa64f 957
6faebb69
JB
958 // If the user releases the mouse button during a drag, but not over the
959 // canvas, then it doesn't count as a zooming action.
960 Dygraph.addEvent(document, 'mouseup', function(event) {
961 if (isZooming || isPanning) {
962 isZooming = false;
963 dragStartX = null;
964 dragStartY = null;
6a1aa64f 965 }
bce01b0f 966
6faebb69
JB
967 if (isPanning) {
968 isPanning = false;
969 draggingDate = null;
970 dateRange = null;
971 for (var i = 0; i < self.axes_.length; i++) {
972 delete self.axes_[i].draggingValue;
973 delete self.axes_[i].dragValueRange;
974 }
bce01b0f 975 }
6faebb69 976 });
6a1aa64f
DV
977
978 // Temporarily cancel the dragging event when the mouse leaves the graph
6faebb69
JB
979 Dygraph.addEvent(this.mouseEventElement_, 'mouseout', function(event) {
980 if (isZooming) {
981 dragEndX = null;
982 dragEndY = null;
6a1aa64f 983 }
6faebb69 984 });
6a1aa64f 985
6faebb69
JB
986 // If the mouse is released on the canvas during a drag event, then it's a
987 // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
988 Dygraph.addEvent(this.mouseEventElement_, 'mouseup', function(event) {
989 if (isZooming) {
990 isZooming = false;
991 dragEndX = getX(event);
992 dragEndY = getY(event);
993 var regionWidth = Math.abs(dragEndX - dragStartX);
994 var regionHeight = Math.abs(dragEndY - dragStartY);
995
996 if (regionWidth < 2 && regionHeight < 2 &&
997 self.lastx_ != undefined && self.lastx_ != -1) {
998 // TODO(danvk): pass along more info about the points, e.g. 'x'
999 if (self.attr_('clickCallback') != null) {
1000 self.attr_('clickCallback')(event, self.lastx_, self.selPoints_);
1001 }
1002 if (self.attr_('pointClickCallback')) {
1003 // check if the click was on a particular point.
1004 var closestIdx = -1;
1005 var closestDistance = 0;
1006 for (var i = 0; i < self.selPoints_.length; i++) {
1007 var p = self.selPoints_[i];
1008 var distance = Math.pow(p.canvasx - dragEndX, 2) +
1009 Math.pow(p.canvasy - dragEndY, 2);
1010 if (closestIdx == -1 || distance < closestDistance) {
1011 closestDistance = distance;
1012 closestIdx = i;
1013 }
1014 }
1e1bf7df 1015
6faebb69
JB
1016 // Allow any click within two pixels of the dot.
1017 var radius = self.attr_('highlightCircleSize') + 2;
1018 if (closestDistance <= 5 * 5) {
1019 self.attr_('pointClickCallback')(event, self.selPoints_[closestIdx]);
1020 }
1021 }
1022 }
6a1aa64f 1023
6faebb69
JB
1024 if (regionWidth >= 10 && dragDirection == Dygraph.HORIZONTAL) {
1025 self.doZoomX_(Math.min(dragStartX, dragEndX),
1026 Math.max(dragStartX, dragEndX));
1027 } else if (regionHeight >= 10 && dragDirection == Dygraph.VERTICAL){
1028 self.doZoomY_(Math.min(dragStartY, dragEndY),
1029 Math.max(dragStartY, dragEndY));
6a1aa64f 1030 } else {
6faebb69
JB
1031 self.canvas_.getContext("2d").clearRect(0, 0,
1032 self.canvas_.width,
1033 self.canvas_.height);
6a1aa64f
DV
1034 }
1035
6faebb69
JB
1036 dragStartX = null;
1037 dragStartY = null;
6a1aa64f 1038 }
2b188b3d 1039
6faebb69
JB
1040 if (isPanning) {
1041 isPanning = false;
1042 is2DPan = false;
1043 draggingDate = null;
1044 dateRange = null;
1045 valueRange = null;
6a1aa64f 1046 }
6faebb69 1047 });
8b83c6cc 1048
6faebb69
JB
1049 // Double-clicking zooms back out
1050 Dygraph.addEvent(this.mouseEventElement_, 'dblclick', function(event) {
1051 // Disable zooming out if panning.
1052 if (event.altKey || event.shiftKey) return;
1053
1054 self.doUnzoom_();
6a1aa64f
DV
1055 });
1056};
1057
1058/**
1059 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
1060 * up any previous zoom rectangles that were drawn. This could be optimized to
1061 * avoid extra redrawing, but it's tricky to avoid interactions with the status
1062 * dots.
8b83c6cc 1063 *
39b0e098
RK
1064 * @param {Number} direction the direction of the zoom rectangle. Acceptable
1065 * values are Dygraph.HORIZONTAL and Dygraph.VERTICAL.
6a1aa64f
DV
1066 * @param {Number} startX The X position where the drag started, in canvas
1067 * coordinates.
1068 * @param {Number} endX The current X position of the drag, in canvas coords.
8b83c6cc
RK
1069 * @param {Number} startY The Y position where the drag started, in canvas
1070 * coordinates.
1071 * @param {Number} endY The current Y position of the drag, in canvas coords.
39b0e098 1072 * @param {Number} prevDirection the value of direction on the previous call to
8b83c6cc 1073 * this function. Used to avoid excess redrawing
6a1aa64f
DV
1074 * @param {Number} prevEndX The value of endX on the previous call to this
1075 * function. Used to avoid excess redrawing
8b83c6cc
RK
1076 * @param {Number} prevEndY The value of endY on the previous call to this
1077 * function. Used to avoid excess redrawing
6a1aa64f
DV
1078 * @private
1079 */
8b83c6cc
RK
1080Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY, endY,
1081 prevDirection, prevEndX, prevEndY) {
6a1aa64f
DV
1082 var ctx = this.canvas_.getContext("2d");
1083
1084 // Clean up from the previous rect if necessary
39b0e098 1085 if (prevDirection == Dygraph.HORIZONTAL) {
6a1aa64f
DV
1086 ctx.clearRect(Math.min(startX, prevEndX), 0,
1087 Math.abs(startX - prevEndX), this.height_);
39b0e098 1088 } else if (prevDirection == Dygraph.VERTICAL){
8b83c6cc
RK
1089 ctx.clearRect(0, Math.min(startY, prevEndY),
1090 this.width_, Math.abs(startY - prevEndY));
6a1aa64f
DV
1091 }
1092
1093 // Draw a light-grey rectangle to show the new viewing area
39b0e098 1094 if (direction == Dygraph.HORIZONTAL) {
8b83c6cc
RK
1095 if (endX && startX) {
1096 ctx.fillStyle = "rgba(128,128,128,0.33)";
1097 ctx.fillRect(Math.min(startX, endX), 0,
1098 Math.abs(endX - startX), this.height_);
1099 }
1100 }
39b0e098 1101 if (direction == Dygraph.VERTICAL) {
8b83c6cc
RK
1102 if (endY && startY) {
1103 ctx.fillStyle = "rgba(128,128,128,0.33)";
1104 ctx.fillRect(0, Math.min(startY, endY),
1105 this.width_, Math.abs(endY - startY));
1106 }
6a1aa64f
DV
1107 }
1108};
1109
1110/**
8b83c6cc
RK
1111 * Zoom to something containing [lowX, highX]. These are pixel coordinates in
1112 * the canvas. The exact zoom window may be slightly larger if there are no data
1113 * points near lowX or highX. Don't confuse this function with doZoomXDates,
1114 * which accepts dates that match the raw data. This function redraws the graph.
d58ae307 1115 *
6a1aa64f
DV
1116 * @param {Number} lowX The leftmost pixel value that should be visible.
1117 * @param {Number} highX The rightmost pixel value that should be visible.
1118 * @private
1119 */
8b83c6cc 1120Dygraph.prototype.doZoomX_ = function(lowX, highX) {
6a1aa64f 1121 // Find the earliest and latest dates contained in this canvasx range.
8b83c6cc 1122 // Convert the call to date ranges of the raw data.
56623f3b
DV
1123 var r = this.toDataCoords(lowX, null);
1124 var minDate = r[0];
1125 r = this.toDataCoords(highX, null);
1126 var maxDate = r[0];
8b83c6cc
RK
1127 this.doZoomXDates_(minDate, maxDate);
1128};
6a1aa64f 1129
8b83c6cc
RK
1130/**
1131 * Zoom to something containing [minDate, maxDate] values. Don't confuse this
1132 * method with doZoomX which accepts pixel coordinates. This function redraws
1133 * the graph.
d58ae307 1134 *
8b83c6cc
RK
1135 * @param {Number} minDate The minimum date that should be visible.
1136 * @param {Number} maxDate The maximum date that should be visible.
1137 * @private
1138 */
1139Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) {
6a1aa64f 1140 this.dateWindow_ = [minDate, maxDate];
26ca7938 1141 this.drawGraph_();
285a6bda 1142 if (this.attr_("zoomCallback")) {
ac139d19 1143 this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges());
8b83c6cc
RK
1144 }
1145};
1146
1147/**
1148 * Zoom to something containing [lowY, highY]. These are pixel coordinates in
d58ae307
DV
1149 * the canvas. This function redraws the graph.
1150 *
8b83c6cc
RK
1151 * @param {Number} lowY The topmost pixel value that should be visible.
1152 * @param {Number} highY The lowest pixel value that should be visible.
1153 * @private
1154 */
1155Dygraph.prototype.doZoomY_ = function(lowY, highY) {
d58ae307
DV
1156 // Find the highest and lowest values in pixel range for each axis.
1157 // Note that lowY (in pixels) corresponds to the max Value (in data coords).
1158 // This is because pixels increase as you go down on the screen, whereas data
1159 // coordinates increase as you go up the screen.
1160 var valueRanges = [];
1161 for (var i = 0; i < this.axes_.length; i++) {
1162 var hi = this.toDataCoords(null, lowY, i);
1163 var low = this.toDataCoords(null, highY, i);
1164 this.axes_[i].valueWindow = [low[1], hi[1]];
1165 valueRanges.push([low[1], hi[1]]);
1166 }
8b83c6cc 1167
66c380c4 1168 this.drawGraph_();
8b83c6cc 1169 if (this.attr_("zoomCallback")) {
d58ae307
DV
1170 var xRange = this.xAxisRange();
1171 this.attr_("zoomCallback")(xRange[0], xRange[1], this.yAxisRanges());
8b83c6cc
RK
1172 }
1173};
1174
1175/**
1176 * Reset the zoom to the original view coordinates. This is the same as
1177 * double-clicking on the graph.
d58ae307 1178 *
8b83c6cc
RK
1179 * @private
1180 */
1181Dygraph.prototype.doUnzoom_ = function() {
d58ae307 1182 var dirty = false;
8b83c6cc 1183 if (this.dateWindow_ != null) {
d58ae307 1184 dirty = true;
8b83c6cc
RK
1185 this.dateWindow_ = null;
1186 }
d58ae307
DV
1187
1188 for (var i = 0; i < this.axes_.length; i++) {
1189 if (this.axes_[i].valueWindow != null) {
1190 dirty = true;
1191 delete this.axes_[i].valueWindow;
1192 }
8b83c6cc
RK
1193 }
1194
1195 if (dirty) {
437c0979
RK
1196 // Putting the drawing operation before the callback because it resets
1197 // yAxisRange.
66c380c4 1198 this.drawGraph_();
8b83c6cc
RK
1199 if (this.attr_("zoomCallback")) {
1200 var minDate = this.rawData_[0][0];
1201 var maxDate = this.rawData_[this.rawData_.length - 1][0];
d58ae307 1202 this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges());
8b83c6cc 1203 }
67e650dc 1204 }
6a1aa64f
DV
1205};
1206
1207/**
1208 * When the mouse moves in the canvas, display information about a nearby data
1209 * point and draw dots over those points in the data series. This function
1210 * takes care of cleanup of previously-drawn dots.
1211 * @param {Object} event The mousemove event from the browser.
1212 * @private
1213 */
285a6bda 1214Dygraph.prototype.mouseMove_ = function(event) {
eb7bf005 1215 var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_);
6a1aa64f
DV
1216 var points = this.layout_.points;
1217
1218 var lastx = -1;
1219 var lasty = -1;
1220
1221 // Loop through all the points and find the date nearest to our current
1222 // location.
1223 var minDist = 1e+100;
1224 var idx = -1;
1225 for (var i = 0; i < points.length; i++) {
8a7cc60e
RK
1226 var point = points[i];
1227 if (point == null) continue;
6faebb69 1228 var dist = Math.abs(points[i].canvasx - canvasx);
f032c51d 1229 if (dist > minDist) continue;
6a1aa64f
DV
1230 minDist = dist;
1231 idx = i;
1232 }
1233 if (idx >= 0) lastx = points[idx].xval;
1234 // Check that you can really highlight the last day's data
8a7cc60e
RK
1235 var last = points[points.length-1];
1236 if (last != null && canvasx > last.canvasx)
6a1aa64f
DV
1237 lastx = points[points.length-1].xval;
1238
1239 // Extract the points we've selected
b258a3da 1240 this.selPoints_ = [];
50360fd0 1241 var l = points.length;
416b05ad
NK
1242 if (!this.attr_("stackedGraph")) {
1243 for (var i = 0; i < l; i++) {
1244 if (points[i].xval == lastx) {
1245 this.selPoints_.push(points[i]);
1246 }
1247 }
1248 } else {
354e15ab
DE
1249 // Need to 'unstack' points starting from the bottom
1250 var cumulative_sum = 0;
416b05ad
NK
1251 for (var i = l - 1; i >= 0; i--) {
1252 if (points[i].xval == lastx) {
354e15ab 1253 var p = {}; // Clone the point since we modify it
d4139cd8
NK
1254 for (var k in points[i]) {
1255 p[k] = points[i][k];
50360fd0
NK
1256 }
1257 p.yval -= cumulative_sum;
1258 cumulative_sum += p.yval;
d4139cd8 1259 this.selPoints_.push(p);
12e4c741 1260 }
6a1aa64f 1261 }
354e15ab 1262 this.selPoints_.reverse();
6a1aa64f
DV
1263 }
1264
b258a3da 1265 if (this.attr_("highlightCallback")) {
a4c6a67c 1266 var px = this.lastx_;
dd082dda 1267 if (px !== null && lastx != px) {
344ba8c0 1268 // only fire if the selected point has changed.
2ddb1197 1269 this.attr_("highlightCallback")(event, lastx, this.selPoints_, this.idxToRow_(idx));
43af96e7 1270 }
12e4c741 1271 }
43af96e7 1272
239c712d
NAG
1273 // Save last x position for callbacks.
1274 this.lastx_ = lastx;
50360fd0 1275
239c712d
NAG
1276 this.updateSelection_();
1277};
b258a3da 1278
239c712d 1279/**
1903f1e4 1280 * Transforms layout_.points index into data row number.
2ddb1197 1281 * @param int layout_.points index
1903f1e4 1282 * @return int row number, or -1 if none could be found.
2ddb1197
SC
1283 * @private
1284 */
1285Dygraph.prototype.idxToRow_ = function(idx) {
1903f1e4 1286 if (idx < 0) return -1;
2ddb1197 1287
1903f1e4
DV
1288 for (var i in this.layout_.datasets) {
1289 if (idx < this.layout_.datasets[i].length) {
1290 return this.boundaryIds_[0][0]+idx;
1291 }
1292 idx -= this.layout_.datasets[i].length;
1293 }
1294 return -1;
1295};
2ddb1197
SC
1296
1297/**
239c712d
NAG
1298 * Draw dots over the selectied points in the data series. This function
1299 * takes care of cleanup of previously-drawn dots.
1300 * @private
1301 */
1302Dygraph.prototype.updateSelection_ = function() {
6a1aa64f 1303 // Clear the previously drawn vertical, if there is one
6a1aa64f
DV
1304 var ctx = this.canvas_.getContext("2d");
1305 if (this.previousVerticalX_ >= 0) {
46dde5f9
DV
1306 // Determine the maximum highlight circle size.
1307 var maxCircleSize = 0;
227b93cc
DV
1308 var labels = this.attr_('labels');
1309 for (var i = 1; i < labels.length; i++) {
1310 var r = this.attr_('highlightCircleSize', labels[i]);
46dde5f9
DV
1311 if (r > maxCircleSize) maxCircleSize = r;
1312 }
6a1aa64f 1313 var px = this.previousVerticalX_;
46dde5f9
DV
1314 ctx.clearRect(px - maxCircleSize - 1, 0,
1315 2 * maxCircleSize + 2, this.height_);
6a1aa64f
DV
1316 }
1317
584ceeaa
DV
1318 var isOK = function(x) { return x && !isNaN(x); };
1319
d160cc3b 1320 if (this.selPoints_.length > 0) {
b258a3da 1321 var canvasx = this.selPoints_[0].canvasx;
6a1aa64f
DV
1322
1323 // Set the status message to indicate the selected point(s)
6be8e54c
JB
1324 var replace = this.attr_('xValueFormatter')(
1325 this.lastx_, this.numXDigits_ + this.numExtraDigits_) + ":";
50360fd0 1326 var fmtFunc = this.attr_('yValueFormatter');
6a1aa64f 1327 var clen = this.colors_.length;
d160cc3b
NK
1328
1329 if (this.attr_('showLabelsOnHighlight')) {
1330 // Set the status message to indicate the selected point(s)
d160cc3b 1331 for (var i = 0; i < this.selPoints_.length; i++) {
129569a5 1332 if (!this.attr_("labelsShowZeroValues") && this.selPoints_[i].yval == 0) continue;
d160cc3b
NK
1333 if (!isOK(this.selPoints_[i].canvasy)) continue;
1334 if (this.attr_("labelsSeparateLines")) {
1335 replace += "<br/>";
1336 }
1337 var point = this.selPoints_[i];
8fb6dc24 1338 var c = new RGBColor(this.plotter_.colors[point.name]);
6be8e54c 1339 var yval = fmtFunc(point.yval, this.numYDigits_ + this.numExtraDigits_);
d160cc3b
NK
1340 replace += " <b><font color='" + c.toHex() + "'>"
1341 + point.name + "</font></b>:"
1342 + yval;
6a1aa64f 1343 }
50360fd0 1344
d160cc3b 1345 this.attr_("labelsDiv").innerHTML = replace;
6a1aa64f 1346 }
6a1aa64f 1347
6a1aa64f 1348 // Draw colored circles over the center of each selected point
43af96e7 1349 ctx.save();
b258a3da 1350 for (var i = 0; i < this.selPoints_.length; i++) {
f032c51d 1351 if (!isOK(this.selPoints_[i].canvasy)) continue;
227b93cc
DV
1352 var circleSize =
1353 this.attr_('highlightCircleSize', this.selPoints_[i].name);
6a1aa64f 1354 ctx.beginPath();
563c70ca 1355 ctx.fillStyle = this.plotter_.colors[this.selPoints_[i].name];
f032c51d 1356 ctx.arc(canvasx, this.selPoints_[i].canvasy, circleSize,
7bf6a9fe 1357 0, 2 * Math.PI, false);
6a1aa64f
DV
1358 ctx.fill();
1359 }
1360 ctx.restore();
1361
1362 this.previousVerticalX_ = canvasx;
1363 }
1364};
1365
1366/**
239c712d
NAG
1367 * Set manually set selected dots, and display information about them
1368 * @param int row number that should by highlighted
1369 * false value clears the selection
1370 * @public
1371 */
1372Dygraph.prototype.setSelection = function(row) {
1373 // Extract the points we've selected
1374 this.selPoints_ = [];
1375 var pos = 0;
50360fd0 1376
239c712d 1377 if (row !== false) {
16269f6e
NAG
1378 row = row-this.boundaryIds_[0][0];
1379 }
50360fd0 1380
16269f6e 1381 if (row !== false && row >= 0) {
239c712d 1382 for (var i in this.layout_.datasets) {
16269f6e 1383 if (row < this.layout_.datasets[i].length) {
38f33a44 1384 var point = this.layout_.points[pos+row];
1385
1386 if (this.attr_("stackedGraph")) {
8c03ba63 1387 point = this.layout_.unstackPointAtIndex(pos+row);
38f33a44 1388 }
1389
1390 this.selPoints_.push(point);
16269f6e 1391 }
239c712d
NAG
1392 pos += this.layout_.datasets[i].length;
1393 }
16269f6e 1394 }
50360fd0 1395
16269f6e 1396 if (this.selPoints_.length) {
239c712d
NAG
1397 this.lastx_ = this.selPoints_[0].xval;
1398 this.updateSelection_();
1399 } else {
1400 this.lastx_ = -1;
1401 this.clearSelection();
1402 }
1403
1404};
1405
1406/**
6a1aa64f
DV
1407 * The mouse has left the canvas. Clear out whatever artifacts remain
1408 * @param {Object} event the mouseout event from the browser.
1409 * @private
1410 */
285a6bda 1411Dygraph.prototype.mouseOut_ = function(event) {
a4c6a67c
AV
1412 if (this.attr_("unhighlightCallback")) {
1413 this.attr_("unhighlightCallback")(event);
1414 }
1415
43af96e7 1416 if (this.attr_("hideOverlayOnMouseOut")) {
239c712d 1417 this.clearSelection();
43af96e7 1418 }
6a1aa64f
DV
1419};
1420
239c712d
NAG
1421/**
1422 * Remove all selection from the canvas
1423 * @public
1424 */
1425Dygraph.prototype.clearSelection = function() {
1426 // Get rid of the overlay data
1427 var ctx = this.canvas_.getContext("2d");
1428 ctx.clearRect(0, 0, this.width_, this.height_);
1429 this.attr_("labelsDiv").innerHTML = "";
1430 this.selPoints_ = [];
1431 this.lastx_ = -1;
1432}
1433
103b7292
NAG
1434/**
1435 * Returns the number of the currently selected row
1436 * @return int row number, of -1 if nothing is selected
1437 * @public
1438 */
1439Dygraph.prototype.getSelection = function() {
1440 if (!this.selPoints_ || this.selPoints_.length < 1) {
1441 return -1;
1442 }
50360fd0 1443
103b7292
NAG
1444 for (var row=0; row<this.layout_.points.length; row++ ) {
1445 if (this.layout_.points[row].x == this.selPoints_[0].x) {
16269f6e 1446 return row + this.boundaryIds_[0][0];
103b7292
NAG
1447 }
1448 }
1449 return -1;
1450}
1451
285a6bda 1452Dygraph.zeropad = function(x) {
32988383
DV
1453 if (x < 10) return "0" + x; else return "" + x;
1454}
1455
6a1aa64f 1456/**
6b8e33dd
DV
1457 * Return a string version of the hours, minutes and seconds portion of a date.
1458 * @param {Number} date The JavaScript date (ms since epoch)
1459 * @return {String} A time of the form "HH:MM:SS"
1460 * @private
1461 */
bf640e56 1462Dygraph.hmsString_ = function(date) {
285a6bda 1463 var zeropad = Dygraph.zeropad;
6b8e33dd
DV
1464 var d = new Date(date);
1465 if (d.getSeconds()) {
1466 return zeropad(d.getHours()) + ":" +
1467 zeropad(d.getMinutes()) + ":" +
1468 zeropad(d.getSeconds());
6b8e33dd 1469 } else {
054531ca 1470 return zeropad(d.getHours()) + ":" + zeropad(d.getMinutes());
6b8e33dd
DV
1471 }
1472}
1473
1474/**
bf640e56
AV
1475 * Convert a JS date to a string appropriate to display on an axis that
1476 * is displaying values at the stated granularity.
1477 * @param {Date} date The date to format
1478 * @param {Number} granularity One of the Dygraph granularity constants
1479 * @return {String} The formatted date
1480 * @private
1481 */
1482Dygraph.dateAxisFormatter = function(date, granularity) {
6faebb69 1483 if (granularity >= Dygraph.MONTHLY) {
bf640e56
AV
1484 return date.strftime('%b %y');
1485 } else {
31eddad3 1486 var frac = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds();
bf640e56
AV
1487 if (frac == 0 || granularity >= Dygraph.DAILY) {
1488 return new Date(date.getTime() + 3600*1000).strftime('%d%b');
1489 } else {
1490 return Dygraph.hmsString_(date.getTime());
1491 }
1492 }
1493}
1494
1495/**
6a1aa64f
DV
1496 * Convert a JS date (millis since epoch) to YYYY/MM/DD
1497 * @param {Number} date The JavaScript date (ms since epoch)
1498 * @return {String} A date of the form "YYYY/MM/DD"
1499 * @private
1500 */
6be8e54c 1501Dygraph.dateString_ = function(date) {
285a6bda 1502 var zeropad = Dygraph.zeropad;
6a1aa64f
DV
1503 var d = new Date(date);
1504
1505 // Get the year:
1506 var year = "" + d.getFullYear();
1507 // Get a 0 padded month string
6b8e33dd 1508 var month = zeropad(d.getMonth() + 1); //months are 0-offset, sigh
6a1aa64f 1509 // Get a 0 padded day string
6b8e33dd 1510 var day = zeropad(d.getDate());
6a1aa64f 1511
6b8e33dd
DV
1512 var ret = "";
1513 var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
bf640e56 1514 if (frac) ret = " " + Dygraph.hmsString_(date);
6b8e33dd
DV
1515
1516 return year + "/" + month + "/" + day + ret;
6a1aa64f
DV
1517};
1518
1519/**
6a1aa64f
DV
1520 * Fires when there's data available to be graphed.
1521 * @param {String} data Raw CSV data to be plotted
1522 * @private
1523 */
285a6bda 1524Dygraph.prototype.loadedEvent_ = function(data) {
6a1aa64f 1525 this.rawData_ = this.parseCSV_(data);
26ca7938 1526 this.predraw_();
6a1aa64f
DV
1527};
1528
285a6bda 1529Dygraph.prototype.months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
8846615a 1530 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
285a6bda 1531Dygraph.prototype.quarters = ["Jan", "Apr", "Jul", "Oct"];
6a1aa64f
DV
1532
1533/**
1534 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1535 * @private
1536 */
285a6bda 1537Dygraph.prototype.addXTicks_ = function() {
6a1aa64f 1538 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
6be8e54c
JB
1539 var opts = {xTicks: []};
1540 var formatter = this.attr_('xTicker');
6a1aa64f 1541 if (this.dateWindow_) {
6be8e54c 1542 opts.xTicks = formatter(this.dateWindow_[0], this.dateWindow_[1], this);
6a1aa64f 1543 } else {
6be8e54c
JB
1544 // numericTicks() returns multiple values.
1545 var ret = formatter(this.rawData_[0][0],
1546 this.rawData_[this.rawData_.length - 1][0], this);
1547 opts.xTicks = ret.ticks;
1548 this.numXDigits_ = ret.numDigits;
3c1d225b 1549 }
6be8e54c 1550 this.layout_.updateOptions(opts);
32988383
DV
1551};
1552
1553// Time granularity enumeration
285a6bda 1554Dygraph.SECONDLY = 0;
20a41c17
DV
1555Dygraph.TWO_SECONDLY = 1;
1556Dygraph.FIVE_SECONDLY = 2;
1557Dygraph.TEN_SECONDLY = 3;
1558Dygraph.THIRTY_SECONDLY = 4;
1559Dygraph.MINUTELY = 5;
1560Dygraph.TWO_MINUTELY = 6;
1561Dygraph.FIVE_MINUTELY = 7;
1562Dygraph.TEN_MINUTELY = 8;
1563Dygraph.THIRTY_MINUTELY = 9;
1564Dygraph.HOURLY = 10;
1565Dygraph.TWO_HOURLY = 11;
1566Dygraph.SIX_HOURLY = 12;
1567Dygraph.DAILY = 13;
1568Dygraph.WEEKLY = 14;
1569Dygraph.MONTHLY = 15;
1570Dygraph.QUARTERLY = 16;
1571Dygraph.BIANNUAL = 17;
1572Dygraph.ANNUAL = 18;
1573Dygraph.DECADAL = 19;
6faebb69 1574Dygraph.NUM_GRANULARITIES = 20;
285a6bda
DV
1575
1576Dygraph.SHORT_SPACINGS = [];
1577Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY] = 1000 * 1;
20a41c17
DV
1578Dygraph.SHORT_SPACINGS[Dygraph.TWO_SECONDLY] = 1000 * 2;
1579Dygraph.SHORT_SPACINGS[Dygraph.FIVE_SECONDLY] = 1000 * 5;
285a6bda
DV
1580Dygraph.SHORT_SPACINGS[Dygraph.TEN_SECONDLY] = 1000 * 10;
1581Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_SECONDLY] = 1000 * 30;
1582Dygraph.SHORT_SPACINGS[Dygraph.MINUTELY] = 1000 * 60;
20a41c17
DV
1583Dygraph.SHORT_SPACINGS[Dygraph.TWO_MINUTELY] = 1000 * 60 * 2;
1584Dygraph.SHORT_SPACINGS[Dygraph.FIVE_MINUTELY] = 1000 * 60 * 5;
285a6bda
DV
1585Dygraph.SHORT_SPACINGS[Dygraph.TEN_MINUTELY] = 1000 * 60 * 10;
1586Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_MINUTELY] = 1000 * 60 * 30;
1587Dygraph.SHORT_SPACINGS[Dygraph.HOURLY] = 1000 * 3600;
20a41c17 1588Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY] = 1000 * 3600 * 2;
805d5519 1589Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY] = 1000 * 3600 * 6;
285a6bda
DV
1590Dygraph.SHORT_SPACINGS[Dygraph.DAILY] = 1000 * 86400;
1591Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY] = 1000 * 604800;
32988383
DV
1592
1593// NumXTicks()
1594//
1595// If we used this time granularity, how many ticks would there be?
1596// This is only an approximation, but it's generally good enough.
1597//
285a6bda
DV
1598Dygraph.prototype.NumXTicks = function(start_time, end_time, granularity) {
1599 if (granularity < Dygraph.MONTHLY) {
32988383 1600 // Generate one tick mark for every fixed interval of time.
285a6bda 1601 var spacing = Dygraph.SHORT_SPACINGS[granularity];
32988383
DV
1602 return Math.floor(0.5 + 1.0 * (end_time - start_time) / spacing);
1603 } else {
1604 var year_mod = 1; // e.g. to only print one point every 10 years.
1605 var num_months = 12;
285a6bda
DV
1606 if (granularity == Dygraph.QUARTERLY) num_months = 3;
1607 if (granularity == Dygraph.BIANNUAL) num_months = 2;
1608 if (granularity == Dygraph.ANNUAL) num_months = 1;
1609 if (granularity == Dygraph.DECADAL) { num_months = 1; year_mod = 10; }
32988383
DV
1610
1611 var msInYear = 365.2524 * 24 * 3600 * 1000;
1612 var num_years = 1.0 * (end_time - start_time) / msInYear;
1613 return Math.floor(0.5 + 1.0 * num_years * num_months / year_mod);
1614 }
1615};
1616
1617// GetXAxis()
1618//
1619// Construct an x-axis of nicely-formatted times on meaningful boundaries
1620// (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1621//
1622// Returns an array containing {v: millis, label: label} dictionaries.
1623//
285a6bda 1624Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) {
bf640e56 1625 var formatter = this.attr_("xAxisLabelFormatter");
32988383 1626 var ticks = [];
285a6bda 1627 if (granularity < Dygraph.MONTHLY) {
32988383 1628 // Generate one tick mark for every fixed interval of time.
285a6bda 1629 var spacing = Dygraph.SHORT_SPACINGS[granularity];
3d29302c 1630 var format = '%d%b'; // e.g. "1Jan"
076c9622
DV
1631
1632 // Find a time less than start_time which occurs on a "nice" time boundary
1633 // for this granularity.
1634 var g = spacing / 1000;
076c9622
DV
1635 var d = new Date(start_time);
1636 if (g <= 60) { // seconds
1637 var x = d.getSeconds(); d.setSeconds(x - x % g);
1638 } else {
1639 d.setSeconds(0);
1640 g /= 60;
1641 if (g <= 60) { // minutes
1642 var x = d.getMinutes(); d.setMinutes(x - x % g);
1643 } else {
1644 d.setMinutes(0);
1645 g /= 60;
1646
1647 if (g <= 24) { // days
1648 var x = d.getHours(); d.setHours(x - x % g);
1649 } else {
1650 d.setHours(0);
1651 g /= 24;
1652
1653 if (g == 7) { // one week
20a41c17 1654 d.setDate(d.getDate() - d.getDay());
076c9622
DV
1655 }
1656 }
1657 }
328bb812 1658 }
076c9622
DV
1659 start_time = d.getTime();
1660
32988383 1661 for (var t = start_time; t <= end_time; t += spacing) {
bf640e56 1662 ticks.push({ v:t, label: formatter(new Date(t), granularity) });
32988383
DV
1663 }
1664 } else {
1665 // Display a tick mark on the first of a set of months of each year.
1666 // Years get a tick mark iff y % year_mod == 0. This is useful for
1667 // displaying a tick mark once every 10 years, say, on long time scales.
1668 var months;
1669 var year_mod = 1; // e.g. to only print one point every 10 years.
1670
285a6bda 1671 if (granularity == Dygraph.MONTHLY) {
32988383 1672 months = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
285a6bda 1673 } else if (granularity == Dygraph.QUARTERLY) {
32988383 1674 months = [ 0, 3, 6, 9 ];
285a6bda 1675 } else if (granularity == Dygraph.BIANNUAL) {
32988383 1676 months = [ 0, 6 ];
285a6bda 1677 } else if (granularity == Dygraph.ANNUAL) {
32988383 1678 months = [ 0 ];
285a6bda 1679 } else if (granularity == Dygraph.DECADAL) {
32988383
DV
1680 months = [ 0 ];
1681 year_mod = 10;
1682 }
1683
1684 var start_year = new Date(start_time).getFullYear();
1685 var end_year = new Date(end_time).getFullYear();
285a6bda 1686 var zeropad = Dygraph.zeropad;
32988383
DV
1687 for (var i = start_year; i <= end_year; i++) {
1688 if (i % year_mod != 0) continue;
1689 for (var j = 0; j < months.length; j++) {
1690 var date_str = i + "/" + zeropad(1 + months[j]) + "/01";
1691 var t = Date.parse(date_str);
1692 if (t < start_time || t > end_time) continue;
bf640e56 1693 ticks.push({ v:t, label: formatter(new Date(t), granularity) });
32988383
DV
1694 }
1695 }
1696 }
1697
1698 return ticks;
1699};
1700
6a1aa64f
DV
1701
1702/**
1703 * Add ticks to the x-axis based on a date range.
1704 * @param {Number} startDate Start of the date window (millis since epoch)
1705 * @param {Number} endDate End of the date window (millis since epoch)
1706 * @return {Array.<Object>} Array of {label, value} tuples.
1707 * @public
1708 */
285a6bda 1709Dygraph.dateTicker = function(startDate, endDate, self) {
32988383 1710 var chosen = -1;
285a6bda
DV
1711 for (var i = 0; i < Dygraph.NUM_GRANULARITIES; i++) {
1712 var num_ticks = self.NumXTicks(startDate, endDate, i);
1713 if (self.width_ / num_ticks >= self.attr_('pixelsPerXLabel')) {
32988383
DV
1714 chosen = i;
1715 break;
2769de62 1716 }
6a1aa64f
DV
1717 }
1718
32988383 1719 if (chosen >= 0) {
285a6bda 1720 return self.GetXAxis(startDate, endDate, chosen);
6a1aa64f 1721 } else {
32988383 1722 // TODO(danvk): signal error.
6a1aa64f 1723 }
6a1aa64f
DV
1724};
1725
1726/**
3c1d225b
JB
1727 * Determine the number of significant figures in a Number up to the specified
1728 * precision. Note that there is no way to determine if a trailing '0' is
1729 * significant or not, so by convention we return 1 for all of the following
1730 * inputs: 1, 1.0, 1.00, 1.000 etc.
1731 * @param {Number} x The input value.
1732 * @param {Number} opt_maxPrecision Optional maximum precision to consider.
1733 * Default and maximum allowed value is 13.
1734 * @return {Number} The number of significant figures which is >= 1.
1735 */
1736Dygraph.significantFigures = function(x, opt_maxPrecision) {
1737 var precision = Math.max(opt_maxPrecision || 13, 13);
1738
fff1de86 1739 // Convert the number to its exponential notation form and work backwards,
3c1d225b
JB
1740 // ignoring the 'e+xx' bit. This may seem like a hack, but doing a loop and
1741 // dividing by 10 leads to roundoff errors. By using toExponential(), we let
1742 // the JavaScript interpreter handle the low level bits of the Number for us.
1743 var s = x.toExponential(precision);
1744 var ePos = s.lastIndexOf('e'); // -1 case handled by return below.
1745
1746 for (var i = ePos - 1; i >= 0; i--) {
1747 if (s[i] == '.') {
1748 // Got to the decimal place. We'll call this 1 digit of precision because
1749 // we can't know for sure how many trailing 0s are significant.
1750 return 1;
1751 } else if (s[i] != '0') {
1752 // Found the first non-zero digit. Return the number of characters
1753 // except for the '.'.
1754 return i; // This is i - 1 + 1 (-1 is for '.', +1 is for 0 based index).
1755 }
1756 }
1757
1758 // Occurs if toExponential() doesn't return a string containing 'e', which
1759 // should never happen.
1760 return 1;
1761};
1762
1763/**
6a1aa64f
DV
1764 * Add ticks when the x axis has numbers on it (instead of dates)
1765 * @param {Number} startDate Start of the date window (millis since epoch)
1766 * @param {Number} endDate End of the date window (millis since epoch)
84fc6aa7 1767 * @param self
f30cf740 1768 * @param {function} attribute accessor function.
6a1aa64f
DV
1769 * @return {Array.<Object>} Array of {label, value} tuples.
1770 * @public
1771 */
0d64e596 1772Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) {
70c80071
DV
1773 var attr = function(k) {
1774 if (axis_props && axis_props.hasOwnProperty(k)) return axis_props[k];
1775 return self.attr_(k);
1776 };
f09fc545 1777
0d64e596
DV
1778 var ticks = [];
1779 if (vals) {
1780 for (var i = 0; i < vals.length; i++) {
fff1de86 1781 ticks[i].push({v: vals[i]});
0d64e596 1782 }
f09e46d4 1783 } else {
0d64e596
DV
1784 // Basic idea:
1785 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1786 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks).
1787 // The first spacing greater than pixelsPerYLabel is what we use.
1788 // TODO(danvk): version that works on a log scale.
f09fc545 1789 if (attr("labelsKMG2")) {
0d64e596 1790 var mults = [1, 2, 4, 8];
f09e46d4 1791 } else {
0d64e596 1792 var mults = [1, 2, 5];
f09e46d4 1793 }
0d64e596
DV
1794 var scale, low_val, high_val, nTicks;
1795 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1796 var pixelsPerTick = attr('pixelsPerYLabel');
1797 for (var i = -10; i < 50; i++) {
1798 if (attr("labelsKMG2")) {
1799 var base_scale = Math.pow(16, i);
1800 } else {
1801 var base_scale = Math.pow(10, i);
1802 }
1803 for (var j = 0; j < mults.length; j++) {
1804 scale = base_scale * mults[j];
1805 low_val = Math.floor(minV / scale) * scale;
1806 high_val = Math.ceil(maxV / scale) * scale;
1807 nTicks = Math.abs(high_val - low_val) / scale;
1808 var spacing = self.height_ / nTicks;
1809 // wish I could break out of both loops at once...
1810 if (spacing > pixelsPerTick) break;
1811 }
285a6bda 1812 if (spacing > pixelsPerTick) break;
c6336f04 1813 }
0d64e596
DV
1814
1815 // Construct the set of ticks.
1816 // Allow reverse y-axis if it's explicitly requested.
1817 if (low_val > high_val) scale *= -1;
1818 for (var i = 0; i < nTicks; i++) {
1819 var tickV = low_val + i * scale;
fff1de86 1820 ticks.push( {v: tickV} );
0d64e596 1821 }
6a1aa64f
DV
1822 }
1823
0d64e596 1824 // Add formatted labels to the ticks.
ed11be50
DV
1825 var k;
1826 var k_labels = [];
f09fc545 1827 if (attr("labelsKMB")) {
ed11be50
DV
1828 k = 1000;
1829 k_labels = [ "K", "M", "B", "T" ];
1830 }
f09fc545 1831 if (attr("labelsKMG2")) {
ed11be50
DV
1832 if (k) self.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
1833 k = 1024;
1834 k_labels = [ "k", "M", "G", "T" ];
1835 }
3c1d225b
JB
1836 var formatter = attr('yAxisLabelFormatter') ?
1837 attr('yAxisLabelFormatter') : attr('yValueFormatter');
1838
1839 // Determine the number of decimal places needed for the labels below by
1840 // taking the maximum number of significant figures for any label. We must
1841 // take the max because we can't tell if trailing 0s are significant.
1842 var numDigits = 0;
1843 for (var i = 0; i < ticks.length; i++) {
fff1de86 1844 numDigits = Math.max(Dygraph.significantFigures(ticks[i].v), numDigits);
3c1d225b 1845 }
ed11be50 1846
0d64e596
DV
1847 for (var i = 0; i < ticks.length; i++) {
1848 var tickV = ticks[i].v;
0af6e346 1849 var absTickV = Math.abs(tickV);
3c1d225b
JB
1850 var label = (formatter !== undefined) ?
1851 formatter(tickV, numDigits) : tickV.toPrecision(numDigits);
1852 if (k_labels.length > 0) {
ed11be50
DV
1853 // Round up to an appropriate unit.
1854 var n = k*k*k*k;
1855 for (var j = 3; j >= 0; j--, n /= k) {
1856 if (absTickV >= n) {
3c1d225b 1857 label = (tickV / n).toPrecision(numDigits) + k_labels[j];
ed11be50
DV
1858 break;
1859 }
afefbcdb 1860 }
6a1aa64f 1861 }
0d64e596 1862 ticks[i].label = label;
6a1aa64f 1863 }
3c1d225b 1864 return {ticks: ticks, numDigits: numDigits};
6a1aa64f
DV
1865};
1866
5011e7a1
DV
1867// Computes the range of the data series (including confidence intervals).
1868// series is either [ [x1, y1], [x2, y2], ... ] or
1869// [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1870// Returns [low, high]
1871Dygraph.prototype.extremeValues_ = function(series) {
1872 var minY = null, maxY = null;
1873
9922b78b 1874 var bars = this.attr_("errorBars") || this.attr_("customBars");
5011e7a1
DV
1875 if (bars) {
1876 // With custom bars, maxY is the max of the high values.
1877 for (var j = 0; j < series.length; j++) {
1878 var y = series[j][1][0];
1879 if (!y) continue;
1880 var low = y - series[j][1][1];
1881 var high = y + series[j][1][2];
1882 if (low > y) low = y; // this can happen with custom bars,
1883 if (high < y) high = y; // e.g. in tests/custom-bars.html
1884 if (maxY == null || high > maxY) {
1885 maxY = high;
1886 }
1887 if (minY == null || low < minY) {
1888 minY = low;
1889 }
1890 }
1891 } else {
1892 for (var j = 0; j < series.length; j++) {
1893 var y = series[j][1];
d12999d3 1894 if (y === null || isNaN(y)) continue;
5011e7a1
DV
1895 if (maxY == null || y > maxY) {
1896 maxY = y;
1897 }
1898 if (minY == null || y < minY) {
1899 minY = y;
1900 }
1901 }
1902 }
1903
1904 return [minY, maxY];
1905};
1906
6a1aa64f 1907/**
26ca7938
DV
1908 * This function is called once when the chart's data is changed or the options
1909 * dictionary is updated. It is _not_ called when the user pans or zooms. The
1910 * idea is that values derived from the chart's data can be computed here,
1911 * rather than every time the chart is drawn. This includes things like the
1912 * number of axes, rolling averages, etc.
1913 */
1914Dygraph.prototype.predraw_ = function() {
1915 // TODO(danvk): move more computations out of drawGraph_ and into here.
1916 this.computeYAxes_();
1917
1918 // Create a new plotter.
70c80071 1919 if (this.plotter_) this.plotter_.clear();
26ca7938
DV
1920 this.plotter_ = new DygraphCanvasRenderer(this,
1921 this.hidden_, this.layout_,
1922 this.renderOptions_);
1923
0abfbd7e
DV
1924 // The roller sits in the bottom left corner of the chart. We don't know where
1925 // this will be until the options are available, so it's positioned here.
8c69de65 1926 this.createRollInterface_();
26ca7938 1927
0abfbd7e
DV
1928 // Same thing applies for the labelsDiv. It's right edge should be flush with
1929 // the right edge of the charting area (which may not be the same as the right
1930 // edge of the div, if we have two y-axes.
1931 this.positionLabelsDiv_();
1932
26ca7938
DV
1933 // If the data or options have changed, then we'd better redraw.
1934 this.drawGraph_();
1935};
1936
1937/**
2f5e7e1a 1938=======
26ca7938
DV
1939 * Update the graph with new data. This method is called when the viewing area
1940 * has changed. If the underlying data or options have changed, predraw_ will
1941 * be called before drawGraph_ is called.
6a1aa64f
DV
1942 * @private
1943 */
26ca7938
DV
1944Dygraph.prototype.drawGraph_ = function() {
1945 var data = this.rawData_;
1946
fe0b7c03
DV
1947 // This is used to set the second parameter to drawCallback, below.
1948 var is_initial_draw = this.is_initial_draw_;
1949 this.is_initial_draw_ = false;
1950
3bd9c228 1951 var minY = null, maxY = null;
6a1aa64f 1952 this.layout_.removeAllDatasets();
285a6bda 1953 this.setColors_();
9317362d 1954 this.attrs_['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
285a6bda 1955
354e15ab
DE
1956 // Loop over the fields (series). Go from the last to the first,
1957 // because if they're stacked that's how we accumulate the values.
43af96e7 1958
354e15ab
DE
1959 var cumulative_y = []; // For stacked series.
1960 var datasets = [];
1961
f09fc545
DV
1962 var extremes = {}; // series name -> [low, high]
1963
354e15ab
DE
1964 // Loop over all fields and create datasets
1965 for (var i = data[0].length - 1; i >= 1; i--) {
1cf11047
DV
1966 if (!this.visibility()[i - 1]) continue;
1967
f09fc545 1968 var seriesName = this.attr_("labels")[i];
450fe64b
DV
1969 var connectSeparatedPoints = this.attr_('connectSeparatedPoints', i);
1970
6a1aa64f
DV
1971 var series = [];
1972 for (var j = 0; j < data.length; j++) {
4a634fc7 1973 if (data[j][i] != null || !connectSeparatedPoints) {
f032c51d 1974 var date = data[j][0];
563c70ca 1975 series.push([date, data[j][i]]);
f032c51d 1976 }
6a1aa64f 1977 }
2f5e7e1a
DV
1978
1979 // TODO(danvk): move this into predraw_. It's insane to do it here.
6a1aa64f
DV
1980 series = this.rollingAverage(series, this.rollPeriod_);
1981
1982 // Prune down to the desired range, if necessary (for zooming)
1a26f3fb
DV
1983 // Because there can be lines going to points outside of the visible area,
1984 // we actually prune to visible points, plus one on either side.
9922b78b 1985 var bars = this.attr_("errorBars") || this.attr_("customBars");
6a1aa64f
DV
1986 if (this.dateWindow_) {
1987 var low = this.dateWindow_[0];
1988 var high= this.dateWindow_[1];
1989 var pruned = [];
1a26f3fb
DV
1990 // TODO(danvk): do binary search instead of linear search.
1991 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
1992 var firstIdx = null, lastIdx = null;
6a1aa64f 1993 for (var k = 0; k < series.length; k++) {
1a26f3fb
DV
1994 if (series[k][0] >= low && firstIdx === null) {
1995 firstIdx = k;
1996 }
1997 if (series[k][0] <= high) {
1998 lastIdx = k;
6a1aa64f
DV
1999 }
2000 }
1a26f3fb
DV
2001 if (firstIdx === null) firstIdx = 0;
2002 if (firstIdx > 0) firstIdx--;
2003 if (lastIdx === null) lastIdx = series.length - 1;
2004 if (lastIdx < series.length - 1) lastIdx++;
16269f6e 2005 this.boundaryIds_[i-1] = [firstIdx, lastIdx];
1a26f3fb
DV
2006 for (var k = firstIdx; k <= lastIdx; k++) {
2007 pruned.push(series[k]);
6a1aa64f
DV
2008 }
2009 series = pruned;
16269f6e
NAG
2010 } else {
2011 this.boundaryIds_[i-1] = [0, series.length-1];
6a1aa64f
DV
2012 }
2013
f09fc545 2014 var seriesExtremes = this.extremeValues_(series);
5011e7a1 2015
6a1aa64f 2016 if (bars) {
354e15ab
DE
2017 for (var j=0; j<series.length; j++) {
2018 val = [series[j][0], series[j][1][0], series[j][1][1], series[j][1][2]];
2019 series[j] = val;
2020 }
43af96e7 2021 } else if (this.attr_("stackedGraph")) {
43af96e7
NK
2022 var l = series.length;
2023 var actual_y;
2024 for (var j = 0; j < l; j++) {
354e15ab
DE
2025 // If one data set has a NaN, let all subsequent stacked
2026 // sets inherit the NaN -- only start at 0 for the first set.
2027 var x = series[j][0];
41b0f691 2028 if (cumulative_y[x] === undefined) {
354e15ab 2029 cumulative_y[x] = 0;
41b0f691 2030 }
43af96e7
NK
2031
2032 actual_y = series[j][1];
354e15ab 2033 cumulative_y[x] += actual_y;
43af96e7 2034
354e15ab 2035 series[j] = [x, cumulative_y[x]]
43af96e7 2036
41b0f691
DV
2037 if (cumulative_y[x] > seriesExtremes[1]) {
2038 seriesExtremes[1] = cumulative_y[x];
2039 }
2040 if (cumulative_y[x] < seriesExtremes[0]) {
2041 seriesExtremes[0] = cumulative_y[x];
2042 }
43af96e7 2043 }
6a1aa64f 2044 }
41b0f691 2045 extremes[seriesName] = seriesExtremes;
354e15ab
DE
2046
2047 datasets[i] = series;
6a1aa64f
DV
2048 }
2049
354e15ab 2050 for (var i = 1; i < datasets.length; i++) {
4523c1f6 2051 if (!this.visibility()[i - 1]) continue;
354e15ab 2052 this.layout_.addDataset(this.attr_("labels")[i], datasets[i]);
43af96e7
NK
2053 }
2054
6faebb69
JB
2055 this.computeYAxisRanges_(extremes);
2056 this.layout_.updateOptions( { yAxes: this.axes_,
2057 seriesToAxisMap: this.seriesToAxisMap_
9012dd21 2058 } );
f09fc545 2059
6a1aa64f
DV
2060 this.addXTicks_();
2061
2062 // Tell PlotKit to use this new data and render itself
d033ae1c 2063 this.layout_.updateOptions({dateWindow: this.dateWindow_});
6a1aa64f
DV
2064 this.layout_.evaluateWithError();
2065 this.plotter_.clear();
2066 this.plotter_.render();
f6401bf6 2067 this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width,
2f5e7e1a 2068 this.canvas_.height);
599fb4ad
DV
2069
2070 if (this.attr_("drawCallback") !== null) {
fe0b7c03 2071 this.attr_("drawCallback")(this, is_initial_draw);
599fb4ad 2072 }
6a1aa64f
DV
2073};
2074
2075/**
26ca7938
DV
2076 * Determine properties of the y-axes which are independent of the data
2077 * currently being displayed. This includes things like the number of axes and
2078 * the style of the axes. It does not include the range of each axis and its
2079 * tick marks.
2080 * This fills in this.axes_ and this.seriesToAxisMap_.
2081 * axes_ = [ { options } ]
2082 * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... }
2083 * indices are into the axes_ array.
f09fc545 2084 */
26ca7938
DV
2085Dygraph.prototype.computeYAxes_ = function() {
2086 this.axes_ = [{}]; // always have at least one y-axis.
2087 this.seriesToAxisMap_ = {};
2088
2089 // Get a list of series names.
2090 var labels = this.attr_("labels");
1c77a3a1 2091 var series = {};
26ca7938 2092 for (var i = 1; i < labels.length; i++) series[labels[i]] = (i - 1);
f09fc545
DV
2093
2094 // all options which could be applied per-axis:
2095 var axisOptions = [
2096 'includeZero',
2097 'valueRange',
2098 'labelsKMB',
2099 'labelsKMG2',
2100 'pixelsPerYLabel',
2101 'yAxisLabelWidth',
2102 'axisLabelFontSize',
2103 'axisTickSize'
2104 ];
2105
2106 // Copy global axis options over to the first axis.
2107 for (var i = 0; i < axisOptions.length; i++) {
2108 var k = axisOptions[i];
2109 var v = this.attr_(k);
26ca7938 2110 if (v) this.axes_[0][k] = v;
f09fc545
DV
2111 }
2112
2113 // Go through once and add all the axes.
26ca7938
DV
2114 for (var seriesName in series) {
2115 if (!series.hasOwnProperty(seriesName)) continue;
f09fc545
DV
2116 var axis = this.attr_("axis", seriesName);
2117 if (axis == null) {
26ca7938 2118 this.seriesToAxisMap_[seriesName] = 0;
f09fc545
DV
2119 continue;
2120 }
2121 if (typeof(axis) == 'object') {
2122 // Add a new axis, making a copy of its per-axis options.
2123 var opts = {};
26ca7938 2124 Dygraph.update(opts, this.axes_[0]);
f09fc545
DV
2125 Dygraph.update(opts, { valueRange: null }); // shouldn't inherit this.
2126 Dygraph.update(opts, axis);
26ca7938
DV
2127 this.axes_.push(opts);
2128 this.seriesToAxisMap_[seriesName] = this.axes_.length - 1;
f09fc545
DV
2129 }
2130 }
2131
2132 // Go through one more time and assign series to an axis defined by another
2133 // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } }
26ca7938
DV
2134 for (var seriesName in series) {
2135 if (!series.hasOwnProperty(seriesName)) continue;
f09fc545
DV
2136 var axis = this.attr_("axis", seriesName);
2137 if (typeof(axis) == 'string') {
26ca7938 2138 if (!this.seriesToAxisMap_.hasOwnProperty(axis)) {
f09fc545
DV
2139 this.error("Series " + seriesName + " wants to share a y-axis with " +
2140 "series " + axis + ", which does not define its own axis.");
2141 return null;
2142 }
26ca7938
DV
2143 var idx = this.seriesToAxisMap_[axis];
2144 this.seriesToAxisMap_[seriesName] = idx;
f09fc545
DV
2145 }
2146 }
1c77a3a1
DV
2147
2148 // Now we remove series from seriesToAxisMap_ which are not visible. We do
2149 // this last so that hiding the first series doesn't destroy the axis
2150 // properties of the primary axis.
2151 var seriesToAxisFiltered = {};
2152 var vis = this.visibility();
2153 for (var i = 1; i < labels.length; i++) {
2154 var s = labels[i];
2155 if (vis[i - 1]) seriesToAxisFiltered[s] = this.seriesToAxisMap_[s];
2156 }
2157 this.seriesToAxisMap_ = seriesToAxisFiltered;
26ca7938
DV
2158};
2159
2160/**
2161 * Returns the number of y-axes on the chart.
2162 * @return {Number} the number of axes.
2163 */
2164Dygraph.prototype.numAxes = function() {
2165 var last_axis = 0;
2166 for (var series in this.seriesToAxisMap_) {
2167 if (!this.seriesToAxisMap_.hasOwnProperty(series)) continue;
2168 var idx = this.seriesToAxisMap_[series];
2169 if (idx > last_axis) last_axis = idx;
2170 }
2171 return 1 + last_axis;
2172};
2173
2174/**
2175 * Determine the value range and tick marks for each axis.
2176 * @param {Object} extremes A mapping from seriesName -> [low, high]
2177 * This fills in the valueRange and ticks fields in each entry of this.axes_.
2178 */
2179Dygraph.prototype.computeYAxisRanges_ = function(extremes) {
2180 // Build a map from axis number -> [list of series names]
2181 var seriesForAxis = [];
2182 for (var series in this.seriesToAxisMap_) {
2183 if (!this.seriesToAxisMap_.hasOwnProperty(series)) continue;
2184 var idx = this.seriesToAxisMap_[series];
2185 while (seriesForAxis.length <= idx) seriesForAxis.push([]);
2186 seriesForAxis[idx].push(series);
2187 }
f09fc545
DV
2188
2189 // Compute extreme values, a span and tick marks for each axis.
26ca7938
DV
2190 for (var i = 0; i < this.axes_.length; i++) {
2191 var axis = this.axes_[i];
d58ae307
DV
2192 if (axis.valueWindow) {
2193 // This is only set if the user has zoomed on the y-axis. It is never set
2194 // by a user. It takes precedence over axis.valueRange because, if you set
2195 // valueRange, you'd still expect to be able to pan.
2196 axis.computedValueRange = [axis.valueWindow[0], axis.valueWindow[1]];
2197 } else if (axis.valueRange) {
2198 // This is a user-set value range for this axis.
26ca7938
DV
2199 axis.computedValueRange = [axis.valueRange[0], axis.valueRange[1]];
2200 } else {
1c77a3a1 2201 // Calculate the extremes of extremes.
f09fc545
DV
2202 var series = seriesForAxis[i];
2203 var minY = Infinity; // extremes[series[0]][0];
2204 var maxY = -Infinity; // extremes[series[0]][1];
2205 for (var j = 0; j < series.length; j++) {
2206 minY = Math.min(extremes[series[j]][0], minY);
e3b6727e 2207 maxY = Math.max(extremes[series[j]][1], maxY);
f09fc545
DV
2208 }
2209 if (axis.includeZero && minY > 0) minY = 0;
2210
2211 // Add some padding and round up to an integer to be human-friendly.
2212 var span = maxY - minY;
2213 // special case: if we have no sense of scale, use +/-10% of the sole value.
2214 if (span == 0) { span = maxY; }
2215 var maxAxisY = maxY + 0.1 * span;
2216 var minAxisY = minY - 0.1 * span;
2217
2218 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
2f5e7e1a
DV
2219 if (!this.attr_("avoidMinZero")) {
2220 if (minAxisY < 0 && minY >= 0) minAxisY = 0;
2221 if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0;
2222 }
f09fc545
DV
2223
2224 if (this.attr_("includeZero")) {
2225 if (maxY < 0) maxAxisY = 0;
2226 if (minY > 0) minAxisY = 0;
2227 }
2228
26ca7938 2229 axis.computedValueRange = [minAxisY, maxAxisY];
f09fc545
DV
2230 }
2231
0d64e596
DV
2232 // Add ticks. By default, all axes inherit the tick positions of the
2233 // primary axis. However, if an axis is specifically marked as having
2234 // independent ticks, then that is permissible as well.
2235 if (i == 0 || axis.independentTicks) {
3c1d225b 2236 var ret =
0d64e596
DV
2237 Dygraph.numericTicks(axis.computedValueRange[0],
2238 axis.computedValueRange[1],
2239 this,
2240 axis);
3c1d225b 2241 axis.ticks = ret.ticks;
6be8e54c 2242 this.numYDigits_ = ret.numDigits;
0d64e596
DV
2243 } else {
2244 var p_axis = this.axes_[0];
2245 var p_ticks = p_axis.ticks;
2246 var p_scale = p_axis.computedValueRange[1] - p_axis.computedValueRange[0];
2247 var scale = axis.computedValueRange[1] - axis.computedValueRange[0];
2248 var tick_values = [];
2249 for (var i = 0; i < p_ticks.length; i++) {
2250 var y_frac = (p_ticks[i].v - p_axis.computedValueRange[0]) / p_scale;
2251 var y_val = axis.computedValueRange[0] + y_frac * scale;
2252 tick_values.push(y_val);
2253 }
2254
3c1d225b 2255 var ret =
0d64e596
DV
2256 Dygraph.numericTicks(axis.computedValueRange[0],
2257 axis.computedValueRange[1],
2258 this, axis, tick_values);
3c1d225b 2259 axis.ticks = ret.ticks;
6be8e54c 2260 this.numYDigits_ = ret.numDigits;
0d64e596 2261 }
f09fc545 2262 }
f09fc545
DV
2263};
2264
2265/**
6a1aa64f
DV
2266 * Calculates the rolling average of a data set.
2267 * If originalData is [label, val], rolls the average of those.
2268 * If originalData is [label, [, it's interpreted as [value, stddev]
2269 * and the roll is returned in the same form, with appropriately reduced
2270 * stddev for each value.
2271 * Note that this is where fractional input (i.e. '5/10') is converted into
2272 * decimal values.
2273 * @param {Array} originalData The data in the appropriate format (see above)
6faebb69
JB
2274 * @param {Number} rollPeriod The number of points over which to average the
2275 * data
6a1aa64f 2276 */
285a6bda 2277Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) {
6a1aa64f
DV
2278 if (originalData.length < 2)
2279 return originalData;
2280 var rollPeriod = Math.min(rollPeriod, originalData.length - 1);
2281 var rollingData = [];
285a6bda 2282 var sigma = this.attr_("sigma");
6a1aa64f
DV
2283
2284 if (this.fractions_) {
2285 var num = 0;
2286 var den = 0; // numerator/denominator
2287 var mult = 100.0;
2288 for (var i = 0; i < originalData.length; i++) {
2289 num += originalData[i][1][0];
2290 den += originalData[i][1][1];
2291 if (i - rollPeriod >= 0) {
2292 num -= originalData[i - rollPeriod][1][0];
2293 den -= originalData[i - rollPeriod][1][1];
2294 }
2295
2296 var date = originalData[i][0];
2297 var value = den ? num / den : 0.0;
285a6bda 2298 if (this.attr_("errorBars")) {
6a1aa64f
DV
2299 if (this.wilsonInterval_) {
2300 // For more details on this confidence interval, see:
2301 // http://en.wikipedia.org/wiki/Binomial_confidence_interval
2302 if (den) {
2303 var p = value < 0 ? 0 : value, n = den;
2304 var pm = sigma * Math.sqrt(p*(1-p)/n + sigma*sigma/(4*n*n));
2305 var denom = 1 + sigma * sigma / den;
2306 var low = (p + sigma * sigma / (2 * den) - pm) / denom;
2307 var high = (p + sigma * sigma / (2 * den) + pm) / denom;
2308 rollingData[i] = [date,
2309 [p * mult, (p - low) * mult, (high - p) * mult]];
2310 } else {
2311 rollingData[i] = [date, [0, 0, 0]];
2312 }
2313 } else {
2314 var stddev = den ? sigma * Math.sqrt(value * (1 - value) / den) : 1.0;
2315 rollingData[i] = [date, [mult * value, mult * stddev, mult * stddev]];
2316 }
2317 } else {
2318 rollingData[i] = [date, mult * value];
2319 }
2320 }
9922b78b 2321 } else if (this.attr_("customBars")) {
f6885d6a
DV
2322 var low = 0;
2323 var mid = 0;
2324 var high = 0;
2325 var count = 0;
6a1aa64f
DV
2326 for (var i = 0; i < originalData.length; i++) {
2327 var data = originalData[i][1];
2328 var y = data[1];
2329 rollingData[i] = [originalData[i][0], [y, y - data[0], data[2] - y]];
f6885d6a 2330
8b91c51f 2331 if (y != null && !isNaN(y)) {
49a7d0d5
DV
2332 low += data[0];
2333 mid += y;
2334 high += data[2];
2335 count += 1;
2336 }
f6885d6a
DV
2337 if (i - rollPeriod >= 0) {
2338 var prev = originalData[i - rollPeriod];
8b91c51f 2339 if (prev[1][1] != null && !isNaN(prev[1][1])) {
49a7d0d5
DV
2340 low -= prev[1][0];
2341 mid -= prev[1][1];
2342 high -= prev[1][2];
2343 count -= 1;
2344 }
f6885d6a
DV
2345 }
2346 rollingData[i] = [originalData[i][0], [ 1.0 * mid / count,
2347 1.0 * (mid - low) / count,
2348 1.0 * (high - mid) / count ]];
2769de62 2349 }
6a1aa64f
DV
2350 } else {
2351 // Calculate the rolling average for the first rollPeriod - 1 points where
6faebb69 2352 // there is not enough data to roll over the full number of points
6a1aa64f 2353 var num_init_points = Math.min(rollPeriod - 1, originalData.length - 2);
285a6bda 2354 if (!this.attr_("errorBars")){
5011e7a1
DV
2355 if (rollPeriod == 1) {
2356 return originalData;
2357 }
2358
2847c1cf 2359 for (var i = 0; i < originalData.length; i++) {
6a1aa64f 2360 var sum = 0;
5011e7a1 2361 var num_ok = 0;
2847c1cf
DV
2362 for (var j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
2363 var y = originalData[j][1];
8b91c51f 2364 if (y == null || isNaN(y)) continue;
5011e7a1 2365 num_ok++;
2847c1cf 2366 sum += originalData[j][1];
6a1aa64f 2367 }
5011e7a1 2368 if (num_ok) {
2847c1cf 2369 rollingData[i] = [originalData[i][0], sum / num_ok];
5011e7a1 2370 } else {
2847c1cf 2371 rollingData[i] = [originalData[i][0], null];
5011e7a1 2372 }
6a1aa64f 2373 }
2847c1cf
DV
2374
2375 } else {
2376 for (var i = 0; i < originalData.length; i++) {
6a1aa64f
DV
2377 var sum = 0;
2378 var variance = 0;
5011e7a1 2379 var num_ok = 0;
2847c1cf 2380 for (var j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
5011e7a1 2381 var y = originalData[j][1][0];
8b91c51f 2382 if (y == null || isNaN(y)) continue;
5011e7a1 2383 num_ok++;
6a1aa64f
DV
2384 sum += originalData[j][1][0];
2385 variance += Math.pow(originalData[j][1][1], 2);
2386 }
5011e7a1
DV
2387 if (num_ok) {
2388 var stddev = Math.sqrt(variance) / num_ok;
2389 rollingData[i] = [originalData[i][0],
2390 [sum / num_ok, sigma * stddev, sigma * stddev]];
2391 } else {
2392 rollingData[i] = [originalData[i][0], [null, null, null]];
2393 }
6a1aa64f
DV
2394 }
2395 }
2396 }
2397
2398 return rollingData;
2399};
2400
2401/**
2402 * Parses a date, returning the number of milliseconds since epoch. This can be
285a6bda
DV
2403 * passed in as an xValueParser in the Dygraph constructor.
2404 * TODO(danvk): enumerate formats that this understands.
6a1aa64f
DV
2405 * @param {String} A date in YYYYMMDD format.
2406 * @return {Number} Milliseconds since epoch.
2407 * @public
2408 */
285a6bda 2409Dygraph.dateParser = function(dateStr, self) {
6a1aa64f 2410 var dateStrSlashed;
285a6bda 2411 var d;
986a5026 2412 if (dateStr.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
6a1aa64f 2413 dateStrSlashed = dateStr.replace("-", "/", "g");
353a0294
DV
2414 while (dateStrSlashed.search("-") != -1) {
2415 dateStrSlashed = dateStrSlashed.replace("-", "/");
2416 }
285a6bda 2417 d = Date.parse(dateStrSlashed);
2769de62 2418 } else if (dateStr.length == 8) { // e.g. '20090712'
285a6bda 2419 // TODO(danvk): remove support for this format. It's confusing.
6a1aa64f
DV
2420 dateStrSlashed = dateStr.substr(0,4) + "/" + dateStr.substr(4,2)
2421 + "/" + dateStr.substr(6,2);
285a6bda 2422 d = Date.parse(dateStrSlashed);
2769de62
DV
2423 } else {
2424 // Any format that Date.parse will accept, e.g. "2009/07/12" or
2425 // "2009/07/12 12:34:56"
285a6bda
DV
2426 d = Date.parse(dateStr);
2427 }
2428
2429 if (!d || isNaN(d)) {
2430 self.error("Couldn't parse " + dateStr + " as a date");
2431 }
2432 return d;
2433};
2434
2435/**
2436 * Detects the type of the str (date or numeric) and sets the various
2437 * formatting attributes in this.attrs_ based on this type.
2438 * @param {String} str An x value.
2439 * @private
2440 */
2441Dygraph.prototype.detectTypeFromString_ = function(str) {
2442 var isDate = false;
2443 if (str.indexOf('-') >= 0 ||
2444 str.indexOf('/') >= 0 ||
2445 isNaN(parseFloat(str))) {
2446 isDate = true;
2447 } else if (str.length == 8 && str > '19700101' && str < '20371231') {
2448 // TODO(danvk): remove support for this format.
2449 isDate = true;
2450 }
2451
2452 if (isDate) {
2453 this.attrs_.xValueFormatter = Dygraph.dateString_;
2454 this.attrs_.xValueParser = Dygraph.dateParser;
2455 this.attrs_.xTicker = Dygraph.dateTicker;
bf640e56 2456 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
285a6bda 2457 } else {
6be8e54c 2458 this.attrs_.xValueFormatter = this.attrs_.yValueFormatter;
285a6bda
DV
2459 this.attrs_.xValueParser = function(x) { return parseFloat(x); };
2460 this.attrs_.xTicker = Dygraph.numericTicks;
bf640e56 2461 this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;
6a1aa64f 2462 }
6a1aa64f
DV
2463};
2464
2465/**
2466 * Parses a string in a special csv format. We expect a csv file where each
2467 * line is a date point, and the first field in each line is the date string.
2468 * We also expect that all remaining fields represent series.
285a6bda 2469 * if the errorBars attribute is set, then interpret the fields as:
6a1aa64f
DV
2470 * date, series1, stddev1, series2, stddev2, ...
2471 * @param {Array.<Object>} data See above.
2472 * @private
285a6bda
DV
2473 *
2474 * @return Array.<Object> An array with one entry for each row. These entries
2475 * are an array of cells in that row. The first entry is the parsed x-value for
2476 * the row. The second, third, etc. are the y-values. These can take on one of
2477 * three forms, depending on the CSV and constructor parameters:
2478 * 1. numeric value
2479 * 2. [ value, stddev ]
2480 * 3. [ low value, center value, high value ]
6a1aa64f 2481 */
285a6bda 2482Dygraph.prototype.parseCSV_ = function(data) {
6a1aa64f
DV
2483 var ret = [];
2484 var lines = data.split("\n");
3d67f03b
DV
2485
2486 // Use the default delimiter or fall back to a tab if that makes sense.
2487 var delim = this.attr_('delimiter');
2488 if (lines[0].indexOf(delim) == -1 && lines[0].indexOf('\t') >= 0) {
2489 delim = '\t';
2490 }
2491
285a6bda 2492 var start = 0;
6a1aa64f 2493 if (this.labelsFromCSV_) {
285a6bda 2494 start = 1;
3d67f03b 2495 this.attrs_.labels = lines[0].split(delim);
6a1aa64f
DV
2496 }
2497
03b522a4
DV
2498 // Parse the x as a float or return null if it's not a number.
2499 var parseFloatOrNull = function(x) {
41333ec0 2500 var val = parseFloat(x);
1f7f664b
DV
2501 // isFinite() returns false for NaN and +/-Infinity.
2502 return isFinite(val) ? val : null;
03b522a4
DV
2503 };
2504
285a6bda
DV
2505 var xParser;
2506 var defaultParserSet = false; // attempt to auto-detect x value type
2507 var expectedCols = this.attr_("labels").length;
987840a2 2508 var outOfOrder = false;
6a1aa64f
DV
2509 for (var i = start; i < lines.length; i++) {
2510 var line = lines[i];
2511 if (line.length == 0) continue; // skip blank lines
3d67f03b
DV
2512 if (line[0] == '#') continue; // skip comment lines
2513 var inFields = line.split(delim);
285a6bda 2514 if (inFields.length < 2) continue;
6a1aa64f
DV
2515
2516 var fields = [];
285a6bda
DV
2517 if (!defaultParserSet) {
2518 this.detectTypeFromString_(inFields[0]);
2519 xParser = this.attr_("xValueParser");
2520 defaultParserSet = true;
2521 }
2522 fields[0] = xParser(inFields[0], this);
6a1aa64f
DV
2523
2524 // If fractions are expected, parse the numbers as "A/B"
2525 if (this.fractions_) {
2526 for (var j = 1; j < inFields.length; j++) {
2527 // TODO(danvk): figure out an appropriate way to flag parse errors.
2528 var vals = inFields[j].split("/");
03b522a4 2529 fields[j] = [parseFloatOrNull(vals[0]), parseFloatOrNull(vals[1])];
6a1aa64f 2530 }
285a6bda 2531 } else if (this.attr_("errorBars")) {
6a1aa64f
DV
2532 // If there are error bars, values are (value, stddev) pairs
2533 for (var j = 1; j < inFields.length; j += 2)
03b522a4
DV
2534 fields[(j + 1) / 2] = [parseFloatOrNull(inFields[j]),
2535 parseFloatOrNull(inFields[j + 1])];
9922b78b 2536 } else if (this.attr_("customBars")) {
6a1aa64f
DV
2537 // Bars are a low;center;high tuple
2538 for (var j = 1; j < inFields.length; j++) {
2539 var vals = inFields[j].split(";");
03b522a4
DV
2540 fields[j] = [ parseFloatOrNull(vals[0]),
2541 parseFloatOrNull(vals[1]),
2542 parseFloatOrNull(vals[2]) ];
6a1aa64f
DV
2543 }
2544 } else {
2545 // Values are just numbers
285a6bda 2546 for (var j = 1; j < inFields.length; j++) {
03b522a4 2547 fields[j] = parseFloatOrNull(inFields[j]);
285a6bda 2548 }
6a1aa64f 2549 }
987840a2
DV
2550 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
2551 outOfOrder = true;
2552 }
6a1aa64f 2553 ret.push(fields);
285a6bda
DV
2554
2555 if (fields.length != expectedCols) {
2556 this.error("Number of columns in line " + i + " (" + fields.length +
2557 ") does not agree with number of labels (" + expectedCols +
2558 ") " + line);
2559 }
6a1aa64f 2560 }
987840a2
DV
2561
2562 if (outOfOrder) {
2563 this.warn("CSV is out of order; order it correctly to speed loading.");
2564 ret.sort(function(a,b) { return a[0] - b[0] });
2565 }
2566
6a1aa64f
DV
2567 return ret;
2568};
2569
2570/**
285a6bda
DV
2571 * The user has provided their data as a pre-packaged JS array. If the x values
2572 * are numeric, this is the same as dygraphs' internal format. If the x values
2573 * are dates, we need to convert them from Date objects to ms since epoch.
2574 * @param {Array.<Object>} data
2575 * @return {Array.<Object>} data with numeric x values.
2576 */
2577Dygraph.prototype.parseArray_ = function(data) {
2578 // Peek at the first x value to see if it's numeric.
2579 if (data.length == 0) {
2580 this.error("Can't plot empty data set");
2581 return null;
2582 }
2583 if (data[0].length == 0) {
2584 this.error("Data set cannot contain an empty row");
2585 return null;
2586 }
2587
2588 if (this.attr_("labels") == null) {
2589 this.warn("Using default labels. Set labels explicitly via 'labels' " +
2590 "in the options parameter");
2591 this.attrs_.labels = [ "X" ];
2592 for (var i = 1; i < data[0].length; i++) {
2593 this.attrs_.labels.push("Y" + i);
2594 }
2595 }
2596
2dda3850 2597 if (Dygraph.isDateLike(data[0][0])) {
285a6bda
DV
2598 // Some intelligent defaults for a date x-axis.
2599 this.attrs_.xValueFormatter = Dygraph.dateString_;
bf640e56 2600 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
285a6bda
DV
2601 this.attrs_.xTicker = Dygraph.dateTicker;
2602
2603 // Assume they're all dates.
e3ab7b40 2604 var parsedData = Dygraph.clone(data);
285a6bda
DV
2605 for (var i = 0; i < data.length; i++) {
2606 if (parsedData[i].length == 0) {
a323ff4a 2607 this.error("Row " + (1 + i) + " of data is empty");
285a6bda
DV
2608 return null;
2609 }
2610 if (parsedData[i][0] == null
3a909ec5
DV
2611 || typeof(parsedData[i][0].getTime) != 'function'
2612 || isNaN(parsedData[i][0].getTime())) {
be96a1f5 2613 this.error("x value in row " + (1 + i) + " is not a Date");
285a6bda
DV
2614 return null;
2615 }
2616 parsedData[i][0] = parsedData[i][0].getTime();
2617 }
2618 return parsedData;
2619 } else {
2620 // Some intelligent defaults for a numeric x-axis.
6be8e54c 2621 this.attrs_.xValueFormatter = this.attrs_.yValueFormatter;
285a6bda
DV
2622 this.attrs_.xTicker = Dygraph.numericTicks;
2623 return data;
2624 }
2625};
2626
2627/**
79420a1e
DV
2628 * Parses a DataTable object from gviz.
2629 * The data is expected to have a first column that is either a date or a
2630 * number. All subsequent columns must be numbers. If there is a clear mismatch
2631 * between this.xValueParser_ and the type of the first column, it will be
a685723c 2632 * fixed. Fills out rawData_.
79420a1e
DV
2633 * @param {Array.<Object>} data See above.
2634 * @private
2635 */
285a6bda 2636Dygraph.prototype.parseDataTable_ = function(data) {
79420a1e
DV
2637 var cols = data.getNumberOfColumns();
2638 var rows = data.getNumberOfRows();
2639
d955e223 2640 var indepType = data.getColumnType(0);
4440f6c8 2641 if (indepType == 'date' || indepType == 'datetime') {
285a6bda
DV
2642 this.attrs_.xValueFormatter = Dygraph.dateString_;
2643 this.attrs_.xValueParser = Dygraph.dateParser;
2644 this.attrs_.xTicker = Dygraph.dateTicker;
bf640e56 2645 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
33127159 2646 } else if (indepType == 'number') {
6be8e54c 2647 this.attrs_.xValueFormatter = this.attrs_.yValueFormatter;
285a6bda
DV
2648 this.attrs_.xValueParser = function(x) { return parseFloat(x); };
2649 this.attrs_.xTicker = Dygraph.numericTicks;
bf640e56 2650 this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;
285a6bda 2651 } else {
987840a2
DV
2652 this.error("only 'date', 'datetime' and 'number' types are supported for " +
2653 "column 1 of DataTable input (Got '" + indepType + "')");
79420a1e
DV
2654 return null;
2655 }
2656
a685723c
DV
2657 // Array of the column indices which contain data (and not annotations).
2658 var colIdx = [];
2659 var annotationCols = {}; // data index -> [annotation cols]
2660 var hasAnnotations = false;
2661 for (var i = 1; i < cols; i++) {
2662 var type = data.getColumnType(i);
2663 if (type == 'number') {
2664 colIdx.push(i);
2665 } else if (type == 'string' && this.attr_('displayAnnotations')) {
2666 // This is OK -- it's an annotation column.
2667 var dataIdx = colIdx[colIdx.length - 1];
2668 if (!annotationCols.hasOwnProperty(dataIdx)) {
2669 annotationCols[dataIdx] = [i];
2670 } else {
2671 annotationCols[dataIdx].push(i);
2672 }
2673 hasAnnotations = true;
2674 } else {
2675 this.error("Only 'number' is supported as a dependent type with Gviz." +
2676 " 'string' is only supported if displayAnnotations is true");
2677 }
2678 }
2679
2680 // Read column labels
2681 // TODO(danvk): add support back for errorBars
2682 var labels = [data.getColumnLabel(0)];
2683 for (var i = 0; i < colIdx.length; i++) {
2684 labels.push(data.getColumnLabel(colIdx[i]));
f9348814 2685 if (this.attr_("errorBars")) i += 1;
a685723c
DV
2686 }
2687 this.attrs_.labels = labels;
2688 cols = labels.length;
2689
79420a1e 2690 var ret = [];
987840a2 2691 var outOfOrder = false;
a685723c 2692 var annotations = [];
79420a1e
DV
2693 for (var i = 0; i < rows; i++) {
2694 var row = [];
debe4434
DV
2695 if (typeof(data.getValue(i, 0)) === 'undefined' ||
2696 data.getValue(i, 0) === null) {
129569a5
FD
2697 this.warn("Ignoring row " + i +
2698 " of DataTable because of undefined or null first column.");
debe4434
DV
2699 continue;
2700 }
2701
c21d2c2d 2702 if (indepType == 'date' || indepType == 'datetime') {
d955e223
DV
2703 row.push(data.getValue(i, 0).getTime());
2704 } else {
2705 row.push(data.getValue(i, 0));
2706 }
3e3f84e4 2707 if (!this.attr_("errorBars")) {
a685723c
DV
2708 for (var j = 0; j < colIdx.length; j++) {
2709 var col = colIdx[j];
2710 row.push(data.getValue(i, col));
2711 if (hasAnnotations &&
2712 annotationCols.hasOwnProperty(col) &&
2713 data.getValue(i, annotationCols[col][0]) != null) {
2714 var ann = {};
2715 ann.series = data.getColumnLabel(col);
2716 ann.xval = row[0];
2717 ann.shortText = String.fromCharCode(65 /* A */ + annotations.length)
2718 ann.text = '';
2719 for (var k = 0; k < annotationCols[col].length; k++) {
2720 if (k) ann.text += "\n";
2721 ann.text += data.getValue(i, annotationCols[col][k]);
2722 }
2723 annotations.push(ann);
2724 }
3e3f84e4
DV
2725 }
2726 } else {
2727 for (var j = 0; j < cols - 1; j++) {
2728 row.push([ data.getValue(i, 1 + 2 * j), data.getValue(i, 2 + 2 * j) ]);
2729 }
79420a1e 2730 }
987840a2
DV
2731 if (ret.length > 0 && row[0] < ret[ret.length - 1][0]) {
2732 outOfOrder = true;
2733 }
1f7f664b
DV
2734
2735 // Strip out infinities, which give dygraphs problems later on.
2736 for (var j = 0; j < row.length; j++) {
2737 if (!isFinite(row[j])) row[j] = null;
2738 }
243d96e8 2739 ret.push(row);
79420a1e 2740 }
987840a2
DV
2741
2742 if (outOfOrder) {
2743 this.warn("DataTable is out of order; order it correctly to speed loading.");
2744 ret.sort(function(a,b) { return a[0] - b[0] });
2745 }
a685723c
DV
2746 this.rawData_ = ret;
2747
2748 if (annotations.length > 0) {
2749 this.setAnnotations(annotations, true);
2750 }
79420a1e
DV
2751}
2752
24e5350c 2753// These functions are all based on MochiKit.
fc80a396
DV
2754Dygraph.update = function (self, o) {
2755 if (typeof(o) != 'undefined' && o !== null) {
2756 for (var k in o) {
85b99f0b
DV
2757 if (o.hasOwnProperty(k)) {
2758 self[k] = o[k];
2759 }
fc80a396
DV
2760 }
2761 }
2762 return self;
2763};
2764
2dda3850
DV
2765Dygraph.isArrayLike = function (o) {
2766 var typ = typeof(o);
2767 if (
c21d2c2d 2768 (typ != 'object' && !(typ == 'function' &&
2dda3850
DV
2769 typeof(o.item) == 'function')) ||
2770 o === null ||
2771 typeof(o.length) != 'number' ||
2772 o.nodeType === 3
2773 ) {
2774 return false;
2775 }
2776 return true;
2777};
2778
2779Dygraph.isDateLike = function (o) {
2780 if (typeof(o) != "object" || o === null ||
2781 typeof(o.getTime) != 'function') {
2782 return false;
2783 }
2784 return true;
2785};
2786
e3ab7b40
DV
2787Dygraph.clone = function(o) {
2788 // TODO(danvk): figure out how MochiKit's version works
2789 var r = [];
2790 for (var i = 0; i < o.length; i++) {
2791 if (Dygraph.isArrayLike(o[i])) {
2792 r.push(Dygraph.clone(o[i]));
2793 } else {
2794 r.push(o[i]);
2795 }
2796 }
2797 return r;
24e5350c
DV
2798};
2799
2dda3850 2800
79420a1e 2801/**
6a1aa64f
DV
2802 * Get the CSV data. If it's in a function, call that function. If it's in a
2803 * file, do an XMLHttpRequest to get it.
2804 * @private
2805 */
285a6bda 2806Dygraph.prototype.start_ = function() {
6a1aa64f 2807 if (typeof this.file_ == 'function') {
285a6bda 2808 // CSV string. Pretend we got it via XHR.
6a1aa64f 2809 this.loadedEvent_(this.file_());
2dda3850 2810 } else if (Dygraph.isArrayLike(this.file_)) {
285a6bda 2811 this.rawData_ = this.parseArray_(this.file_);
26ca7938 2812 this.predraw_();
79420a1e
DV
2813 } else if (typeof this.file_ == 'object' &&
2814 typeof this.file_.getColumnRange == 'function') {
2815 // must be a DataTable from gviz.
a685723c 2816 this.parseDataTable_(this.file_);
26ca7938 2817 this.predraw_();
285a6bda
DV
2818 } else if (typeof this.file_ == 'string') {
2819 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
2820 if (this.file_.indexOf('\n') >= 0) {
2821 this.loadedEvent_(this.file_);
2822 } else {
2823 var req = new XMLHttpRequest();
2824 var caller = this;
2825 req.onreadystatechange = function () {
2826 if (req.readyState == 4) {
2827 if (req.status == 200) {
2828 caller.loadedEvent_(req.responseText);
2829 }
6a1aa64f 2830 }
285a6bda 2831 };
6a1aa64f 2832
285a6bda
DV
2833 req.open("GET", this.file_, true);
2834 req.send(null);
2835 }
2836 } else {
2837 this.error("Unknown data format: " + (typeof this.file_));
6a1aa64f
DV
2838 }
2839};
2840
2841/**
2842 * Changes various properties of the graph. These can include:
2843 * <ul>
2844 * <li>file: changes the source data for the graph</li>
2845 * <li>errorBars: changes whether the data contains stddev</li>
2846 * </ul>
2847 * @param {Object} attrs The new properties and values
2848 */
285a6bda
DV
2849Dygraph.prototype.updateOptions = function(attrs) {
2850 // TODO(danvk): this is a mess. Rethink this function.
c65f2303 2851 if ('rollPeriod' in attrs) {
6a1aa64f
DV
2852 this.rollPeriod_ = attrs.rollPeriod;
2853 }
c65f2303 2854 if ('dateWindow' in attrs) {
6a1aa64f
DV
2855 this.dateWindow_ = attrs.dateWindow;
2856 }
450fe64b
DV
2857
2858 // TODO(danvk): validate per-series options.
46dde5f9
DV
2859 // Supported:
2860 // strokeWidth
2861 // pointSize
2862 // drawPoints
2863 // highlightCircleSize
450fe64b 2864
fc80a396 2865 Dygraph.update(this.user_attrs_, attrs);
87bb7958 2866 Dygraph.update(this.renderOptions_, attrs);
285a6bda
DV
2867
2868 this.labelsFromCSV_ = (this.attr_("labels") == null);
2869
2870 // TODO(danvk): this doesn't match the constructor logic
2871 this.layout_.updateOptions({ 'errorBars': this.attr_("errorBars") });
5e50289f 2872 if (attrs['file']) {
6a1aa64f
DV
2873 this.file_ = attrs['file'];
2874 this.start_();
2875 } else {
26ca7938 2876 this.predraw_();
6a1aa64f
DV
2877 }
2878};
2879
2880/**
697e70b2
DV
2881 * Resizes the dygraph. If no parameters are specified, resizes to fill the
2882 * containing div (which has presumably changed size since the dygraph was
2883 * instantiated. If the width/height are specified, the div will be resized.
964f30c6
DV
2884 *
2885 * This is far more efficient than destroying and re-instantiating a
2886 * Dygraph, since it doesn't have to reparse the underlying data.
2887 *
697e70b2
DV
2888 * @param {Number} width Width (in pixels)
2889 * @param {Number} height Height (in pixels)
2890 */
2891Dygraph.prototype.resize = function(width, height) {
e8c7ef86
DV
2892 if (this.resize_lock) {
2893 return;
2894 }
2895 this.resize_lock = true;
2896
697e70b2
DV
2897 if ((width === null) != (height === null)) {
2898 this.warn("Dygraph.resize() should be called with zero parameters or " +
2899 "two non-NULL parameters. Pretending it was zero.");
2900 width = height = null;
2901 }
2902
b16e6369 2903 // TODO(danvk): there should be a clear() method.
697e70b2 2904 this.maindiv_.innerHTML = "";
b16e6369
DV
2905 this.attrs_.labelsDiv = null;
2906
697e70b2
DV
2907 if (width) {
2908 this.maindiv_.style.width = width + "px";
2909 this.maindiv_.style.height = height + "px";
2910 this.width_ = width;
2911 this.height_ = height;
2912 } else {
2913 this.width_ = this.maindiv_.offsetWidth;
2914 this.height_ = this.maindiv_.offsetHeight;
2915 }
2916
2917 this.createInterface_();
26ca7938 2918 this.predraw_();
e8c7ef86
DV
2919
2920 this.resize_lock = false;
697e70b2
DV
2921};
2922
2923/**
6faebb69 2924 * Adjusts the number of points in the rolling average. Updates the graph to
6a1aa64f 2925 * reflect the new averaging period.
6faebb69 2926 * @param {Number} length Number of points over which to average the data.
6a1aa64f 2927 */
285a6bda 2928Dygraph.prototype.adjustRoll = function(length) {
6a1aa64f 2929 this.rollPeriod_ = length;
26ca7938 2930 this.predraw_();
6a1aa64f 2931};
540d00f1 2932
f8cfec73 2933/**
1cf11047
DV
2934 * Returns a boolean array of visibility statuses.
2935 */
2936Dygraph.prototype.visibility = function() {
2937 // Do lazy-initialization, so that this happens after we know the number of
2938 // data series.
2939 if (!this.attr_("visibility")) {
f38dec01 2940 this.attrs_["visibility"] = [];
1cf11047
DV
2941 }
2942 while (this.attr_("visibility").length < this.rawData_[0].length - 1) {
f38dec01 2943 this.attr_("visibility").push(true);
1cf11047
DV
2944 }
2945 return this.attr_("visibility");
2946};
2947
2948/**
2949 * Changes the visiblity of a series.
2950 */
2951Dygraph.prototype.setVisibility = function(num, value) {
2952 var x = this.visibility();
a6c109c1 2953 if (num < 0 || num >= x.length) {
1cf11047
DV
2954 this.warn("invalid series number in setVisibility: " + num);
2955 } else {
2956 x[num] = value;
26ca7938 2957 this.predraw_();
1cf11047
DV
2958 }
2959};
2960
2961/**
5c528fa2
DV
2962 * Update the list of annotations and redraw the chart.
2963 */
a685723c 2964Dygraph.prototype.setAnnotations = function(ann, suppressDraw) {
3c51ab74
DV
2965 // Only add the annotation CSS rule once we know it will be used.
2966 Dygraph.addAnnotationRule();
5c528fa2
DV
2967 this.annotations_ = ann;
2968 this.layout_.setAnnotations(this.annotations_);
a685723c 2969 if (!suppressDraw) {
26ca7938 2970 this.predraw_();
a685723c 2971 }
5c528fa2
DV
2972};
2973
2974/**
2975 * Return the list of annotations.
2976 */
2977Dygraph.prototype.annotations = function() {
2978 return this.annotations_;
2979};
2980
46dde5f9
DV
2981/**
2982 * Get the index of a series (column) given its name. The first column is the
2983 * x-axis, so the data series start with index 1.
2984 */
2985Dygraph.prototype.indexFromSetName = function(name) {
2986 var labels = this.attr_("labels");
2987 for (var i = 0; i < labels.length; i++) {
2988 if (labels[i] == name) return i;
2989 }
2990 return null;
2991};
2992
5c528fa2
DV
2993Dygraph.addAnnotationRule = function() {
2994 if (Dygraph.addedAnnotationCSS) return;
2995
5c528fa2
DV
2996 var rule = "border: 1px solid black; " +
2997 "background-color: white; " +
2998 "text-align: center;";
22186871
DV
2999
3000 var styleSheetElement = document.createElement("style");
3001 styleSheetElement.type = "text/css";
3002 document.getElementsByTagName("head")[0].appendChild(styleSheetElement);
3003
3004 // Find the first style sheet that we can access.
3005 // We may not add a rule to a style sheet from another domain for security
3006 // reasons. This sometimes comes up when using gviz, since the Google gviz JS
3007 // adds its own style sheets from google.com.
3008 for (var i = 0; i < document.styleSheets.length; i++) {
3009 if (document.styleSheets[i].disabled) continue;
3010 var mysheet = document.styleSheets[i];
3011 try {
3012 if (mysheet.insertRule) { // Firefox
3013 var idx = mysheet.cssRules ? mysheet.cssRules.length : 0;
3014 mysheet.insertRule(".dygraphDefaultAnnotation { " + rule + " }", idx);
3015 } else if (mysheet.addRule) { // IE
3016 mysheet.addRule(".dygraphDefaultAnnotation", rule);
3017 }
3018 Dygraph.addedAnnotationCSS = true;
3019 return;
3020 } catch(err) {
3021 // Was likely a security exception.
3022 }
5c528fa2
DV
3023 }
3024
22186871 3025 this.warn("Unable to add default annotation CSS rule; display may be off.");
5c528fa2
DV
3026}
3027
3028/**
f8cfec73
DV
3029 * Create a new canvas element. This is more complex than a simple
3030 * document.createElement("canvas") because of IE and excanvas.
3031 */
3032Dygraph.createCanvas = function() {
3033 var canvas = document.createElement("canvas");
3034
3035 isIE = (/MSIE/.test(navigator.userAgent) && !window.opera);
8b8f2d59 3036 if (isIE && (typeof(G_vmlCanvasManager) != 'undefined')) {
f8cfec73
DV
3037 canvas = G_vmlCanvasManager.initElement(canvas);
3038 }
3039
3040 return canvas;
3041};
3042
540d00f1
DV
3043
3044/**
285a6bda 3045 * A wrapper around Dygraph that implements the gviz API.
540d00f1
DV
3046 * @param {Object} container The DOM object the visualization should live in.
3047 */
285a6bda 3048Dygraph.GVizChart = function(container) {
540d00f1
DV
3049 this.container = container;
3050}
3051
285a6bda 3052Dygraph.GVizChart.prototype.draw = function(data, options) {
c91f4ae8
DV
3053 // Clear out any existing dygraph.
3054 // TODO(danvk): would it make more sense to simply redraw using the current
3055 // date_graph object?
540d00f1 3056 this.container.innerHTML = '';
c91f4ae8
DV
3057 if (typeof(this.date_graph) != 'undefined') {
3058 this.date_graph.destroy();
3059 }
3060
285a6bda 3061 this.date_graph = new Dygraph(this.container, data, options);
540d00f1 3062}
285a6bda 3063
239c712d
NAG
3064/**
3065 * Google charts compatible setSelection
50360fd0 3066 * Only row selection is supported, all points in the row will be highlighted
239c712d
NAG
3067 * @param {Array} array of the selected cells
3068 * @public
3069 */
3070Dygraph.GVizChart.prototype.setSelection = function(selection_array) {
3071 var row = false;
3072 if (selection_array.length) {
3073 row = selection_array[0].row;
3074 }
3075 this.date_graph.setSelection(row);
3076}
3077
103b7292
NAG
3078/**
3079 * Google charts compatible getSelection implementation
3080 * @return {Array} array of the selected cells
3081 * @public
3082 */
3083Dygraph.GVizChart.prototype.getSelection = function() {
3084 var selection = [];
50360fd0 3085
103b7292 3086 var row = this.date_graph.getSelection();
50360fd0 3087
103b7292 3088 if (row < 0) return selection;
50360fd0 3089
103b7292
NAG
3090 col = 1;
3091 for (var i in this.date_graph.layout_.datasets) {
3092 selection.push({row: row, column: col});
3093 col++;
3094 }
3095
3096 return selection;
3097}
3098
285a6bda
DV
3099// Older pages may still use this name.
3100DateGraph = Dygraph;