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