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. DateGraph can handle multiple series with or without error bars. The
7 * date/value ranges will be automatically set. DateGraph 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 DateGraph(document.getElementById("graphdiv"),
16 ["Series 1", "Series 2"],
20 The CSV file is of the form
25 If null is passed as the third parameter (series names), then the first line
26 of the CSV file is assumed to contain names for each series.
28 If the 'errorBars' option is set in the constructor, the input should be of
31 YYYYMMDD,A1,sigmaA1,B1,sigmaB1,...
32 YYYYMMDD,A2,sigmaA2,B2,sigmaB2,...
34 If the 'fractions' option is set, the input should be of the form:
36 YYYYMMDD,A1/B1,A2/B2,...
37 YYYYMMDD,A1/B1,A2/B2,...
39 And error bars will be calculated automatically using a binomial distribution.
41 For further documentation and examples, see http://www/~danvk/dg/
46 * An interactive, zoomable graph
47 * @param {String | Function} file A file containing CSV data or a function that
48 * returns this data. The expected format for each line is
49 * YYYYMMDD,val1,val2,... or, if attrs.errorBars is set,
50 * YYYYMMDD,val1,stddev1,val2,stddev2,...
51 * @param {Array.<String>} labels Labels for the data series
52 * @param {Object} attrs Various other attributes, e.g. errorBars determines
53 * whether the input data contains error ranges.
55 DateGraph
= function(div
, file
, labels
, attrs
) {
56 if (arguments
.length
> 0)
57 this.__init__(div
, file
, labels
, attrs
);
60 DateGraph
.NAME
= "DateGraph";
61 DateGraph
.VERSION
= "1.1";
62 DateGraph
.__repr__
= function() {
63 return "[" + this.NAME
+ " " + this.VERSION
+ "]";
65 DateGraph
.toString
= function() {
66 return this.__repr__();
69 // Various default values
70 DateGraph
.DEFAULT_ROLL_PERIOD
= 1;
71 DateGraph
.DEFAULT_WIDTH
= 480;
72 DateGraph
.DEFAULT_HEIGHT
= 320;
73 DateGraph
.DEFAULT_STROKE_WIDTH
= 1.0;
74 DateGraph
.AXIS_LINE_WIDTH
= 0.3;
76 // Default attribute values.
77 DateGraph
.DEFAULT_ATTRS
= {
81 // TODO(danvk): move defaults from createStatusMessage_ here.
84 // TODO(danvk): default padding
88 * Initializes the DateGraph. This creates a new DIV and constructs the PlotKit
89 * and interaction <canvas> inside of it. See the constructor for details
91 * @param {String | Function} file Source data
92 * @param {Array.<String>} labels Names of the data series
93 * @param {Object} attrs Miscellaneous other options
96 DateGraph
.prototype.__init__
= function(div
, file
, labels
, attrs
) {
97 // Copy the important bits into the object
98 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
100 this.labels_
= labels
;
102 this.rollPeriod_
= attrs
.rollPeriod
|| DateGraph
.DEFAULT_ROLL_PERIOD
;
103 this.previousVerticalX_
= -1;
104 this.width_
= parseInt(div
.style
.width
, 10);
105 this.height_
= parseInt(div
.style
.height
, 10);
106 this.errorBars_
= attrs
.errorBars
|| false;
107 this.fractions_
= attrs
.fractions
|| false;
108 this.strokeWidth_
= attrs
.strokeWidth
|| DateGraph
.DEFAULT_STROKE_WIDTH
;
109 this.dateWindow_
= attrs
.dateWindow
|| null;
110 this.valueRange_
= attrs
.valueRange
|| null;
111 this.labelsSeparateLines
= attrs
.labelsSeparateLines
|| false;
112 this.labelsDiv_
= attrs
.labelsDiv
|| null;
113 this.labelsKMB_
= attrs
.labelsKMB
|| false;
114 this.minTickSize_
= attrs
.minTickSize
|| 0;
115 this.xValueParser_
= attrs
.xValueParser
|| DateGraph
.prototype.dateParser
;
116 this.xValueFormatter_
= attrs
.xValueFormatter
||
117 DateGraph
.prototype.dateString_
;
118 this.xTicker_
= attrs
.xTicker
|| DateGraph
.prototype.dateTicker
;
119 this.sigma_
= attrs
.sigma
|| 2.0;
120 this.wilsonInterval_
= attrs
.wilsonInterval
|| true;
121 this.customBars_
= attrs
.customBars
|| false;
123 this.attrs_
= DateGraph
.DEFAULT_ATTRS
;
124 MochiKit
.Base
.update(this.attrs_
, attrs
);
126 if (typeof this.attrs_
.pixelsPerXLabel
== 'undefined') {
127 this.attrs_
.pixelsPerXLabel
= 60;
130 // Make a note of whether labels will be pulled from the CSV file.
131 this.labelsFromCSV_
= (this.labels_
== null);
132 if (this.labels_
== null)
135 // Prototype of the callback is "void clickCallback(event, date)"
136 this.clickCallback_
= attrs
.clickCallback
|| null;
138 // Prototype of zoom callback is "void dragCallback(minDate, maxDate)"
139 this.zoomCallback_
= attrs
.zoomCallback
|| null;
141 // Create the containing DIV and other interactive elements
142 this.createInterface_();
144 // Create the PlotKit grapher
145 this.layoutOptions_
= { 'errorBars': (this.errorBars_
|| this.customBars_
),
146 'xOriginIsZero': false };
147 MochiKit
.Base
.update(this.layoutOptions_
, attrs
);
148 this.setColors_(attrs
);
150 this.layout_
= new DateGraphLayout(this.layoutOptions_
);
152 this.renderOptions_
= { colorScheme
: this.colors_
,
154 strokeWidth
: this.strokeWidth_
,
155 axisLabelFontSize
: 14,
156 axisLineWidth
: DateGraph
.AXIS_LINE_WIDTH
};
157 MochiKit
.Base
.update(this.renderOptions_
, attrs
);
158 this.plotter_
= new DateGraphCanvasRenderer(this.hidden_
, this.layout_
,
159 this.renderOptions_
);
161 this.createStatusMessage_();
162 this.createRollInterface_();
163 this.createDragInterface_();
165 // connect(window, 'onload', this, function(e) { this.start_(); });
170 * Returns the current rolling period, as set by the user or an option.
171 * @return {Number} The number of days in the rolling window
173 DateGraph
.prototype.rollPeriod
= function() {
174 return this.rollPeriod_
;
178 * Generates interface elements for the DateGraph: a containing div, a div to
179 * display the current point, and a textbox to adjust the rolling average
183 DateGraph
.prototype.createInterface_
= function() {
184 // Create the all-enclosing graph div
185 var enclosing
= this.maindiv_
;
187 this.graphDiv
= MochiKit
.DOM
.DIV( { style
: { 'width': this.width_
+ "px",
188 'height': this.height_
+ "px"
190 appendChildNodes(enclosing
, this.graphDiv
);
192 // Create the canvas to store
193 var canvas
= MochiKit
.DOM
.CANVAS
;
194 this.canvas_
= canvas( { style
: { 'position': 'absolute' },
196 height
: this.height_
});
197 appendChildNodes(this.graphDiv
, this.canvas_
);
199 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
200 connect(this.hidden_
, 'onmousemove', this, function(e
) { this.mouseMove_(e
) });
201 connect(this.hidden_
, 'onmouseout', this, function(e
) { this.mouseOut_(e
) });
205 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
206 * this particular canvas. All DateGraph work is done on this.canvas_.
207 * @param {Object} canvas The DateGraph canvas to over which to overlay the plot
208 * @return {Object} The newly-created canvas
211 DateGraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
212 var h
= document
.createElement("canvas");
213 h
.style
.position
= "absolute";
214 h
.style
.top
= canvas
.style
.top
;
215 h
.style
.left
= canvas
.style
.left
;
216 h
.width
= this.width_
;
217 h
.height
= this.height_
;
218 MochiKit
.DOM
.appendChildNodes(this.graphDiv
, h
);
223 * Generate a set of distinct colors for the data series. This is done with a
224 * color wheel. Saturation/Value are customizable, and the hue is
225 * equally-spaced around the color wheel. If a custom set of colors is
226 * specified, that is used instead.
227 * @param {Object} attrs Various attributes, e.g. saturation and value
230 DateGraph
.prototype.setColors_
= function(attrs
) {
231 var num
= this.labels_
.length
;
234 var sat
= attrs
.colorSaturation
|| 1.0;
235 var val
= attrs
.colorValue
|| 0.5;
236 for (var i
= 1; i
<= num
; i
++) {
237 var hue
= (1.0*i
/(1+num
));
238 this.colors_
.push( MochiKit
.Color
.Color
.fromHSV(hue
, sat
, val
) );
241 for (var i
= 0; i
< num
; i
++) {
242 var colorStr
= attrs
.colors
[i
% attrs
.colors
.length
];
243 this.colors_
.push( MochiKit
.Color
.Color
.fromString(colorStr
) );
249 * Create the div that contains information on the selected point(s)
250 * This goes in the top right of the canvas, unless an external div has already
254 DateGraph
.prototype.createStatusMessage_
= function(){
255 if (!this.labelsDiv_
) {
256 var divWidth
= this.attrs_
.labelsDivWidth
;
257 var messagestyle
= { "style": {
258 "position": "absolute",
261 "width": divWidth
+ "px",
263 "left": this.width_
- divWidth
+ "px",
264 "background": "white",
266 "overflow": "hidden"}};
267 MochiKit
.Base
.update(messagestyle
["style"], this.attrs_
.labelsDivStyles
);
268 this.labelsDiv_
= MochiKit
.DOM
.DIV(messagestyle
);
269 MochiKit
.DOM
.appendChildNodes(this.graphDiv
, this.labelsDiv_
);
274 * Create the text box to adjust the averaging period
275 * @return {Object} The newly-created text box
278 DateGraph
.prototype.createRollInterface_
= function() {
279 var padding
= this.plotter_
.options
.padding
;
280 if (typeof this.attrs_
.showRoller
== 'undefined') {
281 this.attrs_
.showRoller
= false;
283 var display
= this.attrs_
.showRoller
? "block" : "none";
284 var textAttr
= { "type": "text",
286 "value": this.rollPeriod_
,
287 "style": { "position": "absolute",
289 "top": (this.height_
- 25 - padding
.bottom
) + "px",
290 "left": (padding
.left
+1) + "px",
293 var roller
= MochiKit
.DOM
.INPUT(textAttr
);
294 var pa
= this.graphDiv
;
295 MochiKit
.DOM
.appendChildNodes(pa
, roller
);
296 connect(roller
, 'onchange', this,
297 function() { this.adjustRoll(roller
.value
); });
302 * Set up all the mouse handlers needed to capture dragging behavior for zoom
303 * events. Uses MochiKit.Signal to attach all the event handlers.
306 DateGraph
.prototype.createDragInterface_
= function() {
309 // Tracks whether the mouse is down right now
310 var mouseDown
= false;
311 var dragStartX
= null;
312 var dragStartY
= null;
317 // Utility function to convert page-wide coordinates to canvas coords
320 var getX
= function(e
) { return e
.mouse().page
.x
- px
};
321 var getY
= function(e
) { return e
.mouse().page
.y
- py
};
323 // Draw zoom rectangles when the mouse is down and the user moves around
324 connect(this.hidden_
, 'onmousemove', function(event
) {
326 dragEndX
= getX(event
);
327 dragEndY
= getY(event
);
329 self
.drawZoomRect_(dragStartX
, dragEndX
, prevEndX
);
334 // Track the beginning of drag events
335 connect(this.hidden_
, 'onmousedown', function(event
) {
337 px
= PlotKit
.Base
.findPosX(self
.canvas_
);
338 py
= PlotKit
.Base
.findPosY(self
.canvas_
);
339 dragStartX
= getX(event
);
340 dragStartY
= getY(event
);
343 // If the user releases the mouse button during a drag, but not over the
344 // canvas, then it doesn't count as a zooming action.
345 connect(document
, 'onmouseup', this, function(event
) {
353 // Temporarily cancel the dragging event when the mouse leaves the graph
354 connect(this.hidden_
, 'onmouseout', this, function(event
) {
361 // If the mouse is released on the canvas during a drag event, then it's a
362 // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
363 connect(this.hidden_
, 'onmouseup', this, function(event
) {
366 dragEndX
= getX(event
);
367 dragEndY
= getY(event
);
368 var regionWidth
= Math
.abs(dragEndX
- dragStartX
);
369 var regionHeight
= Math
.abs(dragEndY
- dragStartY
);
371 if (regionWidth
< 2 && regionHeight
< 2 &&
372 self
.clickCallback_
!= null &&
373 self
.lastx_
!= undefined
) {
374 self
.clickCallback_(event
, new Date(self
.lastx_
));
377 if (regionWidth
>= 10) {
378 self
.doZoom_(Math
.min(dragStartX
, dragEndX
),
379 Math
.max(dragStartX
, dragEndX
));
381 self
.canvas_
.getContext("2d").clearRect(0, 0,
383 self
.canvas_
.height
);
391 // Double-clicking zooms back out
392 connect(this.hidden_
, 'ondblclick', this, function(event
) {
393 self
.dateWindow_
= null;
394 self
.drawGraph_(self
.rawData_
);
395 var minDate
= self
.rawData_
[0][0];
396 var maxDate
= self
.rawData_
[self
.rawData_
.length
- 1][0];
397 if (self
.zoomCallback_
) {
398 self
.zoomCallback_(minDate
, maxDate
);
404 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
405 * up any previous zoom rectangles that were drawn. This could be optimized to
406 * avoid extra redrawing, but it's tricky to avoid interactions with the status
408 * @param {Number} startX The X position where the drag started, in canvas
410 * @param {Number} endX The current X position of the drag, in canvas coords.
411 * @param {Number} prevEndX The value of endX on the previous call to this
412 * function. Used to avoid excess redrawing
415 DateGraph
.prototype.drawZoomRect_
= function(startX
, endX
, prevEndX
) {
416 var ctx
= this.canvas_
.getContext("2d");
418 // Clean up from the previous rect if necessary
420 ctx
.clearRect(Math
.min(startX
, prevEndX
), 0,
421 Math
.abs(startX
- prevEndX
), this.height_
);
424 // Draw a light-grey rectangle to show the new viewing area
425 if (endX
&& startX
) {
426 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
427 ctx
.fillRect(Math
.min(startX
, endX
), 0,
428 Math
.abs(endX
- startX
), this.height_
);
433 * Zoom to something containing [lowX, highX]. These are pixel coordinates
434 * in the canvas. The exact zoom window may be slightly larger if there are no
435 * data points near lowX or highX. This function redraws the graph.
436 * @param {Number} lowX The leftmost pixel value that should be visible.
437 * @param {Number} highX The rightmost pixel value that should be visible.
440 DateGraph
.prototype.doZoom_
= function(lowX
, highX
) {
441 // Find the earliest and latest dates contained in this canvasx range.
442 var points
= this.layout_
.points
;
445 // Find the nearest [minDate, maxDate] that contains [lowX, highX]
446 for (var i
= 0; i
< points
.length
; i
++) {
447 var cx
= points
[i
].canvasx
;
448 var x
= points
[i
].xval
;
449 if (cx
< lowX
&& (minDate
== null || x
> minDate
)) minDate
= x
;
450 if (cx
> highX
&& (maxDate
== null || x
< maxDate
)) maxDate
= x
;
452 // Use the extremes if either is missing
453 if (minDate
== null) minDate
= points
[0].xval
;
454 if (maxDate
== null) maxDate
= points
[points
.length
-1].xval
;
456 this.dateWindow_
= [minDate
, maxDate
];
457 this.drawGraph_(this.rawData_
);
458 if (this.zoomCallback_
) {
459 this.zoomCallback_(minDate
, maxDate
);
464 * When the mouse moves in the canvas, display information about a nearby data
465 * point and draw dots over those points in the data series. This function
466 * takes care of cleanup of previously-drawn dots.
467 * @param {Object} event The mousemove event from the browser.
470 DateGraph
.prototype.mouseMove_
= function(event
) {
471 var canvasx
= event
.mouse().page
.x
- PlotKit
.Base
.findPosX(this.hidden_
);
472 var points
= this.layout_
.points
;
477 // Loop through all the points and find the date nearest to our current
479 var minDist
= 1e+100;
481 for (var i
= 0; i
< points
.length
; i
++) {
482 var dist
= Math
.abs(points
[i
].canvasx
- canvasx
);
483 if (dist
> minDist
) break;
487 if (idx
>= 0) lastx
= points
[idx
].xval
;
488 // Check that you can really highlight the last day's data
489 if (canvasx
> points
[points
.length
-1].canvasx
)
490 lastx
= points
[points
.length
-1].xval
;
492 // Extract the points we've selected
494 for (var i
= 0; i
< points
.length
; i
++) {
495 if (points
[i
].xval
== lastx
) {
496 selPoints
.push(points
[i
]);
500 // Clear the previously drawn vertical, if there is one
502 var ctx
= this.canvas_
.getContext("2d");
503 if (this.previousVerticalX_
>= 0) {
504 var px
= this.previousVerticalX_
;
505 ctx
.clearRect(px
- circleSize
- 1, 0, 2 * circleSize
+ 2, this.height_
);
508 if (selPoints
.length
> 0) {
509 var canvasx
= selPoints
[0].canvasx
;
511 // Set the status message to indicate the selected point(s)
512 var replace
= this.xValueFormatter_(lastx
) + ":";
513 var clen
= this.colors_
.length
;
514 for (var i
= 0; i
< selPoints
.length
; i
++) {
515 if (this.labelsSeparateLines
) {
518 var point
= selPoints
[i
];
519 replace
+= " <b><font color='" + this.colors_
[i
%clen
].toHexString() + "'>"
520 + point
.name
+ "</font></b>:"
521 + this.round_(point
.yval
, 2);
523 this.labelsDiv_
.innerHTML
= replace
;
525 // Save last x position for callbacks.
528 // Draw colored circles over the center of each selected point
530 for (var i
= 0; i
< selPoints
.length
; i
++) {
532 ctx
.fillStyle
= this.colors_
[i
%clen
].toRGBString();
533 ctx
.arc(canvasx
, selPoints
[i
%clen
].canvasy
, circleSize
, 0, 360, false);
538 this.previousVerticalX_
= canvasx
;
543 * The mouse has left the canvas. Clear out whatever artifacts remain
544 * @param {Object} event the mouseout event from the browser.
547 DateGraph
.prototype.mouseOut_
= function(event
) {
548 // Get rid of the overlay data
549 var ctx
= this.canvas_
.getContext("2d");
550 ctx
.clearRect(0, 0, this.width_
, this.height_
);
551 this.labelsDiv_
.innerHTML
= "";
554 DateGraph
.zeropad
= function(x
) {
555 if (x
< 10) return "0" + x
; else return "" + x
;
559 * Return a string version of the hours, minutes and seconds portion of a date.
560 * @param {Number} date The JavaScript date (ms since epoch)
561 * @return {String} A time of the form "HH:MM:SS"
564 DateGraph
.prototype.hmsString_
= function(date
) {
565 var zeropad
= DateGraph
.zeropad
;
566 var d
= new Date(date
);
567 if (d
.getSeconds()) {
568 return zeropad(d
.getHours()) + ":" +
569 zeropad(d
.getMinutes()) + ":" +
570 zeropad(d
.getSeconds());
571 } else if (d
.getMinutes()) {
572 return zeropad(d
.getHours()) + ":" + zeropad(d
.getMinutes());
574 return zeropad(d
.getHours());
579 * Convert a JS date (millis since epoch) to YYYY/MM/DD
580 * @param {Number} date The JavaScript date (ms since epoch)
581 * @return {String} A date of the form "YYYY/MM/DD"
584 DateGraph
.prototype.dateString_
= function(date
) {
585 var zeropad
= DateGraph
.zeropad
;
586 var d
= new Date(date
);
589 var year
= "" + d
.getFullYear();
590 // Get a 0 padded month string
591 var month
= zeropad(d
.getMonth() + 1); //months are 0-offset, sigh
592 // Get a 0 padded day string
593 var day
= zeropad(d
.getDate());
596 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
597 if (frac
) ret
= " " + this.hmsString_(date
);
599 return year
+ "/" + month + "/" + day
+ ret
;
603 * Round a number to the specified number of digits past the decimal point.
604 * @param {Number} num The number to round
605 * @param {Number} places The number of decimals to which to round
606 * @return {Number} The rounded number
609 DateGraph
.prototype.round_
= function(num
, places
) {
610 var shift
= Math
.pow(10, places
);
611 return Math
.round(num
* shift
)/shift
;
615 * Fires when there's data available to be graphed.
616 * @param {String} data Raw CSV data to be plotted
619 DateGraph
.prototype.loadedEvent_
= function(data
) {
620 this.rawData_
= this.parseCSV_(data
);
621 this.drawGraph_(this.rawData_
);
624 DateGraph
.prototype.months
= ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
625 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
626 DateGraph
.prototype.quarters
= ["Jan", "Apr", "Jul", "Oct"];
629 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
632 DateGraph
.prototype.addXTicks_
= function() {
633 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
634 var startDate
, endDate
;
635 if (this.dateWindow_
) {
636 startDate
= this.dateWindow_
[0];
637 endDate
= this.dateWindow_
[1];
639 startDate
= this.rawData_
[0][0];
640 endDate
= this.rawData_
[this.rawData_
.length
- 1][0];
643 var xTicks
= this.xTicker_(startDate
, endDate
);
644 this.layout_
.updateOptions({xTicks
: xTicks
});
647 // Time granularity enumeration
648 DateGraph
.SECONDLY
= 0;
649 DateGraph
.MINUTELY
= 1;
650 DateGraph
.HOURLY
= 2;
652 DateGraph
.WEEKLY
= 4;
653 DateGraph
.MONTHLY
= 5;
654 DateGraph
.QUARTERLY
= 6;
655 DateGraph
.BIANNUAL
= 7;
656 DateGraph
.ANNUAL
= 8;
657 DateGraph
.DECADAL
= 9;
658 DateGraph
.NUM_GRANULARITIES
= 10;
660 DateGraph
.SHORT_SPACINGS
= [];
661 DateGraph
.SHORT_SPACINGS
[DateGraph
.SECONDLY
] = 1000 * 1;
662 DateGraph
.SHORT_SPACINGS
[DateGraph
.MINUTELY
] = 1000 * 60;
663 DateGraph
.SHORT_SPACINGS
[DateGraph
.HOURLY
] = 1000 * 3600;
664 DateGraph
.SHORT_SPACINGS
[DateGraph
.DAILY
] = 1000 * 86400;
665 DateGraph
.SHORT_SPACINGS
[DateGraph
.WEEKLY
] = 1000 * 604800;
669 // If we used this time granularity, how many ticks would there be?
670 // This is only an approximation, but it's generally good enough.
672 DateGraph
.prototype.NumXTicks
= function(start_time
, end_time
, granularity
) {
673 if (granularity
< DateGraph
.MONTHLY
) {
674 // Generate one tick mark for every fixed interval of time.
675 var spacing
= DateGraph
.SHORT_SPACINGS
[granularity
];
676 return Math
.floor(0.5 + 1.0 * (end_time
- start_time
) / spacing
);
678 var year_mod
= 1; // e.g. to only print one point every 10 years.
680 if (granularity
== DateGraph
.QUARTERLY
) num_months
= 3;
681 if (granularity
== DateGraph
.BIANNUAL
) num_months
= 2;
682 if (granularity
== DateGraph
.ANNUAL
) num_months
= 1;
683 if (granularity
== DateGraph
.DECADAL
) { num_months
= 1; year_mod
= 10; }
685 var msInYear
= 365.2524 * 24 * 3600 * 1000;
686 var num_years
= 1.0 * (end_time
- start_time
) / msInYear
;
687 return Math
.floor(0.5 + 1.0 * num_years
* num_months
/ year_mod
);
693 // Construct an x-axis of nicely-formatted times on meaningful boundaries
694 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
696 // Returns an array containing {v: millis, label: label} dictionaries.
698 DateGraph
.prototype.GetXAxis
= function(start_time
, end_time
, granularity
) {
700 if (granularity
< DateGraph
.MONTHLY
) {
701 // Generate one tick mark for every fixed interval of time.
702 var spacing
= DateGraph
.SHORT_SPACINGS
[granularity
];
703 var format
= '%d%b'; // e.g. "1 Jan"
704 for (var t
= start_time
; t
<= end_time
; t
+= spacing
) {
706 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
707 if (frac
== 0 || granularity
>= DateGraph
.DAILY
) {
708 // the extra hour covers DST problems.
709 ticks
.push({ v
:t
, label
: new Date(t
+ 3600*1000).strftime(format
) });
711 ticks
.push({ v
:t
, label
: this.hmsString_(t
) });
715 // Display a tick mark on the first of a set of months of each year.
716 // Years get a tick mark iff y % year_mod == 0. This is useful for
717 // displaying a tick mark once every 10 years, say, on long time scales.
719 var year_mod
= 1; // e.g. to only print one point every 10 years.
721 // TODO(danvk): use CachingRoundTime where appropriate to get boundaries.
722 if (granularity
== DateGraph
.MONTHLY
) {
723 months
= [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
724 } else if (granularity
== DateGraph
.QUARTERLY
) {
725 months
= [ 0, 3, 6, 9 ];
726 } else if (granularity
== DateGraph
.BIANNUAL
) {
728 } else if (granularity
== DateGraph
.ANNUAL
) {
730 } else if (granularity
== DateGraph
.DECADAL
) {
735 var start_year
= new Date(start_time
).getFullYear();
736 var end_year
= new Date(end_time
).getFullYear();
737 var zeropad
= DateGraph
.zeropad
;
738 for (var i
= start_year
; i
<= end_year
; i
++) {
739 if (i
% year_mod
!= 0) continue;
740 for (var j
= 0; j
< months
.length
; j
++) {
741 var date_str
= i
+ "/" + zeropad(1 + months[j]) + "/01";
742 var t
= Date
.parse(date_str
);
743 if (t
< start_time
|| t
> end_time
) continue;
744 ticks
.push({ v
:t
, label
: new Date(t
).strftime('%b %y') });
754 * Add ticks to the x-axis based on a date range.
755 * @param {Number} startDate Start of the date window (millis since epoch)
756 * @param {Number} endDate End of the date window (millis since epoch)
757 * @return {Array.<Object>} Array of {label, value} tuples.
760 DateGraph
.prototype.dateTicker
= function(startDate
, endDate
) {
762 for (var i
= 0; i
< DateGraph
.NUM_GRANULARITIES
; i
++) {
763 var num_ticks
= this.NumXTicks(startDate
, endDate
, i
);
764 if (this.width_
/ num_ticks
>= this.attrs_
.pixelsPerXLabel
) {
771 return this.GetXAxis(startDate
, endDate
, chosen
);
773 // TODO(danvk): signal error.
778 * Add ticks when the x axis has numbers on it (instead of dates)
779 * @param {Number} startDate Start of the date window (millis since epoch)
780 * @param {Number} endDate End of the date window (millis since epoch)
781 * @return {Array.<Object>} Array of {label, value} tuples.
784 DateGraph
.prototype.numericTicks
= function(minV
, maxV
) {
789 scale
= Math
.pow( 10, Math
.floor(Math
.log(maxV
)/Math
.log(10.0)) );
792 // Add a smallish number of ticks at human-friendly points
793 var nTicks
= (maxV
- minV
) / scale
;
794 while (2 * nTicks
< 20) {
797 if ((maxV
- minV
) / nTicks
< this.minTickSize_
) {
798 nTicks
= this.round_((maxV
- minV
) / this.minTickSize_
, 1);
801 // Construct labels for the ticks
803 for (var i
= 0; i
<= nTicks
; i
++) {
804 var tickV
= minV
+ i
* (maxV
- minV
) / nTicks
;
805 var label
= this.round_(tickV
, 2);
806 if (this.labelsKMB_
) {
808 if (tickV
>= k
*k
*k
) {
809 label
= this.round_(tickV
/(k
*k
*k
), 1) + "B";
810 } else if (tickV
>= k
*k
) {
811 label
= this.round_(tickV
/(k
*k
), 1) + "M";
812 } else if (tickV
>= k
) {
813 label
= this.round_(tickV
/k
, 1) + "K";
816 ticks
.push( {label
: label
, v
: tickV
} );
822 * Adds appropriate ticks on the y-axis
823 * @param {Number} minY The minimum Y value in the data set
824 * @param {Number} maxY The maximum Y value in the data set
827 DateGraph
.prototype.addYTicks_
= function(minY
, maxY
) {
828 // Set the number of ticks so that the labels are human-friendly.
829 var ticks
= this.numericTicks(minY
, maxY
);
830 this.layout_
.updateOptions( { yAxis
: [minY
, maxY
],
835 * Update the graph with new data. Data is in the format
836 * [ [date1, val1, val2, ...], [date2, val1, val2, ...] if errorBars=false
837 * or, if errorBars=true,
838 * [ [date1, [val1,stddev1], [val2,stddev2], ...], [date2, ...], ...]
839 * @param {Array.<Object>} data The data (see above)
842 DateGraph
.prototype.drawGraph_
= function(data
) {
844 this.layout_
.removeAllDatasets();
845 // Loop over all fields in the dataset
846 for (var i
= 1; i
< data
[0].length
; i
++) {
848 for (var j
= 0; j
< data
.length
; j
++) {
849 var date
= data
[j
][0];
850 series
[j
] = [date
, data
[j
][i
]];
852 series
= this.rollingAverage(series
, this.rollPeriod_
);
854 // Prune down to the desired range, if necessary (for zooming)
855 var bars
= this.errorBars_
|| this.customBars_
;
856 if (this.dateWindow_
) {
857 var low
= this.dateWindow_
[0];
858 var high
= this.dateWindow_
[1];
860 for (var k
= 0; k
< series
.length
; k
++) {
861 if (series
[k
][0] >= low
&& series
[k
][0] <= high
) {
862 pruned
.push(series
[k
]);
863 var y
= bars
? series
[k
][1][0] : series
[k
][1];
864 if (maxY
== null || y
> maxY
) maxY
= y
;
869 for (var j
= 0; j
< series
.length
; j
++) {
870 var y
= bars
? series
[j
][1][0] : series
[j
][1];
871 if (maxY
== null || y
> maxY
) {
872 maxY
= bars
? y
+ series
[j
][1][1] : y
;
879 for (var j
=0; j
<series
.length
; j
++)
880 vals
[j
] = [series
[j
][0],
881 series
[j
][1][0], series
[j
][1][1], series
[j
][1][2]];
882 this.layout_
.addDataset(this.labels_
[i
- 1], vals
);
884 this.layout_
.addDataset(this.labels_
[i
- 1], series
);
888 // Use some heuristics to come up with a good maxY value, unless it's been
889 // set explicitly by the user.
890 if (this.valueRange_
!= null) {
891 this.addYTicks_(this.valueRange_
[0], this.valueRange_
[1]);
893 // Add some padding and round up to an integer to be human-friendly.
895 if (maxY
<= 0.0) maxY
= 1.0;
897 var scale
= Math
.pow(10, Math
.floor(Math
.log(maxY
) / Math
.log(10.0)));
898 maxY
= scale
* Math
.ceil(maxY
/ scale
);
900 this.addYTicks_(0, maxY
);
905 // Tell PlotKit to use this new data and render itself
906 this.layout_
.evaluateWithError();
907 this.plotter_
.clear();
908 this.plotter_
.render();
909 this.canvas_
.getContext('2d').clearRect(0, 0,
910 this.canvas_
.width
, this.canvas_
.height
);
914 * Calculates the rolling average of a data set.
915 * If originalData is [label, val], rolls the average of those.
916 * If originalData is [label, [, it's interpreted as [value, stddev]
917 * and the roll is returned in the same form, with appropriately reduced
918 * stddev for each value.
919 * Note that this is where fractional input (i.e. '5/10') is converted into
921 * @param {Array} originalData The data in the appropriate format (see above)
922 * @param {Number} rollPeriod The number of days over which to average the data
924 DateGraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
925 if (originalData
.length
< 2)
927 var rollPeriod
= Math
.min(rollPeriod
, originalData
.length
- 1);
928 var rollingData
= [];
929 var sigma
= this.sigma_
;
931 if (this.fractions_
) {
933 var den
= 0; // numerator/denominator
935 for (var i
= 0; i
< originalData
.length
; i
++) {
936 num
+= originalData
[i
][1][0];
937 den
+= originalData
[i
][1][1];
938 if (i
- rollPeriod
>= 0) {
939 num
-= originalData
[i
- rollPeriod
][1][0];
940 den
-= originalData
[i
- rollPeriod
][1][1];
943 var date
= originalData
[i
][0];
944 var value
= den
? num
/ den
: 0.0;
945 if (this.errorBars_
) {
946 if (this.wilsonInterval_
) {
947 // For more details on this confidence interval, see:
948 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
950 var p
= value
< 0 ? 0 : value
, n
= den
;
951 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
952 var denom
= 1 + sigma
* sigma
/ den
;
953 var low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
954 var high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
955 rollingData
[i
] = [date
,
956 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
958 rollingData
[i
] = [date
, [0, 0, 0]];
961 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
962 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
965 rollingData
[i
] = [date
, mult
* value
];
968 } else if (this.customBars_
) {
973 for (var i
= 0; i
< originalData
.length
; i
++) {
974 var data
= originalData
[i
][1];
976 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
982 if (i
- rollPeriod
>= 0) {
983 var prev
= originalData
[i
- rollPeriod
];
989 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
990 1.0 * (mid
- low
) / count
,
991 1.0 * (high
- mid
) / count
]];
994 // Calculate the rolling average for the first rollPeriod - 1 points where
995 // there is not enough data to roll over the full number of days
996 var num_init_points
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
997 if (!this.errorBars_
){
998 for (var i
= 0; i
< num_init_points
; i
++) {
1000 for (var j
= 0; j
< i
+ 1; j
++)
1001 sum
+= originalData
[j
][1];
1002 rollingData
[i
] = [originalData
[i
][0], sum
/ (i
+ 1)];
1004 // Calculate the rolling average for the remaining points
1005 for (var i
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
1006 i
< originalData
.length
;
1009 for (var j
= i
- rollPeriod
+ 1; j
< i
+ 1; j
++)
1010 sum
+= originalData
[j
][1];
1011 rollingData
[i
] = [originalData
[i
][0], sum
/ rollPeriod
];
1014 for (var i
= 0; i
< num_init_points
; i
++) {
1017 for (var j
= 0; j
< i
+ 1; j
++) {
1018 sum
+= originalData
[j
][1][0];
1019 variance
+= Math
.pow(originalData
[j
][1][1], 2);
1021 var stddev
= Math
.sqrt(variance
)/(i
+1);
1022 rollingData
[i
] = [originalData
[i
][0],
1023 [sum
/(i
+1), sigma
* stddev
, sigma
* stddev
]];
1025 // Calculate the rolling average for the remaining points
1026 for (var i
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
1027 i
< originalData
.length
;
1031 for (var j
= i
- rollPeriod
+ 1; j
< i
+ 1; j
++) {
1032 sum
+= originalData
[j
][1][0];
1033 variance
+= Math
.pow(originalData
[j
][1][1], 2);
1035 var stddev
= Math
.sqrt(variance
) / rollPeriod
;
1036 rollingData
[i
] = [originalData
[i
][0],
1037 [sum
/ rollPeriod
, sigma
* stddev
, sigma
* stddev
]];
1046 * Parses a date, returning the number of milliseconds since epoch. This can be
1047 * passed in as an xValueParser in the DateGraph constructor.
1048 * @param {String} A date in YYYYMMDD format.
1049 * @return {Number} Milliseconds since epoch.
1052 DateGraph
.prototype.dateParser
= function(dateStr
) {
1054 if (dateStr
.length
== 10 && dateStr
.search("-") != -1) { // e.g. '2009-07-12'
1055 dateStrSlashed
= dateStr
.replace("-", "/", "g");
1056 while (dateStrSlashed
.search("-") != -1) {
1057 dateStrSlashed
= dateStrSlashed
.replace("-", "/");
1059 return Date
.parse(dateStrSlashed
);
1060 } else if (dateStr
.length
== 8) { // e.g. '20090712'
1061 dateStrSlashed
= dateStr
.substr(0,4) + "/" + dateStr
.substr(4,2)
1062 + "/" + dateStr
.substr(6,2);
1063 return Date
.parse(dateStrSlashed
);
1065 // Any format that Date.parse will accept, e.g. "2009/07/12" or
1066 // "2009/07/12 12:34:56"
1067 return Date
.parse(dateStr
);
1072 * Parses a string in a special csv format. We expect a csv file where each
1073 * line is a date point, and the first field in each line is the date string.
1074 * We also expect that all remaining fields represent series.
1075 * if this.errorBars_ is set, then interpret the fields as:
1076 * date, series1, stddev1, series2, stddev2, ...
1077 * @param {Array.<Object>} data See above.
1080 DateGraph
.prototype.parseCSV_
= function(data
) {
1082 var lines
= data
.split("\n");
1083 var start
= this.labelsFromCSV_
? 1 : 0;
1084 if (this.labelsFromCSV_
) {
1085 var labels
= lines
[0].split(",");
1086 labels
.shift(); // a "date" parameter is assumed.
1087 this.labels_
= labels
;
1088 // regenerate automatic colors.
1089 this.setColors_(this.attrs_
);
1090 this.renderOptions_
.colorScheme
= this.colors_
;
1091 MochiKit
.Base
.update(this.plotter_
.options
, this.renderOptions_
);
1092 MochiKit
.Base
.update(this.layoutOptions_
, this.attrs_
);
1095 for (var i
= start
; i
< lines
.length
; i
++) {
1096 var line
= lines
[i
];
1097 if (line
.length
== 0) continue; // skip blank lines
1098 var inFields
= line
.split(',');
1099 if (inFields
.length
< 2)
1103 fields
[0] = this.xValueParser_(inFields
[0]);
1105 // If fractions are expected, parse the numbers as "A/B
"
1106 if (this.fractions_) {
1107 for (var j = 1; j < inFields.length; j++) {
1108 // TODO(danvk): figure out an appropriate way to flag parse errors.
1109 var vals = inFields[j].split("/");
1110 fields[j] = [parseFloat(vals[0]), parseFloat(vals[1])];
1112 } else if (this.errorBars_) {
1113 // If there are error bars, values are (value, stddev) pairs
1114 for (var j = 1; j < inFields.length; j += 2)
1115 fields[(j + 1) / 2] = [parseFloat(inFields[j]),
1116 parseFloat(inFields[j + 1])];
1117 } else if (this.customBars_) {
1118 // Bars are a low;center;high tuple
1119 for (var j = 1; j < inFields.length; j++) {
1120 var vals = inFields[j].split(";");
1121 fields[j] = [ parseFloat(vals[0]),
1122 parseFloat(vals[1]),
1123 parseFloat(vals[2]) ];
1126 // Values are just numbers
1127 for (var j = 1; j < inFields.length; j++)
1128 fields[j] = parseFloat(inFields[j]);
1136 * Parses a DataTable object from gviz.
1137 * The data is expected to have a first column that is either a date or a
1138 * number. All subsequent columns must be numbers. If there is a clear mismatch
1139 * between this.xValueParser_ and the type of the first column, it will be
1140 * fixed. Returned value is in the same format as return value of parseCSV_.
1141 * @param {Array.<Object>} data See above.
1144 DateGraph.prototype.parseDataTable_ = function(data) {
1145 var cols = data.getNumberOfColumns();
1146 var rows = data.getNumberOfRows();
1148 // Read column labels
1150 for (var i = 0; i < cols; i++) {
1151 labels.push(data.getColumnLabel(i));
1153 labels.shift(); // the x-axis parameter is assumed and unnamed.
1154 this.labels_ = labels;
1155 // regenerate automatic colors.
1156 this.setColors_(this.attrs_);
1157 this.renderOptions_.colorScheme = this.colors_;
1158 MochiKit.Base.update(this.plotter_.options, this.renderOptions_);
1159 MochiKit.Base.update(this.layoutOptions_, this.attrs_);
1161 var indepType = data.getColumnType(0);
1162 if (indepType != 'date' && indepType != 'number') {
1163 // TODO(danvk): standardize error reporting.
1164 alert("only
'date' and
'number' types are supported
for column
1" +
1165 "of DataTable
input (Got
'" + indepType + "')");
1170 for (var i = 0; i < rows; i++) {
1172 if (indepType == 'date') {
1173 row.push(data.getValue(i, 0).getTime());
1175 row.push(data.getValue(i, 0));
1177 for (var j = 1; j < cols; j++) {
1178 row.push(data.getValue(i, j));
1186 * Get the CSV data. If it's in a function, call that function. If it's in a
1187 * file, do an XMLHttpRequest to get it.
1190 DateGraph.prototype.start_ = function() {
1191 if (typeof this.file_ == 'function') {
1192 // Stubbed out to allow this to run off a filesystem
1193 this.loadedEvent_(this.file_());
1194 } else if (typeof this.file_ == 'object' &&
1195 typeof this.file_.getColumnRange == 'function') {
1196 // must be a DataTable from gviz.
1197 this.rawData_ = this.parseDataTable_(this.file_);
1198 this.drawGraph_(this.rawData_);
1200 var req = new XMLHttpRequest();
1202 req.onreadystatechange = function () {
1203 if (req.readyState == 4) {
1204 if (req.status == 200) {
1205 caller.loadedEvent_(req.responseText);
1210 req.open("GET
", this.file_, true);
1216 * Changes various properties of the graph. These can include:
1218 * <li>file: changes the source data for the graph</li>
1219 * <li>errorBars: changes whether the data contains stddev</li>
1221 * @param {Object} attrs The new properties and values
1223 DateGraph.prototype.updateOptions = function(attrs) {
1224 if (attrs.errorBars) {
1225 this.errorBars_ = attrs.errorBars;
1227 if (attrs.customBars) {
1228 this.customBars_ = attrs.customBars;
1230 if (attrs.strokeWidth) {
1231 this.strokeWidth_ = attrs.strokeWidth;
1233 if (attrs.rollPeriod) {
1234 this.rollPeriod_ = attrs.rollPeriod;
1236 if (attrs.dateWindow) {
1237 this.dateWindow_ = attrs.dateWindow;
1239 if (attrs.valueRange) {
1240 this.valueRange_ = attrs.valueRange;
1242 if (attrs.minTickSize) {
1243 this.minTickSize_ = attrs.minTickSize;
1245 if (typeof(attrs.labels) != 'undefined') {
1246 this.labels_ = attrs.labels;
1247 this.labelsFromCSV_ = (attrs.labels == null);
1249 this.layout_.updateOptions({ 'errorBars': this.errorBars_ });
1250 if (attrs['file'] && attrs['file'] != this.file_) {
1251 this.file_ = attrs['file'];
1254 this.drawGraph_(this.rawData_);
1259 * Adjusts the number of days in the rolling average. Updates the graph to
1260 * reflect the new averaging period.
1261 * @param {Number} length Number of days over which to average the data.
1263 DateGraph.prototype.adjustRoll = function(length) {
1264 this.rollPeriod_ = length;
1265 this.drawGraph_(this.rawData_);
1270 * A wrapper around DateGraph that implements the gviz API.
1271 * @param {Object} container The DOM object the visualization should live in.
1273 DateGraph.GVizChart = function(container) {
1274 this.container = container;
1277 DateGraph.GVizChart.prototype.draw = function(data, options) {
1278 this.container.innerHTML = '';
1279 this.date_graph = new DateGraph(this.container, data, null, options || {});