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