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,
132 // Various logging levels.
138 // Used for initializing annotation CSS rules only once.
139 Dygraph
.addedAnnotationCSS
= false;
141 Dygraph
.prototype.__old_init__
= function(div
, file
, labels
, attrs
) {
142 // Labels is no longer a constructor parameter, since it's typically set
143 // directly from the data source. It also conains a name for the x-axis,
144 // which the previous constructor form did not.
145 if (labels
!= null) {
146 var new_labels
= ["Date"];
147 for (var i
= 0; i
< labels
.length
; i
++) new_labels
.push(labels
[i
]);
148 Dygraph
.update(attrs
, { 'labels': new_labels
});
150 this.__init__(div
, file
, attrs
);
154 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
155 * and interaction <canvas> inside of it. See the constructor for details
157 * @param {Element} div the Element to render the graph into.
158 * @param {String | Function} file Source data
159 * @param {Object} attrs Miscellaneous other options
162 Dygraph
.prototype.__init__
= function(div
, file
, attrs
) {
163 // Support two-argument constructor
164 if (attrs
== null) { attrs
= {}; }
166 // Copy the important bits into the object
167 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
170 this.rollPeriod_
= attrs
.rollPeriod
|| Dygraph
.DEFAULT_ROLL_PERIOD
;
171 this.previousVerticalX_
= -1;
172 this.fractions_
= attrs
.fractions
|| false;
173 this.dateWindow_
= attrs
.dateWindow
|| null;
174 this.valueRange_
= attrs
.valueRange
|| null;
175 this.wilsonInterval_
= attrs
.wilsonInterval
|| true;
176 this.is_initial_draw_
= true;
177 this.annotations_
= [];
179 // Clear the div. This ensure that, if multiple dygraphs are passed the same
180 // div, then only one will be drawn.
183 // If the div isn't already sized then inherit from our attrs or
184 // give it a default size.
185 if (div
.style
.width
== '') {
186 div
.style
.width
= attrs
.width
|| Dygraph
.DEFAULT_WIDTH
+ "px";
188 if (div
.style
.height
== '') {
189 div
.style
.height
= attrs
.height
|| Dygraph
.DEFAULT_HEIGHT
+ "px";
191 this.width_
= parseInt(div
.style
.width
, 10);
192 this.height_
= parseInt(div
.style
.height
, 10);
193 // The div might have been specified as percent of the current window size,
194 // convert that to an appropriate number of pixels.
195 if (div
.style
.width
.indexOf("%") == div
.style
.width
.length
- 1) {
196 this.width_
= div
.offsetWidth
;
198 if (div
.style
.height
.indexOf("%") == div
.style
.height
.length
- 1) {
199 this.height_
= div
.offsetHeight
;
202 if (this.width_
== 0) {
203 this.error("dygraph has zero width. Please specify a width in pixels.");
205 if (this.height_
== 0) {
206 this.error("dygraph has zero height. Please specify a height in pixels.");
209 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
210 if (attrs
['stackedGraph']) {
211 attrs
['fillGraph'] = true;
212 // TODO(nikhilk): Add any other stackedGraph checks here.
215 // Dygraphs has many options, some of which interact with one another.
216 // To keep track of everything, we maintain two sets of options:
218 // this.user_attrs_ only options explicitly set by the user.
219 // this.attrs_ defaults, options derived from user_attrs_, data.
221 // Options are then accessed this.attr_('attr'), which first looks at
222 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
223 // defaults without overriding behavior that the user specifically asks for.
224 this.user_attrs_
= {};
225 Dygraph
.update(this.user_attrs_
, attrs
);
228 Dygraph
.update(this.attrs_
, Dygraph
.DEFAULT_ATTRS
);
230 this.boundaryIds_
= [];
232 // Make a note of whether labels will be pulled from the CSV file.
233 this.labelsFromCSV_
= (this.attr_("labels") == null);
235 Dygraph
.addAnnotationRule();
237 // Create the containing DIV and other interactive elements
238 this.createInterface_();
243 Dygraph
.prototype.attr_
= function(name
, seriesName
) {
245 typeof(this.user_attrs_
[seriesName
]) != 'undefined' &&
246 this.user_attrs_
[seriesName
] != null &&
247 typeof(this.user_attrs_
[seriesName
][name
]) != 'undefined') {
248 return this.user_attrs_
[seriesName
][name
];
249 } else if (typeof(this.user_attrs_
[name
]) != 'undefined') {
250 return this.user_attrs_
[name
];
251 } else if (typeof(this.attrs_
[name
]) != 'undefined') {
252 return this.attrs_
[name
];
258 // TODO(danvk): any way I can get the line numbers to be this.warn call?
259 Dygraph
.prototype.log
= function(severity
, message
) {
260 if (typeof(console
) != 'undefined') {
263 console
.debug('dygraphs: ' + message
);
266 console
.info('dygraphs: ' + message
);
268 case Dygraph
.WARNING
:
269 console
.warn('dygraphs: ' + message
);
272 console
.error('dygraphs: ' + message
);
277 Dygraph
.prototype.info
= function(message
) {
278 this.log(Dygraph
.INFO
, message
);
280 Dygraph
.prototype.warn
= function(message
) {
281 this.log(Dygraph
.WARNING
, message
);
283 Dygraph
.prototype.error
= function(message
) {
284 this.log(Dygraph
.ERROR
, message
);
288 * Returns the current rolling period, as set by the user or an option.
289 * @return {Number} The number of days in the rolling window
291 Dygraph
.prototype.rollPeriod
= function() {
292 return this.rollPeriod_
;
296 * Returns the currently-visible x-range. This can be affected by zooming,
297 * panning or a call to updateOptions.
298 * Returns a two-element array: [left, right].
299 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
301 Dygraph
.prototype.xAxisRange
= function() {
302 if (this.dateWindow_
) return this.dateWindow_
;
304 // The entire chart is visible.
305 var left
= this.rawData_
[0][0];
306 var right
= this.rawData_
[this.rawData_
.length
- 1][0];
307 return [left
, right
];
311 * Returns the currently-visible y-range. This can be affected by zooming,
312 * panning or a call to updateOptions.
313 * Returns a two-element array: [bottom, top].
315 Dygraph
.prototype.yAxisRange
= function() {
316 return this.displayedYRange_
;
320 * Convert from data coordinates to canvas/div X/Y coordinates.
321 * Returns a two-element array: [X, Y]
323 Dygraph
.prototype.toDomCoords
= function(x
, y
) {
324 var ret
= [null, null];
325 var area
= this.plotter_
.area
;
327 var xRange
= this.xAxisRange();
328 ret
[0] = area
.x
+ (x
- xRange
[0]) / (xRange
[1] - xRange
[0]) * area
.w
;
332 var yRange
= this.yAxisRange();
333 ret
[1] = area
.y
+ (yRange
[1] - y
) / (yRange
[1] - yRange
[0]) * area
.h
;
339 // TODO(danvk): use these functions throughout dygraphs.
341 * Convert from canvas/div coords to data coordinates.
342 * Returns a two-element array: [X, Y]
344 Dygraph
.prototype.toDataCoords
= function(x
, y
) {
345 var ret
= [null, null];
346 var area
= this.plotter_
.area
;
348 var xRange
= this.xAxisRange();
349 ret
[0] = xRange
[0] + (x
- area
.x
) / area
.w
* (xRange
[1] - xRange
[0]);
353 var yRange
= this.yAxisRange();
354 ret
[1] = yRange
[0] + (area
.h
- y
) / area
.h
* (yRange
[1] - yRange
[0]);
361 * Returns the number of columns (including the independent variable).
363 Dygraph
.prototype.numColumns
= function() {
364 return this.rawData_
[0].length
;
368 * Returns the number of rows (excluding any header/label row).
370 Dygraph
.prototype.numRows
= function() {
371 return this.rawData_
.length
;
375 * Returns the value in the given row and column. If the row and column exceed
376 * the bounds on the data, returns null. Also returns null if the value is
379 Dygraph
.prototype.getValue
= function(row
, col
) {
380 if (row
< 0 || row
> this.rawData_
.length
) return null;
381 if (col
< 0 || col
> this.rawData_
[row
].length
) return null;
383 return this.rawData_
[row
][col
];
386 Dygraph
.addEvent
= function(el
, evt
, fn
) {
387 var normed_fn
= function(e
) {
388 if (!e
) var e
= window
.event
;
391 if (window
.addEventListener
) { // Mozilla, Netscape, Firefox
392 el
.addEventListener(evt
, normed_fn
, false);
394 el
.attachEvent('on' + evt
, normed_fn
);
398 Dygraph
.clipCanvas_
= function(cnv
, clip
) {
399 var ctx
= cnv
.getContext("2d");
401 ctx
.rect(clip
.left
, clip
.top
, clip
.width
, clip
.height
);
406 * Generates interface elements for the Dygraph: a containing div, a div to
407 * display the current point, and a textbox to adjust the rolling average
408 * period. Also creates the Renderer/Layout elements.
411 Dygraph
.prototype.createInterface_
= function() {
412 // Create the all-enclosing graph div
413 var enclosing
= this.maindiv_
;
415 this.graphDiv
= document
.createElement("div");
416 this.graphDiv
.style
.width
= this.width_
+ "px";
417 this.graphDiv
.style
.height
= this.height_
+ "px";
418 enclosing
.appendChild(this.graphDiv
);
422 left
: this.attr_("yAxisLabelWidth") + 2 * this.attr_("axisTickSize")
424 clip
.width
= this.width_
- clip
.left
- this.attr_("rightGap");
425 clip
.height
= this.height_
- this.attr_("axisLabelFontSize")
426 - 2 * this.attr_("axisTickSize");
427 this.clippingArea_
= clip
;
429 // Create the canvas for interactive parts of the chart.
430 this.canvas_
= Dygraph
.createCanvas();
431 this.canvas_
.style
.position
= "absolute";
432 this.canvas_
.width
= this.width_
;
433 this.canvas_
.height
= this.height_
;
434 this.canvas_
.style
.width
= this.width_
+ "px"; // for IE
435 this.canvas_
.style
.height
= this.height_
+ "px"; // for IE
437 // ... and for static parts of the chart.
438 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
440 // The interactive parts of the graph are drawn on top of the chart.
441 this.graphDiv
.appendChild(this.hidden_
);
442 this.graphDiv
.appendChild(this.canvas_
);
443 this.mouseEventElement_
= this.canvas_
;
445 // Make sure we don't overdraw.
446 Dygraph
.clipCanvas_(this.hidden_
, this.clippingArea_
);
447 Dygraph
.clipCanvas_(this.canvas_
, this.clippingArea_
);
450 Dygraph
.addEvent(this.mouseEventElement_
, 'mousemove', function(e
) {
451 dygraph
.mouseMove_(e
);
453 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseout', function(e
) {
454 dygraph
.mouseOut_(e
);
457 // Create the grapher
458 // TODO(danvk): why does the Layout need its own set of options?
459 this.layoutOptions_
= { 'xOriginIsZero': false };
460 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
461 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
462 Dygraph
.update(this.layoutOptions_
, {
463 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
465 this.layout_
= new DygraphLayout(this, this.layoutOptions_
);
467 // TODO(danvk): why does the Renderer need its own set of options?
468 this.renderOptions_
= { colorScheme
: this.colors_
,
470 axisLineWidth
: Dygraph
.AXIS_LINE_WIDTH
};
471 Dygraph
.update(this.renderOptions_
, this.attrs_
);
472 Dygraph
.update(this.renderOptions_
, this.user_attrs_
);
473 this.plotter_
= new DygraphCanvasRenderer(this,
474 this.hidden_
, this.layout_
,
475 this.renderOptions_
);
477 this.createStatusMessage_();
478 this.createRollInterface_();
479 this.createDragInterface_();
483 * Detach DOM elements in the dygraph and null out all data references.
484 * Calling this when you're done with a dygraph can dramatically reduce memory
485 * usage. See, e.g., the tests/perf.html example.
487 Dygraph
.prototype.destroy
= function() {
488 var removeRecursive
= function(node
) {
489 while (node
.hasChildNodes()) {
490 removeRecursive(node
.firstChild
);
491 node
.removeChild(node
.firstChild
);
494 removeRecursive(this.maindiv_
);
496 var nullOut
= function(obj
) {
498 if (typeof(obj
[n
]) === 'object') {
504 // These may not all be necessary, but it can't hurt...
505 nullOut(this.layout_
);
506 nullOut(this.plotter_
);
511 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
512 * this particular canvas. All Dygraph work is done on this.canvas_.
513 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
514 * @return {Object} The newly-created canvas
517 Dygraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
518 var h
= Dygraph
.createCanvas();
519 h
.style
.position
= "absolute";
520 // TODO(danvk): h should be offset from canvas. canvas needs to include
521 // some extra area to make it easier to zoom in on the far left and far
522 // right. h needs to be precisely the plot area, so that clipping occurs.
523 h
.style
.top
= canvas
.style
.top
;
524 h
.style
.left
= canvas
.style
.left
;
525 h
.width
= this.width_
;
526 h
.height
= this.height_
;
527 h
.style
.width
= this.width_
+ "px"; // for IE
528 h
.style
.height
= this.height_
+ "px"; // for IE
532 // Taken from MochiKit.Color
533 Dygraph
.hsvToRGB
= function (hue
, saturation
, value
) {
537 if (saturation
=== 0) {
542 var i
= Math
.floor(hue
* 6);
543 var f
= (hue
* 6) - i
;
544 var p
= value
* (1 - saturation
);
545 var q
= value
* (1 - (saturation
* f
));
546 var t
= value
* (1 - (saturation
* (1 - f
)));
548 case 1: red
= q
; green
= value
; blue
= p
; break;
549 case 2: red
= p
; green
= value
; blue
= t
; break;
550 case 3: red
= p
; green
= q
; blue
= value
; break;
551 case 4: red
= t
; green
= p
; blue
= value
; break;
552 case 5: red
= value
; green
= p
; blue
= q
; break;
553 case 6: // fall through
554 case 0: red
= value
; green
= t
; blue
= p
; break;
557 red
= Math
.floor(255 * red
+ 0.5);
558 green
= Math
.floor(255 * green
+ 0.5);
559 blue
= Math
.floor(255 * blue
+ 0.5);
560 return 'rgb(' + red
+ ',' + green
+ ',' + blue
+ ')';
565 * Generate a set of distinct colors for the data series. This is done with a
566 * color wheel. Saturation/Value are customizable, and the hue is
567 * equally-spaced around the color wheel. If a custom set of colors is
568 * specified, that is used instead.
571 Dygraph
.prototype.setColors_
= function() {
572 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
573 // away with this.renderOptions_.
574 var num
= this.attr_("labels").length
- 1;
576 var colors
= this.attr_('colors');
578 var sat
= this.attr_('colorSaturation') || 1.0;
579 var val
= this.attr_('colorValue') || 0.5;
580 var half
= Math
.ceil(num
/ 2);
581 for (var i
= 1; i
<= num
; i
++) {
582 if (!this.visibility()[i
-1]) continue;
583 // alternate colors for high contrast.
584 var idx
= i
% 2 ? Math
.ceil(i
/ 2) : (half + i / 2);
585 var hue
= (1.0 * idx
/ (1 + num
));
586 this.colors_
.push(Dygraph
.hsvToRGB(hue
, sat
, val
));
589 for (var i
= 0; i
< num
; i
++) {
590 if (!this.visibility()[i
]) continue;
591 var colorStr
= colors
[i
% colors
.length
];
592 this.colors_
.push(colorStr
);
596 // TODO(danvk): update this w/r
/t/ the
new options system
.
597 this.renderOptions_
.colorScheme
= this.colors_
;
598 Dygraph
.update(this.plotter_
.options
, this.renderOptions_
);
599 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
600 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
604 * Return the list of colors. This is either the list of colors passed in the
605 * attributes, or the autogenerated list of rgb(r,g,b) strings.
606 * @return {Array<string>} The list of colors.
608 Dygraph
.prototype.getColors
= function() {
612 // The following functions are from quirksmode.org with a modification for Safari from
613 // http://blog.firetree.net/2005/07/04/javascript-find-position/
614 // http://www.quirksmode.org/js
/findpos
.html
615 Dygraph
.findPosX
= function(obj
) {
620 curleft
+= obj
.offsetLeft
;
621 if(!obj
.offsetParent
)
623 obj
= obj
.offsetParent
;
630 Dygraph
.findPosY
= function(obj
) {
635 curtop
+= obj
.offsetTop
;
636 if(!obj
.offsetParent
)
638 obj
= obj
.offsetParent
;
648 * Create the div that contains information on the selected point(s)
649 * This goes in the top right of the canvas, unless an external div has already
653 Dygraph
.prototype.createStatusMessage_
= function() {
654 var userLabelsDiv
= this.user_attrs_
["labelsDiv"];
655 if (userLabelsDiv
&& null != userLabelsDiv
656 && (typeof(userLabelsDiv
) == "string" || userLabelsDiv
instanceof String
)) {
657 this.user_attrs_
["labelsDiv"] = document
.getElementById(userLabelsDiv
);
659 if (!this.attr_("labelsDiv")) {
660 var divWidth
= this.attr_('labelsDivWidth');
662 "position": "absolute",
665 "width": divWidth
+ "px",
667 "left": (this.width_
- divWidth
- 2) + "px",
668 "background": "white",
670 "overflow": "hidden"};
671 Dygraph
.update(messagestyle
, this.attr_('labelsDivStyles'));
672 var div
= document
.createElement("div");
673 for (var name
in messagestyle
) {
674 if (messagestyle
.hasOwnProperty(name
)) {
675 div
.style
[name
] = messagestyle
[name
];
678 this.graphDiv
.appendChild(div
);
679 this.attrs_
.labelsDiv
= div
;
684 * Create the text box to adjust the averaging period
685 * @return {Object} The newly-created text box
688 Dygraph
.prototype.createRollInterface_
= function() {
689 var display
= this.attr_('showRoller') ? "block" : "none";
690 var textAttr
= { "position": "absolute",
692 "top": (this.plotter_
.area
.h
- 25) + "px",
693 "left": (this.plotter_
.area
.x
+ 1) + "px",
696 var roller
= document
.createElement("input");
697 roller
.type
= "text";
699 roller
.value
= this.rollPeriod_
;
700 for (var name
in textAttr
) {
701 if (textAttr
.hasOwnProperty(name
)) {
702 roller
.style
[name
] = textAttr
[name
];
706 var pa
= this.graphDiv
;
707 pa
.appendChild(roller
);
709 roller
.onchange
= function() { dygraph
.adjustRoll(roller
.value
); };
713 // These functions are taken from MochiKit.Signal
714 Dygraph
.pageX
= function(e
) {
716 return (!e
.pageX
|| e
.pageX
< 0) ? 0 : e
.pageX
;
719 var b
= document
.body
;
721 (de
.scrollLeft
|| b
.scrollLeft
) -
722 (de
.clientLeft
|| 0);
726 Dygraph
.pageY
= function(e
) {
728 return (!e
.pageY
|| e
.pageY
< 0) ? 0 : e
.pageY
;
731 var b
= document
.body
;
733 (de
.scrollTop
|| b
.scrollTop
) -
739 * Set up all the mouse handlers needed to capture dragging behavior for zoom
743 Dygraph
.prototype.createDragInterface_
= function() {
746 // Tracks whether the mouse is down right now
747 var isZooming
= false;
748 var isPanning
= false;
749 var dragStartX
= null;
750 var dragStartY
= null;
754 var draggingDate
= null;
755 var dateRange
= null;
757 // Utility function to convert page-wide coordinates to canvas coords
760 var getX
= function(e
) { return Dygraph
.pageX(e
) - px
};
761 var getY
= function(e
) { return Dygraph
.pageY(e
) - py
};
763 // Draw zoom rectangles when the mouse is down and the user moves around
764 Dygraph
.addEvent(this.mouseEventElement_
, 'mousemove', function(event
) {
766 dragEndX
= getX(event
);
767 dragEndY
= getY(event
);
769 self
.drawZoomRect_(dragStartX
, dragEndX
, prevEndX
);
771 } else if (isPanning
) {
772 dragEndX
= getX(event
);
773 dragEndY
= getY(event
);
775 // Want to have it so that:
776 // 1. draggingDate appears at dragEndX
777 // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
779 self
.dateWindow_
[0] = draggingDate
- (dragEndX
/ self
.width_
) * dateRange
;
780 self
.dateWindow_
[1] = self
.dateWindow_
[0] + dateRange
;
781 self
.drawGraph_(self
.rawData_
);
785 // Track the beginning of drag events
786 Dygraph
.addEvent(this.mouseEventElement_
, 'mousedown', function(event
) {
787 px
= Dygraph
.findPosX(self
.canvas_
);
788 py
= Dygraph
.findPosY(self
.canvas_
);
789 dragStartX
= getX(event
);
790 dragStartY
= getY(event
);
792 if (event
.altKey
|| event
.shiftKey
) {
793 if (!self
.dateWindow_
) return; // have to be zoomed in to pan.
795 dateRange
= self
.dateWindow_
[1] - self
.dateWindow_
[0];
796 draggingDate
= (dragStartX
/ self
.width_
) * dateRange
+
803 // If the user releases the mouse button during a drag, but not over the
804 // canvas, then it doesn't count as a zooming action.
805 Dygraph
.addEvent(document
, 'mouseup', function(event
) {
806 if (isZooming
|| isPanning
) {
819 // Temporarily cancel the dragging event when the mouse leaves the graph
820 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseout', function(event
) {
827 // If the mouse is released on the canvas during a drag event, then it's a
828 // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
829 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseup', function(event
) {
832 dragEndX
= getX(event
);
833 dragEndY
= getY(event
);
834 var regionWidth
= Math
.abs(dragEndX
- dragStartX
);
835 var regionHeight
= Math
.abs(dragEndY
- dragStartY
);
837 if (regionWidth
< 2 && regionHeight
< 2 &&
838 self
.lastx_
!= undefined
&& self
.lastx_
!= -1) {
839 // TODO(danvk): pass along more info about the points, e.g. 'x'
840 if (self
.attr_('clickCallback') != null) {
841 self
.attr_('clickCallback')(event
, self
.lastx_
, self
.selPoints_
);
843 if (self
.attr_('pointClickCallback')) {
844 // check if the click was on a particular point.
846 var closestDistance
= 0;
847 for (var i
= 0; i
< self
.selPoints_
.length
; i
++) {
848 var p
= self
.selPoints_
[i
];
849 var distance
= Math
.pow(p
.canvasx
- dragEndX
, 2) +
850 Math
.pow(p
.canvasy
- dragEndY
, 2);
851 if (closestIdx
== -1 || distance
< closestDistance
) {
852 closestDistance
= distance
;
857 // Allow any click within two pixels of the dot.
858 var radius
= self
.attr_('highlightCircleSize') + 2;
859 if (closestDistance
<= 5 * 5) {
860 self
.attr_('pointClickCallback')(event
, self
.selPoints_
[closestIdx
]);
865 if (regionWidth
>= 10) {
866 self
.doZoom_(Math
.min(dragStartX
, dragEndX
),
867 Math
.max(dragStartX
, dragEndX
));
869 self
.canvas_
.getContext("2d").clearRect(0, 0,
871 self
.canvas_
.height
);
885 // Double-clicking zooms back out
886 Dygraph
.addEvent(this.mouseEventElement_
, 'dblclick', function(event
) {
887 if (self
.dateWindow_
== null) return;
888 self
.dateWindow_
= null;
889 self
.drawGraph_(self
.rawData_
);
890 var minDate
= self
.rawData_
[0][0];
891 var maxDate
= self
.rawData_
[self
.rawData_
.length
- 1][0];
892 if (self
.attr_("zoomCallback")) {
893 self
.attr_("zoomCallback")(minDate
, maxDate
);
899 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
900 * up any previous zoom rectangles that were drawn. This could be optimized to
901 * avoid extra redrawing, but it's tricky to avoid interactions with the status
903 * @param {Number} startX The X position where the drag started, in canvas
905 * @param {Number} endX The current X position of the drag, in canvas coords.
906 * @param {Number} prevEndX The value of endX on the previous call to this
907 * function. Used to avoid excess redrawing
910 Dygraph
.prototype.drawZoomRect_
= function(startX
, endX
, prevEndX
) {
911 var ctx
= this.canvas_
.getContext("2d");
913 // Clean up from the previous rect if necessary
915 ctx
.clearRect(Math
.min(startX
, prevEndX
), 0,
916 Math
.abs(startX
- prevEndX
), this.height_
);
919 // Draw a light-grey rectangle to show the new viewing area
920 if (endX
&& startX
) {
921 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
922 ctx
.fillRect(Math
.min(startX
, endX
), 0,
923 Math
.abs(endX
- startX
), this.height_
);
928 * Zoom to something containing [lowX, highX]. These are pixel coordinates
929 * in the canvas. The exact zoom window may be slightly larger if there are no
930 * data points near lowX or highX. This function redraws the graph.
931 * @param {Number} lowX The leftmost pixel value that should be visible.
932 * @param {Number} highX The rightmost pixel value that should be visible.
935 Dygraph
.prototype.doZoom_
= function(lowX
, highX
) {
936 // Find the earliest and latest dates contained in this canvasx range.
937 var r
= this.toDataCoords(lowX
, null);
939 r
= this.toDataCoords(highX
, null);
942 this.dateWindow_
= [minDate
, maxDate
];
943 this.drawGraph_(this.rawData_
);
944 if (this.attr_("zoomCallback")) {
945 this.attr_("zoomCallback")(minDate
, maxDate
);
950 * When the mouse moves in the canvas, display information about a nearby data
951 * point and draw dots over those points in the data series. This function
952 * takes care of cleanup of previously-drawn dots.
953 * @param {Object} event The mousemove event from the browser.
956 Dygraph
.prototype.mouseMove_
= function(event
) {
957 var canvasx
= Dygraph
.pageX(event
) - Dygraph
.findPosX(this.mouseEventElement_
);
958 var points
= this.layout_
.points
;
963 // Loop through all the points and find the date nearest to our current
965 var minDist
= 1e+100;
967 for (var i
= 0; i
< points
.length
; i
++) {
968 var dist
= Math
.abs(points
[i
].canvasx
- canvasx
);
969 if (dist
> minDist
) continue;
973 if (idx
>= 0) lastx
= points
[idx
].xval
;
974 // Check that you can really highlight the last day's data
975 if (canvasx
> points
[points
.length
-1].canvasx
)
976 lastx
= points
[points
.length
-1].xval
;
978 // Extract the points we've selected
979 this.selPoints_
= [];
980 var l
= points
.length
;
981 if (!this.attr_("stackedGraph")) {
982 for (var i
= 0; i
< l
; i
++) {
983 if (points
[i
].xval
== lastx
) {
984 this.selPoints_
.push(points
[i
]);
988 // Need to 'unstack' points starting from the bottom
989 var cumulative_sum
= 0;
990 for (var i
= l
- 1; i
>= 0; i
--) {
991 if (points
[i
].xval
== lastx
) {
992 var p
= {}; // Clone the point since we modify it
993 for (var k
in points
[i
]) {
996 p
.yval
-= cumulative_sum
;
997 cumulative_sum
+= p
.yval
;
998 this.selPoints_
.push(p
);
1001 this.selPoints_
.reverse();
1004 if (this.attr_("highlightCallback")) {
1005 var px
= this.lastx_
;
1006 if (px
!== null && lastx
!= px
) {
1007 // only fire if the selected point has changed.
1008 this.attr_("highlightCallback")(event
, lastx
, this.selPoints_
);
1012 // Save last x position for callbacks.
1013 this.lastx_
= lastx
;
1015 this.updateSelection_();
1019 * Draw dots over the selectied points in the data series. This function
1020 * takes care of cleanup of previously-drawn dots.
1023 Dygraph
.prototype.updateSelection_
= function() {
1024 // Clear the previously drawn vertical, if there is one
1025 var ctx
= this.canvas_
.getContext("2d");
1026 if (this.previousVerticalX_
>= 0) {
1027 // Determine the maximum highlight circle size.
1028 var maxCircleSize
= 0;
1029 var labels
= this.attr_('labels');
1030 for (var i
= 1; i
< labels
.length
; i
++) {
1031 var r
= this.attr_('highlightCircleSize', labels
[i
]);
1032 if (r
> maxCircleSize
) maxCircleSize
= r
;
1034 var px
= this.previousVerticalX_
;
1035 ctx
.clearRect(px
- maxCircleSize
- 1, 0,
1036 2 * maxCircleSize
+ 2, this.height_
);
1039 var isOK
= function(x
) { return x
&& !isNaN(x
); };
1041 if (this.selPoints_
.length
> 0) {
1042 var canvasx
= this.selPoints_
[0].canvasx
;
1044 // Set the status message to indicate the selected point(s)
1045 var replace
= this.attr_('xValueFormatter')(this.lastx_
, this) + ":";
1046 var fmtFunc
= this.attr_('yValueFormatter');
1047 var clen
= this.colors_
.length
;
1049 if (this.attr_('showLabelsOnHighlight')) {
1050 // Set the status message to indicate the selected point(s)
1051 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1052 if (!this.attr_("labelsShowZeroValues") && this.selPoints_
[i
].yval
== 0) continue;
1053 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1054 if (this.attr_("labelsSeparateLines")) {
1057 var point
= this.selPoints_
[i
];
1058 var c
= new RGBColor(this.colors_
[i
%clen
]);
1059 var yval
= fmtFunc(point
.yval
);
1060 replace
+= " <b><font color='" + c
.toHex() + "'>"
1061 + point
.name
+ "</font></b>:"
1065 this.attr_("labelsDiv").innerHTML
= replace
;
1068 // Draw colored circles over the center of each selected point
1070 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1071 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1073 this.attr_('highlightCircleSize', this.selPoints_
[i
].name
);
1075 ctx
.fillStyle
= this.plotter_
.colors
[this.selPoints_
[i
].name
];
1076 ctx
.arc(canvasx
, this.selPoints_
[i
].canvasy
, circleSize
,
1077 0, 2 * Math
.PI
, false);
1082 this.previousVerticalX_
= canvasx
;
1087 * Set manually set selected dots, and display information about them
1088 * @param int row number that should by highlighted
1089 * false value clears the selection
1092 Dygraph
.prototype.setSelection
= function(row
) {
1093 // Extract the points we've selected
1094 this.selPoints_
= [];
1097 if (row
!== false) {
1098 row
= row
-this.boundaryIds_
[0][0];
1101 if (row
!== false && row
>= 0) {
1102 for (var i
in this.layout_
.datasets
) {
1103 if (row
< this.layout_
.datasets
[i
].length
) {
1104 var point
= this.layout_
.points
[pos
+row
];
1106 if (this.attr_("stackedGraph")) {
1107 point
= this.layout_
.unstackPointAtIndex(pos
+row
);
1110 this.selPoints_
.push(point
);
1112 pos
+= this.layout_
.datasets
[i
].length
;
1116 if (this.selPoints_
.length
) {
1117 this.lastx_
= this.selPoints_
[0].xval
;
1118 this.updateSelection_();
1121 this.clearSelection();
1127 * The mouse has left the canvas. Clear out whatever artifacts remain
1128 * @param {Object} event the mouseout event from the browser.
1131 Dygraph
.prototype.mouseOut_
= function(event
) {
1132 if (this.attr_("unhighlightCallback")) {
1133 this.attr_("unhighlightCallback")(event
);
1136 if (this.attr_("hideOverlayOnMouseOut")) {
1137 this.clearSelection();
1142 * Remove all selection from the canvas
1145 Dygraph
.prototype.clearSelection
= function() {
1146 // Get rid of the overlay data
1147 var ctx
= this.canvas_
.getContext("2d");
1148 ctx
.clearRect(0, 0, this.width_
, this.height_
);
1149 this.attr_("labelsDiv").innerHTML
= "";
1150 this.selPoints_
= [];
1155 * Returns the number of the currently selected row
1156 * @return int row number, of -1 if nothing is selected
1159 Dygraph
.prototype.getSelection
= function() {
1160 if (!this.selPoints_
|| this.selPoints_
.length
< 1) {
1164 for (var row
=0; row
<this.layout_
.points
.length
; row
++ ) {
1165 if (this.layout_
.points
[row
].x
== this.selPoints_
[0].x
) {
1166 return row
+ this.boundaryIds_
[0][0];
1172 Dygraph
.zeropad
= function(x
) {
1173 if (x
< 10) return "0" + x
; else return "" + x
;
1177 * Return a string version of the hours, minutes and seconds portion of a date.
1178 * @param {Number} date The JavaScript date (ms since epoch)
1179 * @return {String} A time of the form "HH:MM:SS"
1182 Dygraph
.hmsString_
= function(date
) {
1183 var zeropad
= Dygraph
.zeropad
;
1184 var d
= new Date(date
);
1185 if (d
.getSeconds()) {
1186 return zeropad(d
.getHours()) + ":" +
1187 zeropad(d
.getMinutes()) + ":" +
1188 zeropad(d
.getSeconds());
1190 return zeropad(d
.getHours()) + ":" + zeropad(d
.getMinutes());
1195 * Convert a JS date to a string appropriate to display on an axis that
1196 * is displaying values at the stated granularity.
1197 * @param {Date} date The date to format
1198 * @param {Number} granularity One of the Dygraph granularity constants
1199 * @return {String} The formatted date
1202 Dygraph
.dateAxisFormatter
= function(date
, granularity
) {
1203 if (granularity
>= Dygraph
.MONTHLY
) {
1204 return date
.strftime('%b %y');
1206 var frac
= date
.getHours() * 3600 + date
.getMinutes() * 60 + date
.getSeconds() + date
.getMilliseconds();
1207 if (frac
== 0 || granularity
>= Dygraph
.DAILY
) {
1208 return new Date(date
.getTime() + 3600*1000).strftime('%d%b');
1210 return Dygraph
.hmsString_(date
.getTime());
1216 * Convert a JS date (millis since epoch) to YYYY/MM/DD
1217 * @param {Number} date The JavaScript date (ms since epoch)
1218 * @return {String} A date of the form "YYYY/MM/DD"
1221 Dygraph
.dateString_
= function(date
, self
) {
1222 var zeropad
= Dygraph
.zeropad
;
1223 var d
= new Date(date
);
1226 var year
= "" + d
.getFullYear();
1227 // Get a 0 padded month string
1228 var month
= zeropad(d
.getMonth() + 1); //months are 0-offset, sigh
1229 // Get a 0 padded day string
1230 var day
= zeropad(d
.getDate());
1233 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
1234 if (frac
) ret
= " " + Dygraph
.hmsString_(date
);
1236 return year
+ "/" + month + "/" + day
+ ret
;
1240 * Round a number to the specified number of digits past the decimal point.
1241 * @param {Number} num The number to round
1242 * @param {Number} places The number of decimals to which to round
1243 * @return {Number} The rounded number
1246 Dygraph
.round_
= function(num
, places
) {
1247 var shift
= Math
.pow(10, places
);
1248 return Math
.round(num
* shift
)/shift
;
1252 * Fires when there's data available to be graphed.
1253 * @param {String} data Raw CSV data to be plotted
1256 Dygraph
.prototype.loadedEvent_
= function(data
) {
1257 this.rawData_
= this.parseCSV_(data
);
1258 this.drawGraph_(this.rawData_
);
1261 Dygraph
.prototype.months
= ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
1262 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1263 Dygraph
.prototype.quarters
= ["Jan", "Apr", "Jul", "Oct"];
1266 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1269 Dygraph
.prototype.addXTicks_
= function() {
1270 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1271 var startDate
, endDate
;
1272 if (this.dateWindow_
) {
1273 startDate
= this.dateWindow_
[0];
1274 endDate
= this.dateWindow_
[1];
1276 startDate
= this.rawData_
[0][0];
1277 endDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1280 var xTicks
= this.attr_('xTicker')(startDate
, endDate
, this);
1281 this.layout_
.updateOptions({xTicks
: xTicks
});
1284 // Time granularity enumeration
1285 Dygraph
.SECONDLY
= 0;
1286 Dygraph
.TWO_SECONDLY
= 1;
1287 Dygraph
.FIVE_SECONDLY
= 2;
1288 Dygraph
.TEN_SECONDLY
= 3;
1289 Dygraph
.THIRTY_SECONDLY
= 4;
1290 Dygraph
.MINUTELY
= 5;
1291 Dygraph
.TWO_MINUTELY
= 6;
1292 Dygraph
.FIVE_MINUTELY
= 7;
1293 Dygraph
.TEN_MINUTELY
= 8;
1294 Dygraph
.THIRTY_MINUTELY
= 9;
1295 Dygraph
.HOURLY
= 10;
1296 Dygraph
.TWO_HOURLY
= 11;
1297 Dygraph
.SIX_HOURLY
= 12;
1299 Dygraph
.WEEKLY
= 14;
1300 Dygraph
.MONTHLY
= 15;
1301 Dygraph
.QUARTERLY
= 16;
1302 Dygraph
.BIANNUAL
= 17;
1303 Dygraph
.ANNUAL
= 18;
1304 Dygraph
.DECADAL
= 19;
1305 Dygraph
.NUM_GRANULARITIES
= 20;
1307 Dygraph
.SHORT_SPACINGS
= [];
1308 Dygraph
.SHORT_SPACINGS
[Dygraph
.SECONDLY
] = 1000 * 1;
1309 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_SECONDLY
] = 1000 * 2;
1310 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_SECONDLY
] = 1000 * 5;
1311 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_SECONDLY
] = 1000 * 10;
1312 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_SECONDLY
] = 1000 * 30;
1313 Dygraph
.SHORT_SPACINGS
[Dygraph
.MINUTELY
] = 1000 * 60;
1314 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_MINUTELY
] = 1000 * 60 * 2;
1315 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_MINUTELY
] = 1000 * 60 * 5;
1316 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_MINUTELY
] = 1000 * 60 * 10;
1317 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_MINUTELY
] = 1000 * 60 * 30;
1318 Dygraph
.SHORT_SPACINGS
[Dygraph
.HOURLY
] = 1000 * 3600;
1319 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_HOURLY
] = 1000 * 3600 * 2;
1320 Dygraph
.SHORT_SPACINGS
[Dygraph
.SIX_HOURLY
] = 1000 * 3600 * 6;
1321 Dygraph
.SHORT_SPACINGS
[Dygraph
.DAILY
] = 1000 * 86400;
1322 Dygraph
.SHORT_SPACINGS
[Dygraph
.WEEKLY
] = 1000 * 604800;
1326 // If we used this time granularity, how many ticks would there be?
1327 // This is only an approximation, but it's generally good enough.
1329 Dygraph
.prototype.NumXTicks
= function(start_time
, end_time
, granularity
) {
1330 if (granularity
< Dygraph
.MONTHLY
) {
1331 // Generate one tick mark for every fixed interval of time.
1332 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1333 return Math
.floor(0.5 + 1.0 * (end_time
- start_time
) / spacing
);
1335 var year_mod
= 1; // e.g. to only print one point every 10 years.
1336 var num_months
= 12;
1337 if (granularity
== Dygraph
.QUARTERLY
) num_months
= 3;
1338 if (granularity
== Dygraph
.BIANNUAL
) num_months
= 2;
1339 if (granularity
== Dygraph
.ANNUAL
) num_months
= 1;
1340 if (granularity
== Dygraph
.DECADAL
) { num_months
= 1; year_mod
= 10; }
1342 var msInYear
= 365.2524 * 24 * 3600 * 1000;
1343 var num_years
= 1.0 * (end_time
- start_time
) / msInYear
;
1344 return Math
.floor(0.5 + 1.0 * num_years
* num_months
/ year_mod
);
1350 // Construct an x-axis of nicely-formatted times on meaningful boundaries
1351 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1353 // Returns an array containing {v: millis, label: label} dictionaries.
1355 Dygraph
.prototype.GetXAxis
= function(start_time
, end_time
, granularity
) {
1356 var formatter
= this.attr_("xAxisLabelFormatter");
1358 if (granularity
< Dygraph
.MONTHLY
) {
1359 // Generate one tick mark for every fixed interval of time.
1360 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1361 var format
= '%d%b'; // e.g. "1Jan"
1363 // Find a time less than start_time which occurs on a "nice" time boundary
1364 // for this granularity.
1365 var g
= spacing
/ 1000;
1366 var d
= new Date(start_time
);
1367 if (g
<= 60) { // seconds
1368 var x
= d
.getSeconds(); d
.setSeconds(x
- x
% g
);
1372 if (g
<= 60) { // minutes
1373 var x
= d
.getMinutes(); d
.setMinutes(x
- x
% g
);
1378 if (g
<= 24) { // days
1379 var x
= d
.getHours(); d
.setHours(x
- x
% g
);
1384 if (g
== 7) { // one week
1385 d
.setDate(d
.getDate() - d
.getDay());
1390 start_time
= d
.getTime();
1392 for (var t
= start_time
; t
<= end_time
; t
+= spacing
) {
1393 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1396 // Display a tick mark on the first of a set of months of each year.
1397 // Years get a tick mark iff y % year_mod == 0. This is useful for
1398 // displaying a tick mark once every 10 years, say, on long time scales.
1400 var year_mod
= 1; // e.g. to only print one point every 10 years.
1402 if (granularity
== Dygraph
.MONTHLY
) {
1403 months
= [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
1404 } else if (granularity
== Dygraph
.QUARTERLY
) {
1405 months
= [ 0, 3, 6, 9 ];
1406 } else if (granularity
== Dygraph
.BIANNUAL
) {
1408 } else if (granularity
== Dygraph
.ANNUAL
) {
1410 } else if (granularity
== Dygraph
.DECADAL
) {
1415 var start_year
= new Date(start_time
).getFullYear();
1416 var end_year
= new Date(end_time
).getFullYear();
1417 var zeropad
= Dygraph
.zeropad
;
1418 for (var i
= start_year
; i
<= end_year
; i
++) {
1419 if (i
% year_mod
!= 0) continue;
1420 for (var j
= 0; j
< months
.length
; j
++) {
1421 var date_str
= i
+ "/" + zeropad(1 + months[j]) + "/01";
1422 var t
= Date
.parse(date_str
);
1423 if (t
< start_time
|| t
> end_time
) continue;
1424 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1434 * Add ticks to the x-axis based on a date range.
1435 * @param {Number} startDate Start of the date window (millis since epoch)
1436 * @param {Number} endDate End of the date window (millis since epoch)
1437 * @return {Array.<Object>} Array of {label, value} tuples.
1440 Dygraph
.dateTicker
= function(startDate
, endDate
, self
) {
1442 for (var i
= 0; i
< Dygraph
.NUM_GRANULARITIES
; i
++) {
1443 var num_ticks
= self
.NumXTicks(startDate
, endDate
, i
);
1444 if (self
.width_
/ num_ticks
>= self
.attr_('pixelsPerXLabel')) {
1451 return self
.GetXAxis(startDate
, endDate
, chosen
);
1453 // TODO(danvk): signal error.
1458 * Add ticks when the x axis has numbers on it (instead of dates)
1459 * @param {Number} startDate Start of the date window (millis since epoch)
1460 * @param {Number} endDate End of the date window (millis since epoch)
1461 * @return {Array.<Object>} Array of {label, value} tuples.
1464 Dygraph
.numericTicks
= function(minV
, maxV
, self
) {
1466 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1467 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks
).
1468 // The first spacing greater than pixelsPerYLabel is what we use.
1469 // TODO(danvk): version that works on a log scale.
1470 if (self
.attr_("labelsKMG2")) {
1471 var mults
= [1, 2, 4, 8];
1473 var mults
= [1, 2, 5];
1475 var scale
, low_val
, high_val
, nTicks
;
1476 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1477 var pixelsPerTick
= self
.attr_('pixelsPerYLabel');
1478 for (var i
= -10; i
< 50; i
++) {
1479 if (self
.attr_("labelsKMG2")) {
1480 var base_scale
= Math
.pow(16, i
);
1482 var base_scale
= Math
.pow(10, i
);
1484 for (var j
= 0; j
< mults
.length
; j
++) {
1485 scale
= base_scale
* mults
[j
];
1486 low_val
= Math
.floor(minV
/ scale
) * scale
;
1487 high_val
= Math
.ceil(maxV
/ scale
) * scale
;
1488 nTicks
= Math
.abs(high_val
- low_val
) / scale
;
1489 var spacing
= self
.height_
/ nTicks
;
1490 // wish I could break out of both loops at once...
1491 if (spacing
> pixelsPerTick
) break;
1493 if (spacing
> pixelsPerTick
) break;
1496 // Construct labels for the ticks
1500 if (self
.attr_("labelsKMB")) {
1502 k_labels
= [ "K", "M", "B", "T" ];
1504 if (self
.attr_("labelsKMG2")) {
1505 if (k
) self
.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
1507 k_labels
= [ "k", "M", "G", "T" ];
1510 // Allow reverse y-axis if it's explicitly requested.
1511 if (low_val
> high_val
) scale
*= -1;
1513 for (var i
= 0; i
< nTicks
; i
++) {
1514 var tickV
= low_val
+ i
* scale
;
1515 var absTickV
= Math
.abs(tickV
);
1516 var label
= Dygraph
.round_(tickV
, 2);
1517 if (k_labels
.length
) {
1518 // Round up to an appropriate unit.
1520 for (var j
= 3; j
>= 0; j
--, n
/= k
) {
1521 if (absTickV
>= n
) {
1522 label
= Dygraph
.round_(tickV
/ n
, 1) + k_labels
[j
];
1527 ticks
.push( {label
: label
, v
: tickV
} );
1533 * Adds appropriate ticks on the y-axis
1534 * @param {Number} minY The minimum Y value in the data set
1535 * @param {Number} maxY The maximum Y value in the data set
1538 Dygraph
.prototype.addYTicks_
= function(minY
, maxY
) {
1539 // Set the number of ticks so that the labels are human-friendly.
1540 // TODO(danvk): make this an attribute as well.
1541 var ticks
= Dygraph
.numericTicks(minY
, maxY
, this);
1542 this.layout_
.updateOptions( { yAxis
: [minY
, maxY
],
1546 // Computes the range of the data series (including confidence intervals).
1547 // series is either [ [x1, y1], [x2, y2], ... ] or
1548 // [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1549 // Returns [low, high]
1550 Dygraph
.prototype.extremeValues_
= function(series
) {
1551 var minY
= null, maxY
= null;
1553 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1555 // With custom bars, maxY is the max of the high values.
1556 for (var j
= 0; j
< series
.length
; j
++) {
1557 var y
= series
[j
][1][0];
1559 var low
= y
- series
[j
][1][1];
1560 var high
= y
+ series
[j
][1][2];
1561 if (low
> y
) low
= y
; // this can happen with custom bars,
1562 if (high
< y
) high
= y
; // e.g. in tests/custom-bars
.html
1563 if (maxY
== null || high
> maxY
) {
1566 if (minY
== null || low
< minY
) {
1571 for (var j
= 0; j
< series
.length
; j
++) {
1572 var y
= series
[j
][1];
1573 if (y
=== null || isNaN(y
)) continue;
1574 if (maxY
== null || y
> maxY
) {
1577 if (minY
== null || y
< minY
) {
1583 return [minY
, maxY
];
1587 * Update the graph with new data. Data is in the format
1588 * [ [date1, val1, val2, ...], [date2, val1, val2, ...] if errorBars=false
1589 * or, if errorBars=true,
1590 * [ [date1, [val1,stddev1], [val2,stddev2], ...], [date2, ...], ...]
1591 * @param {Array.<Object>} data The data (see above)
1594 Dygraph
.prototype.drawGraph_
= function(data
) {
1595 // This is used to set the second parameter to drawCallback, below.
1596 var is_initial_draw
= this.is_initial_draw_
;
1597 this.is_initial_draw_
= false;
1599 var minY
= null, maxY
= null;
1600 this.layout_
.removeAllDatasets();
1602 this.attrs_
['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
1604 // Loop over the fields (series). Go from the last to the first,
1605 // because if they're stacked that's how we accumulate the values.
1607 var cumulative_y
= []; // For stacked series.
1610 // Loop over all fields and create datasets
1611 for (var i
= data
[0].length
- 1; i
>= 1; i
--) {
1612 if (!this.visibility()[i
- 1]) continue;
1614 var connectSeparatedPoints
= this.attr_('connectSeparatedPoints', i
);
1617 for (var j
= 0; j
< data
.length
; j
++) {
1618 if (data
[j
][i
] != null || !connectSeparatedPoints
) {
1619 var date
= data
[j
][0];
1620 series
.push([date
, data
[j
][i
]]);
1623 series
= this.rollingAverage(series
, this.rollPeriod_
);
1625 // Prune down to the desired range, if necessary (for zooming)
1626 // Because there can be lines going to points outside of the visible area,
1627 // we actually prune to visible points, plus one on either side.
1628 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1629 if (this.dateWindow_
) {
1630 var low
= this.dateWindow_
[0];
1631 var high
= this.dateWindow_
[1];
1633 // TODO(danvk): do binary search instead of linear search.
1634 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
1635 var firstIdx
= null, lastIdx
= null;
1636 for (var k
= 0; k
< series
.length
; k
++) {
1637 if (series
[k
][0] >= low
&& firstIdx
=== null) {
1640 if (series
[k
][0] <= high
) {
1644 if (firstIdx
=== null) firstIdx
= 0;
1645 if (firstIdx
> 0) firstIdx
--;
1646 if (lastIdx
=== null) lastIdx
= series
.length
- 1;
1647 if (lastIdx
< series
.length
- 1) lastIdx
++;
1648 this.boundaryIds_
[i
-1] = [firstIdx
, lastIdx
];
1649 for (var k
= firstIdx
; k
<= lastIdx
; k
++) {
1650 pruned
.push(series
[k
]);
1654 this.boundaryIds_
[i
-1] = [0, series
.length
-1];
1657 var extremes
= this.extremeValues_(series
);
1658 var thisMinY
= extremes
[0];
1659 var thisMaxY
= extremes
[1];
1660 if (minY
=== null || thisMinY
< minY
) minY
= thisMinY
;
1661 if (maxY
=== null || thisMaxY
> maxY
) maxY
= thisMaxY
;
1664 for (var j
=0; j
<series
.length
; j
++) {
1665 val
= [series
[j
][0], series
[j
][1][0], series
[j
][1][1], series
[j
][1][2]];
1668 } else if (this.attr_("stackedGraph")) {
1669 var l
= series
.length
;
1671 for (var j
= 0; j
< l
; j
++) {
1672 // If one data set has a NaN, let all subsequent stacked
1673 // sets inherit the NaN -- only start at 0 for the first set.
1674 var x
= series
[j
][0];
1675 if (cumulative_y
[x
] === undefined
)
1676 cumulative_y
[x
] = 0;
1678 actual_y
= series
[j
][1];
1679 cumulative_y
[x
] += actual_y
;
1681 series
[j
] = [x
, cumulative_y
[x
]]
1683 if (!maxY
|| cumulative_y
[x
] > maxY
)
1684 maxY
= cumulative_y
[x
];
1688 datasets
[i
] = series
;
1691 for (var i
= 1; i
< datasets
.length
; i
++) {
1692 if (!this.visibility()[i
- 1]) continue;
1693 this.layout_
.addDataset(this.attr_("labels")[i
], datasets
[i
]);
1696 // Use some heuristics to come up with a good maxY value, unless it's been
1697 // set explicitly by the user.
1698 if (this.valueRange_
!= null) {
1699 this.addYTicks_(this.valueRange_
[0], this.valueRange_
[1]);
1700 this.displayedYRange_
= this.valueRange_
;
1702 // This affects the calculation of span, below.
1703 if (this.attr_("includeZero") && minY
> 0) {
1707 // Add some padding and round up to an integer to be human-friendly.
1708 var span
= maxY
- minY
;
1709 // special case: if we have no sense of scale, use +/-10% of the sole value
.
1710 if (span
== 0) { span
= maxY
; }
1711 var maxAxisY
= maxY
+ 0.1 * span
;
1712 var minAxisY
= minY
- 0.1 * span
;
1714 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
1715 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
1716 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
1718 if (this.attr_("includeZero")) {
1719 if (maxY
< 0) maxAxisY
= 0;
1720 if (minY
> 0) minAxisY
= 0;
1723 this.addYTicks_(minAxisY
, maxAxisY
);
1724 this.displayedYRange_
= [minAxisY
, maxAxisY
];
1729 // Tell PlotKit to use this new data and render itself
1730 this.layout_
.updateOptions({dateWindow
: this.dateWindow_
});
1731 this.layout_
.evaluateWithError();
1732 this.plotter_
.clear();
1733 this.plotter_
.render();
1734 this.canvas_
.getContext('2d').clearRect(0, 0, this.canvas_
.width
,
1735 this.canvas_
.height
);
1737 if (this.attr_("drawCallback") !== null) {
1738 this.attr_("drawCallback")(this, is_initial_draw
);
1743 * Calculates the rolling average of a data set.
1744 * If originalData is [label, val], rolls the average of those.
1745 * If originalData is [label, [, it's interpreted as [value, stddev]
1746 * and the roll is returned in the same form, with appropriately reduced
1747 * stddev for each value.
1748 * Note that this is where fractional input (i.e. '5/10') is converted into
1750 * @param {Array} originalData The data in the appropriate format (see above)
1751 * @param {Number} rollPeriod The number of days over which to average the data
1753 Dygraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
1754 if (originalData
.length
< 2)
1755 return originalData
;
1756 var rollPeriod
= Math
.min(rollPeriod
, originalData
.length
- 1);
1757 var rollingData
= [];
1758 var sigma
= this.attr_("sigma");
1760 if (this.fractions_
) {
1762 var den
= 0; // numerator/denominator
1764 for (var i
= 0; i
< originalData
.length
; i
++) {
1765 num
+= originalData
[i
][1][0];
1766 den
+= originalData
[i
][1][1];
1767 if (i
- rollPeriod
>= 0) {
1768 num
-= originalData
[i
- rollPeriod
][1][0];
1769 den
-= originalData
[i
- rollPeriod
][1][1];
1772 var date
= originalData
[i
][0];
1773 var value
= den
? num
/ den
: 0.0;
1774 if (this.attr_("errorBars")) {
1775 if (this.wilsonInterval_
) {
1776 // For more details on this confidence interval, see:
1777 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
1779 var p
= value
< 0 ? 0 : value
, n
= den
;
1780 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
1781 var denom
= 1 + sigma
* sigma
/ den
;
1782 var low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
1783 var high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
1784 rollingData
[i
] = [date
,
1785 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
1787 rollingData
[i
] = [date
, [0, 0, 0]];
1790 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
1791 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
1794 rollingData
[i
] = [date
, mult
* value
];
1797 } else if (this.attr_("customBars")) {
1802 for (var i
= 0; i
< originalData
.length
; i
++) {
1803 var data
= originalData
[i
][1];
1805 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
1807 if (y
!= null && !isNaN(y
)) {
1813 if (i
- rollPeriod
>= 0) {
1814 var prev
= originalData
[i
- rollPeriod
];
1815 if (prev
[1][1] != null && !isNaN(prev
[1][1])) {
1822 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
1823 1.0 * (mid
- low
) / count
,
1824 1.0 * (high
- mid
) / count
]];
1827 // Calculate the rolling average for the first rollPeriod - 1 points where
1828 // there is not enough data to roll over the full number of days
1829 var num_init_points
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
1830 if (!this.attr_("errorBars")){
1831 if (rollPeriod
== 1) {
1832 return originalData
;
1835 for (var i
= 0; i
< originalData
.length
; i
++) {
1838 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
1839 var y
= originalData
[j
][1];
1840 if (y
== null || isNaN(y
)) continue;
1842 sum
+= originalData
[j
][1];
1845 rollingData
[i
] = [originalData
[i
][0], sum
/ num_ok
];
1847 rollingData
[i
] = [originalData
[i
][0], null];
1852 for (var i
= 0; i
< originalData
.length
; i
++) {
1856 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
1857 var y
= originalData
[j
][1][0];
1858 if (y
== null || isNaN(y
)) continue;
1860 sum
+= originalData
[j
][1][0];
1861 variance
+= Math
.pow(originalData
[j
][1][1], 2);
1864 var stddev
= Math
.sqrt(variance
) / num_ok
;
1865 rollingData
[i
] = [originalData
[i
][0],
1866 [sum
/ num_ok
, sigma
* stddev
, sigma
* stddev
]];
1868 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
1878 * Parses a date, returning the number of milliseconds since epoch. This can be
1879 * passed in as an xValueParser in the Dygraph constructor.
1880 * TODO(danvk): enumerate formats that this understands.
1881 * @param {String} A date in YYYYMMDD format.
1882 * @return {Number} Milliseconds since epoch.
1885 Dygraph
.dateParser
= function(dateStr
, self
) {
1888 if (dateStr
.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
1889 dateStrSlashed
= dateStr
.replace("-", "/", "g");
1890 while (dateStrSlashed
.search("-") != -1) {
1891 dateStrSlashed
= dateStrSlashed
.replace("-", "/");
1893 d
= Date
.parse(dateStrSlashed
);
1894 } else if (dateStr
.length
== 8) { // e.g. '20090712'
1895 // TODO(danvk): remove support for this format. It's confusing.
1896 dateStrSlashed
= dateStr
.substr(0,4) + "/" + dateStr
.substr(4,2)
1897 + "/" + dateStr
.substr(6,2);
1898 d
= Date
.parse(dateStrSlashed
);
1900 // Any format that Date.parse will accept, e.g. "2009/07/12" or
1901 // "2009/07/12 12:34:56"
1902 d
= Date
.parse(dateStr
);
1905 if (!d
|| isNaN(d
)) {
1906 self
.error("Couldn't parse " + dateStr
+ " as a date");
1912 * Detects the type of the str (date or numeric) and sets the various
1913 * formatting attributes in this.attrs_ based on this type.
1914 * @param {String} str An x value.
1917 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
1919 if (str
.indexOf('-') >= 0 ||
1920 str
.indexOf('/') >= 0 ||
1921 isNaN(parseFloat(str
))) {
1923 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
1924 // TODO(danvk): remove support for this format.
1929 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
1930 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
1931 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
1932 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
1934 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1935 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
1936 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1937 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
1942 * Parses a string in a special csv format. We expect a csv file where each
1943 * line is a date point, and the first field in each line is the date string.
1944 * We also expect that all remaining fields represent series.
1945 * if the errorBars attribute is set, then interpret the fields as:
1946 * date, series1, stddev1, series2, stddev2, ...
1947 * @param {Array.<Object>} data See above.
1950 * @return Array.<Object> An array with one entry for each row. These entries
1951 * are an array of cells in that row. The first entry is the parsed x-value for
1952 * the row. The second, third, etc. are the y-values. These can take on one of
1953 * three forms, depending on the CSV and constructor parameters:
1955 * 2. [ value, stddev ]
1956 * 3. [ low value, center value, high value ]
1958 Dygraph
.prototype.parseCSV_
= function(data
) {
1960 var lines
= data
.split("\n");
1962 // Use the default delimiter or fall back to a tab if that makes sense.
1963 var delim
= this.attr_('delimiter');
1964 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
1969 if (this.labelsFromCSV_
) {
1971 this.attrs_
.labels
= lines
[0].split(delim
);
1974 // Parse the x as a float or return null if it's not a number.
1975 var parseFloatOrNull
= function(x
) {
1976 var val
= parseFloat(x
);
1977 return isNaN(val
) ? null : val
;
1981 var defaultParserSet
= false; // attempt to auto-detect x value type
1982 var expectedCols
= this.attr_("labels").length
;
1983 var outOfOrder
= false;
1984 for (var i
= start
; i
< lines
.length
; i
++) {
1985 var line
= lines
[i
];
1986 if (line
.length
== 0) continue; // skip blank lines
1987 if (line
[0] == '#') continue; // skip comment lines
1988 var inFields
= line
.split(delim
);
1989 if (inFields
.length
< 2) continue;
1992 if (!defaultParserSet
) {
1993 this.detectTypeFromString_(inFields
[0]);
1994 xParser
= this.attr_("xValueParser");
1995 defaultParserSet
= true;
1997 fields
[0] = xParser(inFields
[0], this);
1999 // If fractions are expected, parse the numbers as "A/B
"
2000 if (this.fractions_) {
2001 for (var j = 1; j < inFields.length; j++) {
2002 // TODO(danvk): figure out an appropriate way to flag parse errors.
2003 var vals = inFields[j].split("/");
2004 fields[j] = [parseFloatOrNull(vals[0]), parseFloatOrNull(vals[1])];
2006 } else if (this.attr_("errorBars
")) {
2007 // If there are error bars, values are (value, stddev) pairs
2008 for (var j = 1; j < inFields.length; j += 2)
2009 fields[(j + 1) / 2] = [parseFloatOrNull(inFields[j]),
2010 parseFloatOrNull(inFields[j + 1])];
2011 } else if (this.attr_("customBars
")) {
2012 // Bars are a low;center;high tuple
2013 for (var j = 1; j < inFields.length; j++) {
2014 var vals = inFields[j].split(";");
2015 fields[j] = [ parseFloatOrNull(vals[0]),
2016 parseFloatOrNull(vals[1]),
2017 parseFloatOrNull(vals[2]) ];
2020 // Values are just numbers
2021 for (var j = 1; j < inFields.length; j++) {
2022 fields[j] = parseFloatOrNull(inFields[j]);
2025 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
2030 if (fields.length != expectedCols) {
2031 this.error("Number of columns
in line
" + i + " (" + fields.length +
2032 ") does not agree
with number of
labels (" + expectedCols +
2038 this.warn("CSV is out of order
; order it correctly to speed loading
.");
2039 ret.sort(function(a,b) { return a[0] - b[0] });
2046 * The user has provided their data as a pre-packaged JS array. If the x values
2047 * are numeric, this is the same as dygraphs' internal format. If the x values
2048 * are dates, we need to convert them from Date objects to ms since epoch.
2049 * @param {Array.<Object>} data
2050 * @return {Array.<Object>} data with numeric x values.
2052 Dygraph.prototype.parseArray_ = function(data) {
2053 // Peek at the first x value to see if it's numeric.
2054 if (data.length == 0) {
2055 this.error("Can
't plot empty data set");
2058 if (data[0].length == 0) {
2059 this.error("Data set cannot contain an empty row");
2063 if (this.attr_("labels") == null) {
2064 this.warn("Using default labels. Set labels explicitly via 'labels
' " +
2065 "in the options parameter");
2066 this.attrs_.labels = [ "X" ];
2067 for (var i = 1; i < data[0].length; i++) {
2068 this.attrs_.labels.push("Y" + i);
2072 if (Dygraph.isDateLike(data[0][0])) {
2073 // Some intelligent defaults for a date x-axis.
2074 this.attrs_.xValueFormatter = Dygraph.dateString_;
2075 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
2076 this.attrs_.xTicker = Dygraph.dateTicker;
2078 // Assume they're all dates
.
2079 var parsedData
= Dygraph
.clone(data
);
2080 for (var i
= 0; i
< data
.length
; i
++) {
2081 if (parsedData
[i
].length
== 0) {
2082 this.error("Row " + (1 + i
) + " of data is empty");
2085 if (parsedData
[i
][0] == null
2086 || typeof(parsedData
[i
][0].getTime
) != 'function'
2087 || isNaN(parsedData
[i
][0].getTime())) {
2088 this.error("x value in row " + (1 + i
) + " is not a Date");
2091 parsedData
[i
][0] = parsedData
[i
][0].getTime();
2095 // Some intelligent defaults for a numeric x-axis.
2096 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2097 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2103 * Parses a DataTable object from gviz.
2104 * The data is expected to have a first column that is either a date or a
2105 * number. All subsequent columns must be numbers. If there is a clear mismatch
2106 * between this.xValueParser_ and the type of the first column, it will be
2107 * fixed. Fills out rawData_.
2108 * @param {Array.<Object>} data See above.
2111 Dygraph
.prototype.parseDataTable_
= function(data
) {
2112 var cols
= data
.getNumberOfColumns();
2113 var rows
= data
.getNumberOfRows();
2115 var indepType
= data
.getColumnType(0);
2116 if (indepType
== 'date' || indepType
== 'datetime') {
2117 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
2118 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2119 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
2120 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2121 } else if (indepType
== 'number') {
2122 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2123 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2124 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2125 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
2127 this.error("only 'date', 'datetime' and 'number' types are supported for " +
2128 "column 1 of DataTable input (Got '" + indepType
+ "')");
2132 // Array of the column indices which contain data (and not annotations).
2134 var annotationCols
= {}; // data index -> [annotation cols]
2135 var hasAnnotations
= false;
2136 for (var i
= 1; i
< cols
; i
++) {
2137 var type
= data
.getColumnType(i
);
2138 if (type
== 'number') {
2140 } else if (type
== 'string' && this.attr_('displayAnnotations')) {
2141 // This is OK -- it's an annotation column.
2142 var dataIdx
= colIdx
[colIdx
.length
- 1];
2143 if (!annotationCols
.hasOwnProperty(dataIdx
)) {
2144 annotationCols
[dataIdx
] = [i
];
2146 annotationCols
[dataIdx
].push(i
);
2148 hasAnnotations
= true;
2150 this.error("Only 'number' is supported as a dependent type with Gviz." +
2151 " 'string' is only supported if displayAnnotations is true");
2155 // Read column labels
2156 // TODO(danvk): add support back for errorBars
2157 var labels
= [data
.getColumnLabel(0)];
2158 for (var i
= 0; i
< colIdx
.length
; i
++) {
2159 labels
.push(data
.getColumnLabel(colIdx
[i
]));
2161 this.attrs_
.labels
= labels
;
2162 cols
= labels
.length
;
2165 var outOfOrder
= false;
2166 var annotations
= [];
2167 for (var i
= 0; i
< rows
; i
++) {
2169 if (typeof(data
.getValue(i
, 0)) === 'undefined' ||
2170 data
.getValue(i
, 0) === null) {
2171 this.warn("Ignoring row " + i
+
2172 " of DataTable because of undefined or null first column.");
2176 if (indepType
== 'date' || indepType
== 'datetime') {
2177 row
.push(data
.getValue(i
, 0).getTime());
2179 row
.push(data
.getValue(i
, 0));
2181 if (!this.attr_("errorBars")) {
2182 for (var j
= 0; j
< colIdx
.length
; j
++) {
2183 var col
= colIdx
[j
];
2184 row
.push(data
.getValue(i
, col
));
2185 if (hasAnnotations
&&
2186 annotationCols
.hasOwnProperty(col
) &&
2187 data
.getValue(i
, annotationCols
[col
][0]) != null) {
2189 ann
.series
= data
.getColumnLabel(col
);
2191 ann
.shortText
= String
.fromCharCode(65 /* A */ + annotations
.length
)
2193 for (var k
= 0; k
< annotationCols
[col
].length
; k
++) {
2194 if (k
) ann
.text
+= "\n";
2195 ann
.text
+= data
.getValue(i
, annotationCols
[col
][k
]);
2197 annotations
.push(ann
);
2201 for (var j
= 0; j
< cols
- 1; j
++) {
2202 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
2205 if (ret
.length
> 0 && row
[0] < ret
[ret
.length
- 1][0]) {
2212 this.warn("DataTable is out of order; order it correctly to speed loading.");
2213 ret
.sort(function(a
,b
) { return a
[0] - b
[0] });
2215 this.rawData_
= ret
;
2217 if (annotations
.length
> 0) {
2218 this.setAnnotations(annotations
, true);
2222 // These functions are all based on MochiKit.
2223 Dygraph
.update
= function (self
, o
) {
2224 if (typeof(o
) != 'undefined' && o
!== null) {
2226 if (o
.hasOwnProperty(k
)) {
2234 Dygraph
.isArrayLike
= function (o
) {
2235 var typ
= typeof(o
);
2237 (typ
!= 'object' && !(typ
== 'function' &&
2238 typeof(o
.item
) == 'function')) ||
2240 typeof(o
.length
) != 'number' ||
2248 Dygraph
.isDateLike
= function (o
) {
2249 if (typeof(o
) != "object" || o
=== null ||
2250 typeof(o
.getTime
) != 'function') {
2256 Dygraph
.clone
= function(o
) {
2257 // TODO(danvk): figure out how MochiKit's version works
2259 for (var i
= 0; i
< o
.length
; i
++) {
2260 if (Dygraph
.isArrayLike(o
[i
])) {
2261 r
.push(Dygraph
.clone(o
[i
]));
2271 * Get the CSV data. If it's in a function, call that function. If it's in a
2272 * file, do an XMLHttpRequest to get it.
2275 Dygraph
.prototype.start_
= function() {
2276 if (typeof this.file_
== 'function') {
2277 // CSV string. Pretend we got it via XHR.
2278 this.loadedEvent_(this.file_());
2279 } else if (Dygraph
.isArrayLike(this.file_
)) {
2280 this.rawData_
= this.parseArray_(this.file_
);
2281 this.drawGraph_(this.rawData_
);
2282 } else if (typeof this.file_
== 'object' &&
2283 typeof this.file_
.getColumnRange
== 'function') {
2284 // must be a DataTable from gviz.
2285 this.parseDataTable_(this.file_
);
2286 this.drawGraph_(this.rawData_
);
2287 } else if (typeof this.file_
== 'string') {
2288 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
2289 if (this.file_
.indexOf('\n') >= 0) {
2290 this.loadedEvent_(this.file_
);
2292 var req
= new XMLHttpRequest();
2294 req
.onreadystatechange
= function () {
2295 if (req
.readyState
== 4) {
2296 if (req
.status
== 200) {
2297 caller
.loadedEvent_(req
.responseText
);
2302 req
.open("GET", this.file_
, true);
2306 this.error("Unknown data format: " + (typeof this.file_
));
2311 * Changes various properties of the graph. These can include:
2313 * <li>file: changes the source data for the graph</li>
2314 * <li>errorBars: changes whether the data contains stddev</li>
2316 * @param {Object} attrs The new properties and values
2318 Dygraph
.prototype.updateOptions
= function(attrs
) {
2319 // TODO(danvk): this is a mess. Rethink this function.
2320 if (attrs
.rollPeriod
) {
2321 this.rollPeriod_
= attrs
.rollPeriod
;
2323 if (attrs
.dateWindow
) {
2324 this.dateWindow_
= attrs
.dateWindow
;
2326 if (attrs
.valueRange
) {
2327 this.valueRange_
= attrs
.valueRange
;
2330 // TODO(danvk): validate per-series options.
2335 // highlightCircleSize
2337 Dygraph
.update(this.user_attrs_
, attrs
);
2338 Dygraph
.update(this.renderOptions_
, attrs
);
2340 this.labelsFromCSV_
= (this.attr_("labels") == null);
2342 // TODO(danvk): this doesn't match the constructor logic
2343 this.layout_
.updateOptions({ 'errorBars': this.attr_("errorBars") });
2344 if (attrs
['file']) {
2345 this.file_
= attrs
['file'];
2348 this.drawGraph_(this.rawData_
);
2353 * Resizes the dygraph. If no parameters are specified, resizes to fill the
2354 * containing div (which has presumably changed size since the dygraph was
2355 * instantiated. If the width/height are specified, the div will be resized.
2357 * This is far more efficient than destroying and re-instantiating a
2358 * Dygraph, since it doesn't have to reparse the underlying data.
2360 * @param {Number} width Width (in pixels)
2361 * @param {Number} height Height (in pixels)
2363 Dygraph
.prototype.resize
= function(width
, height
) {
2364 if (this.resize_lock
) {
2367 this.resize_lock
= true;
2369 if ((width
=== null) != (height
=== null)) {
2370 this.warn("Dygraph.resize() should be called with zero parameters or " +
2371 "two non-NULL parameters. Pretending it was zero.");
2372 width
= height
= null;
2375 // TODO(danvk): there should be a clear() method.
2376 this.maindiv_
.innerHTML
= "";
2377 this.attrs_
.labelsDiv
= null;
2380 this.maindiv_
.style
.width
= width
+ "px";
2381 this.maindiv_
.style
.height
= height
+ "px";
2382 this.width_
= width
;
2383 this.height_
= height
;
2385 this.width_
= this.maindiv_
.offsetWidth
;
2386 this.height_
= this.maindiv_
.offsetHeight
;
2389 this.createInterface_();
2390 this.drawGraph_(this.rawData_
);
2392 this.resize_lock
= false;
2396 * Adjusts the number of days in the rolling average. Updates the graph to
2397 * reflect the new averaging period.
2398 * @param {Number} length Number of days over which to average the data.
2400 Dygraph
.prototype.adjustRoll
= function(length
) {
2401 this.rollPeriod_
= length
;
2402 this.drawGraph_(this.rawData_
);
2406 * Returns a boolean array of visibility statuses.
2408 Dygraph
.prototype.visibility
= function() {
2409 // Do lazy-initialization, so that this happens after we know the number of
2411 if (!this.attr_("visibility")) {
2412 this.attrs_
["visibility"] = [];
2414 while (this.attr_("visibility").length
< this.rawData_
[0].length
- 1) {
2415 this.attr_("visibility").push(true);
2417 return this.attr_("visibility");
2421 * Changes the visiblity of a series.
2423 Dygraph
.prototype.setVisibility
= function(num
, value
) {
2424 var x
= this.visibility();
2425 if (num
< 0 && num
>= x
.length
) {
2426 this.warn("invalid series number in setVisibility: " + num
);
2429 this.drawGraph_(this.rawData_
);
2434 * Update the list of annotations and redraw the chart.
2436 Dygraph
.prototype.setAnnotations
= function(ann
, suppressDraw
) {
2437 this.annotations_
= ann
;
2438 this.layout_
.setAnnotations(this.annotations_
);
2439 if (!suppressDraw
) {
2440 this.drawGraph_(this.rawData_
);
2445 * Return the list of annotations.
2447 Dygraph
.prototype.annotations
= function() {
2448 return this.annotations_
;
2452 * Get the index of a series (column) given its name. The first column is the
2453 * x-axis, so the data series start with index 1.
2455 Dygraph
.prototype.indexFromSetName
= function(name
) {
2456 var labels
= this.attr_("labels");
2457 for (var i
= 0; i
< labels
.length
; i
++) {
2458 if (labels
[i
] == name
) return i
;
2463 Dygraph
.addAnnotationRule
= function() {
2464 if (Dygraph
.addedAnnotationCSS
) return;
2467 if (document
.styleSheets
.length
> 0) {
2468 mysheet
= document
.styleSheets
[0];
2470 var styleSheetElement
= document
.createElement("style");
2471 styleSheetElement
.type
= "text/css";
2472 document
.getElementsByTagName("head")[0].appendChild(styleSheetElement
);
2473 for(i
= 0; i
< document
.styleSheets
.length
; i
++) {
2474 if (document
.styleSheets
[i
].disabled
) continue;
2475 mysheet
= document
.styleSheets
[i
];
2479 var rule
= "border: 1px solid black; " +
2480 "background-color: white; " +
2481 "text-align: center;";
2482 if (mysheet
.insertRule
) { // Firefox
2483 mysheet
.insertRule(".dygraphDefaultAnnotation { " + rule
+ " }", 0);
2484 } else if (mysheet
.addRule
) { // IE
2485 mysheet
.addRule(".dygraphDefaultAnnotation", rule
);
2488 Dygraph
.addedAnnotationCSS
= true;
2492 * Create a new canvas element. This is more complex than a simple
2493 * document.createElement("canvas") because of IE and excanvas.
2495 Dygraph
.createCanvas
= function() {
2496 var canvas
= document
.createElement("canvas");
2498 isIE
= (/MSIE/.test(navigator
.userAgent
) && !window
.opera
);
2500 canvas
= G_vmlCanvasManager
.initElement(canvas
);
2508 * A wrapper around Dygraph that implements the gviz API.
2509 * @param {Object} container The DOM object the visualization should live in.
2511 Dygraph
.GVizChart
= function(container
) {
2512 this.container
= container
;
2515 Dygraph
.GVizChart
.prototype.draw
= function(data
, options
) {
2516 this.container
.innerHTML
= '';
2517 this.date_graph
= new Dygraph(this.container
, data
, options
);
2521 * Google charts compatible setSelection
2522 * Only row selection is supported, all points in the row will be highlighted
2523 * @param {Array} array of the selected cells
2526 Dygraph
.GVizChart
.prototype.setSelection
= function(selection_array
) {
2528 if (selection_array
.length
) {
2529 row
= selection_array
[0].row
;
2531 this.date_graph
.setSelection(row
);
2535 * Google charts compatible getSelection implementation
2536 * @return {Array} array of the selected cells
2539 Dygraph
.GVizChart
.prototype.getSelection
= function() {
2542 var row
= this.date_graph
.getSelection();
2544 if (row
< 0) return selection
;
2547 for (var i
in this.date_graph
.layout_
.datasets
) {
2548 selection
.push({row
: row
, column
: col
});
2555 // Older pages may still use this name.
2556 DateGraph
= Dygraph
;