1 // Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
2 // All Rights Reserved.
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)
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
19 The CSV file is of the form
21 Date,SeriesA,SeriesB,SeriesC
25 If the 'errorBars' option is set in the constructor, the input should be of
28 Date,SeriesA,SeriesB,...
29 YYYYMMDD,A1,sigmaA1,B1,sigmaB1,...
30 YYYYMMDD,A2,sigmaA2,B2,sigmaB2,...
32 If the 'fractions' option is set, the input should be of the form:
34 Date,SeriesA,SeriesB,...
35 YYYYMMDD,A1/B1,A2/B2,...
36 YYYYMMDD,A1/B1,A2/B2,...
38 And error bars will be calculated automatically using a binomial distribution.
40 For further documentation and examples, see http://www.danvk.org/dygraphs
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.
53 Dygraph
= 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]);
62 this.__init__(div
, data
, opts
);
67 Dygraph
.NAME
= "Dygraph";
68 Dygraph
.VERSION
= "1.2";
69 Dygraph
.__repr__
= function() {
70 return "[" + this.NAME
+ " " + this.VERSION
+ "]";
72 Dygraph
.toString
= function() {
73 return this.__repr__();
76 // Various default values
77 Dygraph
.DEFAULT_ROLL_PERIOD
= 1;
78 Dygraph
.DEFAULT_WIDTH
= 480;
79 Dygraph
.DEFAULT_HEIGHT
= 320;
80 Dygraph
.AXIS_LINE_WIDTH
= 0.3;
82 // Default attribute values.
83 Dygraph
.DEFAULT_ATTRS
= {
84 highlightCircleSize
: 3,
90 // TODO(danvk): move defaults from createStatusMessage_ here.
92 labelsSeparateLines
: false,
93 labelsShowZeroValues
: true,
96 showLabelsOnHighlight
: true,
98 yValueFormatter
: function(x
) { return Dygraph
.round_(x
, 2); },
103 axisLabelFontSize
: 14,
106 xAxisLabelFormatter
: Dygraph
.dateAxisFormatter
,
110 xValueFormatter
: Dygraph
.dateString_
,
111 xValueParser
: Dygraph
.dateParser
,
112 xTicker
: Dygraph
.dateTicker
,
120 wilsonInterval
: true, // only relevant if fractions is true
124 connectSeparatedPoints
: false,
127 hideOverlayOnMouseOut
: true,
133 // Various logging levels.
139 // Used for initializing annotation CSS rules only once.
140 Dygraph
.addedAnnotationCSS
= false;
142 Dygraph
.prototype.__old_init__
= function(div
, file
, labels
, attrs
) {
143 // Labels is no longer a constructor parameter, since it's typically set
144 // directly from the data source. It also conains a name for the x-axis,
145 // which the previous constructor form did not.
146 if (labels
!= null) {
147 var new_labels
= ["Date"];
148 for (var i
= 0; i
< labels
.length
; i
++) new_labels
.push(labels
[i
]);
149 Dygraph
.update(attrs
, { 'labels': new_labels
});
151 this.__init__(div
, file
, attrs
);
155 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
156 * and interaction <canvas> inside of it. See the constructor for details
158 * @param {Element} div the Element to render the graph into.
159 * @param {String | Function} file Source data
160 * @param {Object} attrs Miscellaneous other options
163 Dygraph
.prototype.__init__
= function(div
, file
, attrs
) {
164 // Support two-argument constructor
165 if (attrs
== null) { attrs
= {}; }
167 // Copy the important bits into the object
168 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
171 this.rollPeriod_
= attrs
.rollPeriod
|| Dygraph
.DEFAULT_ROLL_PERIOD
;
172 this.previousVerticalX_
= -1;
173 this.fractions_
= attrs
.fractions
|| false;
174 this.dateWindow_
= attrs
.dateWindow
|| null;
175 this.valueRange_
= attrs
.valueRange
|| null;
176 this.wilsonInterval_
= attrs
.wilsonInterval
|| true;
177 this.is_initial_draw_
= true;
178 this.annotations_
= [];
180 // Clear the div. This ensure that, if multiple dygraphs are passed the same
181 // div, then only one will be drawn.
184 // If the div isn't already sized then inherit from our attrs or
185 // give it a default size.
186 if (div
.style
.width
== '') {
187 div
.style
.width
= attrs
.width
|| Dygraph
.DEFAULT_WIDTH
+ "px";
189 if (div
.style
.height
== '') {
190 div
.style
.height
= attrs
.height
|| Dygraph
.DEFAULT_HEIGHT
+ "px";
192 this.width_
= parseInt(div
.style
.width
, 10);
193 this.height_
= parseInt(div
.style
.height
, 10);
194 // The div might have been specified as percent of the current window size,
195 // convert that to an appropriate number of pixels.
196 if (div
.style
.width
.indexOf("%") == div
.style
.width
.length
- 1) {
197 this.width_
= div
.offsetWidth
;
199 if (div
.style
.height
.indexOf("%") == div
.style
.height
.length
- 1) {
200 this.height_
= div
.offsetHeight
;
203 if (this.width_
== 0) {
204 this.error("dygraph has zero width. Please specify a width in pixels.");
206 if (this.height_
== 0) {
207 this.error("dygraph has zero height. Please specify a height in pixels.");
210 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
211 if (attrs
['stackedGraph']) {
212 attrs
['fillGraph'] = true;
213 // TODO(nikhilk): Add any other stackedGraph checks here.
216 // Dygraphs has many options, some of which interact with one another.
217 // To keep track of everything, we maintain two sets of options:
219 // this.user_attrs_ only options explicitly set by the user.
220 // this.attrs_ defaults, options derived from user_attrs_, data.
222 // Options are then accessed this.attr_('attr'), which first looks at
223 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
224 // defaults without overriding behavior that the user specifically asks for.
225 this.user_attrs_
= {};
226 Dygraph
.update(this.user_attrs_
, attrs
);
229 Dygraph
.update(this.attrs_
, Dygraph
.DEFAULT_ATTRS
);
231 this.boundaryIds_
= [];
233 // Make a note of whether labels will be pulled from the CSV file.
234 this.labelsFromCSV_
= (this.attr_("labels") == null);
236 Dygraph
.addAnnotationRule();
238 // Create the containing DIV and other interactive elements
239 this.createInterface_();
244 Dygraph
.prototype.attr_
= function(name
, seriesName
) {
246 typeof(this.user_attrs_
[seriesName
]) != 'undefined' &&
247 this.user_attrs_
[seriesName
] != null &&
248 typeof(this.user_attrs_
[seriesName
][name
]) != 'undefined') {
249 return this.user_attrs_
[seriesName
][name
];
250 } else if (typeof(this.user_attrs_
[name
]) != 'undefined') {
251 return this.user_attrs_
[name
];
252 } else if (typeof(this.attrs_
[name
]) != 'undefined') {
253 return this.attrs_
[name
];
259 // TODO(danvk): any way I can get the line numbers to be this.warn call?
260 Dygraph
.prototype.log
= function(severity
, message
) {
261 if (typeof(console
) != 'undefined') {
264 console
.debug('dygraphs: ' + message
);
267 console
.info('dygraphs: ' + message
);
269 case Dygraph
.WARNING
:
270 console
.warn('dygraphs: ' + message
);
273 console
.error('dygraphs: ' + message
);
278 Dygraph
.prototype.info
= function(message
) {
279 this.log(Dygraph
.INFO
, message
);
281 Dygraph
.prototype.warn
= function(message
) {
282 this.log(Dygraph
.WARNING
, message
);
284 Dygraph
.prototype.error
= function(message
) {
285 this.log(Dygraph
.ERROR
, message
);
289 * Returns the current rolling period, as set by the user or an option.
290 * @return {Number} The number of days in the rolling window
292 Dygraph
.prototype.rollPeriod
= function() {
293 return this.rollPeriod_
;
297 * Returns the currently-visible x-range. This can be affected by zooming,
298 * panning or a call to updateOptions.
299 * Returns a two-element array: [left, right].
300 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
302 Dygraph
.prototype.xAxisRange
= function() {
303 if (this.dateWindow_
) return this.dateWindow_
;
305 // The entire chart is visible.
306 var left
= this.rawData_
[0][0];
307 var right
= this.rawData_
[this.rawData_
.length
- 1][0];
308 return [left
, right
];
312 * Returns the currently-visible y-range. This can be affected by zooming,
313 * panning or a call to updateOptions.
314 * Returns a two-element array: [bottom, top].
316 Dygraph
.prototype.yAxisRange
= function() {
317 return this.displayedYRange_
;
321 * Convert from data coordinates to canvas/div X/Y coordinates.
322 * Returns a two-element array: [X, Y]
324 Dygraph
.prototype.toDomCoords
= function(x
, y
) {
325 var ret
= [null, null];
326 var area
= this.plotter_
.area
;
328 var xRange
= this.xAxisRange();
329 ret
[0] = area
.x
+ (x
- xRange
[0]) / (xRange
[1] - xRange
[0]) * area
.w
;
333 var yRange
= this.yAxisRange();
334 ret
[1] = area
.y
+ (yRange
[1] - y
) / (yRange
[1] - yRange
[0]) * area
.h
;
340 // TODO(danvk): use these functions throughout dygraphs.
342 * Convert from canvas/div coords to data coordinates.
343 * Returns a two-element array: [X, Y]
345 Dygraph
.prototype.toDataCoords
= function(x
, y
) {
346 var ret
= [null, null];
347 var area
= this.plotter_
.area
;
349 var xRange
= this.xAxisRange();
350 ret
[0] = xRange
[0] + (x
- area
.x
) / area
.w
* (xRange
[1] - xRange
[0]);
354 var yRange
= this.yAxisRange();
355 ret
[1] = yRange
[0] + (area
.h
- y
) / area
.h
* (yRange
[1] - yRange
[0]);
362 * Returns the number of columns (including the independent variable).
364 Dygraph
.prototype.numColumns
= function() {
365 return this.rawData_
[0].length
;
369 * Returns the number of rows (excluding any header/label row).
371 Dygraph
.prototype.numRows
= function() {
372 return this.rawData_
.length
;
376 * Returns the value in the given row and column. If the row and column exceed
377 * the bounds on the data, returns null. Also returns null if the value is
380 Dygraph
.prototype.getValue
= function(row
, col
) {
381 if (row
< 0 || row
> this.rawData_
.length
) return null;
382 if (col
< 0 || col
> this.rawData_
[row
].length
) return null;
384 return this.rawData_
[row
][col
];
387 Dygraph
.addEvent
= function(el
, evt
, fn
) {
388 var normed_fn
= function(e
) {
389 if (!e
) var e
= window
.event
;
392 if (window
.addEventListener
) { // Mozilla, Netscape, Firefox
393 el
.addEventListener(evt
, normed_fn
, false);
395 el
.attachEvent('on' + evt
, normed_fn
);
399 Dygraph
.clipCanvas_
= function(cnv
, clip
) {
400 var ctx
= cnv
.getContext("2d");
402 ctx
.rect(clip
.left
, clip
.top
, clip
.width
, clip
.height
);
407 * Generates interface elements for the Dygraph: a containing div, a div to
408 * display the current point, and a textbox to adjust the rolling average
409 * period. Also creates the Renderer/Layout elements.
412 Dygraph
.prototype.createInterface_
= function() {
413 // Create the all-enclosing graph div
414 var enclosing
= this.maindiv_
;
416 this.graphDiv
= document
.createElement("div");
417 this.graphDiv
.style
.width
= this.width_
+ "px";
418 this.graphDiv
.style
.height
= this.height_
+ "px";
419 enclosing
.appendChild(this.graphDiv
);
423 left
: this.attr_("yAxisLabelWidth") + 2 * this.attr_("axisTickSize")
425 clip
.width
= this.width_
- clip
.left
- this.attr_("rightGap");
426 clip
.height
= this.height_
- this.attr_("axisLabelFontSize")
427 - 2 * this.attr_("axisTickSize");
428 this.clippingArea_
= clip
;
430 // Create the canvas for interactive parts of the chart.
431 this.canvas_
= Dygraph
.createCanvas();
432 this.canvas_
.style
.position
= "absolute";
433 this.canvas_
.width
= this.width_
;
434 this.canvas_
.height
= this.height_
;
435 this.canvas_
.style
.width
= this.width_
+ "px"; // for IE
436 this.canvas_
.style
.height
= this.height_
+ "px"; // for IE
438 // ... and for static parts of the chart.
439 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
441 // The interactive parts of the graph are drawn on top of the chart.
442 this.graphDiv
.appendChild(this.hidden_
);
443 this.graphDiv
.appendChild(this.canvas_
);
444 this.mouseEventElement_
= this.canvas_
;
446 // Make sure we don't overdraw.
447 Dygraph
.clipCanvas_(this.hidden_
, this.clippingArea_
);
448 Dygraph
.clipCanvas_(this.canvas_
, this.clippingArea_
);
451 Dygraph
.addEvent(this.mouseEventElement_
, 'mousemove', function(e
) {
452 dygraph
.mouseMove_(e
);
454 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseout', function(e
) {
455 dygraph
.mouseOut_(e
);
458 // Create the grapher
459 // TODO(danvk): why does the Layout need its own set of options?
460 this.layoutOptions_
= { 'xOriginIsZero': false };
461 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
462 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
463 Dygraph
.update(this.layoutOptions_
, {
464 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
466 this.layout_
= new DygraphLayout(this, this.layoutOptions_
);
468 // TODO(danvk): why does the Renderer need its own set of options?
469 this.renderOptions_
= { colorScheme
: this.colors_
,
471 axisLineWidth
: Dygraph
.AXIS_LINE_WIDTH
};
472 Dygraph
.update(this.renderOptions_
, this.attrs_
);
473 Dygraph
.update(this.renderOptions_
, this.user_attrs_
);
474 this.plotter_
= new DygraphCanvasRenderer(this,
475 this.hidden_
, this.layout_
,
476 this.renderOptions_
);
478 this.createStatusMessage_();
479 this.createRollInterface_();
480 this.createDragInterface_();
484 * Detach DOM elements in the dygraph and null out all data references.
485 * Calling this when you're done with a dygraph can dramatically reduce memory
486 * usage. See, e.g., the tests/perf.html example.
488 Dygraph
.prototype.destroy
= function() {
489 var removeRecursive
= function(node
) {
490 while (node
.hasChildNodes()) {
491 removeRecursive(node
.firstChild
);
492 node
.removeChild(node
.firstChild
);
495 removeRecursive(this.maindiv_
);
497 var nullOut
= function(obj
) {
499 if (typeof(obj
[n
]) === 'object') {
505 // These may not all be necessary, but it can't hurt...
506 nullOut(this.layout_
);
507 nullOut(this.plotter_
);
512 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
513 * this particular canvas. All Dygraph work is done on this.canvas_.
514 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
515 * @return {Object} The newly-created canvas
518 Dygraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
519 var h
= Dygraph
.createCanvas();
520 h
.style
.position
= "absolute";
521 // TODO(danvk): h should be offset from canvas. canvas needs to include
522 // some extra area to make it easier to zoom in on the far left and far
523 // right. h needs to be precisely the plot area, so that clipping occurs.
524 h
.style
.top
= canvas
.style
.top
;
525 h
.style
.left
= canvas
.style
.left
;
526 h
.width
= this.width_
;
527 h
.height
= this.height_
;
528 h
.style
.width
= this.width_
+ "px"; // for IE
529 h
.style
.height
= this.height_
+ "px"; // for IE
533 // Taken from MochiKit.Color
534 Dygraph
.hsvToRGB
= function (hue
, saturation
, value
) {
538 if (saturation
=== 0) {
543 var i
= Math
.floor(hue
* 6);
544 var f
= (hue
* 6) - i
;
545 var p
= value
* (1 - saturation
);
546 var q
= value
* (1 - (saturation
* f
));
547 var t
= value
* (1 - (saturation
* (1 - f
)));
549 case 1: red
= q
; green
= value
; blue
= p
; break;
550 case 2: red
= p
; green
= value
; blue
= t
; break;
551 case 3: red
= p
; green
= q
; blue
= value
; break;
552 case 4: red
= t
; green
= p
; blue
= value
; break;
553 case 5: red
= value
; green
= p
; blue
= q
; break;
554 case 6: // fall through
555 case 0: red
= value
; green
= t
; blue
= p
; break;
558 red
= Math
.floor(255 * red
+ 0.5);
559 green
= Math
.floor(255 * green
+ 0.5);
560 blue
= Math
.floor(255 * blue
+ 0.5);
561 return 'rgb(' + red
+ ',' + green
+ ',' + blue
+ ')';
566 * Generate a set of distinct colors for the data series. This is done with a
567 * color wheel. Saturation/Value are customizable, and the hue is
568 * equally-spaced around the color wheel. If a custom set of colors is
569 * specified, that is used instead.
572 Dygraph
.prototype.setColors_
= function() {
573 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
574 // away with this.renderOptions_.
575 var num
= this.attr_("labels").length
- 1;
577 var colors
= this.attr_('colors');
579 var sat
= this.attr_('colorSaturation') || 1.0;
580 var val
= this.attr_('colorValue') || 0.5;
581 var half
= Math
.ceil(num
/ 2);
582 for (var i
= 1; i
<= num
; i
++) {
583 if (!this.visibility()[i
-1]) continue;
584 // alternate colors for high contrast.
585 var idx
= i
% 2 ? Math
.ceil(i
/ 2) : (half + i / 2);
586 var hue
= (1.0 * idx
/ (1 + num
));
587 this.colors_
.push(Dygraph
.hsvToRGB(hue
, sat
, val
));
590 for (var i
= 0; i
< num
; i
++) {
591 if (!this.visibility()[i
]) continue;
592 var colorStr
= colors
[i
% colors
.length
];
593 this.colors_
.push(colorStr
);
597 // TODO(danvk): update this w/r
/t/ the
new options system
.
598 this.renderOptions_
.colorScheme
= this.colors_
;
599 Dygraph
.update(this.plotter_
.options
, this.renderOptions_
);
600 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
601 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
605 * Return the list of colors. This is either the list of colors passed in the
606 * attributes, or the autogenerated list of rgb(r,g,b) strings.
607 * @return {Array<string>} The list of colors.
609 Dygraph
.prototype.getColors
= function() {
613 // The following functions are from quirksmode.org with a modification for Safari from
614 // http://blog.firetree.net/2005/07/04/javascript-find-position/
615 // http://www.quirksmode.org/js
/findpos
.html
616 Dygraph
.findPosX
= function(obj
) {
621 curleft
+= obj
.offsetLeft
;
622 if(!obj
.offsetParent
)
624 obj
= obj
.offsetParent
;
631 Dygraph
.findPosY
= function(obj
) {
636 curtop
+= obj
.offsetTop
;
637 if(!obj
.offsetParent
)
639 obj
= obj
.offsetParent
;
649 * Create the div that contains information on the selected point(s)
650 * This goes in the top right of the canvas, unless an external div has already
654 Dygraph
.prototype.createStatusMessage_
= function() {
655 var userLabelsDiv
= this.user_attrs_
["labelsDiv"];
656 if (userLabelsDiv
&& null != userLabelsDiv
657 && (typeof(userLabelsDiv
) == "string" || userLabelsDiv
instanceof String
)) {
658 this.user_attrs_
["labelsDiv"] = document
.getElementById(userLabelsDiv
);
660 if (!this.attr_("labelsDiv")) {
661 var divWidth
= this.attr_('labelsDivWidth');
663 "position": "absolute",
666 "width": divWidth
+ "px",
668 "left": (this.width_
- divWidth
- 2) + "px",
669 "background": "white",
671 "overflow": "hidden"};
672 Dygraph
.update(messagestyle
, this.attr_('labelsDivStyles'));
673 var div
= document
.createElement("div");
674 for (var name
in messagestyle
) {
675 if (messagestyle
.hasOwnProperty(name
)) {
676 div
.style
[name
] = messagestyle
[name
];
679 this.graphDiv
.appendChild(div
);
680 this.attrs_
.labelsDiv
= div
;
685 * Create the text box to adjust the averaging period
686 * @return {Object} The newly-created text box
689 Dygraph
.prototype.createRollInterface_
= function() {
690 var display
= this.attr_('showRoller') ? "block" : "none";
691 var textAttr
= { "position": "absolute",
693 "top": (this.plotter_
.area
.h
- 25) + "px",
694 "left": (this.plotter_
.area
.x
+ 1) + "px",
697 var roller
= document
.createElement("input");
698 roller
.type
= "text";
700 roller
.value
= this.rollPeriod_
;
701 for (var name
in textAttr
) {
702 if (textAttr
.hasOwnProperty(name
)) {
703 roller
.style
[name
] = textAttr
[name
];
707 var pa
= this.graphDiv
;
708 pa
.appendChild(roller
);
710 roller
.onchange
= function() { dygraph
.adjustRoll(roller
.value
); };
714 // These functions are taken from MochiKit.Signal
715 Dygraph
.pageX
= function(e
) {
717 return (!e
.pageX
|| e
.pageX
< 0) ? 0 : e
.pageX
;
720 var b
= document
.body
;
722 (de
.scrollLeft
|| b
.scrollLeft
) -
723 (de
.clientLeft
|| 0);
727 Dygraph
.pageY
= function(e
) {
729 return (!e
.pageY
|| e
.pageY
< 0) ? 0 : e
.pageY
;
732 var b
= document
.body
;
734 (de
.scrollTop
|| b
.scrollTop
) -
740 * Set up all the mouse handlers needed to capture dragging behavior for zoom
744 Dygraph
.prototype.createDragInterface_
= function() {
747 // Tracks whether the mouse is down right now
748 var isZooming
= false;
749 var isPanning
= false;
750 var dragStartX
= null;
751 var dragStartY
= null;
755 var draggingDate
= null;
756 var dateRange
= null;
758 // Utility function to convert page-wide coordinates to canvas coords
761 var getX
= function(e
) { return Dygraph
.pageX(e
) - px
};
762 var getY
= function(e
) { return Dygraph
.pageY(e
) - py
};
764 // Draw zoom rectangles when the mouse is down and the user moves around
765 Dygraph
.addEvent(this.mouseEventElement_
, 'mousemove', function(event
) {
767 dragEndX
= getX(event
);
768 dragEndY
= getY(event
);
770 self
.drawZoomRect_(dragStartX
, dragEndX
, prevEndX
);
772 } else if (isPanning
) {
773 dragEndX
= getX(event
);
774 dragEndY
= getY(event
);
776 // Want to have it so that:
777 // 1. draggingDate appears at dragEndX
778 // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
780 self
.dateWindow_
[0] = draggingDate
- (dragEndX
/ self
.width_
) * dateRange
;
781 self
.dateWindow_
[1] = self
.dateWindow_
[0] + dateRange
;
782 self
.drawGraph_(self
.rawData_
);
786 // Track the beginning of drag events
787 Dygraph
.addEvent(this.mouseEventElement_
, 'mousedown', function(event
) {
788 px
= Dygraph
.findPosX(self
.canvas_
);
789 py
= Dygraph
.findPosY(self
.canvas_
);
790 dragStartX
= getX(event
);
791 dragStartY
= getY(event
);
793 if (event
.altKey
|| event
.shiftKey
) {
794 if (!self
.dateWindow_
) return; // have to be zoomed in to pan.
796 dateRange
= self
.dateWindow_
[1] - self
.dateWindow_
[0];
797 draggingDate
= (dragStartX
/ self
.width_
) * dateRange
+
804 // If the user releases the mouse button during a drag, but not over the
805 // canvas, then it doesn't count as a zooming action.
806 Dygraph
.addEvent(document
, 'mouseup', function(event
) {
807 if (isZooming
|| isPanning
) {
820 // Temporarily cancel the dragging event when the mouse leaves the graph
821 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseout', function(event
) {
828 // If the mouse is released on the canvas during a drag event, then it's a
829 // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
830 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseup', function(event
) {
833 dragEndX
= getX(event
);
834 dragEndY
= getY(event
);
835 var regionWidth
= Math
.abs(dragEndX
- dragStartX
);
836 var regionHeight
= Math
.abs(dragEndY
- dragStartY
);
838 if (regionWidth
< 2 && regionHeight
< 2 &&
839 self
.lastx_
!= undefined
&& self
.lastx_
!= -1) {
840 // TODO(danvk): pass along more info about the points, e.g. 'x'
841 if (self
.attr_('clickCallback') != null) {
842 self
.attr_('clickCallback')(event
, self
.lastx_
, self
.selPoints_
);
844 if (self
.attr_('pointClickCallback')) {
845 // check if the click was on a particular point.
847 var closestDistance
= 0;
848 for (var i
= 0; i
< self
.selPoints_
.length
; i
++) {
849 var p
= self
.selPoints_
[i
];
850 var distance
= Math
.pow(p
.canvasx
- dragEndX
, 2) +
851 Math
.pow(p
.canvasy
- dragEndY
, 2);
852 if (closestIdx
== -1 || distance
< closestDistance
) {
853 closestDistance
= distance
;
858 // Allow any click within two pixels of the dot.
859 var radius
= self
.attr_('highlightCircleSize') + 2;
860 if (closestDistance
<= 5 * 5) {
861 self
.attr_('pointClickCallback')(event
, self
.selPoints_
[closestIdx
]);
866 if (regionWidth
>= 10) {
867 self
.doZoom_(Math
.min(dragStartX
, dragEndX
),
868 Math
.max(dragStartX
, dragEndX
));
870 self
.canvas_
.getContext("2d").clearRect(0, 0,
872 self
.canvas_
.height
);
886 // Double-clicking zooms back out
887 Dygraph
.addEvent(this.mouseEventElement_
, 'dblclick', function(event
) {
888 if (self
.dateWindow_
== null) return;
889 self
.dateWindow_
= null;
890 self
.drawGraph_(self
.rawData_
);
891 var minDate
= self
.rawData_
[0][0];
892 var maxDate
= self
.rawData_
[self
.rawData_
.length
- 1][0];
893 if (self
.attr_("zoomCallback")) {
894 self
.attr_("zoomCallback")(minDate
, maxDate
);
900 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
901 * up any previous zoom rectangles that were drawn. This could be optimized to
902 * avoid extra redrawing, but it's tricky to avoid interactions with the status
904 * @param {Number} startX The X position where the drag started, in canvas
906 * @param {Number} endX The current X position of the drag, in canvas coords.
907 * @param {Number} prevEndX The value of endX on the previous call to this
908 * function. Used to avoid excess redrawing
911 Dygraph
.prototype.drawZoomRect_
= function(startX
, endX
, prevEndX
) {
912 var ctx
= this.canvas_
.getContext("2d");
914 // Clean up from the previous rect if necessary
916 ctx
.clearRect(Math
.min(startX
, prevEndX
), 0,
917 Math
.abs(startX
- prevEndX
), this.height_
);
920 // Draw a light-grey rectangle to show the new viewing area
921 if (endX
&& startX
) {
922 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
923 ctx
.fillRect(Math
.min(startX
, endX
), 0,
924 Math
.abs(endX
- startX
), this.height_
);
929 * Zoom to something containing [lowX, highX]. These are pixel coordinates
930 * in the canvas. The exact zoom window may be slightly larger if there are no
931 * data points near lowX or highX. This function redraws the graph.
932 * @param {Number} lowX The leftmost pixel value that should be visible.
933 * @param {Number} highX The rightmost pixel value that should be visible.
936 Dygraph
.prototype.doZoom_
= function(lowX
, highX
) {
937 // Find the earliest and latest dates contained in this canvasx range.
938 var r
= this.toDataCoords(lowX
, null);
940 r
= this.toDataCoords(highX
, null);
943 this.dateWindow_
= [minDate
, maxDate
];
944 this.drawGraph_(this.rawData_
);
945 if (this.attr_("zoomCallback")) {
946 this.attr_("zoomCallback")(minDate
, maxDate
);
951 * When the mouse moves in the canvas, display information about a nearby data
952 * point and draw dots over those points in the data series. This function
953 * takes care of cleanup of previously-drawn dots.
954 * @param {Object} event The mousemove event from the browser.
957 Dygraph
.prototype.mouseMove_
= function(event
) {
958 var canvasx
= Dygraph
.pageX(event
) - Dygraph
.findPosX(this.mouseEventElement_
);
959 var points
= this.layout_
.points
;
964 // Loop through all the points and find the date nearest to our current
966 var minDist
= 1e+100;
968 for (var i
= 0; i
< points
.length
; i
++) {
969 var dist
= Math
.abs(points
[i
].canvasx
- canvasx
);
970 if (dist
> minDist
) continue;
974 if (idx
>= 0) lastx
= points
[idx
].xval
;
975 // Check that you can really highlight the last day's data
976 if (canvasx
> points
[points
.length
-1].canvasx
)
977 lastx
= points
[points
.length
-1].xval
;
979 // Extract the points we've selected
980 this.selPoints_
= [];
981 var l
= points
.length
;
982 if (!this.attr_("stackedGraph")) {
983 for (var i
= 0; i
< l
; i
++) {
984 if (points
[i
].xval
== lastx
) {
985 this.selPoints_
.push(points
[i
]);
989 // Need to 'unstack' points starting from the bottom
990 var cumulative_sum
= 0;
991 for (var i
= l
- 1; i
>= 0; i
--) {
992 if (points
[i
].xval
== lastx
) {
993 var p
= {}; // Clone the point since we modify it
994 for (var k
in points
[i
]) {
997 p
.yval
-= cumulative_sum
;
998 cumulative_sum
+= p
.yval
;
999 this.selPoints_
.push(p
);
1002 this.selPoints_
.reverse();
1005 if (this.attr_("highlightCallback")) {
1006 var px
= this.lastx_
;
1007 if (px
!== null && lastx
!= px
) {
1008 // only fire if the selected point has changed.
1009 this.attr_("highlightCallback")(event
, lastx
, this.selPoints_
);
1013 // Save last x position for callbacks.
1014 this.lastx_
= lastx
;
1016 this.updateSelection_();
1020 * Draw dots over the selectied points in the data series. This function
1021 * takes care of cleanup of previously-drawn dots.
1024 Dygraph
.prototype.updateSelection_
= function() {
1025 // Clear the previously drawn vertical, if there is one
1026 var ctx
= this.canvas_
.getContext("2d");
1027 if (this.previousVerticalX_
>= 0) {
1028 // Determine the maximum highlight circle size.
1029 var maxCircleSize
= 0;
1030 var labels
= this.attr_('labels');
1031 for (var i
= 1; i
< labels
.length
; i
++) {
1032 var r
= this.attr_('highlightCircleSize', labels
[i
]);
1033 if (r
> maxCircleSize
) maxCircleSize
= r
;
1035 var px
= this.previousVerticalX_
;
1036 ctx
.clearRect(px
- maxCircleSize
- 1, 0,
1037 2 * maxCircleSize
+ 2, this.height_
);
1040 var isOK
= function(x
) { return x
&& !isNaN(x
); };
1042 if (this.selPoints_
.length
> 0) {
1043 var canvasx
= this.selPoints_
[0].canvasx
;
1045 // Set the status message to indicate the selected point(s)
1046 var replace
= this.attr_('xValueFormatter')(this.lastx_
, this) + ":";
1047 var fmtFunc
= this.attr_('yValueFormatter');
1048 var clen
= this.colors_
.length
;
1050 if (this.attr_('showLabelsOnHighlight')) {
1051 // Set the status message to indicate the selected point(s)
1052 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1053 if (!this.attr_("labelsShowZeroValues") && this.selPoints_
[i
].yval
== 0) continue;
1054 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1055 if (this.attr_("labelsSeparateLines")) {
1058 var point
= this.selPoints_
[i
];
1059 var c
= new RGBColor(this.plotter_
.colors
[point
.name
]);
1060 var yval
= fmtFunc(point
.yval
);
1061 replace
+= " <b><font color='" + c
.toHex() + "'>"
1062 + point
.name
+ "</font></b>:"
1066 this.attr_("labelsDiv").innerHTML
= replace
;
1069 // Draw colored circles over the center of each selected point
1071 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1072 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1074 this.attr_('highlightCircleSize', this.selPoints_
[i
].name
);
1076 ctx
.fillStyle
= this.plotter_
.colors
[this.selPoints_
[i
].name
];
1077 ctx
.arc(canvasx
, this.selPoints_
[i
].canvasy
, circleSize
,
1078 0, 2 * Math
.PI
, false);
1083 this.previousVerticalX_
= canvasx
;
1088 * Set manually set selected dots, and display information about them
1089 * @param int row number that should by highlighted
1090 * false value clears the selection
1093 Dygraph
.prototype.setSelection
= function(row
) {
1094 // Extract the points we've selected
1095 this.selPoints_
= [];
1098 if (row
!== false) {
1099 row
= row
-this.boundaryIds_
[0][0];
1102 if (row
!== false && row
>= 0) {
1103 for (var i
in this.layout_
.datasets
) {
1104 if (row
< this.layout_
.datasets
[i
].length
) {
1105 var point
= this.layout_
.points
[pos
+row
];
1107 if (this.attr_("stackedGraph")) {
1108 point
= this.layout_
.unstackPointAtIndex(pos
+row
);
1111 this.selPoints_
.push(point
);
1113 pos
+= this.layout_
.datasets
[i
].length
;
1117 if (this.selPoints_
.length
) {
1118 this.lastx_
= this.selPoints_
[0].xval
;
1119 this.updateSelection_();
1122 this.clearSelection();
1128 * The mouse has left the canvas. Clear out whatever artifacts remain
1129 * @param {Object} event the mouseout event from the browser.
1132 Dygraph
.prototype.mouseOut_
= function(event
) {
1133 if (this.attr_("unhighlightCallback")) {
1134 this.attr_("unhighlightCallback")(event
);
1137 if (this.attr_("hideOverlayOnMouseOut")) {
1138 this.clearSelection();
1143 * Remove all selection from the canvas
1146 Dygraph
.prototype.clearSelection
= function() {
1147 // Get rid of the overlay data
1148 var ctx
= this.canvas_
.getContext("2d");
1149 ctx
.clearRect(0, 0, this.width_
, this.height_
);
1150 this.attr_("labelsDiv").innerHTML
= "";
1151 this.selPoints_
= [];
1156 * Returns the number of the currently selected row
1157 * @return int row number, of -1 if nothing is selected
1160 Dygraph
.prototype.getSelection
= function() {
1161 if (!this.selPoints_
|| this.selPoints_
.length
< 1) {
1165 for (var row
=0; row
<this.layout_
.points
.length
; row
++ ) {
1166 if (this.layout_
.points
[row
].x
== this.selPoints_
[0].x
) {
1167 return row
+ this.boundaryIds_
[0][0];
1173 Dygraph
.zeropad
= function(x
) {
1174 if (x
< 10) return "0" + x
; else return "" + x
;
1178 * Return a string version of the hours, minutes and seconds portion of a date.
1179 * @param {Number} date The JavaScript date (ms since epoch)
1180 * @return {String} A time of the form "HH:MM:SS"
1183 Dygraph
.hmsString_
= function(date
) {
1184 var zeropad
= Dygraph
.zeropad
;
1185 var d
= new Date(date
);
1186 if (d
.getSeconds()) {
1187 return zeropad(d
.getHours()) + ":" +
1188 zeropad(d
.getMinutes()) + ":" +
1189 zeropad(d
.getSeconds());
1191 return zeropad(d
.getHours()) + ":" + zeropad(d
.getMinutes());
1196 * Convert a JS date to a string appropriate to display on an axis that
1197 * is displaying values at the stated granularity.
1198 * @param {Date} date The date to format
1199 * @param {Number} granularity One of the Dygraph granularity constants
1200 * @return {String} The formatted date
1203 Dygraph
.dateAxisFormatter
= function(date
, granularity
) {
1204 if (granularity
>= Dygraph
.MONTHLY
) {
1205 return date
.strftime('%b %y');
1207 var frac
= date
.getHours() * 3600 + date
.getMinutes() * 60 + date
.getSeconds() + date
.getMilliseconds();
1208 if (frac
== 0 || granularity
>= Dygraph
.DAILY
) {
1209 return new Date(date
.getTime() + 3600*1000).strftime('%d%b');
1211 return Dygraph
.hmsString_(date
.getTime());
1217 * Convert a JS date (millis since epoch) to YYYY/MM/DD
1218 * @param {Number} date The JavaScript date (ms since epoch)
1219 * @return {String} A date of the form "YYYY/MM/DD"
1222 Dygraph
.dateString_
= function(date
, self
) {
1223 var zeropad
= Dygraph
.zeropad
;
1224 var d
= new Date(date
);
1227 var year
= "" + d
.getFullYear();
1228 // Get a 0 padded month string
1229 var month
= zeropad(d
.getMonth() + 1); //months are 0-offset, sigh
1230 // Get a 0 padded day string
1231 var day
= zeropad(d
.getDate());
1234 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
1235 if (frac
) ret
= " " + Dygraph
.hmsString_(date
);
1237 return year
+ "/" + month + "/" + day
+ ret
;
1241 * Round a number to the specified number of digits past the decimal point.
1242 * @param {Number} num The number to round
1243 * @param {Number} places The number of decimals to which to round
1244 * @return {Number} The rounded number
1247 Dygraph
.round_
= function(num
, places
) {
1248 var shift
= Math
.pow(10, places
);
1249 return Math
.round(num
* shift
)/shift
;
1253 * Fires when there's data available to be graphed.
1254 * @param {String} data Raw CSV data to be plotted
1257 Dygraph
.prototype.loadedEvent_
= function(data
) {
1258 this.rawData_
= this.parseCSV_(data
);
1259 this.drawGraph_(this.rawData_
);
1262 Dygraph
.prototype.months
= ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
1263 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1264 Dygraph
.prototype.quarters
= ["Jan", "Apr", "Jul", "Oct"];
1267 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1270 Dygraph
.prototype.addXTicks_
= function() {
1271 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1272 var startDate
, endDate
;
1273 if (this.dateWindow_
) {
1274 startDate
= this.dateWindow_
[0];
1275 endDate
= this.dateWindow_
[1];
1277 startDate
= this.rawData_
[0][0];
1278 endDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1281 var xTicks
= this.attr_('xTicker')(startDate
, endDate
, this);
1282 this.layout_
.updateOptions({xTicks
: xTicks
});
1285 // Time granularity enumeration
1286 Dygraph
.SECONDLY
= 0;
1287 Dygraph
.TWO_SECONDLY
= 1;
1288 Dygraph
.FIVE_SECONDLY
= 2;
1289 Dygraph
.TEN_SECONDLY
= 3;
1290 Dygraph
.THIRTY_SECONDLY
= 4;
1291 Dygraph
.MINUTELY
= 5;
1292 Dygraph
.TWO_MINUTELY
= 6;
1293 Dygraph
.FIVE_MINUTELY
= 7;
1294 Dygraph
.TEN_MINUTELY
= 8;
1295 Dygraph
.THIRTY_MINUTELY
= 9;
1296 Dygraph
.HOURLY
= 10;
1297 Dygraph
.TWO_HOURLY
= 11;
1298 Dygraph
.SIX_HOURLY
= 12;
1300 Dygraph
.WEEKLY
= 14;
1301 Dygraph
.MONTHLY
= 15;
1302 Dygraph
.QUARTERLY
= 16;
1303 Dygraph
.BIANNUAL
= 17;
1304 Dygraph
.ANNUAL
= 18;
1305 Dygraph
.DECADAL
= 19;
1306 Dygraph
.NUM_GRANULARITIES
= 20;
1308 Dygraph
.SHORT_SPACINGS
= [];
1309 Dygraph
.SHORT_SPACINGS
[Dygraph
.SECONDLY
] = 1000 * 1;
1310 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_SECONDLY
] = 1000 * 2;
1311 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_SECONDLY
] = 1000 * 5;
1312 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_SECONDLY
] = 1000 * 10;
1313 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_SECONDLY
] = 1000 * 30;
1314 Dygraph
.SHORT_SPACINGS
[Dygraph
.MINUTELY
] = 1000 * 60;
1315 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_MINUTELY
] = 1000 * 60 * 2;
1316 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_MINUTELY
] = 1000 * 60 * 5;
1317 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_MINUTELY
] = 1000 * 60 * 10;
1318 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_MINUTELY
] = 1000 * 60 * 30;
1319 Dygraph
.SHORT_SPACINGS
[Dygraph
.HOURLY
] = 1000 * 3600;
1320 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_HOURLY
] = 1000 * 3600 * 2;
1321 Dygraph
.SHORT_SPACINGS
[Dygraph
.SIX_HOURLY
] = 1000 * 3600 * 6;
1322 Dygraph
.SHORT_SPACINGS
[Dygraph
.DAILY
] = 1000 * 86400;
1323 Dygraph
.SHORT_SPACINGS
[Dygraph
.WEEKLY
] = 1000 * 604800;
1327 // If we used this time granularity, how many ticks would there be?
1328 // This is only an approximation, but it's generally good enough.
1330 Dygraph
.prototype.NumXTicks
= function(start_time
, end_time
, granularity
) {
1331 if (granularity
< Dygraph
.MONTHLY
) {
1332 // Generate one tick mark for every fixed interval of time.
1333 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1334 return Math
.floor(0.5 + 1.0 * (end_time
- start_time
) / spacing
);
1336 var year_mod
= 1; // e.g. to only print one point every 10 years.
1337 var num_months
= 12;
1338 if (granularity
== Dygraph
.QUARTERLY
) num_months
= 3;
1339 if (granularity
== Dygraph
.BIANNUAL
) num_months
= 2;
1340 if (granularity
== Dygraph
.ANNUAL
) num_months
= 1;
1341 if (granularity
== Dygraph
.DECADAL
) { num_months
= 1; year_mod
= 10; }
1343 var msInYear
= 365.2524 * 24 * 3600 * 1000;
1344 var num_years
= 1.0 * (end_time
- start_time
) / msInYear
;
1345 return Math
.floor(0.5 + 1.0 * num_years
* num_months
/ year_mod
);
1351 // Construct an x-axis of nicely-formatted times on meaningful boundaries
1352 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1354 // Returns an array containing {v: millis, label: label} dictionaries.
1356 Dygraph
.prototype.GetXAxis
= function(start_time
, end_time
, granularity
) {
1357 var formatter
= this.attr_("xAxisLabelFormatter");
1359 if (granularity
< Dygraph
.MONTHLY
) {
1360 // Generate one tick mark for every fixed interval of time.
1361 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1362 var format
= '%d%b'; // e.g. "1Jan"
1364 // Find a time less than start_time which occurs on a "nice" time boundary
1365 // for this granularity.
1366 var g
= spacing
/ 1000;
1367 var d
= new Date(start_time
);
1368 if (g
<= 60) { // seconds
1369 var x
= d
.getSeconds(); d
.setSeconds(x
- x
% g
);
1373 if (g
<= 60) { // minutes
1374 var x
= d
.getMinutes(); d
.setMinutes(x
- x
% g
);
1379 if (g
<= 24) { // days
1380 var x
= d
.getHours(); d
.setHours(x
- x
% g
);
1385 if (g
== 7) { // one week
1386 d
.setDate(d
.getDate() - d
.getDay());
1391 start_time
= d
.getTime();
1393 for (var t
= start_time
; t
<= end_time
; t
+= spacing
) {
1394 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1397 // Display a tick mark on the first of a set of months of each year.
1398 // Years get a tick mark iff y % year_mod == 0. This is useful for
1399 // displaying a tick mark once every 10 years, say, on long time scales.
1401 var year_mod
= 1; // e.g. to only print one point every 10 years.
1403 if (granularity
== Dygraph
.MONTHLY
) {
1404 months
= [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
1405 } else if (granularity
== Dygraph
.QUARTERLY
) {
1406 months
= [ 0, 3, 6, 9 ];
1407 } else if (granularity
== Dygraph
.BIANNUAL
) {
1409 } else if (granularity
== Dygraph
.ANNUAL
) {
1411 } else if (granularity
== Dygraph
.DECADAL
) {
1416 var start_year
= new Date(start_time
).getFullYear();
1417 var end_year
= new Date(end_time
).getFullYear();
1418 var zeropad
= Dygraph
.zeropad
;
1419 for (var i
= start_year
; i
<= end_year
; i
++) {
1420 if (i
% year_mod
!= 0) continue;
1421 for (var j
= 0; j
< months
.length
; j
++) {
1422 var date_str
= i
+ "/" + zeropad(1 + months[j]) + "/01";
1423 var t
= Date
.parse(date_str
);
1424 if (t
< start_time
|| t
> end_time
) continue;
1425 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1435 * Add ticks to the x-axis based on a date range.
1436 * @param {Number} startDate Start of the date window (millis since epoch)
1437 * @param {Number} endDate End of the date window (millis since epoch)
1438 * @return {Array.<Object>} Array of {label, value} tuples.
1441 Dygraph
.dateTicker
= function(startDate
, endDate
, self
) {
1443 for (var i
= 0; i
< Dygraph
.NUM_GRANULARITIES
; i
++) {
1444 var num_ticks
= self
.NumXTicks(startDate
, endDate
, i
);
1445 if (self
.width_
/ num_ticks
>= self
.attr_('pixelsPerXLabel')) {
1452 return self
.GetXAxis(startDate
, endDate
, chosen
);
1454 // TODO(danvk): signal error.
1459 * Add ticks when the x axis has numbers on it (instead of dates)
1460 * @param {Number} startDate Start of the date window (millis since epoch)
1461 * @param {Number} endDate End of the date window (millis since epoch)
1463 * @param {function} formatter: Optional formatter to use for each tick value
1464 * @return {Array.<Object>} Array of {label, value} tuples.
1467 Dygraph
.numericTicks
= function(minV
, maxV
, self
, formatter
) {
1469 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1470 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks
).
1471 // The first spacing greater than pixelsPerYLabel is what we use.
1472 // TODO(danvk): version that works on a log scale.
1473 if (self
.attr_("labelsKMG2")) {
1474 var mults
= [1, 2, 4, 8];
1476 var mults
= [1, 2, 5];
1478 var scale
, low_val
, high_val
, nTicks
;
1479 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1480 var pixelsPerTick
= self
.attr_('pixelsPerYLabel');
1481 for (var i
= -10; i
< 50; i
++) {
1482 if (self
.attr_("labelsKMG2")) {
1483 var base_scale
= Math
.pow(16, i
);
1485 var base_scale
= Math
.pow(10, i
);
1487 for (var j
= 0; j
< mults
.length
; j
++) {
1488 scale
= base_scale
* mults
[j
];
1489 low_val
= Math
.floor(minV
/ scale
) * scale
;
1490 high_val
= Math
.ceil(maxV
/ scale
) * scale
;
1491 nTicks
= Math
.abs(high_val
- low_val
) / scale
;
1492 var spacing
= self
.height_
/ nTicks
;
1493 // wish I could break out of both loops at once...
1494 if (spacing
> pixelsPerTick
) break;
1496 if (spacing
> pixelsPerTick
) break;
1499 // Construct labels for the ticks
1503 if (self
.attr_("labelsKMB")) {
1505 k_labels
= [ "K", "M", "B", "T" ];
1507 if (self
.attr_("labelsKMG2")) {
1508 if (k
) self
.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
1510 k_labels
= [ "k", "M", "G", "T" ];
1513 // Allow reverse y-axis if it's explicitly requested.
1514 if (low_val
> high_val
) scale
*= -1;
1516 for (var i
= 0; i
< nTicks
; i
++) {
1517 var tickV
= low_val
+ i
* scale
;
1518 var absTickV
= Math
.abs(tickV
);
1520 if (formatter
!= undefined
) {
1521 label
= formatter(tickV
);
1523 label
= Dygraph
.round_(tickV
, 2);
1525 if (k_labels
.length
) {
1526 // Round up to an appropriate unit.
1528 for (var j
= 3; j
>= 0; j
--, n
/= k
) {
1529 if (absTickV
>= n
) {
1530 label
= Dygraph
.round_(tickV
/ n
, 1) + k_labels
[j
];
1535 ticks
.push( {label
: label
, v
: tickV
} );
1541 * Adds appropriate ticks on the y-axis
1542 * @param {Number} minY The minimum Y value in the data set
1543 * @param {Number} maxY The maximum Y value in the data set
1546 Dygraph
.prototype.addYTicks_
= function(minY
, maxY
) {
1547 // Set the number of ticks so that the labels are human-friendly.
1548 // TODO(danvk): make this an attribute as well.
1549 var formatter
= this.attr_('yAxisLabelFormatter') ? this.attr_('yAxisLabelFormatter') : this.attr_('yValueFormatter');
1550 var ticks
= Dygraph
.numericTicks(minY
, maxY
, this, formatter
);
1551 this.layout_
.updateOptions( { yAxis
: [minY
, maxY
],
1555 // Computes the range of the data series (including confidence intervals).
1556 // series is either [ [x1, y1], [x2, y2], ... ] or
1557 // [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1558 // Returns [low, high]
1559 Dygraph
.prototype.extremeValues_
= function(series
) {
1560 var minY
= null, maxY
= null;
1562 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1564 // With custom bars, maxY is the max of the high values.
1565 for (var j
= 0; j
< series
.length
; j
++) {
1566 var y
= series
[j
][1][0];
1568 var low
= y
- series
[j
][1][1];
1569 var high
= y
+ series
[j
][1][2];
1570 if (low
> y
) low
= y
; // this can happen with custom bars,
1571 if (high
< y
) high
= y
; // e.g. in tests/custom-bars
.html
1572 if (maxY
== null || high
> maxY
) {
1575 if (minY
== null || low
< minY
) {
1580 for (var j
= 0; j
< series
.length
; j
++) {
1581 var y
= series
[j
][1];
1582 if (y
=== null || isNaN(y
)) continue;
1583 if (maxY
== null || y
> maxY
) {
1586 if (minY
== null || y
< minY
) {
1592 return [minY
, maxY
];
1596 * Update the graph with new data. Data is in the format
1597 * [ [date1, val1, val2, ...], [date2, val1, val2, ...] if errorBars=false
1598 * or, if errorBars=true,
1599 * [ [date1, [val1,stddev1], [val2,stddev2], ...], [date2, ...], ...]
1600 * @param {Array.<Object>} data The data (see above)
1603 Dygraph
.prototype.drawGraph_
= function(data
) {
1604 // This is used to set the second parameter to drawCallback, below.
1605 var is_initial_draw
= this.is_initial_draw_
;
1606 this.is_initial_draw_
= false;
1608 var minY
= null, maxY
= null;
1609 this.layout_
.removeAllDatasets();
1611 this.attrs_
['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
1613 // Loop over the fields (series). Go from the last to the first,
1614 // because if they're stacked that's how we accumulate the values.
1616 var cumulative_y
= []; // For stacked series.
1619 // Loop over all fields and create datasets
1620 for (var i
= data
[0].length
- 1; i
>= 1; i
--) {
1621 if (!this.visibility()[i
- 1]) continue;
1623 var connectSeparatedPoints
= this.attr_('connectSeparatedPoints', i
);
1626 for (var j
= 0; j
< data
.length
; j
++) {
1627 if (data
[j
][i
] != null || !connectSeparatedPoints
) {
1628 var date
= data
[j
][0];
1629 series
.push([date
, data
[j
][i
]]);
1632 series
= this.rollingAverage(series
, this.rollPeriod_
);
1634 // Prune down to the desired range, if necessary (for zooming)
1635 // Because there can be lines going to points outside of the visible area,
1636 // we actually prune to visible points, plus one on either side.
1637 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1638 if (this.dateWindow_
) {
1639 var low
= this.dateWindow_
[0];
1640 var high
= this.dateWindow_
[1];
1642 // TODO(danvk): do binary search instead of linear search.
1643 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
1644 var firstIdx
= null, lastIdx
= null;
1645 for (var k
= 0; k
< series
.length
; k
++) {
1646 if (series
[k
][0] >= low
&& firstIdx
=== null) {
1649 if (series
[k
][0] <= high
) {
1653 if (firstIdx
=== null) firstIdx
= 0;
1654 if (firstIdx
> 0) firstIdx
--;
1655 if (lastIdx
=== null) lastIdx
= series
.length
- 1;
1656 if (lastIdx
< series
.length
- 1) lastIdx
++;
1657 this.boundaryIds_
[i
-1] = [firstIdx
, lastIdx
];
1658 for (var k
= firstIdx
; k
<= lastIdx
; k
++) {
1659 pruned
.push(series
[k
]);
1663 this.boundaryIds_
[i
-1] = [0, series
.length
-1];
1666 var extremes
= this.extremeValues_(series
);
1667 var thisMinY
= extremes
[0];
1668 var thisMaxY
= extremes
[1];
1669 if (minY
=== null || (thisMinY
!= null && thisMinY
< minY
)) minY
= thisMinY
;
1670 if (maxY
=== null || (thisMaxY
!= null && thisMaxY
> maxY
)) maxY
= thisMaxY
;
1673 for (var j
=0; j
<series
.length
; j
++) {
1674 val
= [series
[j
][0], series
[j
][1][0], series
[j
][1][1], series
[j
][1][2]];
1677 } else if (this.attr_("stackedGraph")) {
1678 var l
= series
.length
;
1680 for (var j
= 0; j
< l
; j
++) {
1681 // If one data set has a NaN, let all subsequent stacked
1682 // sets inherit the NaN -- only start at 0 for the first set.
1683 var x
= series
[j
][0];
1684 if (cumulative_y
[x
] === undefined
)
1685 cumulative_y
[x
] = 0;
1687 actual_y
= series
[j
][1];
1688 cumulative_y
[x
] += actual_y
;
1690 series
[j
] = [x
, cumulative_y
[x
]]
1692 if (!maxY
|| cumulative_y
[x
] > maxY
)
1693 maxY
= cumulative_y
[x
];
1697 datasets
[i
] = series
;
1700 for (var i
= 1; i
< datasets
.length
; i
++) {
1701 if (!this.visibility()[i
- 1]) continue;
1702 this.layout_
.addDataset(this.attr_("labels")[i
], datasets
[i
]);
1705 // Use some heuristics to come up with a good maxY value, unless it's been
1706 // set explicitly by the user.
1707 if (this.valueRange_
!= null) {
1708 this.addYTicks_(this.valueRange_
[0], this.valueRange_
[1]);
1709 this.displayedYRange_
= this.valueRange_
;
1711 // This affects the calculation of span, below.
1712 if (this.attr_("includeZero") && minY
> 0) {
1716 // Add some padding and round up to an integer to be human-friendly.
1717 var span
= maxY
- minY
;
1718 // special case: if we have no sense of scale, use +/-10% of the sole value
.
1719 if (span
== 0) { span
= maxY
; }
1720 var maxAxisY
= maxY
+ 0.1 * span
;
1721 var minAxisY
= minY
- 0.1 * span
;
1723 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
1724 if (!this.attr_("avoidMinZero")) {
1725 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
1726 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
1729 if (this.attr_("includeZero")) {
1730 if (maxY
< 0) maxAxisY
= 0;
1731 if (minY
> 0) minAxisY
= 0;
1734 this.addYTicks_(minAxisY
, maxAxisY
);
1735 this.displayedYRange_
= [minAxisY
, maxAxisY
];
1740 // Tell PlotKit to use this new data and render itself
1741 this.layout_
.updateOptions({dateWindow
: this.dateWindow_
});
1742 this.layout_
.evaluateWithError();
1743 this.plotter_
.clear();
1744 this.plotter_
.render();
1745 this.canvas_
.getContext('2d').clearRect(0, 0, this.canvas_
.width
,
1746 this.canvas_
.height
);
1748 if (this.attr_("drawCallback") !== null) {
1749 this.attr_("drawCallback")(this, is_initial_draw
);
1754 * Calculates the rolling average of a data set.
1755 * If originalData is [label, val], rolls the average of those.
1756 * If originalData is [label, [, it's interpreted as [value, stddev]
1757 * and the roll is returned in the same form, with appropriately reduced
1758 * stddev for each value.
1759 * Note that this is where fractional input (i.e. '5/10') is converted into
1761 * @param {Array} originalData The data in the appropriate format (see above)
1762 * @param {Number} rollPeriod The number of days over which to average the data
1764 Dygraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
1765 if (originalData
.length
< 2)
1766 return originalData
;
1767 var rollPeriod
= Math
.min(rollPeriod
, originalData
.length
- 1);
1768 var rollingData
= [];
1769 var sigma
= this.attr_("sigma");
1771 if (this.fractions_
) {
1773 var den
= 0; // numerator/denominator
1775 for (var i
= 0; i
< originalData
.length
; i
++) {
1776 num
+= originalData
[i
][1][0];
1777 den
+= originalData
[i
][1][1];
1778 if (i
- rollPeriod
>= 0) {
1779 num
-= originalData
[i
- rollPeriod
][1][0];
1780 den
-= originalData
[i
- rollPeriod
][1][1];
1783 var date
= originalData
[i
][0];
1784 var value
= den
? num
/ den
: 0.0;
1785 if (this.attr_("errorBars")) {
1786 if (this.wilsonInterval_
) {
1787 // For more details on this confidence interval, see:
1788 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
1790 var p
= value
< 0 ? 0 : value
, n
= den
;
1791 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
1792 var denom
= 1 + sigma
* sigma
/ den
;
1793 var low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
1794 var high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
1795 rollingData
[i
] = [date
,
1796 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
1798 rollingData
[i
] = [date
, [0, 0, 0]];
1801 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
1802 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
1805 rollingData
[i
] = [date
, mult
* value
];
1808 } else if (this.attr_("customBars")) {
1813 for (var i
= 0; i
< originalData
.length
; i
++) {
1814 var data
= originalData
[i
][1];
1816 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
1818 if (y
!= null && !isNaN(y
)) {
1824 if (i
- rollPeriod
>= 0) {
1825 var prev
= originalData
[i
- rollPeriod
];
1826 if (prev
[1][1] != null && !isNaN(prev
[1][1])) {
1833 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
1834 1.0 * (mid
- low
) / count
,
1835 1.0 * (high
- mid
) / count
]];
1838 // Calculate the rolling average for the first rollPeriod - 1 points where
1839 // there is not enough data to roll over the full number of days
1840 var num_init_points
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
1841 if (!this.attr_("errorBars")){
1842 if (rollPeriod
== 1) {
1843 return originalData
;
1846 for (var i
= 0; i
< originalData
.length
; i
++) {
1849 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
1850 var y
= originalData
[j
][1];
1851 if (y
== null || isNaN(y
)) continue;
1853 sum
+= originalData
[j
][1];
1856 rollingData
[i
] = [originalData
[i
][0], sum
/ num_ok
];
1858 rollingData
[i
] = [originalData
[i
][0], null];
1863 for (var i
= 0; i
< originalData
.length
; i
++) {
1867 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
1868 var y
= originalData
[j
][1][0];
1869 if (y
== null || isNaN(y
)) continue;
1871 sum
+= originalData
[j
][1][0];
1872 variance
+= Math
.pow(originalData
[j
][1][1], 2);
1875 var stddev
= Math
.sqrt(variance
) / num_ok
;
1876 rollingData
[i
] = [originalData
[i
][0],
1877 [sum
/ num_ok
, sigma
* stddev
, sigma
* stddev
]];
1879 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
1889 * Parses a date, returning the number of milliseconds since epoch. This can be
1890 * passed in as an xValueParser in the Dygraph constructor.
1891 * TODO(danvk): enumerate formats that this understands.
1892 * @param {String} A date in YYYYMMDD format.
1893 * @return {Number} Milliseconds since epoch.
1896 Dygraph
.dateParser
= function(dateStr
, self
) {
1899 if (dateStr
.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
1900 dateStrSlashed
= dateStr
.replace("-", "/", "g");
1901 while (dateStrSlashed
.search("-") != -1) {
1902 dateStrSlashed
= dateStrSlashed
.replace("-", "/");
1904 d
= Date
.parse(dateStrSlashed
);
1905 } else if (dateStr
.length
== 8) { // e.g. '20090712'
1906 // TODO(danvk): remove support for this format. It's confusing.
1907 dateStrSlashed
= dateStr
.substr(0,4) + "/" + dateStr
.substr(4,2)
1908 + "/" + dateStr
.substr(6,2);
1909 d
= Date
.parse(dateStrSlashed
);
1911 // Any format that Date.parse will accept, e.g. "2009/07/12" or
1912 // "2009/07/12 12:34:56"
1913 d
= Date
.parse(dateStr
);
1916 if (!d
|| isNaN(d
)) {
1917 self
.error("Couldn't parse " + dateStr
+ " as a date");
1923 * Detects the type of the str (date or numeric) and sets the various
1924 * formatting attributes in this.attrs_ based on this type.
1925 * @param {String} str An x value.
1928 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
1930 if (str
.indexOf('-') >= 0 ||
1931 str
.indexOf('/') >= 0 ||
1932 isNaN(parseFloat(str
))) {
1934 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
1935 // TODO(danvk): remove support for this format.
1940 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
1941 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
1942 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
1943 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
1945 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1946 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
1947 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1948 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
1953 * Parses a string in a special csv format. We expect a csv file where each
1954 * line is a date point, and the first field in each line is the date string.
1955 * We also expect that all remaining fields represent series.
1956 * if the errorBars attribute is set, then interpret the fields as:
1957 * date, series1, stddev1, series2, stddev2, ...
1958 * @param {Array.<Object>} data See above.
1961 * @return Array.<Object> An array with one entry for each row. These entries
1962 * are an array of cells in that row. The first entry is the parsed x-value for
1963 * the row. The second, third, etc. are the y-values. These can take on one of
1964 * three forms, depending on the CSV and constructor parameters:
1966 * 2. [ value, stddev ]
1967 * 3. [ low value, center value, high value ]
1969 Dygraph
.prototype.parseCSV_
= function(data
) {
1971 var lines
= data
.split("\n");
1973 // Use the default delimiter or fall back to a tab if that makes sense.
1974 var delim
= this.attr_('delimiter');
1975 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
1980 if (this.labelsFromCSV_
) {
1982 this.attrs_
.labels
= lines
[0].split(delim
);
1985 // Parse the x as a float or return null if it's not a number.
1986 var parseFloatOrNull
= function(x
) {
1987 var val
= parseFloat(x
);
1988 return isNaN(val
) ? null : val
;
1992 var defaultParserSet
= false; // attempt to auto-detect x value type
1993 var expectedCols
= this.attr_("labels").length
;
1994 var outOfOrder
= false;
1995 for (var i
= start
; i
< lines
.length
; i
++) {
1996 var line
= lines
[i
];
1997 if (line
.length
== 0) continue; // skip blank lines
1998 if (line
[0] == '#') continue; // skip comment lines
1999 var inFields
= line
.split(delim
);
2000 if (inFields
.length
< 2) continue;
2003 if (!defaultParserSet
) {
2004 this.detectTypeFromString_(inFields
[0]);
2005 xParser
= this.attr_("xValueParser");
2006 defaultParserSet
= true;
2008 fields
[0] = xParser(inFields
[0], this);
2010 // If fractions are expected, parse the numbers as "A/B
"
2011 if (this.fractions_) {
2012 for (var j = 1; j < inFields.length; j++) {
2013 // TODO(danvk): figure out an appropriate way to flag parse errors.
2014 var vals = inFields[j].split("/");
2015 fields[j] = [parseFloatOrNull(vals[0]), parseFloatOrNull(vals[1])];
2017 } else if (this.attr_("errorBars
")) {
2018 // If there are error bars, values are (value, stddev) pairs
2019 for (var j = 1; j < inFields.length; j += 2)
2020 fields[(j + 1) / 2] = [parseFloatOrNull(inFields[j]),
2021 parseFloatOrNull(inFields[j + 1])];
2022 } else if (this.attr_("customBars
")) {
2023 // Bars are a low;center;high tuple
2024 for (var j = 1; j < inFields.length; j++) {
2025 var vals = inFields[j].split(";");
2026 fields[j] = [ parseFloatOrNull(vals[0]),
2027 parseFloatOrNull(vals[1]),
2028 parseFloatOrNull(vals[2]) ];
2031 // Values are just numbers
2032 for (var j = 1; j < inFields.length; j++) {
2033 fields[j] = parseFloatOrNull(inFields[j]);
2036 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
2041 if (fields.length != expectedCols) {
2042 this.error("Number of columns
in line
" + i + " (" + fields.length +
2043 ") does not agree
with number of
labels (" + expectedCols +
2049 this.warn("CSV is out of order
; order it correctly to speed loading
.");
2050 ret.sort(function(a,b) { return a[0] - b[0] });
2057 * The user has provided their data as a pre-packaged JS array. If the x values
2058 * are numeric, this is the same as dygraphs' internal format. If the x values
2059 * are dates, we need to convert them from Date objects to ms since epoch.
2060 * @param {Array.<Object>} data
2061 * @return {Array.<Object>} data with numeric x values.
2063 Dygraph.prototype.parseArray_ = function(data) {
2064 // Peek at the first x value to see if it's numeric.
2065 if (data.length == 0) {
2066 this.error("Can
't plot empty data set");
2069 if (data[0].length == 0) {
2070 this.error("Data set cannot contain an empty row");
2074 if (this.attr_("labels") == null) {
2075 this.warn("Using default labels. Set labels explicitly via 'labels
' " +
2076 "in the options parameter");
2077 this.attrs_.labels = [ "X" ];
2078 for (var i = 1; i < data[0].length; i++) {
2079 this.attrs_.labels.push("Y" + i);
2083 if (Dygraph.isDateLike(data[0][0])) {
2084 // Some intelligent defaults for a date x-axis.
2085 this.attrs_.xValueFormatter = Dygraph.dateString_;
2086 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
2087 this.attrs_.xTicker = Dygraph.dateTicker;
2089 // Assume they're all dates
.
2090 var parsedData
= Dygraph
.clone(data
);
2091 for (var i
= 0; i
< data
.length
; i
++) {
2092 if (parsedData
[i
].length
== 0) {
2093 this.error("Row " + (1 + i
) + " of data is empty");
2096 if (parsedData
[i
][0] == null
2097 || typeof(parsedData
[i
][0].getTime
) != 'function'
2098 || isNaN(parsedData
[i
][0].getTime())) {
2099 this.error("x value in row " + (1 + i
) + " is not a Date");
2102 parsedData
[i
][0] = parsedData
[i
][0].getTime();
2106 // Some intelligent defaults for a numeric x-axis.
2107 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2108 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2114 * Parses a DataTable object from gviz.
2115 * The data is expected to have a first column that is either a date or a
2116 * number. All subsequent columns must be numbers. If there is a clear mismatch
2117 * between this.xValueParser_ and the type of the first column, it will be
2118 * fixed. Fills out rawData_.
2119 * @param {Array.<Object>} data See above.
2122 Dygraph
.prototype.parseDataTable_
= function(data
) {
2123 var cols
= data
.getNumberOfColumns();
2124 var rows
= data
.getNumberOfRows();
2126 var indepType
= data
.getColumnType(0);
2127 if (indepType
== 'date' || indepType
== 'datetime') {
2128 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
2129 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2130 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
2131 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2132 } else if (indepType
== 'number') {
2133 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2134 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2135 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2136 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
2138 this.error("only 'date', 'datetime' and 'number' types are supported for " +
2139 "column 1 of DataTable input (Got '" + indepType
+ "')");
2143 // Array of the column indices which contain data (and not annotations).
2145 var annotationCols
= {}; // data index -> [annotation cols]
2146 var hasAnnotations
= false;
2147 for (var i
= 1; i
< cols
; i
++) {
2148 var type
= data
.getColumnType(i
);
2149 if (type
== 'number') {
2151 } else if (type
== 'string' && this.attr_('displayAnnotations')) {
2152 // This is OK -- it's an annotation column.
2153 var dataIdx
= colIdx
[colIdx
.length
- 1];
2154 if (!annotationCols
.hasOwnProperty(dataIdx
)) {
2155 annotationCols
[dataIdx
] = [i
];
2157 annotationCols
[dataIdx
].push(i
);
2159 hasAnnotations
= true;
2161 this.error("Only 'number' is supported as a dependent type with Gviz." +
2162 " 'string' is only supported if displayAnnotations is true");
2166 // Read column labels
2167 // TODO(danvk): add support back for errorBars
2168 var labels
= [data
.getColumnLabel(0)];
2169 for (var i
= 0; i
< colIdx
.length
; i
++) {
2170 labels
.push(data
.getColumnLabel(colIdx
[i
]));
2171 if (this.attr_("errorBars")) i
+= 1;
2173 this.attrs_
.labels
= labels
;
2174 cols
= labels
.length
;
2177 var outOfOrder
= false;
2178 var annotations
= [];
2179 for (var i
= 0; i
< rows
; i
++) {
2181 if (typeof(data
.getValue(i
, 0)) === 'undefined' ||
2182 data
.getValue(i
, 0) === null) {
2183 this.warn("Ignoring row " + i
+
2184 " of DataTable because of undefined or null first column.");
2188 if (indepType
== 'date' || indepType
== 'datetime') {
2189 row
.push(data
.getValue(i
, 0).getTime());
2191 row
.push(data
.getValue(i
, 0));
2193 if (!this.attr_("errorBars")) {
2194 for (var j
= 0; j
< colIdx
.length
; j
++) {
2195 var col
= colIdx
[j
];
2196 row
.push(data
.getValue(i
, col
));
2197 if (hasAnnotations
&&
2198 annotationCols
.hasOwnProperty(col
) &&
2199 data
.getValue(i
, annotationCols
[col
][0]) != null) {
2201 ann
.series
= data
.getColumnLabel(col
);
2203 ann
.shortText
= String
.fromCharCode(65 /* A */ + annotations
.length
)
2205 for (var k
= 0; k
< annotationCols
[col
].length
; k
++) {
2206 if (k
) ann
.text
+= "\n";
2207 ann
.text
+= data
.getValue(i
, annotationCols
[col
][k
]);
2209 annotations
.push(ann
);
2213 for (var j
= 0; j
< cols
- 1; j
++) {
2214 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
2217 if (ret
.length
> 0 && row
[0] < ret
[ret
.length
- 1][0]) {
2224 this.warn("DataTable is out of order; order it correctly to speed loading.");
2225 ret
.sort(function(a
,b
) { return a
[0] - b
[0] });
2227 this.rawData_
= ret
;
2229 if (annotations
.length
> 0) {
2230 this.setAnnotations(annotations
, true);
2234 // These functions are all based on MochiKit.
2235 Dygraph
.update
= function (self
, o
) {
2236 if (typeof(o
) != 'undefined' && o
!== null) {
2238 if (o
.hasOwnProperty(k
)) {
2246 Dygraph
.isArrayLike
= function (o
) {
2247 var typ
= typeof(o
);
2249 (typ
!= 'object' && !(typ
== 'function' &&
2250 typeof(o
.item
) == 'function')) ||
2252 typeof(o
.length
) != 'number' ||
2260 Dygraph
.isDateLike
= function (o
) {
2261 if (typeof(o
) != "object" || o
=== null ||
2262 typeof(o
.getTime
) != 'function') {
2268 Dygraph
.clone
= function(o
) {
2269 // TODO(danvk): figure out how MochiKit's version works
2271 for (var i
= 0; i
< o
.length
; i
++) {
2272 if (Dygraph
.isArrayLike(o
[i
])) {
2273 r
.push(Dygraph
.clone(o
[i
]));
2283 * Get the CSV data. If it's in a function, call that function. If it's in a
2284 * file, do an XMLHttpRequest to get it.
2287 Dygraph
.prototype.start_
= function() {
2288 if (typeof this.file_
== 'function') {
2289 // CSV string. Pretend we got it via XHR.
2290 this.loadedEvent_(this.file_());
2291 } else if (Dygraph
.isArrayLike(this.file_
)) {
2292 this.rawData_
= this.parseArray_(this.file_
);
2293 this.drawGraph_(this.rawData_
);
2294 } else if (typeof this.file_
== 'object' &&
2295 typeof this.file_
.getColumnRange
== 'function') {
2296 // must be a DataTable from gviz.
2297 this.parseDataTable_(this.file_
);
2298 this.drawGraph_(this.rawData_
);
2299 } else if (typeof this.file_
== 'string') {
2300 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
2301 if (this.file_
.indexOf('\n') >= 0) {
2302 this.loadedEvent_(this.file_
);
2304 var req
= new XMLHttpRequest();
2306 req
.onreadystatechange
= function () {
2307 if (req
.readyState
== 4) {
2308 if (req
.status
== 200) {
2309 caller
.loadedEvent_(req
.responseText
);
2314 req
.open("GET", this.file_
, true);
2318 this.error("Unknown data format: " + (typeof this.file_
));
2323 * Changes various properties of the graph. These can include:
2325 * <li>file: changes the source data for the graph</li>
2326 * <li>errorBars: changes whether the data contains stddev</li>
2328 * @param {Object} attrs The new properties and values
2330 Dygraph
.prototype.updateOptions
= function(attrs
) {
2331 // TODO(danvk): this is a mess. Rethink this function.
2332 if (attrs
.rollPeriod
) {
2333 this.rollPeriod_
= attrs
.rollPeriod
;
2335 if (attrs
.dateWindow
) {
2336 this.dateWindow_
= attrs
.dateWindow
;
2338 if (attrs
.valueRange
) {
2339 this.valueRange_
= attrs
.valueRange
;
2342 // TODO(danvk): validate per-series options.
2347 // highlightCircleSize
2349 Dygraph
.update(this.user_attrs_
, attrs
);
2350 Dygraph
.update(this.renderOptions_
, attrs
);
2352 this.labelsFromCSV_
= (this.attr_("labels") == null);
2354 // TODO(danvk): this doesn't match the constructor logic
2355 this.layout_
.updateOptions({ 'errorBars': this.attr_("errorBars") });
2356 if (attrs
['file']) {
2357 this.file_
= attrs
['file'];
2360 this.drawGraph_(this.rawData_
);
2365 * Resizes the dygraph. If no parameters are specified, resizes to fill the
2366 * containing div (which has presumably changed size since the dygraph was
2367 * instantiated. If the width/height are specified, the div will be resized.
2369 * This is far more efficient than destroying and re-instantiating a
2370 * Dygraph, since it doesn't have to reparse the underlying data.
2372 * @param {Number} width Width (in pixels)
2373 * @param {Number} height Height (in pixels)
2375 Dygraph
.prototype.resize
= function(width
, height
) {
2376 if (this.resize_lock
) {
2379 this.resize_lock
= true;
2381 if ((width
=== null) != (height
=== null)) {
2382 this.warn("Dygraph.resize() should be called with zero parameters or " +
2383 "two non-NULL parameters. Pretending it was zero.");
2384 width
= height
= null;
2387 // TODO(danvk): there should be a clear() method.
2388 this.maindiv_
.innerHTML
= "";
2389 this.attrs_
.labelsDiv
= null;
2392 this.maindiv_
.style
.width
= width
+ "px";
2393 this.maindiv_
.style
.height
= height
+ "px";
2394 this.width_
= width
;
2395 this.height_
= height
;
2397 this.width_
= this.maindiv_
.offsetWidth
;
2398 this.height_
= this.maindiv_
.offsetHeight
;
2401 this.createInterface_();
2402 this.drawGraph_(this.rawData_
);
2404 this.resize_lock
= false;
2408 * Adjusts the number of days in the rolling average. Updates the graph to
2409 * reflect the new averaging period.
2410 * @param {Number} length Number of days over which to average the data.
2412 Dygraph
.prototype.adjustRoll
= function(length
) {
2413 this.rollPeriod_
= length
;
2414 this.drawGraph_(this.rawData_
);
2418 * Returns a boolean array of visibility statuses.
2420 Dygraph
.prototype.visibility
= function() {
2421 // Do lazy-initialization, so that this happens after we know the number of
2423 if (!this.attr_("visibility")) {
2424 this.attrs_
["visibility"] = [];
2426 while (this.attr_("visibility").length
< this.rawData_
[0].length
- 1) {
2427 this.attr_("visibility").push(true);
2429 return this.attr_("visibility");
2433 * Changes the visiblity of a series.
2435 Dygraph
.prototype.setVisibility
= function(num
, value
) {
2436 var x
= this.visibility();
2437 if (num
< 0 && num
>= x
.length
) {
2438 this.warn("invalid series number in setVisibility: " + num
);
2441 this.drawGraph_(this.rawData_
);
2446 * Update the list of annotations and redraw the chart.
2448 Dygraph
.prototype.setAnnotations
= function(ann
, suppressDraw
) {
2449 this.annotations_
= ann
;
2450 this.layout_
.setAnnotations(this.annotations_
);
2451 if (!suppressDraw
) {
2452 this.drawGraph_(this.rawData_
);
2457 * Return the list of annotations.
2459 Dygraph
.prototype.annotations
= function() {
2460 return this.annotations_
;
2464 * Get the index of a series (column) given its name. The first column is the
2465 * x-axis, so the data series start with index 1.
2467 Dygraph
.prototype.indexFromSetName
= function(name
) {
2468 var labels
= this.attr_("labels");
2469 for (var i
= 0; i
< labels
.length
; i
++) {
2470 if (labels
[i
] == name
) return i
;
2475 Dygraph
.addAnnotationRule
= function() {
2476 if (Dygraph
.addedAnnotationCSS
) return;
2479 if (document
.styleSheets
.length
> 0) {
2480 mysheet
= document
.styleSheets
[0];
2482 var styleSheetElement
= document
.createElement("style");
2483 styleSheetElement
.type
= "text/css";
2484 document
.getElementsByTagName("head")[0].appendChild(styleSheetElement
);
2485 for(i
= 0; i
< document
.styleSheets
.length
; i
++) {
2486 if (document
.styleSheets
[i
].disabled
) continue;
2487 mysheet
= document
.styleSheets
[i
];
2491 var rule
= "border: 1px solid black; " +
2492 "background-color: white; " +
2493 "text-align: center;";
2494 if (mysheet
.insertRule
) { // Firefox
2495 mysheet
.insertRule(".dygraphDefaultAnnotation { " + rule
+ " }", mysheet
.cssRules
.length
);
2496 } else if (mysheet
.addRule
) { // IE
2497 mysheet
.addRule(".dygraphDefaultAnnotation", rule
);
2500 Dygraph
.addedAnnotationCSS
= true;
2504 * Create a new canvas element. This is more complex than a simple
2505 * document.createElement("canvas") because of IE and excanvas.
2507 Dygraph
.createCanvas
= function() {
2508 var canvas
= document
.createElement("canvas");
2510 isIE
= (/MSIE/.test(navigator
.userAgent
) && !window
.opera
);
2511 if (isIE
&& (typeof(G_vmlCanvasManager
) != 'undefined')) {
2512 canvas
= G_vmlCanvasManager
.initElement(canvas
);
2520 * A wrapper around Dygraph that implements the gviz API.
2521 * @param {Object} container The DOM object the visualization should live in.
2523 Dygraph
.GVizChart
= function(container
) {
2524 this.container
= container
;
2527 Dygraph
.GVizChart
.prototype.draw
= function(data
, options
) {
2528 this.container
.innerHTML
= '';
2529 this.date_graph
= new Dygraph(this.container
, data
, options
);
2533 * Google charts compatible setSelection
2534 * Only row selection is supported, all points in the row will be highlighted
2535 * @param {Array} array of the selected cells
2538 Dygraph
.GVizChart
.prototype.setSelection
= function(selection_array
) {
2540 if (selection_array
.length
) {
2541 row
= selection_array
[0].row
;
2543 this.date_graph
.setSelection(row
);
2547 * Google charts compatible getSelection implementation
2548 * @return {Array} array of the selected cells
2551 Dygraph
.GVizChart
.prototype.getSelection
= function() {
2554 var row
= this.date_graph
.getSelection();
2556 if (row
< 0) return selection
;
2559 for (var i
in this.date_graph
.layout_
.datasets
) {
2560 selection
.push({row
: row
, column
: col
});
2567 // Older pages may still use this name.
2568 DateGraph
= Dygraph
;