change to an implementation based on set names
[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
285a6bda 40 For further documentation and examples, see http://www.danvk.org/dygraphs
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
76// Various default values
285a6bda
DV
77Dygraph.DEFAULT_ROLL_PERIOD = 1;
78Dygraph.DEFAULT_WIDTH = 480;
79Dygraph.DEFAULT_HEIGHT = 320;
80Dygraph.AXIS_LINE_WIDTH = 0.3;
6a1aa64f 81
8e4a6af3 82// Default attribute values.
285a6bda 83Dygraph.DEFAULT_ATTRS = {
a9fc39ab 84 highlightCircleSize: 3,
8e4a6af3 85 pixelsPerXLabel: 60,
c6336f04 86 pixelsPerYLabel: 30,
285a6bda 87
8e4a6af3
DV
88 labelsDivWidth: 250,
89 labelsDivStyles: {
90 // TODO(danvk): move defaults from createStatusMessage_ here.
285a6bda
DV
91 },
92 labelsSeparateLines: false,
bcd3ebf0 93 labelsShowZeroValues: true,
285a6bda 94 labelsKMB: false,
afefbcdb 95 labelsKMG2: false,
d160cc3b 96 showLabelsOnHighlight: true,
12e4c741 97
029da4b6 98 yValueFormatter: function(x) { return Dygraph.round_(x, 2); },
285a6bda
DV
99
100 strokeWidth: 1.0,
8e4a6af3 101
8846615a
DV
102 axisTickSize: 3,
103 axisLabelFontSize: 14,
104 xAxisLabelWidth: 50,
105 yAxisLabelWidth: 50,
bf640e56 106 xAxisLabelFormatter: Dygraph.dateAxisFormatter,
8846615a 107 rightGap: 5,
285a6bda
DV
108
109 showRoller: false,
110 xValueFormatter: Dygraph.dateString_,
111 xValueParser: Dygraph.dateParser,
112 xTicker: Dygraph.dateTicker,
113
3d67f03b
DV
114 delimiter: ',',
115
ff00d3e2 116 logScale: false,
285a6bda
DV
117 sigma: 2.0,
118 errorBars: false,
119 fractions: false,
120 wilsonInterval: true, // only relevant if fractions is true
5954ef32 121 customBars: false,
43af96e7
NK
122 fillGraph: false,
123 fillAlpha: 0.15,
f032c51d 124 connectSeparatedPoints: false,
43af96e7
NK
125
126 stackedGraph: false,
afdc483f
NN
127 hideOverlayOnMouseOut: true,
128
129 stepPlot: false
285a6bda
DV
130};
131
132// Various logging levels.
133Dygraph.DEBUG = 1;
134Dygraph.INFO = 2;
135Dygraph.WARNING = 3;
136Dygraph.ERROR = 3;
137
5c528fa2
DV
138// Used for initializing annotation CSS rules only once.
139Dygraph.addedAnnotationCSS = false;
140
285a6bda
DV
141Dygraph.prototype.__old_init__ = function(div, file, labels, attrs) {
142 // Labels is no longer a constructor parameter, since it's typically set
143 // directly from the data source. It also conains a name for the x-axis,
144 // which the previous constructor form did not.
145 if (labels != null) {
146 var new_labels = ["Date"];
147 for (var i = 0; i < labels.length; i++) new_labels.push(labels[i]);
fc80a396 148 Dygraph.update(attrs, { 'labels': new_labels });
285a6bda
DV
149 }
150 this.__init__(div, file, attrs);
8e4a6af3
DV
151};
152
6a1aa64f 153/**
285a6bda 154 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
6a1aa64f
DV
155 * and interaction &lt;canvas&gt; inside of it. See the constructor for details
156 * on the parameters.
12e4c741 157 * @param {Element} div the Element to render the graph into.
6a1aa64f 158 * @param {String | Function} file Source data
6a1aa64f
DV
159 * @param {Object} attrs Miscellaneous other options
160 * @private
161 */
285a6bda
DV
162Dygraph.prototype.__init__ = function(div, file, attrs) {
163 // Support two-argument constructor
164 if (attrs == null) { attrs = {}; }
165
6a1aa64f 166 // Copy the important bits into the object
32988383 167 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
6a1aa64f 168 this.maindiv_ = div;
6a1aa64f 169 this.file_ = file;
285a6bda 170 this.rollPeriod_ = attrs.rollPeriod || Dygraph.DEFAULT_ROLL_PERIOD;
6a1aa64f 171 this.previousVerticalX_ = -1;
6a1aa64f 172 this.fractions_ = attrs.fractions || false;
6a1aa64f
DV
173 this.dateWindow_ = attrs.dateWindow || null;
174 this.valueRange_ = attrs.valueRange || null;
6a1aa64f 175 this.wilsonInterval_ = attrs.wilsonInterval || true;
fe0b7c03 176 this.is_initial_draw_ = true;
5c528fa2 177 this.annotations_ = [];
8e4a6af3 178
f7d6278e
DV
179 // Clear the div. This ensure that, if multiple dygraphs are passed the same
180 // div, then only one will be drawn.
181 div.innerHTML = "";
182
c21d2c2d 183 // If the div isn't already sized then inherit from our attrs or
184 // give it a default size.
285a6bda 185 if (div.style.width == '') {
c21d2c2d 186 div.style.width = attrs.width || Dygraph.DEFAULT_WIDTH + "px";
285a6bda
DV
187 }
188 if (div.style.height == '') {
c21d2c2d 189 div.style.height = attrs.height || Dygraph.DEFAULT_HEIGHT + "px";
32988383 190 }
285a6bda
DV
191 this.width_ = parseInt(div.style.width, 10);
192 this.height_ = parseInt(div.style.height, 10);
c21d2c2d 193 // The div might have been specified as percent of the current window size,
194 // convert that to an appropriate number of pixels.
195 if (div.style.width.indexOf("%") == div.style.width.length - 1) {
c6f45033 196 this.width_ = div.offsetWidth;
c21d2c2d 197 }
198 if (div.style.height.indexOf("%") == div.style.height.length - 1) {
c6f45033 199 this.height_ = div.offsetHeight;
c21d2c2d 200 }
32988383 201
10a6456d
DV
202 if (this.width_ == 0) {
203 this.error("dygraph has zero width. Please specify a width in pixels.");
204 }
205 if (this.height_ == 0) {
206 this.error("dygraph has zero height. Please specify a height in pixels.");
207 }
208
344ba8c0 209 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
43af96e7
NK
210 if (attrs['stackedGraph']) {
211 attrs['fillGraph'] = true;
212 // TODO(nikhilk): Add any other stackedGraph checks here.
213 }
214
285a6bda
DV
215 // Dygraphs has many options, some of which interact with one another.
216 // To keep track of everything, we maintain two sets of options:
217 //
c21d2c2d 218 // this.user_attrs_ only options explicitly set by the user.
285a6bda
DV
219 // this.attrs_ defaults, options derived from user_attrs_, data.
220 //
221 // Options are then accessed this.attr_('attr'), which first looks at
222 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
223 // defaults without overriding behavior that the user specifically asks for.
224 this.user_attrs_ = {};
fc80a396 225 Dygraph.update(this.user_attrs_, attrs);
6a1aa64f 226
285a6bda 227 this.attrs_ = {};
fc80a396 228 Dygraph.update(this.attrs_, Dygraph.DEFAULT_ATTRS);
6a1aa64f 229
16269f6e 230 this.boundaryIds_ = [];
6a1aa64f 231
285a6bda
DV
232 // Make a note of whether labels will be pulled from the CSV file.
233 this.labelsFromCSV_ = (this.attr_("labels") == null);
6a1aa64f 234
5c528fa2
DV
235 Dygraph.addAnnotationRule();
236
6a1aa64f
DV
237 // Create the containing DIV and other interactive elements
238 this.createInterface_();
239
738fc797 240 this.start_();
6a1aa64f
DV
241};
242
227b93cc
DV
243Dygraph.prototype.attr_ = function(name, seriesName) {
244 if (seriesName &&
245 typeof(this.user_attrs_[seriesName]) != 'undefined' &&
246 this.user_attrs_[seriesName] != null &&
247 typeof(this.user_attrs_[seriesName][name]) != 'undefined') {
248 return this.user_attrs_[seriesName][name];
450fe64b 249 } else if (typeof(this.user_attrs_[name]) != 'undefined') {
285a6bda
DV
250 return this.user_attrs_[name];
251 } else if (typeof(this.attrs_[name]) != 'undefined') {
252 return this.attrs_[name];
253 } else {
254 return null;
255 }
256};
257
258// TODO(danvk): any way I can get the line numbers to be this.warn call?
259Dygraph.prototype.log = function(severity, message) {
260 if (typeof(console) != 'undefined') {
261 switch (severity) {
262 case Dygraph.DEBUG:
263 console.debug('dygraphs: ' + message);
264 break;
265 case Dygraph.INFO:
266 console.info('dygraphs: ' + message);
267 break;
268 case Dygraph.WARNING:
269 console.warn('dygraphs: ' + message);
270 break;
271 case Dygraph.ERROR:
272 console.error('dygraphs: ' + message);
273 break;
274 }
275 }
276}
277Dygraph.prototype.info = function(message) {
278 this.log(Dygraph.INFO, message);
279}
280Dygraph.prototype.warn = function(message) {
281 this.log(Dygraph.WARNING, message);
282}
283Dygraph.prototype.error = function(message) {
284 this.log(Dygraph.ERROR, message);
285}
286
6a1aa64f
DV
287/**
288 * Returns the current rolling period, as set by the user or an option.
289 * @return {Number} The number of days in the rolling window
290 */
285a6bda 291Dygraph.prototype.rollPeriod = function() {
6a1aa64f 292 return this.rollPeriod_;
76171648
DV
293};
294
599fb4ad
DV
295/**
296 * Returns the currently-visible x-range. This can be affected by zooming,
297 * panning or a call to updateOptions.
298 * Returns a two-element array: [left, right].
299 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
300 */
301Dygraph.prototype.xAxisRange = function() {
302 if (this.dateWindow_) return this.dateWindow_;
303
304 // The entire chart is visible.
305 var left = this.rawData_[0][0];
306 var right = this.rawData_[this.rawData_.length - 1][0];
307 return [left, right];
308};
309
3230c662
DV
310/**
311 * Returns the currently-visible y-range. This can be affected by zooming,
312 * panning or a call to updateOptions.
313 * Returns a two-element array: [bottom, top].
314 */
315Dygraph.prototype.yAxisRange = function() {
316 return this.displayedYRange_;
317};
318
319/**
320 * Convert from data coordinates to canvas/div X/Y coordinates.
321 * Returns a two-element array: [X, Y]
322 */
323Dygraph.prototype.toDomCoords = function(x, y) {
324 var ret = [null, null];
325 var area = this.plotter_.area;
326 if (x !== null) {
327 var xRange = this.xAxisRange();
328 ret[0] = area.x + (x - xRange[0]) / (xRange[1] - xRange[0]) * area.w;
329 }
330
331 if (y !== null) {
332 var yRange = this.yAxisRange();
891ad846 333 ret[1] = area.y + (yRange[1] - y) / (yRange[1] - yRange[0]) * area.h;
3230c662
DV
334 }
335
336 return ret;
337};
338
56623f3b 339// TODO(danvk): use these functions throughout dygraphs.
3230c662
DV
340/**
341 * Convert from canvas/div coords to data coordinates.
342 * Returns a two-element array: [X, Y]
343 */
344Dygraph.prototype.toDataCoords = function(x, y) {
345 var ret = [null, null];
346 var area = this.plotter_.area;
347 if (x !== null) {
348 var xRange = this.xAxisRange();
349 ret[0] = xRange[0] + (x - area.x) / area.w * (xRange[1] - xRange[0]);
350 }
351
352 if (y !== null) {
353 var yRange = this.yAxisRange();
354 ret[1] = yRange[0] + (area.h - y) / area.h * (yRange[1] - yRange[0]);
355 }
356
357 return ret;
358};
359
e99fde05
DV
360/**
361 * Returns the number of columns (including the independent variable).
362 */
363Dygraph.prototype.numColumns = function() {
364 return this.rawData_[0].length;
365};
366
367/**
368 * Returns the number of rows (excluding any header/label row).
369 */
370Dygraph.prototype.numRows = function() {
371 return this.rawData_.length;
372};
373
374/**
375 * Returns the value in the given row and column. If the row and column exceed
376 * the bounds on the data, returns null. Also returns null if the value is
377 * missing.
378 */
379Dygraph.prototype.getValue = function(row, col) {
380 if (row < 0 || row > this.rawData_.length) return null;
381 if (col < 0 || col > this.rawData_[row].length) return null;
382
383 return this.rawData_[row][col];
384};
385
76171648
DV
386Dygraph.addEvent = function(el, evt, fn) {
387 var normed_fn = function(e) {
388 if (!e) var e = window.event;
389 fn(e);
390 };
391 if (window.addEventListener) { // Mozilla, Netscape, Firefox
392 el.addEventListener(evt, normed_fn, false);
393 } else { // IE
394 el.attachEvent('on' + evt, normed_fn);
395 }
396};
6a1aa64f 397
4c47502d
DV
398Dygraph.clipCanvas_ = function(cnv, clip) {
399 var ctx = cnv.getContext("2d");
400 ctx.beginPath();
401 ctx.rect(clip.left, clip.top, clip.width, clip.height);
402 ctx.clip();
403};
404
6a1aa64f 405/**
285a6bda 406 * Generates interface elements for the Dygraph: a containing div, a div to
6a1aa64f 407 * display the current point, and a textbox to adjust the rolling average
697e70b2 408 * period. Also creates the Renderer/Layout elements.
6a1aa64f
DV
409 * @private
410 */
285a6bda 411Dygraph.prototype.createInterface_ = function() {
6a1aa64f
DV
412 // Create the all-enclosing graph div
413 var enclosing = this.maindiv_;
414
b0c3b730
DV
415 this.graphDiv = document.createElement("div");
416 this.graphDiv.style.width = this.width_ + "px";
417 this.graphDiv.style.height = this.height_ + "px";
418 enclosing.appendChild(this.graphDiv);
419
4c47502d
DV
420 var clip = {
421 top: 0,
422 left: this.attr_("yAxisLabelWidth") + 2 * this.attr_("axisTickSize")
423 };
424 clip.width = this.width_ - clip.left - this.attr_("rightGap");
425 clip.height = this.height_ - this.attr_("axisLabelFontSize")
426 - 2 * this.attr_("axisTickSize");
427 this.clippingArea_ = clip;
428
b0c3b730 429 // Create the canvas for interactive parts of the chart.
f8cfec73 430 this.canvas_ = Dygraph.createCanvas();
b0c3b730
DV
431 this.canvas_.style.position = "absolute";
432 this.canvas_.width = this.width_;
433 this.canvas_.height = this.height_;
f8cfec73
DV
434 this.canvas_.style.width = this.width_ + "px"; // for IE
435 this.canvas_.style.height = this.height_ + "px"; // for IE
b0c3b730
DV
436
437 // ... and for static parts of the chart.
6a1aa64f 438 this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
76171648 439
eb7bf005
EC
440 // The interactive parts of the graph are drawn on top of the chart.
441 this.graphDiv.appendChild(this.hidden_);
442 this.graphDiv.appendChild(this.canvas_);
443 this.mouseEventElement_ = this.canvas_;
444
4c47502d
DV
445 // Make sure we don't overdraw.
446 Dygraph.clipCanvas_(this.hidden_, this.clippingArea_);
447 Dygraph.clipCanvas_(this.canvas_, this.clippingArea_);
448
76171648 449 var dygraph = this;
eb7bf005 450 Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(e) {
76171648
DV
451 dygraph.mouseMove_(e);
452 });
eb7bf005 453 Dygraph.addEvent(this.mouseEventElement_, 'mouseout', function(e) {
76171648
DV
454 dygraph.mouseOut_(e);
455 });
697e70b2
DV
456
457 // Create the grapher
458 // TODO(danvk): why does the Layout need its own set of options?
459 this.layoutOptions_ = { 'xOriginIsZero': false };
460 Dygraph.update(this.layoutOptions_, this.attrs_);
461 Dygraph.update(this.layoutOptions_, this.user_attrs_);
462 Dygraph.update(this.layoutOptions_, {
463 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
464
465 this.layout_ = new DygraphLayout(this, this.layoutOptions_);
466
467 // TODO(danvk): why does the Renderer need its own set of options?
468 this.renderOptions_ = { colorScheme: this.colors_,
469 strokeColor: null,
470 axisLineWidth: Dygraph.AXIS_LINE_WIDTH };
471 Dygraph.update(this.renderOptions_, this.attrs_);
472 Dygraph.update(this.renderOptions_, this.user_attrs_);
473 this.plotter_ = new DygraphCanvasRenderer(this,
474 this.hidden_, this.layout_,
475 this.renderOptions_);
476
477 this.createStatusMessage_();
478 this.createRollInterface_();
479 this.createDragInterface_();
4cfcc38c
DV
480};
481
482/**
483 * Detach DOM elements in the dygraph and null out all data references.
484 * Calling this when you're done with a dygraph can dramatically reduce memory
485 * usage. See, e.g., the tests/perf.html example.
486 */
487Dygraph.prototype.destroy = function() {
488 var removeRecursive = function(node) {
489 while (node.hasChildNodes()) {
490 removeRecursive(node.firstChild);
491 node.removeChild(node.firstChild);
492 }
493 };
494 removeRecursive(this.maindiv_);
495
496 var nullOut = function(obj) {
497 for (var n in obj) {
498 if (typeof(obj[n]) === 'object') {
499 obj[n] = null;
500 }
501 }
502 };
503
504 // These may not all be necessary, but it can't hurt...
505 nullOut(this.layout_);
506 nullOut(this.plotter_);
507 nullOut(this);
508};
6a1aa64f
DV
509
510/**
511 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
285a6bda 512 * this particular canvas. All Dygraph work is done on this.canvas_.
8846615a 513 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
6a1aa64f
DV
514 * @return {Object} The newly-created canvas
515 * @private
516 */
285a6bda 517Dygraph.prototype.createPlotKitCanvas_ = function(canvas) {
f8cfec73 518 var h = Dygraph.createCanvas();
6a1aa64f 519 h.style.position = "absolute";
9ac5e4ae
DV
520 // TODO(danvk): h should be offset from canvas. canvas needs to include
521 // some extra area to make it easier to zoom in on the far left and far
522 // right. h needs to be precisely the plot area, so that clipping occurs.
6a1aa64f
DV
523 h.style.top = canvas.style.top;
524 h.style.left = canvas.style.left;
525 h.width = this.width_;
526 h.height = this.height_;
f8cfec73
DV
527 h.style.width = this.width_ + "px"; // for IE
528 h.style.height = this.height_ + "px"; // for IE
6a1aa64f
DV
529 return h;
530};
531
f474c2a3
DV
532// Taken from MochiKit.Color
533Dygraph.hsvToRGB = function (hue, saturation, value) {
534 var red;
535 var green;
536 var blue;
537 if (saturation === 0) {
538 red = value;
539 green = value;
540 blue = value;
541 } else {
542 var i = Math.floor(hue * 6);
543 var f = (hue * 6) - i;
544 var p = value * (1 - saturation);
545 var q = value * (1 - (saturation * f));
546 var t = value * (1 - (saturation * (1 - f)));
547 switch (i) {
548 case 1: red = q; green = value; blue = p; break;
549 case 2: red = p; green = value; blue = t; break;
550 case 3: red = p; green = q; blue = value; break;
551 case 4: red = t; green = p; blue = value; break;
552 case 5: red = value; green = p; blue = q; break;
553 case 6: // fall through
554 case 0: red = value; green = t; blue = p; break;
555 }
556 }
557 red = Math.floor(255 * red + 0.5);
558 green = Math.floor(255 * green + 0.5);
559 blue = Math.floor(255 * blue + 0.5);
560 return 'rgb(' + red + ',' + green + ',' + blue + ')';
561};
562
563
6a1aa64f
DV
564/**
565 * Generate a set of distinct colors for the data series. This is done with a
566 * color wheel. Saturation/Value are customizable, and the hue is
567 * equally-spaced around the color wheel. If a custom set of colors is
568 * specified, that is used instead.
6a1aa64f
DV
569 * @private
570 */
285a6bda
DV
571Dygraph.prototype.setColors_ = function() {
572 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
573 // away with this.renderOptions_.
574 var num = this.attr_("labels").length - 1;
6a1aa64f 575 this.colors_ = [];
285a6bda
DV
576 var colors = this.attr_('colors');
577 if (!colors) {
578 var sat = this.attr_('colorSaturation') || 1.0;
579 var val = this.attr_('colorValue') || 0.5;
2aa21213 580 var half = Math.ceil(num / 2);
6a1aa64f 581 for (var i = 1; i <= num; i++) {
ec1959eb 582 if (!this.visibility()[i-1]) continue;
43af96e7 583 // alternate colors for high contrast.
2aa21213 584 var idx = i % 2 ? Math.ceil(i / 2) : (half + i / 2);
43af96e7
NK
585 var hue = (1.0 * idx/ (1 + num));
586 this.colors_.push(Dygraph.hsvToRGB(hue, sat, val));
6a1aa64f
DV
587 }
588 } else {
589 for (var i = 0; i < num; i++) {
ec1959eb 590 if (!this.visibility()[i]) continue;
285a6bda 591 var colorStr = colors[i % colors.length];
f474c2a3 592 this.colors_.push(colorStr);
6a1aa64f
DV
593 }
594 }
285a6bda 595
c21d2c2d 596 // TODO(danvk): update this w/r/t/ the new options system.
285a6bda 597 this.renderOptions_.colorScheme = this.colors_;
fc80a396
DV
598 Dygraph.update(this.plotter_.options, this.renderOptions_);
599 Dygraph.update(this.layoutOptions_, this.user_attrs_);
600 Dygraph.update(this.layoutOptions_, this.attrs_);
6a1aa64f
DV
601}
602
43af96e7
NK
603/**
604 * Return the list of colors. This is either the list of colors passed in the
605 * attributes, or the autogenerated list of rgb(r,g,b) strings.
606 * @return {Array<string>} The list of colors.
607 */
608Dygraph.prototype.getColors = function() {
609 return this.colors_;
610};
611
5e60386d
DV
612// The following functions are from quirksmode.org with a modification for Safari from
613// http://blog.firetree.net/2005/07/04/javascript-find-position/
3df0ccf0
DV
614// http://www.quirksmode.org/js/findpos.html
615Dygraph.findPosX = function(obj) {
616 var curleft = 0;
5e60386d 617 if(obj.offsetParent)
50360fd0 618 while(1)
5e60386d 619 {
3df0ccf0 620 curleft += obj.offsetLeft;
5e60386d
DV
621 if(!obj.offsetParent)
622 break;
3df0ccf0
DV
623 obj = obj.offsetParent;
624 }
5e60386d 625 else if(obj.x)
3df0ccf0
DV
626 curleft += obj.x;
627 return curleft;
628};
c21d2c2d 629
3df0ccf0
DV
630Dygraph.findPosY = function(obj) {
631 var curtop = 0;
5e60386d
DV
632 if(obj.offsetParent)
633 while(1)
634 {
3df0ccf0 635 curtop += obj.offsetTop;
5e60386d
DV
636 if(!obj.offsetParent)
637 break;
3df0ccf0
DV
638 obj = obj.offsetParent;
639 }
5e60386d 640 else if(obj.y)
3df0ccf0
DV
641 curtop += obj.y;
642 return curtop;
643};
644
5e60386d 645
71a11a8e 646
6a1aa64f
DV
647/**
648 * Create the div that contains information on the selected point(s)
649 * This goes in the top right of the canvas, unless an external div has already
650 * been specified.
651 * @private
652 */
fedbd797 653Dygraph.prototype.createStatusMessage_ = function() {
654 var userLabelsDiv = this.user_attrs_["labelsDiv"];
655 if (userLabelsDiv && null != userLabelsDiv
656 && (typeof(userLabelsDiv) == "string" || userLabelsDiv instanceof String)) {
657 this.user_attrs_["labelsDiv"] = document.getElementById(userLabelsDiv);
658 }
285a6bda
DV
659 if (!this.attr_("labelsDiv")) {
660 var divWidth = this.attr_('labelsDivWidth');
b0c3b730 661 var messagestyle = {
6a1aa64f
DV
662 "position": "absolute",
663 "fontSize": "14px",
664 "zIndex": 10,
665 "width": divWidth + "px",
666 "top": "0px",
8846615a 667 "left": (this.width_ - divWidth - 2) + "px",
6a1aa64f
DV
668 "background": "white",
669 "textAlign": "left",
b0c3b730 670 "overflow": "hidden"};
fc80a396 671 Dygraph.update(messagestyle, this.attr_('labelsDivStyles'));
b0c3b730
DV
672 var div = document.createElement("div");
673 for (var name in messagestyle) {
85b99f0b
DV
674 if (messagestyle.hasOwnProperty(name)) {
675 div.style[name] = messagestyle[name];
676 }
b0c3b730
DV
677 }
678 this.graphDiv.appendChild(div);
285a6bda 679 this.attrs_.labelsDiv = div;
6a1aa64f
DV
680 }
681};
682
683/**
684 * Create the text box to adjust the averaging period
685 * @return {Object} The newly-created text box
686 * @private
687 */
285a6bda 688Dygraph.prototype.createRollInterface_ = function() {
285a6bda 689 var display = this.attr_('showRoller') ? "block" : "none";
b0c3b730
DV
690 var textAttr = { "position": "absolute",
691 "zIndex": 10,
692 "top": (this.plotter_.area.h - 25) + "px",
693 "left": (this.plotter_.area.x + 1) + "px",
694 "display": display
6a1aa64f 695 };
b0c3b730
DV
696 var roller = document.createElement("input");
697 roller.type = "text";
698 roller.size = "2";
699 roller.value = this.rollPeriod_;
700 for (var name in textAttr) {
85b99f0b
DV
701 if (textAttr.hasOwnProperty(name)) {
702 roller.style[name] = textAttr[name];
703 }
b0c3b730
DV
704 }
705
6a1aa64f 706 var pa = this.graphDiv;
b0c3b730 707 pa.appendChild(roller);
76171648
DV
708 var dygraph = this;
709 roller.onchange = function() { dygraph.adjustRoll(roller.value); };
6a1aa64f 710 return roller;
76171648
DV
711};
712
713// These functions are taken from MochiKit.Signal
714Dygraph.pageX = function(e) {
715 if (e.pageX) {
716 return (!e.pageX || e.pageX < 0) ? 0 : e.pageX;
717 } else {
718 var de = document;
719 var b = document.body;
720 return e.clientX +
721 (de.scrollLeft || b.scrollLeft) -
722 (de.clientLeft || 0);
723 }
724};
725
726Dygraph.pageY = function(e) {
727 if (e.pageY) {
728 return (!e.pageY || e.pageY < 0) ? 0 : e.pageY;
729 } else {
730 var de = document;
731 var b = document.body;
732 return e.clientY +
733 (de.scrollTop || b.scrollTop) -
734 (de.clientTop || 0);
735 }
736};
6a1aa64f
DV
737
738/**
739 * Set up all the mouse handlers needed to capture dragging behavior for zoom
27385109 740 * events.
6a1aa64f
DV
741 * @private
742 */
285a6bda 743Dygraph.prototype.createDragInterface_ = function() {
6a1aa64f
DV
744 var self = this;
745
746 // Tracks whether the mouse is down right now
bce01b0f 747 var isZooming = false;
c776c216 748 var isPanning = false;
6a1aa64f
DV
749 var dragStartX = null;
750 var dragStartY = null;
751 var dragEndX = null;
752 var dragEndY = null;
753 var prevEndX = null;
bce01b0f
DV
754 var draggingDate = null;
755 var dateRange = null;
6a1aa64f
DV
756
757 // Utility function to convert page-wide coordinates to canvas coords
67e650dc
DV
758 var px = 0;
759 var py = 0;
76171648 760 var getX = function(e) { return Dygraph.pageX(e) - px };
1e1bf7df 761 var getY = function(e) { return Dygraph.pageY(e) - py };
6a1aa64f
DV
762
763 // Draw zoom rectangles when the mouse is down and the user moves around
eb7bf005 764 Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(event) {
bce01b0f 765 if (isZooming) {
6a1aa64f
DV
766 dragEndX = getX(event);
767 dragEndY = getY(event);
768
769 self.drawZoomRect_(dragStartX, dragEndX, prevEndX);
770 prevEndX = dragEndX;
bce01b0f
DV
771 } else if (isPanning) {
772 dragEndX = getX(event);
773 dragEndY = getY(event);
774
775 // Want to have it so that:
776 // 1. draggingDate appears at dragEndX
777 // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
778
779 self.dateWindow_[0] = draggingDate - (dragEndX / self.width_) * dateRange;
780 self.dateWindow_[1] = self.dateWindow_[0] + dateRange;
781 self.drawGraph_(self.rawData_);
6a1aa64f
DV
782 }
783 });
784
785 // Track the beginning of drag events
eb7bf005 786 Dygraph.addEvent(this.mouseEventElement_, 'mousedown', function(event) {
3df0ccf0
DV
787 px = Dygraph.findPosX(self.canvas_);
788 py = Dygraph.findPosY(self.canvas_);
6a1aa64f
DV
789 dragStartX = getX(event);
790 dragStartY = getY(event);
bce01b0f 791
2dab69c3 792 if (event.altKey || event.shiftKey) {
d12999d3 793 if (!self.dateWindow_) return; // have to be zoomed in to pan.
bce01b0f
DV
794 isPanning = true;
795 dateRange = self.dateWindow_[1] - self.dateWindow_[0];
d12999d3
DV
796 draggingDate = (dragStartX / self.width_) * dateRange +
797 self.dateWindow_[0];
bce01b0f
DV
798 } else {
799 isZooming = true;
800 }
6a1aa64f
DV
801 });
802
803 // If the user releases the mouse button during a drag, but not over the
804 // canvas, then it doesn't count as a zooming action.
76171648 805 Dygraph.addEvent(document, 'mouseup', function(event) {
bce01b0f
DV
806 if (isZooming || isPanning) {
807 isZooming = false;
6a1aa64f
DV
808 dragStartX = null;
809 dragStartY = null;
810 }
bce01b0f
DV
811
812 if (isPanning) {
813 isPanning = false;
814 draggingDate = null;
815 dateRange = null;
816 }
6a1aa64f
DV
817 });
818
819 // Temporarily cancel the dragging event when the mouse leaves the graph
eb7bf005 820 Dygraph.addEvent(this.mouseEventElement_, 'mouseout', function(event) {
bce01b0f 821 if (isZooming) {
6a1aa64f
DV
822 dragEndX = null;
823 dragEndY = null;
824 }
825 });
826
827 // If the mouse is released on the canvas during a drag event, then it's a
828 // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
eb7bf005 829 Dygraph.addEvent(this.mouseEventElement_, 'mouseup', function(event) {
bce01b0f
DV
830 if (isZooming) {
831 isZooming = false;
6a1aa64f
DV
832 dragEndX = getX(event);
833 dragEndY = getY(event);
834 var regionWidth = Math.abs(dragEndX - dragStartX);
835 var regionHeight = Math.abs(dragEndY - dragStartY);
836
e2b5f2bc
DV
837 if (regionWidth < 2 && regionHeight < 2 &&
838 self.lastx_ != undefined && self.lastx_ != -1) {
2ad87eaa 839 // TODO(danvk): pass along more info about the points, e.g. 'x'
1e1bf7df 840 if (self.attr_('clickCallback') != null) {
1e1bf7df
DV
841 self.attr_('clickCallback')(event, self.lastx_, self.selPoints_);
842 }
843 if (self.attr_('pointClickCallback')) {
844 // check if the click was on a particular point.
845 var closestIdx = -1;
846 var closestDistance = 0;
847 for (var i = 0; i < self.selPoints_.length; i++) {
848 var p = self.selPoints_[i];
849 var distance = Math.pow(p.canvasx - dragEndX, 2) +
850 Math.pow(p.canvasy - dragEndY, 2);
851 if (closestIdx == -1 || distance < closestDistance) {
852 closestDistance = distance;
853 closestIdx = i;
854 }
855 }
856
857 // Allow any click within two pixels of the dot.
2ad87eaa 858 var radius = self.attr_('highlightCircleSize') + 2;
1e1bf7df
DV
859 if (closestDistance <= 5 * 5) {
860 self.attr_('pointClickCallback')(event, self.selPoints_[closestIdx]);
861 }
862 }
6a1aa64f
DV
863 }
864
865 if (regionWidth >= 10) {
866 self.doZoom_(Math.min(dragStartX, dragEndX),
867 Math.max(dragStartX, dragEndX));
868 } else {
869 self.canvas_.getContext("2d").clearRect(0, 0,
870 self.canvas_.width,
871 self.canvas_.height);
872 }
873
874 dragStartX = null;
875 dragStartY = null;
876 }
bce01b0f
DV
877
878 if (isPanning) {
879 isPanning = false;
880 draggingDate = null;
881 dateRange = null;
882 }
6a1aa64f
DV
883 });
884
885 // Double-clicking zooms back out
eb7bf005 886 Dygraph.addEvent(this.mouseEventElement_, 'dblclick', function(event) {
b258a3da 887 if (self.dateWindow_ == null) return;
6a1aa64f
DV
888 self.dateWindow_ = null;
889 self.drawGraph_(self.rawData_);
890 var minDate = self.rawData_[0][0];
891 var maxDate = self.rawData_[self.rawData_.length - 1][0];
285a6bda
DV
892 if (self.attr_("zoomCallback")) {
893 self.attr_("zoomCallback")(minDate, maxDate);
67e650dc 894 }
6a1aa64f
DV
895 });
896};
897
898/**
899 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
900 * up any previous zoom rectangles that were drawn. This could be optimized to
901 * avoid extra redrawing, but it's tricky to avoid interactions with the status
902 * dots.
903 * @param {Number} startX The X position where the drag started, in canvas
904 * coordinates.
905 * @param {Number} endX The current X position of the drag, in canvas coords.
906 * @param {Number} prevEndX The value of endX on the previous call to this
907 * function. Used to avoid excess redrawing
908 * @private
909 */
285a6bda 910Dygraph.prototype.drawZoomRect_ = function(startX, endX, prevEndX) {
6a1aa64f
DV
911 var ctx = this.canvas_.getContext("2d");
912
913 // Clean up from the previous rect if necessary
914 if (prevEndX) {
915 ctx.clearRect(Math.min(startX, prevEndX), 0,
916 Math.abs(startX - prevEndX), this.height_);
917 }
918
919 // Draw a light-grey rectangle to show the new viewing area
920 if (endX && startX) {
921 ctx.fillStyle = "rgba(128,128,128,0.33)";
922 ctx.fillRect(Math.min(startX, endX), 0,
923 Math.abs(endX - startX), this.height_);
924 }
925};
926
927/**
928 * Zoom to something containing [lowX, highX]. These are pixel coordinates
929 * in the canvas. The exact zoom window may be slightly larger if there are no
930 * data points near lowX or highX. This function redraws the graph.
931 * @param {Number} lowX The leftmost pixel value that should be visible.
932 * @param {Number} highX The rightmost pixel value that should be visible.
933 * @private
934 */
285a6bda 935Dygraph.prototype.doZoom_ = function(lowX, highX) {
6a1aa64f 936 // Find the earliest and latest dates contained in this canvasx range.
56623f3b
DV
937 var r = this.toDataCoords(lowX, null);
938 var minDate = r[0];
939 r = this.toDataCoords(highX, null);
940 var maxDate = r[0];
6a1aa64f
DV
941
942 this.dateWindow_ = [minDate, maxDate];
943 this.drawGraph_(this.rawData_);
285a6bda
DV
944 if (this.attr_("zoomCallback")) {
945 this.attr_("zoomCallback")(minDate, maxDate);
67e650dc 946 }
6a1aa64f
DV
947};
948
949/**
950 * When the mouse moves in the canvas, display information about a nearby data
951 * point and draw dots over those points in the data series. This function
952 * takes care of cleanup of previously-drawn dots.
953 * @param {Object} event The mousemove event from the browser.
954 * @private
955 */
285a6bda 956Dygraph.prototype.mouseMove_ = function(event) {
eb7bf005 957 var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_);
6a1aa64f
DV
958 var points = this.layout_.points;
959
960 var lastx = -1;
961 var lasty = -1;
962
963 // Loop through all the points and find the date nearest to our current
964 // location.
965 var minDist = 1e+100;
966 var idx = -1;
967 for (var i = 0; i < points.length; i++) {
968 var dist = Math.abs(points[i].canvasx - canvasx);
f032c51d 969 if (dist > minDist) continue;
6a1aa64f
DV
970 minDist = dist;
971 idx = i;
972 }
973 if (idx >= 0) lastx = points[idx].xval;
974 // Check that you can really highlight the last day's data
975 if (canvasx > points[points.length-1].canvasx)
976 lastx = points[points.length-1].xval;
977
978 // Extract the points we've selected
b258a3da 979 this.selPoints_ = [];
50360fd0 980 var l = points.length;
416b05ad
NK
981 if (!this.attr_("stackedGraph")) {
982 for (var i = 0; i < l; i++) {
983 if (points[i].xval == lastx) {
984 this.selPoints_.push(points[i]);
985 }
986 }
987 } else {
354e15ab
DE
988 // Need to 'unstack' points starting from the bottom
989 var cumulative_sum = 0;
416b05ad
NK
990 for (var i = l - 1; i >= 0; i--) {
991 if (points[i].xval == lastx) {
354e15ab 992 var p = {}; // Clone the point since we modify it
d4139cd8
NK
993 for (var k in points[i]) {
994 p[k] = points[i][k];
50360fd0
NK
995 }
996 p.yval -= cumulative_sum;
997 cumulative_sum += p.yval;
d4139cd8 998 this.selPoints_.push(p);
12e4c741 999 }
6a1aa64f 1000 }
354e15ab 1001 this.selPoints_.reverse();
6a1aa64f
DV
1002 }
1003
b258a3da 1004 if (this.attr_("highlightCallback")) {
a4c6a67c 1005 var px = this.lastx_;
dd082dda 1006 if (px !== null && lastx != px) {
344ba8c0 1007 // only fire if the selected point has changed.
50360fd0 1008 this.attr_("highlightCallback")(event, lastx, this.selPoints_);
43af96e7 1009 }
12e4c741 1010 }
43af96e7 1011
239c712d
NAG
1012 // Save last x position for callbacks.
1013 this.lastx_ = lastx;
50360fd0 1014
239c712d
NAG
1015 this.updateSelection_();
1016};
b258a3da 1017
239c712d
NAG
1018/**
1019 * Draw dots over the selectied points in the data series. This function
1020 * takes care of cleanup of previously-drawn dots.
1021 * @private
1022 */
1023Dygraph.prototype.updateSelection_ = function() {
6a1aa64f 1024 // Clear the previously drawn vertical, if there is one
6a1aa64f
DV
1025 var ctx = this.canvas_.getContext("2d");
1026 if (this.previousVerticalX_ >= 0) {
46dde5f9
DV
1027 // Determine the maximum highlight circle size.
1028 var maxCircleSize = 0;
227b93cc
DV
1029 var labels = this.attr_('labels');
1030 for (var i = 1; i < labels.length; i++) {
1031 var r = this.attr_('highlightCircleSize', labels[i]);
46dde5f9
DV
1032 if (r > maxCircleSize) maxCircleSize = r;
1033 }
6a1aa64f 1034 var px = this.previousVerticalX_;
46dde5f9
DV
1035 ctx.clearRect(px - maxCircleSize - 1, 0,
1036 2 * maxCircleSize + 2, this.height_);
6a1aa64f
DV
1037 }
1038
584ceeaa
DV
1039 var isOK = function(x) { return x && !isNaN(x); };
1040
d160cc3b 1041 if (this.selPoints_.length > 0) {
b258a3da 1042 var canvasx = this.selPoints_[0].canvasx;
6a1aa64f
DV
1043
1044 // Set the status message to indicate the selected point(s)
239c712d 1045 var replace = this.attr_('xValueFormatter')(this.lastx_, this) + ":";
50360fd0 1046 var fmtFunc = this.attr_('yValueFormatter');
6a1aa64f 1047 var clen = this.colors_.length;
d160cc3b
NK
1048
1049 if (this.attr_('showLabelsOnHighlight')) {
1050 // Set the status message to indicate the selected point(s)
d160cc3b 1051 for (var i = 0; i < this.selPoints_.length; i++) {
129569a5 1052 if (!this.attr_("labelsShowZeroValues") && this.selPoints_[i].yval == 0) continue;
d160cc3b
NK
1053 if (!isOK(this.selPoints_[i].canvasy)) continue;
1054 if (this.attr_("labelsSeparateLines")) {
1055 replace += "<br/>";
1056 }
1057 var point = this.selPoints_[i];
1058 var c = new RGBColor(this.colors_[i%clen]);
029da4b6 1059 var yval = fmtFunc(point.yval);
d160cc3b
NK
1060 replace += " <b><font color='" + c.toHex() + "'>"
1061 + point.name + "</font></b>:"
1062 + yval;
6a1aa64f 1063 }
50360fd0 1064
d160cc3b 1065 this.attr_("labelsDiv").innerHTML = replace;
6a1aa64f 1066 }
6a1aa64f 1067
6a1aa64f 1068 // Draw colored circles over the center of each selected point
43af96e7 1069 ctx.save();
b258a3da 1070 for (var i = 0; i < this.selPoints_.length; i++) {
f032c51d 1071 if (!isOK(this.selPoints_[i].canvasy)) continue;
227b93cc
DV
1072 var circleSize =
1073 this.attr_('highlightCircleSize', this.selPoints_[i].name);
6a1aa64f 1074 ctx.beginPath();
563c70ca 1075 ctx.fillStyle = this.plotter_.colors[this.selPoints_[i].name];
f032c51d 1076 ctx.arc(canvasx, this.selPoints_[i].canvasy, circleSize,
7bf6a9fe 1077 0, 2 * Math.PI, false);
6a1aa64f
DV
1078 ctx.fill();
1079 }
1080 ctx.restore();
1081
1082 this.previousVerticalX_ = canvasx;
1083 }
1084};
1085
1086/**
239c712d
NAG
1087 * Set manually set selected dots, and display information about them
1088 * @param int row number that should by highlighted
1089 * false value clears the selection
1090 * @public
1091 */
1092Dygraph.prototype.setSelection = function(row) {
1093 // Extract the points we've selected
1094 this.selPoints_ = [];
1095 var pos = 0;
50360fd0 1096
239c712d 1097 if (row !== false) {
16269f6e
NAG
1098 row = row-this.boundaryIds_[0][0];
1099 }
50360fd0 1100
16269f6e 1101 if (row !== false && row >= 0) {
239c712d 1102 for (var i in this.layout_.datasets) {
16269f6e
NAG
1103 if (row < this.layout_.datasets[i].length) {
1104 this.selPoints_.push(this.layout_.points[pos+row]);
1105 }
239c712d
NAG
1106 pos += this.layout_.datasets[i].length;
1107 }
16269f6e 1108 }
50360fd0 1109
16269f6e 1110 if (this.selPoints_.length) {
239c712d
NAG
1111 this.lastx_ = this.selPoints_[0].xval;
1112 this.updateSelection_();
1113 } else {
1114 this.lastx_ = -1;
1115 this.clearSelection();
1116 }
1117
1118};
1119
1120/**
6a1aa64f
DV
1121 * The mouse has left the canvas. Clear out whatever artifacts remain
1122 * @param {Object} event the mouseout event from the browser.
1123 * @private
1124 */
285a6bda 1125Dygraph.prototype.mouseOut_ = function(event) {
a4c6a67c
AV
1126 if (this.attr_("unhighlightCallback")) {
1127 this.attr_("unhighlightCallback")(event);
1128 }
1129
43af96e7 1130 if (this.attr_("hideOverlayOnMouseOut")) {
239c712d 1131 this.clearSelection();
43af96e7 1132 }
6a1aa64f
DV
1133};
1134
239c712d
NAG
1135/**
1136 * Remove all selection from the canvas
1137 * @public
1138 */
1139Dygraph.prototype.clearSelection = function() {
1140 // Get rid of the overlay data
1141 var ctx = this.canvas_.getContext("2d");
1142 ctx.clearRect(0, 0, this.width_, this.height_);
1143 this.attr_("labelsDiv").innerHTML = "";
1144 this.selPoints_ = [];
1145 this.lastx_ = -1;
1146}
1147
103b7292
NAG
1148/**
1149 * Returns the number of the currently selected row
1150 * @return int row number, of -1 if nothing is selected
1151 * @public
1152 */
1153Dygraph.prototype.getSelection = function() {
1154 if (!this.selPoints_ || this.selPoints_.length < 1) {
1155 return -1;
1156 }
50360fd0 1157
103b7292
NAG
1158 for (var row=0; row<this.layout_.points.length; row++ ) {
1159 if (this.layout_.points[row].x == this.selPoints_[0].x) {
16269f6e 1160 return row + this.boundaryIds_[0][0];
103b7292
NAG
1161 }
1162 }
1163 return -1;
1164}
1165
285a6bda 1166Dygraph.zeropad = function(x) {
32988383
DV
1167 if (x < 10) return "0" + x; else return "" + x;
1168}
1169
6a1aa64f 1170/**
6b8e33dd
DV
1171 * Return a string version of the hours, minutes and seconds portion of a date.
1172 * @param {Number} date The JavaScript date (ms since epoch)
1173 * @return {String} A time of the form "HH:MM:SS"
1174 * @private
1175 */
bf640e56 1176Dygraph.hmsString_ = function(date) {
285a6bda 1177 var zeropad = Dygraph.zeropad;
6b8e33dd
DV
1178 var d = new Date(date);
1179 if (d.getSeconds()) {
1180 return zeropad(d.getHours()) + ":" +
1181 zeropad(d.getMinutes()) + ":" +
1182 zeropad(d.getSeconds());
6b8e33dd 1183 } else {
054531ca 1184 return zeropad(d.getHours()) + ":" + zeropad(d.getMinutes());
6b8e33dd
DV
1185 }
1186}
1187
1188/**
bf640e56
AV
1189 * Convert a JS date to a string appropriate to display on an axis that
1190 * is displaying values at the stated granularity.
1191 * @param {Date} date The date to format
1192 * @param {Number} granularity One of the Dygraph granularity constants
1193 * @return {String} The formatted date
1194 * @private
1195 */
1196Dygraph.dateAxisFormatter = function(date, granularity) {
1197 if (granularity >= Dygraph.MONTHLY) {
1198 return date.strftime('%b %y');
1199 } else {
31eddad3 1200 var frac = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds();
bf640e56
AV
1201 if (frac == 0 || granularity >= Dygraph.DAILY) {
1202 return new Date(date.getTime() + 3600*1000).strftime('%d%b');
1203 } else {
1204 return Dygraph.hmsString_(date.getTime());
1205 }
1206 }
1207}
1208
1209/**
6a1aa64f
DV
1210 * Convert a JS date (millis since epoch) to YYYY/MM/DD
1211 * @param {Number} date The JavaScript date (ms since epoch)
1212 * @return {String} A date of the form "YYYY/MM/DD"
1213 * @private
1214 */
285a6bda
DV
1215Dygraph.dateString_ = function(date, self) {
1216 var zeropad = Dygraph.zeropad;
6a1aa64f
DV
1217 var d = new Date(date);
1218
1219 // Get the year:
1220 var year = "" + d.getFullYear();
1221 // Get a 0 padded month string
6b8e33dd 1222 var month = zeropad(d.getMonth() + 1); //months are 0-offset, sigh
6a1aa64f 1223 // Get a 0 padded day string
6b8e33dd 1224 var day = zeropad(d.getDate());
6a1aa64f 1225
6b8e33dd
DV
1226 var ret = "";
1227 var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
bf640e56 1228 if (frac) ret = " " + Dygraph.hmsString_(date);
6b8e33dd
DV
1229
1230 return year + "/" + month + "/" + day + ret;
6a1aa64f
DV
1231};
1232
1233/**
1234 * Round a number to the specified number of digits past the decimal point.
1235 * @param {Number} num The number to round
1236 * @param {Number} places The number of decimals to which to round
1237 * @return {Number} The rounded number
1238 * @private
1239 */
029da4b6 1240Dygraph.round_ = function(num, places) {
6a1aa64f
DV
1241 var shift = Math.pow(10, places);
1242 return Math.round(num * shift)/shift;
1243};
1244
1245/**
1246 * Fires when there's data available to be graphed.
1247 * @param {String} data Raw CSV data to be plotted
1248 * @private
1249 */
285a6bda 1250Dygraph.prototype.loadedEvent_ = function(data) {
6a1aa64f
DV
1251 this.rawData_ = this.parseCSV_(data);
1252 this.drawGraph_(this.rawData_);
1253};
1254
285a6bda 1255Dygraph.prototype.months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
8846615a 1256 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
285a6bda 1257Dygraph.prototype.quarters = ["Jan", "Apr", "Jul", "Oct"];
6a1aa64f
DV
1258
1259/**
1260 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1261 * @private
1262 */
285a6bda 1263Dygraph.prototype.addXTicks_ = function() {
6a1aa64f
DV
1264 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1265 var startDate, endDate;
1266 if (this.dateWindow_) {
1267 startDate = this.dateWindow_[0];
1268 endDate = this.dateWindow_[1];
1269 } else {
1270 startDate = this.rawData_[0][0];
1271 endDate = this.rawData_[this.rawData_.length - 1][0];
1272 }
1273
285a6bda 1274 var xTicks = this.attr_('xTicker')(startDate, endDate, this);
6a1aa64f 1275 this.layout_.updateOptions({xTicks: xTicks});
32988383
DV
1276};
1277
1278// Time granularity enumeration
285a6bda 1279Dygraph.SECONDLY = 0;
20a41c17
DV
1280Dygraph.TWO_SECONDLY = 1;
1281Dygraph.FIVE_SECONDLY = 2;
1282Dygraph.TEN_SECONDLY = 3;
1283Dygraph.THIRTY_SECONDLY = 4;
1284Dygraph.MINUTELY = 5;
1285Dygraph.TWO_MINUTELY = 6;
1286Dygraph.FIVE_MINUTELY = 7;
1287Dygraph.TEN_MINUTELY = 8;
1288Dygraph.THIRTY_MINUTELY = 9;
1289Dygraph.HOURLY = 10;
1290Dygraph.TWO_HOURLY = 11;
1291Dygraph.SIX_HOURLY = 12;
1292Dygraph.DAILY = 13;
1293Dygraph.WEEKLY = 14;
1294Dygraph.MONTHLY = 15;
1295Dygraph.QUARTERLY = 16;
1296Dygraph.BIANNUAL = 17;
1297Dygraph.ANNUAL = 18;
1298Dygraph.DECADAL = 19;
1299Dygraph.NUM_GRANULARITIES = 20;
285a6bda
DV
1300
1301Dygraph.SHORT_SPACINGS = [];
1302Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY] = 1000 * 1;
20a41c17
DV
1303Dygraph.SHORT_SPACINGS[Dygraph.TWO_SECONDLY] = 1000 * 2;
1304Dygraph.SHORT_SPACINGS[Dygraph.FIVE_SECONDLY] = 1000 * 5;
285a6bda
DV
1305Dygraph.SHORT_SPACINGS[Dygraph.TEN_SECONDLY] = 1000 * 10;
1306Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_SECONDLY] = 1000 * 30;
1307Dygraph.SHORT_SPACINGS[Dygraph.MINUTELY] = 1000 * 60;
20a41c17
DV
1308Dygraph.SHORT_SPACINGS[Dygraph.TWO_MINUTELY] = 1000 * 60 * 2;
1309Dygraph.SHORT_SPACINGS[Dygraph.FIVE_MINUTELY] = 1000 * 60 * 5;
285a6bda
DV
1310Dygraph.SHORT_SPACINGS[Dygraph.TEN_MINUTELY] = 1000 * 60 * 10;
1311Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_MINUTELY] = 1000 * 60 * 30;
1312Dygraph.SHORT_SPACINGS[Dygraph.HOURLY] = 1000 * 3600;
20a41c17 1313Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY] = 1000 * 3600 * 2;
805d5519 1314Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY] = 1000 * 3600 * 6;
285a6bda
DV
1315Dygraph.SHORT_SPACINGS[Dygraph.DAILY] = 1000 * 86400;
1316Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY] = 1000 * 604800;
32988383
DV
1317
1318// NumXTicks()
1319//
1320// If we used this time granularity, how many ticks would there be?
1321// This is only an approximation, but it's generally good enough.
1322//
285a6bda
DV
1323Dygraph.prototype.NumXTicks = function(start_time, end_time, granularity) {
1324 if (granularity < Dygraph.MONTHLY) {
32988383 1325 // Generate one tick mark for every fixed interval of time.
285a6bda 1326 var spacing = Dygraph.SHORT_SPACINGS[granularity];
32988383
DV
1327 return Math.floor(0.5 + 1.0 * (end_time - start_time) / spacing);
1328 } else {
1329 var year_mod = 1; // e.g. to only print one point every 10 years.
1330 var num_months = 12;
285a6bda
DV
1331 if (granularity == Dygraph.QUARTERLY) num_months = 3;
1332 if (granularity == Dygraph.BIANNUAL) num_months = 2;
1333 if (granularity == Dygraph.ANNUAL) num_months = 1;
1334 if (granularity == Dygraph.DECADAL) { num_months = 1; year_mod = 10; }
32988383
DV
1335
1336 var msInYear = 365.2524 * 24 * 3600 * 1000;
1337 var num_years = 1.0 * (end_time - start_time) / msInYear;
1338 return Math.floor(0.5 + 1.0 * num_years * num_months / year_mod);
1339 }
1340};
1341
1342// GetXAxis()
1343//
1344// Construct an x-axis of nicely-formatted times on meaningful boundaries
1345// (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1346//
1347// Returns an array containing {v: millis, label: label} dictionaries.
1348//
285a6bda 1349Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) {
bf640e56 1350 var formatter = this.attr_("xAxisLabelFormatter");
32988383 1351 var ticks = [];
285a6bda 1352 if (granularity < Dygraph.MONTHLY) {
32988383 1353 // Generate one tick mark for every fixed interval of time.
285a6bda 1354 var spacing = Dygraph.SHORT_SPACINGS[granularity];
3d29302c 1355 var format = '%d%b'; // e.g. "1Jan"
076c9622
DV
1356
1357 // Find a time less than start_time which occurs on a "nice" time boundary
1358 // for this granularity.
1359 var g = spacing / 1000;
076c9622
DV
1360 var d = new Date(start_time);
1361 if (g <= 60) { // seconds
1362 var x = d.getSeconds(); d.setSeconds(x - x % g);
1363 } else {
1364 d.setSeconds(0);
1365 g /= 60;
1366 if (g <= 60) { // minutes
1367 var x = d.getMinutes(); d.setMinutes(x - x % g);
1368 } else {
1369 d.setMinutes(0);
1370 g /= 60;
1371
1372 if (g <= 24) { // days
1373 var x = d.getHours(); d.setHours(x - x % g);
1374 } else {
1375 d.setHours(0);
1376 g /= 24;
1377
1378 if (g == 7) { // one week
20a41c17 1379 d.setDate(d.getDate() - d.getDay());
076c9622
DV
1380 }
1381 }
1382 }
328bb812 1383 }
076c9622
DV
1384 start_time = d.getTime();
1385
32988383 1386 for (var t = start_time; t <= end_time; t += spacing) {
bf640e56 1387 ticks.push({ v:t, label: formatter(new Date(t), granularity) });
32988383
DV
1388 }
1389 } else {
1390 // Display a tick mark on the first of a set of months of each year.
1391 // Years get a tick mark iff y % year_mod == 0. This is useful for
1392 // displaying a tick mark once every 10 years, say, on long time scales.
1393 var months;
1394 var year_mod = 1; // e.g. to only print one point every 10 years.
1395
285a6bda 1396 if (granularity == Dygraph.MONTHLY) {
32988383 1397 months = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
285a6bda 1398 } else if (granularity == Dygraph.QUARTERLY) {
32988383 1399 months = [ 0, 3, 6, 9 ];
285a6bda 1400 } else if (granularity == Dygraph.BIANNUAL) {
32988383 1401 months = [ 0, 6 ];
285a6bda 1402 } else if (granularity == Dygraph.ANNUAL) {
32988383 1403 months = [ 0 ];
285a6bda 1404 } else if (granularity == Dygraph.DECADAL) {
32988383
DV
1405 months = [ 0 ];
1406 year_mod = 10;
1407 }
1408
1409 var start_year = new Date(start_time).getFullYear();
1410 var end_year = new Date(end_time).getFullYear();
285a6bda 1411 var zeropad = Dygraph.zeropad;
32988383
DV
1412 for (var i = start_year; i <= end_year; i++) {
1413 if (i % year_mod != 0) continue;
1414 for (var j = 0; j < months.length; j++) {
1415 var date_str = i + "/" + zeropad(1 + months[j]) + "/01";
1416 var t = Date.parse(date_str);
1417 if (t < start_time || t > end_time) continue;
bf640e56 1418 ticks.push({ v:t, label: formatter(new Date(t), granularity) });
32988383
DV
1419 }
1420 }
1421 }
1422
1423 return ticks;
1424};
1425
6a1aa64f
DV
1426
1427/**
1428 * Add ticks to the x-axis based on a date range.
1429 * @param {Number} startDate Start of the date window (millis since epoch)
1430 * @param {Number} endDate End of the date window (millis since epoch)
1431 * @return {Array.<Object>} Array of {label, value} tuples.
1432 * @public
1433 */
285a6bda 1434Dygraph.dateTicker = function(startDate, endDate, self) {
32988383 1435 var chosen = -1;
285a6bda
DV
1436 for (var i = 0; i < Dygraph.NUM_GRANULARITIES; i++) {
1437 var num_ticks = self.NumXTicks(startDate, endDate, i);
1438 if (self.width_ / num_ticks >= self.attr_('pixelsPerXLabel')) {
32988383
DV
1439 chosen = i;
1440 break;
2769de62 1441 }
6a1aa64f
DV
1442 }
1443
32988383 1444 if (chosen >= 0) {
285a6bda 1445 return self.GetXAxis(startDate, endDate, chosen);
6a1aa64f 1446 } else {
32988383 1447 // TODO(danvk): signal error.
6a1aa64f 1448 }
6a1aa64f
DV
1449};
1450
1451/**
1452 * Add ticks when the x axis has numbers on it (instead of dates)
1453 * @param {Number} startDate Start of the date window (millis since epoch)
1454 * @param {Number} endDate End of the date window (millis since epoch)
1455 * @return {Array.<Object>} Array of {label, value} tuples.
1456 * @public
1457 */
285a6bda 1458Dygraph.numericTicks = function(minV, maxV, self) {
c6336f04
DV
1459 // Basic idea:
1460 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1461 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks).
285a6bda 1462 // The first spacing greater than pixelsPerYLabel is what we use.
ff00d3e2 1463 // TODO(danvk): version that works on a log scale.
f09e46d4
DV
1464 if (self.attr_("labelsKMG2")) {
1465 var mults = [1, 2, 4, 8];
1466 } else {
1467 var mults = [1, 2, 5];
1468 }
c6336f04 1469 var scale, low_val, high_val, nTicks;
285a6bda
DV
1470 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1471 var pixelsPerTick = self.attr_('pixelsPerYLabel');
c6336f04 1472 for (var i = -10; i < 50; i++) {
f09e46d4
DV
1473 if (self.attr_("labelsKMG2")) {
1474 var base_scale = Math.pow(16, i);
1475 } else {
1476 var base_scale = Math.pow(10, i);
1477 }
c6336f04
DV
1478 for (var j = 0; j < mults.length; j++) {
1479 scale = base_scale * mults[j];
c6336f04
DV
1480 low_val = Math.floor(minV / scale) * scale;
1481 high_val = Math.ceil(maxV / scale) * scale;
48a0ac91 1482 nTicks = Math.abs(high_val - low_val) / scale;
285a6bda 1483 var spacing = self.height_ / nTicks;
c6336f04 1484 // wish I could break out of both loops at once...
285a6bda 1485 if (spacing > pixelsPerTick) break;
c6336f04 1486 }
285a6bda 1487 if (spacing > pixelsPerTick) break;
6a1aa64f
DV
1488 }
1489
1490 // Construct labels for the ticks
1491 var ticks = [];
ed11be50
DV
1492 var k;
1493 var k_labels = [];
1494 if (self.attr_("labelsKMB")) {
1495 k = 1000;
1496 k_labels = [ "K", "M", "B", "T" ];
1497 }
1498 if (self.attr_("labelsKMG2")) {
1499 if (k) self.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
1500 k = 1024;
1501 k_labels = [ "k", "M", "G", "T" ];
1502 }
1503
e21e69e2 1504 // Allow reverse y-axis if it's explicitly requested.
be7cdb11 1505 if (low_val > high_val) scale *= -1;
48a0ac91 1506
c6336f04
DV
1507 for (var i = 0; i < nTicks; i++) {
1508 var tickV = low_val + i * scale;
0af6e346 1509 var absTickV = Math.abs(tickV);
029da4b6 1510 var label = Dygraph.round_(tickV, 2);
ed11be50
DV
1511 if (k_labels.length) {
1512 // Round up to an appropriate unit.
1513 var n = k*k*k*k;
1514 for (var j = 3; j >= 0; j--, n /= k) {
1515 if (absTickV >= n) {
029da4b6 1516 label = Dygraph.round_(tickV / n, 1) + k_labels[j];
ed11be50
DV
1517 break;
1518 }
afefbcdb 1519 }
6a1aa64f
DV
1520 }
1521 ticks.push( {label: label, v: tickV} );
1522 }
1523 return ticks;
1524};
1525
1526/**
1527 * Adds appropriate ticks on the y-axis
1528 * @param {Number} minY The minimum Y value in the data set
1529 * @param {Number} maxY The maximum Y value in the data set
1530 * @private
1531 */
285a6bda 1532Dygraph.prototype.addYTicks_ = function(minY, maxY) {
6a1aa64f 1533 // Set the number of ticks so that the labels are human-friendly.
285a6bda
DV
1534 // TODO(danvk): make this an attribute as well.
1535 var ticks = Dygraph.numericTicks(minY, maxY, this);
6a1aa64f
DV
1536 this.layout_.updateOptions( { yAxis: [minY, maxY],
1537 yTicks: ticks } );
1538};
1539
5011e7a1
DV
1540// Computes the range of the data series (including confidence intervals).
1541// series is either [ [x1, y1], [x2, y2], ... ] or
1542// [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1543// Returns [low, high]
1544Dygraph.prototype.extremeValues_ = function(series) {
1545 var minY = null, maxY = null;
1546
9922b78b 1547 var bars = this.attr_("errorBars") || this.attr_("customBars");
5011e7a1
DV
1548 if (bars) {
1549 // With custom bars, maxY is the max of the high values.
1550 for (var j = 0; j < series.length; j++) {
1551 var y = series[j][1][0];
1552 if (!y) continue;
1553 var low = y - series[j][1][1];
1554 var high = y + series[j][1][2];
1555 if (low > y) low = y; // this can happen with custom bars,
1556 if (high < y) high = y; // e.g. in tests/custom-bars.html
1557 if (maxY == null || high > maxY) {
1558 maxY = high;
1559 }
1560 if (minY == null || low < minY) {
1561 minY = low;
1562 }
1563 }
1564 } else {
1565 for (var j = 0; j < series.length; j++) {
1566 var y = series[j][1];
d12999d3 1567 if (y === null || isNaN(y)) continue;
5011e7a1
DV
1568 if (maxY == null || y > maxY) {
1569 maxY = y;
1570 }
1571 if (minY == null || y < minY) {
1572 minY = y;
1573 }
1574 }
1575 }
1576
1577 return [minY, maxY];
1578};
1579
6a1aa64f
DV
1580/**
1581 * Update the graph with new data. Data is in the format
1582 * [ [date1, val1, val2, ...], [date2, val1, val2, ...] if errorBars=false
1583 * or, if errorBars=true,
1584 * [ [date1, [val1,stddev1], [val2,stddev2], ...], [date2, ...], ...]
1585 * @param {Array.<Object>} data The data (see above)
1586 * @private
1587 */
285a6bda 1588Dygraph.prototype.drawGraph_ = function(data) {
fe0b7c03
DV
1589 // This is used to set the second parameter to drawCallback, below.
1590 var is_initial_draw = this.is_initial_draw_;
1591 this.is_initial_draw_ = false;
1592
3bd9c228 1593 var minY = null, maxY = null;
6a1aa64f 1594 this.layout_.removeAllDatasets();
285a6bda 1595 this.setColors_();
9317362d 1596 this.attrs_['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
285a6bda 1597
354e15ab
DE
1598 // Loop over the fields (series). Go from the last to the first,
1599 // because if they're stacked that's how we accumulate the values.
43af96e7 1600
354e15ab
DE
1601 var cumulative_y = []; // For stacked series.
1602 var datasets = [];
1603
1604 // Loop over all fields and create datasets
1605 for (var i = data[0].length - 1; i >= 1; i--) {
1cf11047
DV
1606 if (!this.visibility()[i - 1]) continue;
1607
450fe64b
DV
1608 var connectSeparatedPoints = this.attr_('connectSeparatedPoints', i);
1609
6a1aa64f
DV
1610 var series = [];
1611 for (var j = 0; j < data.length; j++) {
4a634fc7 1612 if (data[j][i] != null || !connectSeparatedPoints) {
f032c51d 1613 var date = data[j][0];
563c70ca 1614 series.push([date, data[j][i]]);
f032c51d 1615 }
6a1aa64f
DV
1616 }
1617 series = this.rollingAverage(series, this.rollPeriod_);
1618
1619 // Prune down to the desired range, if necessary (for zooming)
1a26f3fb
DV
1620 // Because there can be lines going to points outside of the visible area,
1621 // we actually prune to visible points, plus one on either side.
9922b78b 1622 var bars = this.attr_("errorBars") || this.attr_("customBars");
6a1aa64f
DV
1623 if (this.dateWindow_) {
1624 var low = this.dateWindow_[0];
1625 var high= this.dateWindow_[1];
1626 var pruned = [];
1a26f3fb
DV
1627 // TODO(danvk): do binary search instead of linear search.
1628 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
1629 var firstIdx = null, lastIdx = null;
6a1aa64f 1630 for (var k = 0; k < series.length; k++) {
1a26f3fb
DV
1631 if (series[k][0] >= low && firstIdx === null) {
1632 firstIdx = k;
1633 }
1634 if (series[k][0] <= high) {
1635 lastIdx = k;
6a1aa64f
DV
1636 }
1637 }
1a26f3fb
DV
1638 if (firstIdx === null) firstIdx = 0;
1639 if (firstIdx > 0) firstIdx--;
1640 if (lastIdx === null) lastIdx = series.length - 1;
1641 if (lastIdx < series.length - 1) lastIdx++;
16269f6e 1642 this.boundaryIds_[i-1] = [firstIdx, lastIdx];
1a26f3fb
DV
1643 for (var k = firstIdx; k <= lastIdx; k++) {
1644 pruned.push(series[k]);
6a1aa64f
DV
1645 }
1646 series = pruned;
16269f6e
NAG
1647 } else {
1648 this.boundaryIds_[i-1] = [0, series.length-1];
6a1aa64f
DV
1649 }
1650
648acd28
DV
1651 var extremes = this.extremeValues_(series);
1652 var thisMinY = extremes[0];
1653 var thisMaxY = extremes[1];
61b78cd6
DV
1654 if (minY === null || thisMinY < minY) minY = thisMinY;
1655 if (maxY === null || thisMaxY > maxY) maxY = thisMaxY;
5011e7a1 1656
6a1aa64f 1657 if (bars) {
354e15ab
DE
1658 for (var j=0; j<series.length; j++) {
1659 val = [series[j][0], series[j][1][0], series[j][1][1], series[j][1][2]];
1660 series[j] = val;
1661 }
43af96e7 1662 } else if (this.attr_("stackedGraph")) {
43af96e7
NK
1663 var l = series.length;
1664 var actual_y;
1665 for (var j = 0; j < l; j++) {
354e15ab
DE
1666 // If one data set has a NaN, let all subsequent stacked
1667 // sets inherit the NaN -- only start at 0 for the first set.
1668 var x = series[j][0];
1669 if (cumulative_y[x] === undefined)
1670 cumulative_y[x] = 0;
43af96e7
NK
1671
1672 actual_y = series[j][1];
354e15ab 1673 cumulative_y[x] += actual_y;
43af96e7 1674
354e15ab 1675 series[j] = [x, cumulative_y[x]]
43af96e7 1676
354e15ab
DE
1677 if (!maxY || cumulative_y[x] > maxY)
1678 maxY = cumulative_y[x];
43af96e7 1679 }
6a1aa64f 1680 }
354e15ab
DE
1681
1682 datasets[i] = series;
6a1aa64f
DV
1683 }
1684
354e15ab 1685 for (var i = 1; i < datasets.length; i++) {
4523c1f6 1686 if (!this.visibility()[i - 1]) continue;
354e15ab 1687 this.layout_.addDataset(this.attr_("labels")[i], datasets[i]);
43af96e7
NK
1688 }
1689
6a1aa64f
DV
1690 // Use some heuristics to come up with a good maxY value, unless it's been
1691 // set explicitly by the user.
1692 if (this.valueRange_ != null) {
1693 this.addYTicks_(this.valueRange_[0], this.valueRange_[1]);
3230c662 1694 this.displayedYRange_ = this.valueRange_;
6a1aa64f 1695 } else {
d053ab5a
DV
1696 // This affects the calculation of span, below.
1697 if (this.attr_("includeZero") && minY > 0) {
1698 minY = 0;
1699 }
1700
6a1aa64f 1701 // Add some padding and round up to an integer to be human-friendly.
3bd9c228 1702 var span = maxY - minY;
93dfacfd
DV
1703 // special case: if we have no sense of scale, use +/-10% of the sole value.
1704 if (span == 0) { span = maxY; }
3bd9c228
DV
1705 var maxAxisY = maxY + 0.1 * span;
1706 var minAxisY = minY - 0.1 * span;
1707
1708 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
ceb009dd
DV
1709 if (minAxisY < 0 && minY >= 0) minAxisY = 0;
1710 if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0;
3bd9c228
DV
1711
1712 if (this.attr_("includeZero")) {
1713 if (maxY < 0) maxAxisY = 0;
1714 if (minY > 0) minAxisY = 0;
1715 }
1716
1717 this.addYTicks_(minAxisY, maxAxisY);
3230c662 1718 this.displayedYRange_ = [minAxisY, maxAxisY];
6a1aa64f
DV
1719 }
1720
1721 this.addXTicks_();
1722
1723 // Tell PlotKit to use this new data and render itself
d033ae1c 1724 this.layout_.updateOptions({dateWindow: this.dateWindow_});
6a1aa64f
DV
1725 this.layout_.evaluateWithError();
1726 this.plotter_.clear();
1727 this.plotter_.render();
f6401bf6
DV
1728 this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width,
1729 this.canvas_.height);
599fb4ad
DV
1730
1731 if (this.attr_("drawCallback") !== null) {
fe0b7c03 1732 this.attr_("drawCallback")(this, is_initial_draw);
599fb4ad 1733 }
6a1aa64f
DV
1734};
1735
1736/**
1737 * Calculates the rolling average of a data set.
1738 * If originalData is [label, val], rolls the average of those.
1739 * If originalData is [label, [, it's interpreted as [value, stddev]
1740 * and the roll is returned in the same form, with appropriately reduced
1741 * stddev for each value.
1742 * Note that this is where fractional input (i.e. '5/10') is converted into
1743 * decimal values.
1744 * @param {Array} originalData The data in the appropriate format (see above)
1745 * @param {Number} rollPeriod The number of days over which to average the data
1746 */
285a6bda 1747Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) {
6a1aa64f
DV
1748 if (originalData.length < 2)
1749 return originalData;
1750 var rollPeriod = Math.min(rollPeriod, originalData.length - 1);
1751 var rollingData = [];
285a6bda 1752 var sigma = this.attr_("sigma");
6a1aa64f
DV
1753
1754 if (this.fractions_) {
1755 var num = 0;
1756 var den = 0; // numerator/denominator
1757 var mult = 100.0;
1758 for (var i = 0; i < originalData.length; i++) {
1759 num += originalData[i][1][0];
1760 den += originalData[i][1][1];
1761 if (i - rollPeriod >= 0) {
1762 num -= originalData[i - rollPeriod][1][0];
1763 den -= originalData[i - rollPeriod][1][1];
1764 }
1765
1766 var date = originalData[i][0];
1767 var value = den ? num / den : 0.0;
285a6bda 1768 if (this.attr_("errorBars")) {
6a1aa64f
DV
1769 if (this.wilsonInterval_) {
1770 // For more details on this confidence interval, see:
1771 // http://en.wikipedia.org/wiki/Binomial_confidence_interval
1772 if (den) {
1773 var p = value < 0 ? 0 : value, n = den;
1774 var pm = sigma * Math.sqrt(p*(1-p)/n + sigma*sigma/(4*n*n));
1775 var denom = 1 + sigma * sigma / den;
1776 var low = (p + sigma * sigma / (2 * den) - pm) / denom;
1777 var high = (p + sigma * sigma / (2 * den) + pm) / denom;
1778 rollingData[i] = [date,
1779 [p * mult, (p - low) * mult, (high - p) * mult]];
1780 } else {
1781 rollingData[i] = [date, [0, 0, 0]];
1782 }
1783 } else {
1784 var stddev = den ? sigma * Math.sqrt(value * (1 - value) / den) : 1.0;
1785 rollingData[i] = [date, [mult * value, mult * stddev, mult * stddev]];
1786 }
1787 } else {
1788 rollingData[i] = [date, mult * value];
1789 }
1790 }
9922b78b 1791 } else if (this.attr_("customBars")) {
f6885d6a
DV
1792 var low = 0;
1793 var mid = 0;
1794 var high = 0;
1795 var count = 0;
6a1aa64f
DV
1796 for (var i = 0; i < originalData.length; i++) {
1797 var data = originalData[i][1];
1798 var y = data[1];
1799 rollingData[i] = [originalData[i][0], [y, y - data[0], data[2] - y]];
f6885d6a 1800
8b91c51f 1801 if (y != null && !isNaN(y)) {
49a7d0d5
DV
1802 low += data[0];
1803 mid += y;
1804 high += data[2];
1805 count += 1;
1806 }
f6885d6a
DV
1807 if (i - rollPeriod >= 0) {
1808 var prev = originalData[i - rollPeriod];
8b91c51f 1809 if (prev[1][1] != null && !isNaN(prev[1][1])) {
49a7d0d5
DV
1810 low -= prev[1][0];
1811 mid -= prev[1][1];
1812 high -= prev[1][2];
1813 count -= 1;
1814 }
f6885d6a
DV
1815 }
1816 rollingData[i] = [originalData[i][0], [ 1.0 * mid / count,
1817 1.0 * (mid - low) / count,
1818 1.0 * (high - mid) / count ]];
2769de62 1819 }
6a1aa64f
DV
1820 } else {
1821 // Calculate the rolling average for the first rollPeriod - 1 points where
1822 // there is not enough data to roll over the full number of days
1823 var num_init_points = Math.min(rollPeriod - 1, originalData.length - 2);
285a6bda 1824 if (!this.attr_("errorBars")){
5011e7a1
DV
1825 if (rollPeriod == 1) {
1826 return originalData;
1827 }
1828
2847c1cf 1829 for (var i = 0; i < originalData.length; i++) {
6a1aa64f 1830 var sum = 0;
5011e7a1 1831 var num_ok = 0;
2847c1cf
DV
1832 for (var j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
1833 var y = originalData[j][1];
8b91c51f 1834 if (y == null || isNaN(y)) continue;
5011e7a1 1835 num_ok++;
2847c1cf 1836 sum += originalData[j][1];
6a1aa64f 1837 }
5011e7a1 1838 if (num_ok) {
2847c1cf 1839 rollingData[i] = [originalData[i][0], sum / num_ok];
5011e7a1 1840 } else {
2847c1cf 1841 rollingData[i] = [originalData[i][0], null];
5011e7a1 1842 }
6a1aa64f 1843 }
2847c1cf
DV
1844
1845 } else {
1846 for (var i = 0; i < originalData.length; i++) {
6a1aa64f
DV
1847 var sum = 0;
1848 var variance = 0;
5011e7a1 1849 var num_ok = 0;
2847c1cf 1850 for (var j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
5011e7a1 1851 var y = originalData[j][1][0];
8b91c51f 1852 if (y == null || isNaN(y)) continue;
5011e7a1 1853 num_ok++;
6a1aa64f
DV
1854 sum += originalData[j][1][0];
1855 variance += Math.pow(originalData[j][1][1], 2);
1856 }
5011e7a1
DV
1857 if (num_ok) {
1858 var stddev = Math.sqrt(variance) / num_ok;
1859 rollingData[i] = [originalData[i][0],
1860 [sum / num_ok, sigma * stddev, sigma * stddev]];
1861 } else {
1862 rollingData[i] = [originalData[i][0], [null, null, null]];
1863 }
6a1aa64f
DV
1864 }
1865 }
1866 }
1867
1868 return rollingData;
1869};
1870
1871/**
1872 * Parses a date, returning the number of milliseconds since epoch. This can be
285a6bda
DV
1873 * passed in as an xValueParser in the Dygraph constructor.
1874 * TODO(danvk): enumerate formats that this understands.
6a1aa64f
DV
1875 * @param {String} A date in YYYYMMDD format.
1876 * @return {Number} Milliseconds since epoch.
1877 * @public
1878 */
285a6bda 1879Dygraph.dateParser = function(dateStr, self) {
6a1aa64f 1880 var dateStrSlashed;
285a6bda 1881 var d;
986a5026 1882 if (dateStr.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
6a1aa64f 1883 dateStrSlashed = dateStr.replace("-", "/", "g");
353a0294
DV
1884 while (dateStrSlashed.search("-") != -1) {
1885 dateStrSlashed = dateStrSlashed.replace("-", "/");
1886 }
285a6bda 1887 d = Date.parse(dateStrSlashed);
2769de62 1888 } else if (dateStr.length == 8) { // e.g. '20090712'
285a6bda 1889 // TODO(danvk): remove support for this format. It's confusing.
6a1aa64f
DV
1890 dateStrSlashed = dateStr.substr(0,4) + "/" + dateStr.substr(4,2)
1891 + "/" + dateStr.substr(6,2);
285a6bda 1892 d = Date.parse(dateStrSlashed);
2769de62
DV
1893 } else {
1894 // Any format that Date.parse will accept, e.g. "2009/07/12" or
1895 // "2009/07/12 12:34:56"
285a6bda
DV
1896 d = Date.parse(dateStr);
1897 }
1898
1899 if (!d || isNaN(d)) {
1900 self.error("Couldn't parse " + dateStr + " as a date");
1901 }
1902 return d;
1903};
1904
1905/**
1906 * Detects the type of the str (date or numeric) and sets the various
1907 * formatting attributes in this.attrs_ based on this type.
1908 * @param {String} str An x value.
1909 * @private
1910 */
1911Dygraph.prototype.detectTypeFromString_ = function(str) {
1912 var isDate = false;
1913 if (str.indexOf('-') >= 0 ||
1914 str.indexOf('/') >= 0 ||
1915 isNaN(parseFloat(str))) {
1916 isDate = true;
1917 } else if (str.length == 8 && str > '19700101' && str < '20371231') {
1918 // TODO(danvk): remove support for this format.
1919 isDate = true;
1920 }
1921
1922 if (isDate) {
1923 this.attrs_.xValueFormatter = Dygraph.dateString_;
1924 this.attrs_.xValueParser = Dygraph.dateParser;
1925 this.attrs_.xTicker = Dygraph.dateTicker;
bf640e56 1926 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
285a6bda
DV
1927 } else {
1928 this.attrs_.xValueFormatter = function(x) { return x; };
1929 this.attrs_.xValueParser = function(x) { return parseFloat(x); };
1930 this.attrs_.xTicker = Dygraph.numericTicks;
bf640e56 1931 this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;
6a1aa64f 1932 }
6a1aa64f
DV
1933};
1934
1935/**
1936 * Parses a string in a special csv format. We expect a csv file where each
1937 * line is a date point, and the first field in each line is the date string.
1938 * We also expect that all remaining fields represent series.
285a6bda 1939 * if the errorBars attribute is set, then interpret the fields as:
6a1aa64f
DV
1940 * date, series1, stddev1, series2, stddev2, ...
1941 * @param {Array.<Object>} data See above.
1942 * @private
285a6bda
DV
1943 *
1944 * @return Array.<Object> An array with one entry for each row. These entries
1945 * are an array of cells in that row. The first entry is the parsed x-value for
1946 * the row. The second, third, etc. are the y-values. These can take on one of
1947 * three forms, depending on the CSV and constructor parameters:
1948 * 1. numeric value
1949 * 2. [ value, stddev ]
1950 * 3. [ low value, center value, high value ]
6a1aa64f 1951 */
285a6bda 1952Dygraph.prototype.parseCSV_ = function(data) {
6a1aa64f
DV
1953 var ret = [];
1954 var lines = data.split("\n");
3d67f03b
DV
1955
1956 // Use the default delimiter or fall back to a tab if that makes sense.
1957 var delim = this.attr_('delimiter');
1958 if (lines[0].indexOf(delim) == -1 && lines[0].indexOf('\t') >= 0) {
1959 delim = '\t';
1960 }
1961
285a6bda 1962 var start = 0;
6a1aa64f 1963 if (this.labelsFromCSV_) {
285a6bda 1964 start = 1;
3d67f03b 1965 this.attrs_.labels = lines[0].split(delim);
6a1aa64f
DV
1966 }
1967
03b522a4
DV
1968 // Parse the x as a float or return null if it's not a number.
1969 var parseFloatOrNull = function(x) {
41333ec0
DV
1970 var val = parseFloat(x);
1971 return isNaN(val) ? null : val;
03b522a4
DV
1972 };
1973
285a6bda
DV
1974 var xParser;
1975 var defaultParserSet = false; // attempt to auto-detect x value type
1976 var expectedCols = this.attr_("labels").length;
987840a2 1977 var outOfOrder = false;
6a1aa64f
DV
1978 for (var i = start; i < lines.length; i++) {
1979 var line = lines[i];
1980 if (line.length == 0) continue; // skip blank lines
3d67f03b
DV
1981 if (line[0] == '#') continue; // skip comment lines
1982 var inFields = line.split(delim);
285a6bda 1983 if (inFields.length < 2) continue;
6a1aa64f
DV
1984
1985 var fields = [];
285a6bda
DV
1986 if (!defaultParserSet) {
1987 this.detectTypeFromString_(inFields[0]);
1988 xParser = this.attr_("xValueParser");
1989 defaultParserSet = true;
1990 }
1991 fields[0] = xParser(inFields[0], this);
6a1aa64f
DV
1992
1993 // If fractions are expected, parse the numbers as "A/B"
1994 if (this.fractions_) {
1995 for (var j = 1; j < inFields.length; j++) {
1996 // TODO(danvk): figure out an appropriate way to flag parse errors.
1997 var vals = inFields[j].split("/");
03b522a4 1998 fields[j] = [parseFloatOrNull(vals[0]), parseFloatOrNull(vals[1])];
6a1aa64f 1999 }
285a6bda 2000 } else if (this.attr_("errorBars")) {
6a1aa64f
DV
2001 // If there are error bars, values are (value, stddev) pairs
2002 for (var j = 1; j < inFields.length; j += 2)
03b522a4
DV
2003 fields[(j + 1) / 2] = [parseFloatOrNull(inFields[j]),
2004 parseFloatOrNull(inFields[j + 1])];
9922b78b 2005 } else if (this.attr_("customBars")) {
6a1aa64f
DV
2006 // Bars are a low;center;high tuple
2007 for (var j = 1; j < inFields.length; j++) {
2008 var vals = inFields[j].split(";");
03b522a4
DV
2009 fields[j] = [ parseFloatOrNull(vals[0]),
2010 parseFloatOrNull(vals[1]),
2011 parseFloatOrNull(vals[2]) ];
6a1aa64f
DV
2012 }
2013 } else {
2014 // Values are just numbers
285a6bda 2015 for (var j = 1; j < inFields.length; j++) {
03b522a4 2016 fields[j] = parseFloatOrNull(inFields[j]);
285a6bda 2017 }
6a1aa64f 2018 }
987840a2
DV
2019 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
2020 outOfOrder = true;
2021 }
6a1aa64f 2022 ret.push(fields);
285a6bda
DV
2023
2024 if (fields.length != expectedCols) {
2025 this.error("Number of columns in line " + i + " (" + fields.length +
2026 ") does not agree with number of labels (" + expectedCols +
2027 ") " + line);
2028 }
6a1aa64f 2029 }
987840a2
DV
2030
2031 if (outOfOrder) {
2032 this.warn("CSV is out of order; order it correctly to speed loading.");
2033 ret.sort(function(a,b) { return a[0] - b[0] });
2034 }
2035
6a1aa64f
DV
2036 return ret;
2037};
2038
2039/**
285a6bda
DV
2040 * The user has provided their data as a pre-packaged JS array. If the x values
2041 * are numeric, this is the same as dygraphs' internal format. If the x values
2042 * are dates, we need to convert them from Date objects to ms since epoch.
2043 * @param {Array.<Object>} data
2044 * @return {Array.<Object>} data with numeric x values.
2045 */
2046Dygraph.prototype.parseArray_ = function(data) {
2047 // Peek at the first x value to see if it's numeric.
2048 if (data.length == 0) {
2049 this.error("Can't plot empty data set");
2050 return null;
2051 }
2052 if (data[0].length == 0) {
2053 this.error("Data set cannot contain an empty row");
2054 return null;
2055 }
2056
2057 if (this.attr_("labels") == null) {
2058 this.warn("Using default labels. Set labels explicitly via 'labels' " +
2059 "in the options parameter");
2060 this.attrs_.labels = [ "X" ];
2061 for (var i = 1; i < data[0].length; i++) {
2062 this.attrs_.labels.push("Y" + i);
2063 }
2064 }
2065
2dda3850 2066 if (Dygraph.isDateLike(data[0][0])) {
285a6bda
DV
2067 // Some intelligent defaults for a date x-axis.
2068 this.attrs_.xValueFormatter = Dygraph.dateString_;
bf640e56 2069 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
285a6bda
DV
2070 this.attrs_.xTicker = Dygraph.dateTicker;
2071
2072 // Assume they're all dates.
e3ab7b40 2073 var parsedData = Dygraph.clone(data);
285a6bda
DV
2074 for (var i = 0; i < data.length; i++) {
2075 if (parsedData[i].length == 0) {
a323ff4a 2076 this.error("Row " + (1 + i) + " of data is empty");
285a6bda
DV
2077 return null;
2078 }
2079 if (parsedData[i][0] == null
3a909ec5
DV
2080 || typeof(parsedData[i][0].getTime) != 'function'
2081 || isNaN(parsedData[i][0].getTime())) {
be96a1f5 2082 this.error("x value in row " + (1 + i) + " is not a Date");
285a6bda
DV
2083 return null;
2084 }
2085 parsedData[i][0] = parsedData[i][0].getTime();
2086 }
2087 return parsedData;
2088 } else {
2089 // Some intelligent defaults for a numeric x-axis.
2090 this.attrs_.xValueFormatter = function(x) { return x; };
2091 this.attrs_.xTicker = Dygraph.numericTicks;
2092 return data;
2093 }
2094};
2095
2096/**
79420a1e
DV
2097 * Parses a DataTable object from gviz.
2098 * The data is expected to have a first column that is either a date or a
2099 * number. All subsequent columns must be numbers. If there is a clear mismatch
2100 * between this.xValueParser_ and the type of the first column, it will be
a685723c 2101 * fixed. Fills out rawData_.
79420a1e
DV
2102 * @param {Array.<Object>} data See above.
2103 * @private
2104 */
285a6bda 2105Dygraph.prototype.parseDataTable_ = function(data) {
79420a1e
DV
2106 var cols = data.getNumberOfColumns();
2107 var rows = data.getNumberOfRows();
2108
d955e223 2109 var indepType = data.getColumnType(0);
4440f6c8 2110 if (indepType == 'date' || indepType == 'datetime') {
285a6bda
DV
2111 this.attrs_.xValueFormatter = Dygraph.dateString_;
2112 this.attrs_.xValueParser = Dygraph.dateParser;
2113 this.attrs_.xTicker = Dygraph.dateTicker;
bf640e56 2114 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
33127159 2115 } else if (indepType == 'number') {
285a6bda
DV
2116 this.attrs_.xValueFormatter = function(x) { return x; };
2117 this.attrs_.xValueParser = function(x) { return parseFloat(x); };
2118 this.attrs_.xTicker = Dygraph.numericTicks;
bf640e56 2119 this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;
285a6bda 2120 } else {
987840a2
DV
2121 this.error("only 'date', 'datetime' and 'number' types are supported for " +
2122 "column 1 of DataTable input (Got '" + indepType + "')");
79420a1e
DV
2123 return null;
2124 }
2125
a685723c
DV
2126 // Array of the column indices which contain data (and not annotations).
2127 var colIdx = [];
2128 var annotationCols = {}; // data index -> [annotation cols]
2129 var hasAnnotations = false;
2130 for (var i = 1; i < cols; i++) {
2131 var type = data.getColumnType(i);
2132 if (type == 'number') {
2133 colIdx.push(i);
2134 } else if (type == 'string' && this.attr_('displayAnnotations')) {
2135 // This is OK -- it's an annotation column.
2136 var dataIdx = colIdx[colIdx.length - 1];
2137 if (!annotationCols.hasOwnProperty(dataIdx)) {
2138 annotationCols[dataIdx] = [i];
2139 } else {
2140 annotationCols[dataIdx].push(i);
2141 }
2142 hasAnnotations = true;
2143 } else {
2144 this.error("Only 'number' is supported as a dependent type with Gviz." +
2145 " 'string' is only supported if displayAnnotations is true");
2146 }
2147 }
2148
2149 // Read column labels
2150 // TODO(danvk): add support back for errorBars
2151 var labels = [data.getColumnLabel(0)];
2152 for (var i = 0; i < colIdx.length; i++) {
2153 labels.push(data.getColumnLabel(colIdx[i]));
2154 }
2155 this.attrs_.labels = labels;
2156 cols = labels.length;
2157
79420a1e 2158 var ret = [];
987840a2 2159 var outOfOrder = false;
a685723c 2160 var annotations = [];
79420a1e
DV
2161 for (var i = 0; i < rows; i++) {
2162 var row = [];
debe4434
DV
2163 if (typeof(data.getValue(i, 0)) === 'undefined' ||
2164 data.getValue(i, 0) === null) {
129569a5
FD
2165 this.warn("Ignoring row " + i +
2166 " of DataTable because of undefined or null first column.");
debe4434
DV
2167 continue;
2168 }
2169
c21d2c2d 2170 if (indepType == 'date' || indepType == 'datetime') {
d955e223
DV
2171 row.push(data.getValue(i, 0).getTime());
2172 } else {
2173 row.push(data.getValue(i, 0));
2174 }
3e3f84e4 2175 if (!this.attr_("errorBars")) {
a685723c
DV
2176 for (var j = 0; j < colIdx.length; j++) {
2177 var col = colIdx[j];
2178 row.push(data.getValue(i, col));
2179 if (hasAnnotations &&
2180 annotationCols.hasOwnProperty(col) &&
2181 data.getValue(i, annotationCols[col][0]) != null) {
2182 var ann = {};
2183 ann.series = data.getColumnLabel(col);
2184 ann.xval = row[0];
2185 ann.shortText = String.fromCharCode(65 /* A */ + annotations.length)
2186 ann.text = '';
2187 for (var k = 0; k < annotationCols[col].length; k++) {
2188 if (k) ann.text += "\n";
2189 ann.text += data.getValue(i, annotationCols[col][k]);
2190 }
2191 annotations.push(ann);
2192 }
3e3f84e4
DV
2193 }
2194 } else {
2195 for (var j = 0; j < cols - 1; j++) {
2196 row.push([ data.getValue(i, 1 + 2 * j), data.getValue(i, 2 + 2 * j) ]);
2197 }
79420a1e 2198 }
987840a2
DV
2199 if (ret.length > 0 && row[0] < ret[ret.length - 1][0]) {
2200 outOfOrder = true;
2201 }
243d96e8 2202 ret.push(row);
79420a1e 2203 }
987840a2
DV
2204
2205 if (outOfOrder) {
2206 this.warn("DataTable is out of order; order it correctly to speed loading.");
2207 ret.sort(function(a,b) { return a[0] - b[0] });
2208 }
a685723c
DV
2209 this.rawData_ = ret;
2210
2211 if (annotations.length > 0) {
2212 this.setAnnotations(annotations, true);
2213 }
79420a1e
DV
2214}
2215
24e5350c 2216// These functions are all based on MochiKit.
fc80a396
DV
2217Dygraph.update = function (self, o) {
2218 if (typeof(o) != 'undefined' && o !== null) {
2219 for (var k in o) {
85b99f0b
DV
2220 if (o.hasOwnProperty(k)) {
2221 self[k] = o[k];
2222 }
fc80a396
DV
2223 }
2224 }
2225 return self;
2226};
2227
2dda3850
DV
2228Dygraph.isArrayLike = function (o) {
2229 var typ = typeof(o);
2230 if (
c21d2c2d 2231 (typ != 'object' && !(typ == 'function' &&
2dda3850
DV
2232 typeof(o.item) == 'function')) ||
2233 o === null ||
2234 typeof(o.length) != 'number' ||
2235 o.nodeType === 3
2236 ) {
2237 return false;
2238 }
2239 return true;
2240};
2241
2242Dygraph.isDateLike = function (o) {
2243 if (typeof(o) != "object" || o === null ||
2244 typeof(o.getTime) != 'function') {
2245 return false;
2246 }
2247 return true;
2248};
2249
e3ab7b40
DV
2250Dygraph.clone = function(o) {
2251 // TODO(danvk): figure out how MochiKit's version works
2252 var r = [];
2253 for (var i = 0; i < o.length; i++) {
2254 if (Dygraph.isArrayLike(o[i])) {
2255 r.push(Dygraph.clone(o[i]));
2256 } else {
2257 r.push(o[i]);
2258 }
2259 }
2260 return r;
24e5350c
DV
2261};
2262
2dda3850 2263
79420a1e 2264/**
6a1aa64f
DV
2265 * Get the CSV data. If it's in a function, call that function. If it's in a
2266 * file, do an XMLHttpRequest to get it.
2267 * @private
2268 */
285a6bda 2269Dygraph.prototype.start_ = function() {
6a1aa64f 2270 if (typeof this.file_ == 'function') {
285a6bda 2271 // CSV string. Pretend we got it via XHR.
6a1aa64f 2272 this.loadedEvent_(this.file_());
2dda3850 2273 } else if (Dygraph.isArrayLike(this.file_)) {
285a6bda
DV
2274 this.rawData_ = this.parseArray_(this.file_);
2275 this.drawGraph_(this.rawData_);
79420a1e
DV
2276 } else if (typeof this.file_ == 'object' &&
2277 typeof this.file_.getColumnRange == 'function') {
2278 // must be a DataTable from gviz.
a685723c 2279 this.parseDataTable_(this.file_);
79420a1e 2280 this.drawGraph_(this.rawData_);
285a6bda
DV
2281 } else if (typeof this.file_ == 'string') {
2282 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
2283 if (this.file_.indexOf('\n') >= 0) {
2284 this.loadedEvent_(this.file_);
2285 } else {
2286 var req = new XMLHttpRequest();
2287 var caller = this;
2288 req.onreadystatechange = function () {
2289 if (req.readyState == 4) {
2290 if (req.status == 200) {
2291 caller.loadedEvent_(req.responseText);
2292 }
6a1aa64f 2293 }
285a6bda 2294 };
6a1aa64f 2295
285a6bda
DV
2296 req.open("GET", this.file_, true);
2297 req.send(null);
2298 }
2299 } else {
2300 this.error("Unknown data format: " + (typeof this.file_));
6a1aa64f
DV
2301 }
2302};
2303
2304/**
2305 * Changes various properties of the graph. These can include:
2306 * <ul>
2307 * <li>file: changes the source data for the graph</li>
2308 * <li>errorBars: changes whether the data contains stddev</li>
2309 * </ul>
2310 * @param {Object} attrs The new properties and values
2311 */
285a6bda
DV
2312Dygraph.prototype.updateOptions = function(attrs) {
2313 // TODO(danvk): this is a mess. Rethink this function.
6a1aa64f
DV
2314 if (attrs.rollPeriod) {
2315 this.rollPeriod_ = attrs.rollPeriod;
2316 }
2317 if (attrs.dateWindow) {
2318 this.dateWindow_ = attrs.dateWindow;
2319 }
2320 if (attrs.valueRange) {
2321 this.valueRange_ = attrs.valueRange;
2322 }
450fe64b
DV
2323
2324 // TODO(danvk): validate per-series options.
46dde5f9
DV
2325 // Supported:
2326 // strokeWidth
2327 // pointSize
2328 // drawPoints
2329 // highlightCircleSize
450fe64b 2330
fc80a396 2331 Dygraph.update(this.user_attrs_, attrs);
87bb7958 2332 Dygraph.update(this.renderOptions_, attrs);
285a6bda
DV
2333
2334 this.labelsFromCSV_ = (this.attr_("labels") == null);
2335
2336 // TODO(danvk): this doesn't match the constructor logic
2337 this.layout_.updateOptions({ 'errorBars': this.attr_("errorBars") });
5e50289f 2338 if (attrs['file']) {
6a1aa64f
DV
2339 this.file_ = attrs['file'];
2340 this.start_();
2341 } else {
2342 this.drawGraph_(this.rawData_);
2343 }
2344};
2345
2346/**
697e70b2
DV
2347 * Resizes the dygraph. If no parameters are specified, resizes to fill the
2348 * containing div (which has presumably changed size since the dygraph was
2349 * instantiated. If the width/height are specified, the div will be resized.
964f30c6
DV
2350 *
2351 * This is far more efficient than destroying and re-instantiating a
2352 * Dygraph, since it doesn't have to reparse the underlying data.
2353 *
697e70b2
DV
2354 * @param {Number} width Width (in pixels)
2355 * @param {Number} height Height (in pixels)
2356 */
2357Dygraph.prototype.resize = function(width, height) {
e8c7ef86
DV
2358 if (this.resize_lock) {
2359 return;
2360 }
2361 this.resize_lock = true;
2362
697e70b2
DV
2363 if ((width === null) != (height === null)) {
2364 this.warn("Dygraph.resize() should be called with zero parameters or " +
2365 "two non-NULL parameters. Pretending it was zero.");
2366 width = height = null;
2367 }
2368
b16e6369 2369 // TODO(danvk): there should be a clear() method.
697e70b2 2370 this.maindiv_.innerHTML = "";
b16e6369
DV
2371 this.attrs_.labelsDiv = null;
2372
697e70b2
DV
2373 if (width) {
2374 this.maindiv_.style.width = width + "px";
2375 this.maindiv_.style.height = height + "px";
2376 this.width_ = width;
2377 this.height_ = height;
2378 } else {
2379 this.width_ = this.maindiv_.offsetWidth;
2380 this.height_ = this.maindiv_.offsetHeight;
2381 }
2382
2383 this.createInterface_();
964f30c6 2384 this.drawGraph_(this.rawData_);
e8c7ef86
DV
2385
2386 this.resize_lock = false;
697e70b2
DV
2387};
2388
2389/**
6a1aa64f
DV
2390 * Adjusts the number of days in the rolling average. Updates the graph to
2391 * reflect the new averaging period.
2392 * @param {Number} length Number of days over which to average the data.
2393 */
285a6bda 2394Dygraph.prototype.adjustRoll = function(length) {
6a1aa64f
DV
2395 this.rollPeriod_ = length;
2396 this.drawGraph_(this.rawData_);
2397};
540d00f1 2398
f8cfec73 2399/**
1cf11047
DV
2400 * Returns a boolean array of visibility statuses.
2401 */
2402Dygraph.prototype.visibility = function() {
2403 // Do lazy-initialization, so that this happens after we know the number of
2404 // data series.
2405 if (!this.attr_("visibility")) {
f38dec01 2406 this.attrs_["visibility"] = [];
1cf11047
DV
2407 }
2408 while (this.attr_("visibility").length < this.rawData_[0].length - 1) {
f38dec01 2409 this.attr_("visibility").push(true);
1cf11047
DV
2410 }
2411 return this.attr_("visibility");
2412};
2413
2414/**
2415 * Changes the visiblity of a series.
2416 */
2417Dygraph.prototype.setVisibility = function(num, value) {
2418 var x = this.visibility();
2419 if (num < 0 && num >= x.length) {
2420 this.warn("invalid series number in setVisibility: " + num);
2421 } else {
2422 x[num] = value;
2423 this.drawGraph_(this.rawData_);
2424 }
2425};
2426
2427/**
5c528fa2
DV
2428 * Update the list of annotations and redraw the chart.
2429 */
a685723c 2430Dygraph.prototype.setAnnotations = function(ann, suppressDraw) {
5c528fa2
DV
2431 this.annotations_ = ann;
2432 this.layout_.setAnnotations(this.annotations_);
a685723c
DV
2433 if (!suppressDraw) {
2434 this.drawGraph_(this.rawData_);
2435 }
5c528fa2
DV
2436};
2437
2438/**
2439 * Return the list of annotations.
2440 */
2441Dygraph.prototype.annotations = function() {
2442 return this.annotations_;
2443};
2444
46dde5f9
DV
2445/**
2446 * Get the index of a series (column) given its name. The first column is the
2447 * x-axis, so the data series start with index 1.
2448 */
2449Dygraph.prototype.indexFromSetName = function(name) {
2450 var labels = this.attr_("labels");
2451 for (var i = 0; i < labels.length; i++) {
2452 if (labels[i] == name) return i;
2453 }
2454 return null;
2455};
2456
5c528fa2
DV
2457Dygraph.addAnnotationRule = function() {
2458 if (Dygraph.addedAnnotationCSS) return;
2459
18a016b1
DV
2460 var mysheet;
2461 if (document.styleSheets.length > 0) {
2462 mysheet = document.styleSheets[0];
2463 } else {
2464 var styleSheetElement = document.createElement("style");
2465 styleSheetElement.type = "text/css";
2466 document.getElementsByTagName("head")[0].appendChild(styleSheetElement);
2467 for(i = 0; i < document.styleSheets.length; i++) {
2468 if (document.styleSheets[i].disabled) continue;
2469 mysheet = document.styleSheets[i];
2470 }
2471 }
2472
5c528fa2
DV
2473 var rule = "border: 1px solid black; " +
2474 "background-color: white; " +
2475 "text-align: center;";
2476 if (mysheet.insertRule) { // Firefox
b39466eb 2477 mysheet.insertRule(".dygraphDefaultAnnotation { " + rule + " }", 0);
5c528fa2
DV
2478 } else if (mysheet.addRule) { // IE
2479 mysheet.addRule(".dygraphDefaultAnnotation", rule);
2480 }
2481
2482 Dygraph.addedAnnotationCSS = true;
2483}
2484
2485/**
f8cfec73
DV
2486 * Create a new canvas element. This is more complex than a simple
2487 * document.createElement("canvas") because of IE and excanvas.
2488 */
2489Dygraph.createCanvas = function() {
2490 var canvas = document.createElement("canvas");
2491
2492 isIE = (/MSIE/.test(navigator.userAgent) && !window.opera);
2493 if (isIE) {
2494 canvas = G_vmlCanvasManager.initElement(canvas);
2495 }
2496
2497 return canvas;
2498};
2499
540d00f1
DV
2500
2501/**
285a6bda 2502 * A wrapper around Dygraph that implements the gviz API.
540d00f1
DV
2503 * @param {Object} container The DOM object the visualization should live in.
2504 */
285a6bda 2505Dygraph.GVizChart = function(container) {
540d00f1
DV
2506 this.container = container;
2507}
2508
285a6bda 2509Dygraph.GVizChart.prototype.draw = function(data, options) {
540d00f1 2510 this.container.innerHTML = '';
285a6bda 2511 this.date_graph = new Dygraph(this.container, data, options);
540d00f1 2512}
285a6bda 2513
239c712d
NAG
2514/**
2515 * Google charts compatible setSelection
50360fd0 2516 * Only row selection is supported, all points in the row will be highlighted
239c712d
NAG
2517 * @param {Array} array of the selected cells
2518 * @public
2519 */
2520Dygraph.GVizChart.prototype.setSelection = function(selection_array) {
2521 var row = false;
2522 if (selection_array.length) {
2523 row = selection_array[0].row;
2524 }
2525 this.date_graph.setSelection(row);
2526}
2527
103b7292
NAG
2528/**
2529 * Google charts compatible getSelection implementation
2530 * @return {Array} array of the selected cells
2531 * @public
2532 */
2533Dygraph.GVizChart.prototype.getSelection = function() {
2534 var selection = [];
50360fd0 2535
103b7292 2536 var row = this.date_graph.getSelection();
50360fd0 2537
103b7292 2538 if (row < 0) return selection;
50360fd0 2539
103b7292
NAG
2540 col = 1;
2541 for (var i in this.date_graph.layout_.datasets) {
2542 selection.push({row: row, column: col});
2543 col++;
2544 }
2545
2546 return selection;
2547}
2548
285a6bda
DV
2549// Older pages may still use this name.
2550DateGraph = Dygraph;