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;
77 * Initializes the DateGraph. This creates a new DIV and constructs the PlotKit
78 * and interaction <canvas> inside of it. See the constructor for details
80 * @param {String | Function} file Source data
81 * @param {Array.<String>} labels Names of the data series
82 * @param {Object} attrs Miscellaneous other options
85 DateGraph
.prototype.__init__
= function(div
, file
, labels
, attrs
) {
86 // Copy the important bits into the object
88 this.labels_
= labels
;
90 this.rollPeriod_
= attrs
.rollPeriod
|| DateGraph
.DEFAULT_ROLL_PERIOD
;
91 this.previousVerticalX_
= -1;
92 this.width_
= parseInt(div
.style
.width
, 10);
93 this.height_
= parseInt(div
.style
.height
, 10);
94 this.errorBars_
= attrs
.errorBars
|| false;
95 this.fractions_
= attrs
.fractions
|| false;
96 this.strokeWidth_
= attrs
.strokeWidth
|| DateGraph
.DEFAULT_STROKE_WIDTH
;
97 this.dateWindow_
= attrs
.dateWindow
|| null;
98 this.valueRange_
= attrs
.valueRange
|| null;
99 this.labelsSeparateLines
= attrs
.labelsSeparateLines
|| false;
100 this.labelsDiv_
= attrs
.labelsDiv
|| null;
101 this.labelsKMB_
= attrs
.labelsKMB
|| false;
102 this.minTickSize_
= attrs
.minTickSize
|| 0;
103 this.xValueParser_
= attrs
.xValueParser
|| DateGraph
.prototype.dateParser
;
104 this.xValueFormatter_
= attrs
.xValueFormatter
||
105 DateGraph
.prototype.dateString_
;
106 this.xTicker_
= attrs
.xTicker
|| DateGraph
.prototype.dateTicker
;
107 this.sigma_
= attrs
.sigma
|| 2.0;
108 this.wilsonInterval_
= attrs
.wilsonInterval
|| true;
109 this.customBars_
= attrs
.customBars
|| false;
112 // Make a note of whether labels will be pulled from the CSV file.
113 this.labelsFromCSV_
= (this.labels_
== null);
114 if (this.labels_
== null)
117 // Prototype of the callback is "void clickCallback(event, date)"
118 this.clickCallback_
= attrs
.clickCallback
|| null;
120 // Prototype of zoom callback is "void dragCallback(minDate, maxDate)"
121 this.zoomCallback_
= attrs
.zoomCallback
|| null;
123 // Create the containing DIV and other interactive elements
124 this.createInterface_();
126 // Create the PlotKit grapher
127 this.layoutOptions_
= { 'errorBars': (this.errorBars_
|| this.customBars_
),
128 'xOriginIsZero': false };
129 MochiKit
.Base
.update(this.layoutOptions_
, attrs
);
130 this.setColors_(attrs
);
132 this.layout_
= new DateGraphLayout(this.layoutOptions_
);
134 this.renderOptions_
= { colorScheme
: this.colors_
,
136 strokeWidth
: this.strokeWidth_
,
137 axisLabelFontSize
: 14,
138 axisLineWidth
: DateGraph
.AXIS_LINE_WIDTH
};
139 MochiKit
.Base
.update(this.renderOptions_
, attrs
);
140 this.plotter_
= new DateGraphCanvasRenderer(this.hidden_
, this.layout_
,
141 this.renderOptions_
);
143 this.createStatusMessage_();
144 this.createRollInterface_();
145 this.createDragInterface_();
147 // connect(window, 'onload', this, function(e) { this.start_(); });
152 * Returns the current rolling period, as set by the user or an option.
153 * @return {Number} The number of days in the rolling window
155 DateGraph
.prototype.rollPeriod
= function() {
156 return this.rollPeriod_
;
160 * Generates interface elements for the DateGraph: a containing div, a div to
161 * display the current point, and a textbox to adjust the rolling average
165 DateGraph
.prototype.createInterface_
= function() {
166 // Create the all-enclosing graph div
167 var enclosing
= this.maindiv_
;
169 this.graphDiv
= MochiKit
.DOM
.DIV( { style
: { 'width': this.width_
+ "px",
170 'height': this.height_
+ "px"
172 appendChildNodes(enclosing
, this.graphDiv
);
174 // Create the canvas to store
175 var canvas
= MochiKit
.DOM
.CANVAS
;
176 this.canvas_
= canvas( { style
: { 'position': 'absolute' },
178 height
: this.height_
});
179 appendChildNodes(this.graphDiv
, this.canvas_
);
181 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
182 connect(this.hidden_
, 'onmousemove', this, function(e
) { this.mouseMove_(e
) });
183 connect(this.hidden_
, 'onmouseout', this, function(e
) { this.mouseOut_(e
) });
187 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
188 * this particular canvas. All DateGraph work is done on this.canvas_.
189 * @param {Object} canvas The DateGraph canvas to over which to overlay the plot
190 * @return {Object} The newly-created canvas
193 DateGraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
194 var h
= document
.createElement("canvas");
195 h
.style
.position
= "absolute";
196 h
.style
.top
= canvas
.style
.top
;
197 h
.style
.left
= canvas
.style
.left
;
198 h
.width
= this.width_
;
199 h
.height
= this.height_
;
200 MochiKit
.DOM
.appendChildNodes(this.graphDiv
, h
);
205 * Generate a set of distinct colors for the data series. This is done with a
206 * color wheel. Saturation/Value are customizable, and the hue is
207 * equally-spaced around the color wheel. If a custom set of colors is
208 * specified, that is used instead.
209 * @param {Object} attrs Various attributes, e.g. saturation and value
212 DateGraph
.prototype.setColors_
= function(attrs
) {
213 var num
= this.labels_
.length
;
216 var sat
= attrs
.colorSaturation
|| 1.0;
217 var val
= attrs
.colorValue
|| 0.5;
218 for (var i
= 1; i
<= num
; i
++) {
219 var hue
= (1.0*i
/(1+num
));
220 this.colors_
.push( MochiKit
.Color
.Color
.fromHSV(hue
, sat
, val
) );
223 for (var i
= 0; i
< num
; i
++) {
224 var colorStr
= attrs
.colors
[i
% attrs
.colors
.length
];
225 this.colors_
.push( MochiKit
.Color
.Color
.fromString(colorStr
) );
231 * Create the div that contains information on the selected point(s)
232 * This goes in the top right of the canvas, unless an external div has already
236 DateGraph
.prototype.createStatusMessage_
= function(){
237 if (!this.labelsDiv_
) {
239 var messagestyle
= { "style": {
240 "position": "absolute",
243 "width": divWidth
+ "px",
245 "left": this.width_
- divWidth
+ "px",
246 "background": "white",
248 "overflow": "hidden"}};
249 this.labelsDiv_
= MochiKit
.DOM
.DIV(messagestyle
);
250 MochiKit
.DOM
.appendChildNodes(this.graphDiv
, this.labelsDiv_
);
255 * Create the text box to adjust the averaging period
256 * @return {Object} The newly-created text box
259 DateGraph
.prototype.createRollInterface_
= function() {
260 var padding
= this.plotter_
.options
.padding
;
261 if (typeof this.attrs_
.showRoller
== 'undefined') {
262 this.attrs_
.showRoller
= false;
264 var display
= this.attrs_
.showRoller
? "block" : "none";
265 var textAttr
= { "type": "text",
267 "value": this.rollPeriod_
,
268 "style": { "position": "absolute",
270 "top": (this.height_
- 25 - padding
.bottom
) + "px",
271 "left": (padding
.left
+1) + "px",
274 var roller
= MochiKit
.DOM
.INPUT(textAttr
);
275 var pa
= this.graphDiv
;
276 MochiKit
.DOM
.appendChildNodes(pa
, roller
);
277 connect(roller
, 'onchange', this,
278 function() { this.adjustRoll(roller
.value
); });
283 * Set up all the mouse handlers needed to capture dragging behavior for zoom
284 * events. Uses MochiKit.Signal to attach all the event handlers.
287 DateGraph
.prototype.createDragInterface_
= function() {
290 // Tracks whether the mouse is down right now
291 var mouseDown
= false;
292 var dragStartX
= null;
293 var dragStartY
= null;
298 // Utility function to convert page-wide coordinates to canvas coords
301 var getX
= function(e
) { return e
.mouse().page
.x
- px
};
302 var getY
= function(e
) { return e
.mouse().page
.y
- py
};
304 // Draw zoom rectangles when the mouse is down and the user moves around
305 connect(this.hidden_
, 'onmousemove', function(event
) {
307 dragEndX
= getX(event
);
308 dragEndY
= getY(event
);
310 self
.drawZoomRect_(dragStartX
, dragEndX
, prevEndX
);
315 // Track the beginning of drag events
316 connect(this.hidden_
, 'onmousedown', function(event
) {
318 px
= PlotKit
.Base
.findPosX(self
.canvas_
);
319 py
= PlotKit
.Base
.findPosY(self
.canvas_
);
320 dragStartX
= getX(event
);
321 dragStartY
= getY(event
);
324 // If the user releases the mouse button during a drag, but not over the
325 // canvas, then it doesn't count as a zooming action.
326 connect(document
, 'onmouseup', this, function(event
) {
334 // Temporarily cancel the dragging event when the mouse leaves the graph
335 connect(this.hidden_
, 'onmouseout', this, function(event
) {
342 // If the mouse is released on the canvas during a drag event, then it's a
343 // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
344 connect(this.hidden_
, 'onmouseup', this, function(event
) {
347 dragEndX
= getX(event
);
348 dragEndY
= getY(event
);
349 var regionWidth
= Math
.abs(dragEndX
- dragStartX
);
350 var regionHeight
= Math
.abs(dragEndY
- dragStartY
);
352 if (regionWidth
< 2 && regionHeight
< 2 &&
353 self
.clickCallback_
!= null &&
354 self
.lastx_
!= undefined
) {
355 self
.clickCallback_(event
, new Date(self
.lastx_
));
358 if (regionWidth
>= 10) {
359 self
.doZoom_(Math
.min(dragStartX
, dragEndX
),
360 Math
.max(dragStartX
, dragEndX
));
362 self
.canvas_
.getContext("2d").clearRect(0, 0,
364 self
.canvas_
.height
);
372 // Double-clicking zooms back out
373 connect(this.hidden_
, 'ondblclick', this, function(event
) {
374 self
.dateWindow_
= null;
375 self
.drawGraph_(self
.rawData_
);
376 var minDate
= self
.rawData_
[0][0];
377 var maxDate
= self
.rawData_
[self
.rawData_
.length
- 1][0];
378 if (self
.zoomCallback_
) {
379 self
.zoomCallback_(minDate
, maxDate
);
385 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
386 * up any previous zoom rectangles that were drawn. This could be optimized to
387 * avoid extra redrawing, but it's tricky to avoid interactions with the status
389 * @param {Number} startX The X position where the drag started, in canvas
391 * @param {Number} endX The current X position of the drag, in canvas coords.
392 * @param {Number} prevEndX The value of endX on the previous call to this
393 * function. Used to avoid excess redrawing
396 DateGraph
.prototype.drawZoomRect_
= function(startX
, endX
, prevEndX
) {
397 var ctx
= this.canvas_
.getContext("2d");
399 // Clean up from the previous rect if necessary
401 ctx
.clearRect(Math
.min(startX
, prevEndX
), 0,
402 Math
.abs(startX
- prevEndX
), this.height_
);
405 // Draw a light-grey rectangle to show the new viewing area
406 if (endX
&& startX
) {
407 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
408 ctx
.fillRect(Math
.min(startX
, endX
), 0,
409 Math
.abs(endX
- startX
), this.height_
);
414 * Zoom to something containing [lowX, highX]. These are pixel coordinates
415 * in the canvas. The exact zoom window may be slightly larger if there are no
416 * data points near lowX or highX. This function redraws the graph.
417 * @param {Number} lowX The leftmost pixel value that should be visible.
418 * @param {Number} highX The rightmost pixel value that should be visible.
421 DateGraph
.prototype.doZoom_
= function(lowX
, highX
) {
422 // Find the earliest and latest dates contained in this canvasx range.
423 var points
= this.layout_
.points
;
426 // Find the nearest [minDate, maxDate] that contains [lowX, highX]
427 for (var i
= 0; i
< points
.length
; i
++) {
428 var cx
= points
[i
].canvasx
;
429 var x
= points
[i
].xval
;
430 if (cx
< lowX
&& (minDate
== null || x
> minDate
)) minDate
= x
;
431 if (cx
> highX
&& (maxDate
== null || x
< maxDate
)) maxDate
= x
;
433 // Use the extremes if either is missing
434 if (minDate
== null) minDate
= points
[0].xval
;
435 if (maxDate
== null) maxDate
= points
[points
.length
-1].xval
;
437 this.dateWindow_
= [minDate
, maxDate
];
438 this.drawGraph_(this.rawData_
);
439 if (this.zoomCallback_
) {
440 this.zoomCallback_(minDate
, maxDate
);
445 * When the mouse moves in the canvas, display information about a nearby data
446 * point and draw dots over those points in the data series. This function
447 * takes care of cleanup of previously-drawn dots.
448 * @param {Object} event The mousemove event from the browser.
451 DateGraph
.prototype.mouseMove_
= function(event
) {
452 var canvasx
= event
.mouse().page
.x
- PlotKit
.Base
.findPosX(this.hidden_
);
453 var points
= this.layout_
.points
;
458 // Loop through all the points and find the date nearest to our current
460 var minDist
= 1e+100;
462 for (var i
= 0; i
< points
.length
; i
++) {
463 var dist
= Math
.abs(points
[i
].canvasx
- canvasx
);
464 if (dist
> minDist
) break;
468 if (idx
>= 0) lastx
= points
[idx
].xval
;
469 // Check that you can really highlight the last day's data
470 if (canvasx
> points
[points
.length
-1].canvasx
)
471 lastx
= points
[points
.length
-1].xval
;
473 // Extract the points we've selected
475 for (var i
= 0; i
< points
.length
; i
++) {
476 if (points
[i
].xval
== lastx
) {
477 selPoints
.push(points
[i
]);
481 // Clear the previously drawn vertical, if there is one
483 var ctx
= this.canvas_
.getContext("2d");
484 if (this.previousVerticalX_
>= 0) {
485 var px
= this.previousVerticalX_
;
486 ctx
.clearRect(px
- circleSize
- 1, 0, 2 * circleSize
+ 2, this.height_
);
489 if (selPoints
.length
> 0) {
490 var canvasx
= selPoints
[0].canvasx
;
492 // Set the status message to indicate the selected point(s)
493 var replace
= this.xValueFormatter_(lastx
) + ":";
494 var clen
= this.colors_
.length
;
495 for (var i
= 0; i
< selPoints
.length
; i
++) {
496 if (this.labelsSeparateLines
) {
499 var point
= selPoints
[i
];
500 replace
+= " <b><font color='" + this.colors_
[i
%clen
].toHexString() + "'>"
501 + point
.name
+ "</font></b>:"
502 + this.round_(point
.yval
, 2);
504 this.labelsDiv_
.innerHTML
= replace
;
506 // Save last x position for callbacks.
509 // Draw colored circles over the center of each selected point
511 for (var i
= 0; i
< selPoints
.length
; i
++) {
513 ctx
.fillStyle
= this.colors_
[i
%clen
].toRGBString();
514 ctx
.arc(canvasx
, selPoints
[i
%clen
].canvasy
, circleSize
, 0, 360, false);
519 this.previousVerticalX_
= canvasx
;
524 * The mouse has left the canvas. Clear out whatever artifacts remain
525 * @param {Object} event the mouseout event from the browser.
528 DateGraph
.prototype.mouseOut_
= function(event
) {
529 // Get rid of the overlay data
530 var ctx
= this.canvas_
.getContext("2d");
531 ctx
.clearRect(0, 0, this.width_
, this.height_
);
532 this.labelsDiv_
.innerHTML
= "";
536 * Convert a JS date (millis since epoch) to YYYY/MM/DD
537 * @param {Number} date The JavaScript date (ms since epoch)
538 * @return {String} A date of the form "YYYY/MM/DD"
541 DateGraph
.prototype.dateString_
= function(date
) {
542 var d
= new Date(date
);
545 var year
= "" + d
.getFullYear();
546 // Get a 0 padded month string
547 var month
= "" + (d
.getMonth() + 1); //months are 0-offset, sigh
548 if (month
.length
< 2) month
= "0" + month
;
549 // Get a 0 padded day string
550 var day
= "" + d
.getDate();
551 if (day
.length
< 2) day
= "0" + day
;
553 return year
+ "/" + month + "/" + day
;
557 * Round a number to the specified number of digits past the decimal point.
558 * @param {Number} num The number to round
559 * @param {Number} places The number of decimals to which to round
560 * @return {Number} The rounded number
563 DateGraph
.prototype.round_
= function(num
, places
) {
564 var shift
= Math
.pow(10, places
);
565 return Math
.round(num
* shift
)/shift
;
569 * Fires when there's data available to be graphed.
570 * @param {String} data Raw CSV data to be plotted
573 DateGraph
.prototype.loadedEvent_
= function(data
) {
574 this.rawData_
= this.parseCSV_(data
);
575 this.drawGraph_(this.rawData_
);
578 DateGraph
.prototype.months
= ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
579 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
580 DateGraph
.prototype.quarters
= ["Jan", "Apr", "Jul", "Oct"];
583 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
586 DateGraph
.prototype.addXTicks_
= function() {
587 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
588 var startDate
, endDate
;
589 if (this.dateWindow_
) {
590 startDate
= this.dateWindow_
[0];
591 endDate
= this.dateWindow_
[1];
593 startDate
= this.rawData_
[0][0];
594 endDate
= this.rawData_
[this.rawData_
.length
- 1][0];
597 var xTicks
= this.xTicker_(startDate
, endDate
);
598 this.layout_
.updateOptions({xTicks
: xTicks
});
602 * Add ticks to the x-axis based on a date range.
603 * @param {Number} startDate Start of the date window (millis since epoch)
604 * @param {Number} endDate End of the date window (millis since epoch)
605 * @return {Array.<Object>} Array of {label, value} tuples.
608 DateGraph
.prototype.dateTicker
= function(startDate
, endDate
) {
609 var ONE_DAY
= 24*60*60*1000;
610 startDate
= startDate
/ ONE_DAY
;
611 endDate
= endDate
/ ONE_DAY
;
612 var dateSpan
= endDate
- startDate
;
615 var isMonthly
= false;
617 if (dateSpan
> 30 * 366) { // decadal
621 } else if (dateSpan
> 4*366) { // annual
624 } else if (dateSpan
> 366) { // quarterly
625 scale
= this.quarters
;
627 } else if (dateSpan
> 40) { // monthly
630 } else if (dateSpan
> 10) { // weekly
631 for (var week
= startDate
- 14; week
< endDate
+ 14; week
+= 7) {
632 scale
.push(week
* ONE_DAY
);
634 } else if (dateSpan
> 1) { // daily
635 for (var day
= startDate
- 14; day
< endDate
+ 14; day
+= 1) {
636 scale
.push(day
* ONE_DAY
);
639 for (var hour
= (startDate
- 1) * 24;
640 hour
< (endDate
+ 1) * 24; hour
+= 1) {
641 scale
.push(hour
* 60*60*1000);
648 var startYear
= 1900 + (new Date(startDate
* ONE_DAY
)).getYear();
649 var endYear
= 1900 + (new Date(endDate
* ONE_DAY
)).getYear();
650 for (var i
= startYear
; i
<= endYear
; i
++) {
651 if (i
% yearMod
!= 0) continue;
652 for (var j
= 0; j
< scale
.length
; j
++ ) {
653 var date
= Date
.parse(scale
[j
] + " 1, " + i
);
654 xTicks
.push( {label
: scale
[j
] + "'" + ("" + i
).substr(2,2), v
: date
} );
658 for (var i
= 0; i
< scale
.length
; i
++) {
659 var date
= new Date(scale
[i
]);
660 var year
= date
.getFullYear().toString();
661 var label
= this.months
[date
.getMonth()] + date
.getDate();
662 label
+= "'" + year
.substr(year
.length
- 2, 2);
663 xTicks
.push( {label
: label
, v
: date
} );
670 * Add ticks when the x axis has numbers on it (instead of dates)
671 * @param {Number} startDate Start of the date window (millis since epoch)
672 * @param {Number} endDate End of the date window (millis since epoch)
673 * @return {Array.<Object>} Array of {label, value} tuples.
676 DateGraph
.prototype.numericTicks
= function(minV
, maxV
) {
681 scale
= Math
.pow( 10, Math
.floor(Math
.log(maxV
)/Math
.log(10.0)) );
684 // Add a smallish number of ticks at human-friendly points
685 var nTicks
= (maxV
- minV
) / scale
;
686 while (2 * nTicks
< 20) {
689 if ((maxV
- minV
) / nTicks
< this.minTickSize_
) {
690 nTicks
= this.round_((maxV
- minV
) / this.minTickSize_
, 1);
693 // Construct labels for the ticks
695 for (var i
= 0; i
<= nTicks
; i
++) {
696 var tickV
= minV
+ i
* (maxV
- minV
) / nTicks
;
697 var label
= this.round_(tickV
, 2);
698 if (this.labelsKMB_
) {
700 if (tickV
>= k
*k
*k
) {
701 label
= this.round_(tickV
/(k
*k
*k
), 1) + "B";
702 } else if (tickV
>= k
*k
) {
703 label
= this.round_(tickV
/(k
*k
), 1) + "M";
704 } else if (tickV
>= k
) {
705 label
= this.round_(tickV
/k
, 1) + "K";
708 ticks
.push( {label
: label
, v
: tickV
} );
714 * Adds appropriate ticks on the y-axis
715 * @param {Number} minY The minimum Y value in the data set
716 * @param {Number} maxY The maximum Y value in the data set
719 DateGraph
.prototype.addYTicks_
= function(minY
, maxY
) {
720 // Set the number of ticks so that the labels are human-friendly.
721 var ticks
= this.numericTicks(minY
, maxY
);
722 this.layout_
.updateOptions( { yAxis
: [minY
, maxY
],
727 * Update the graph with new data. Data is in the format
728 * [ [date1, val1, val2, ...], [date2, val1, val2, ...] if errorBars=false
729 * or, if errorBars=true,
730 * [ [date1, [val1,stddev1], [val2,stddev2], ...], [date2, ...], ...]
731 * @param {Array.<Object>} data The data (see above)
734 DateGraph
.prototype.drawGraph_
= function(data
) {
736 this.layout_
.removeAllDatasets();
737 // Loop over all fields in the dataset
738 for (var i
= 1; i
< data
[0].length
; i
++) {
740 for (var j
= 0; j
< data
.length
; j
++) {
741 var date
= data
[j
][0];
742 series
[j
] = [date
, data
[j
][i
]];
744 series
= this.rollingAverage(series
, this.rollPeriod_
);
746 // Prune down to the desired range, if necessary (for zooming)
747 var bars
= this.errorBars_
|| this.customBars_
;
748 if (this.dateWindow_
) {
749 var low
= this.dateWindow_
[0];
750 var high
= this.dateWindow_
[1];
752 for (var k
= 0; k
< series
.length
; k
++) {
753 if (series
[k
][0] >= low
&& series
[k
][0] <= high
) {
754 pruned
.push(series
[k
]);
755 var y
= bars
? series
[k
][1][0] : series
[k
][1];
756 if (maxY
== null || y
> maxY
) maxY
= y
;
761 for (var j
= 0; j
< series
.length
; j
++) {
762 var y
= bars
? series
[j
][1][0] : series
[j
][1];
763 if (maxY
== null || y
> maxY
) {
764 maxY
= bars
? y
+ series
[j
][1][1] : y
;
771 for (var j
=0; j
<series
.length
; j
++)
772 vals
[j
] = [series
[j
][0],
773 series
[j
][1][0], series
[j
][1][1], series
[j
][1][2]];
774 this.layout_
.addDataset(this.labels_
[i
- 1], vals
);
776 this.layout_
.addDataset(this.labels_
[i
- 1], series
);
780 // Use some heuristics to come up with a good maxY value, unless it's been
781 // set explicitly by the user.
782 if (this.valueRange_
!= null) {
783 this.addYTicks_(this.valueRange_
[0], this.valueRange_
[1]);
785 // Add some padding and round up to an integer to be human-friendly.
787 if (maxY
<= 0.0) maxY
= 1.0;
789 var scale
= Math
.pow(10, Math
.floor(Math
.log(maxY
) / Math
.log(10.0)));
790 maxY
= scale
* Math
.ceil(maxY
/ scale
);
792 this.addYTicks_(0, maxY
);
797 // Tell PlotKit to use this new data and render itself
798 this.layout_
.evaluateWithError();
799 this.plotter_
.clear();
800 this.plotter_
.render();
801 this.canvas_
.getContext('2d').clearRect(0, 0,
802 this.canvas_
.width
, this.canvas_
.height
);
806 * Calculates the rolling average of a data set.
807 * If originalData is [label, val], rolls the average of those.
808 * If originalData is [label, [, it's interpreted as [value, stddev]
809 * and the roll is returned in the same form, with appropriately reduced
810 * stddev for each value.
811 * Note that this is where fractional input (i.e. '5/10') is converted into
813 * @param {Array} originalData The data in the appropriate format (see above)
814 * @param {Number} rollPeriod The number of days over which to average the data
816 DateGraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
817 if (originalData
.length
< 2)
819 var rollPeriod
= Math
.min(rollPeriod
, originalData
.length
- 1);
820 var rollingData
= [];
821 var sigma
= this.sigma_
;
823 if (this.fractions_
) {
825 var den
= 0; // numerator/denominator
827 for (var i
= 0; i
< originalData
.length
; i
++) {
828 num
+= originalData
[i
][1][0];
829 den
+= originalData
[i
][1][1];
830 if (i
- rollPeriod
>= 0) {
831 num
-= originalData
[i
- rollPeriod
][1][0];
832 den
-= originalData
[i
- rollPeriod
][1][1];
835 var date
= originalData
[i
][0];
836 var value
= den
? num
/ den
: 0.0;
837 if (this.errorBars_
) {
838 if (this.wilsonInterval_
) {
839 // For more details on this confidence interval, see:
840 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
842 var p
= value
< 0 ? 0 : value
, n
= den
;
843 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
844 var denom
= 1 + sigma
* sigma
/ den
;
845 var low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
846 var high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
847 rollingData
[i
] = [date
,
848 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
850 rollingData
[i
] = [date
, [0, 0, 0]];
853 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
854 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
857 rollingData
[i
] = [date
, mult
* value
];
860 } else if (this.customBars_
) {
865 for (var i
= 0; i
< originalData
.length
; i
++) {
866 var data
= originalData
[i
][1];
868 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
874 if (i
- rollPeriod
>= 0) {
875 var prev
= originalData
[i
- rollPeriod
];
881 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
882 1.0 * (mid
- low
) / count
,
883 1.0 * (high
- mid
) / count
]];
886 // Calculate the rolling average for the first rollPeriod - 1 points where
887 // there is not enough data to roll over the full number of days
888 var num_init_points
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
889 if (!this.errorBars_
){
890 for (var i
= 0; i
< num_init_points
; i
++) {
892 for (var j
= 0; j
< i
+ 1; j
++)
893 sum
+= originalData
[j
][1];
894 rollingData
[i
] = [originalData
[i
][0], sum
/ (i
+ 1)];
896 // Calculate the rolling average for the remaining points
897 for (var i
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
898 i
< originalData
.length
;
901 for (var j
= i
- rollPeriod
+ 1; j
< i
+ 1; j
++)
902 sum
+= originalData
[j
][1];
903 rollingData
[i
] = [originalData
[i
][0], sum
/ rollPeriod
];
906 for (var i
= 0; i
< num_init_points
; i
++) {
909 for (var j
= 0; j
< i
+ 1; j
++) {
910 sum
+= originalData
[j
][1][0];
911 variance
+= Math
.pow(originalData
[j
][1][1], 2);
913 var stddev
= Math
.sqrt(variance
)/(i
+1);
914 rollingData
[i
] = [originalData
[i
][0],
915 [sum
/(i
+1), sigma
* stddev
, sigma
* stddev
]];
917 // Calculate the rolling average for the remaining points
918 for (var i
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
919 i
< originalData
.length
;
923 for (var j
= i
- rollPeriod
+ 1; j
< i
+ 1; j
++) {
924 sum
+= originalData
[j
][1][0];
925 variance
+= Math
.pow(originalData
[j
][1][1], 2);
927 var stddev
= Math
.sqrt(variance
) / rollPeriod
;
928 rollingData
[i
] = [originalData
[i
][0],
929 [sum
/ rollPeriod
, sigma
* stddev
, sigma
* stddev
]];
938 * Parses a date, returning the number of milliseconds since epoch. This can be
939 * passed in as an xValueParser in the DateGraph constructor.
940 * @param {String} A date in YYYYMMDD format.
941 * @return {Number} Milliseconds since epoch.
944 DateGraph
.prototype.dateParser
= function(dateStr
) {
946 if (dateStr
.length
== 10 && dateStr
.search("-") != -1) { // e.g. '2009-07-12'
947 dateStrSlashed
= dateStr
.replace("-", "/", "g");
948 while (dateStrSlashed
.search("-") != -1) {
949 dateStrSlashed
= dateStrSlashed
.replace("-", "/");
951 return Date
.parse(dateStrSlashed
);
952 } else if (dateStr
.length
== 8) { // e.g. '20090712'
953 dateStrSlashed
= dateStr
.substr(0,4) + "/" + dateStr
.substr(4,2)
954 + "/" + dateStr
.substr(6,2);
955 return Date
.parse(dateStrSlashed
);
957 // Any format that Date.parse will accept, e.g. "2009/07/12" or
958 // "2009/07/12 12:34:56"
959 return Date
.parse(dateStr
);
964 * Parses a string in a special csv format. We expect a csv file where each
965 * line is a date point, and the first field in each line is the date string.
966 * We also expect that all remaining fields represent series.
967 * if this.errorBars_ is set, then interpret the fields as:
968 * date, series1, stddev1, series2, stddev2, ...
969 * @param {Array.<Object>} data See above.
972 DateGraph
.prototype.parseCSV_
= function(data
) {
974 var lines
= data
.split("\n");
975 var start
= this.labelsFromCSV_
? 1 : 0;
976 if (this.labelsFromCSV_
) {
977 var labels
= lines
[0].split(",");
978 labels
.shift(); // a "date" parameter is assumed.
979 this.labels_
= labels
;
980 // regenerate automatic colors.
981 this.setColors_(this.attrs_
);
982 this.renderOptions_
.colorScheme
= this.colors_
;
983 MochiKit
.Base
.update(this.plotter_
.options
, this.renderOptions_
);
984 MochiKit
.Base
.update(this.layoutOptions_
, this.attrs_
);
987 for (var i
= start
; i
< lines
.length
; i
++) {
989 if (line
.length
== 0) continue; // skip blank lines
990 var inFields
= line
.split(',');
991 if (inFields
.length
< 2)
995 fields
[0] = this.xValueParser_(inFields
[0]);
997 // If fractions are expected, parse the numbers as "A/B
"
998 if (this.fractions_) {
999 for (var j = 1; j < inFields.length; j++) {
1000 // TODO(danvk): figure out an appropriate way to flag parse errors.
1001 var vals = inFields[j].split("/");
1002 fields[j] = [parseFloat(vals[0]), parseFloat(vals[1])];
1004 } else if (this.errorBars_) {
1005 // If there are error bars, values are (value, stddev) pairs
1006 for (var j = 1; j < inFields.length; j += 2)
1007 fields[(j + 1) / 2] = [parseFloat(inFields[j]),
1008 parseFloat(inFields[j + 1])];
1009 } else if (this.customBars_) {
1010 // Bars are a low;center;high tuple
1011 for (var j = 1; j < inFields.length; j++) {
1012 var vals = inFields[j].split(";");
1013 fields[j] = [ parseFloat(vals[0]),
1014 parseFloat(vals[1]),
1015 parseFloat(vals[2]) ];
1018 // Values are just numbers
1019 for (var j = 1; j < inFields.length; j++)
1020 fields[j] = parseFloat(inFields[j]);
1028 * Get the CSV data. If it's in a function, call that function. If it's in a
1029 * file, do an XMLHttpRequest to get it.
1032 DateGraph.prototype.start_ = function() {
1033 if (typeof this.file_ == 'function') {
1034 // Stubbed out to allow this to run off a filesystem
1035 this.loadedEvent_(this.file_());
1037 var req = new XMLHttpRequest();
1039 req.onreadystatechange = function () {
1040 if (req.readyState == 4) {
1041 if (req.status == 200) {
1042 caller.loadedEvent_(req.responseText);
1047 req.open("GET
", this.file_, true);
1053 * Changes various properties of the graph. These can include:
1055 * <li>file: changes the source data for the graph</li>
1056 * <li>errorBars: changes whether the data contains stddev</li>
1058 * @param {Object} attrs The new properties and values
1060 DateGraph.prototype.updateOptions = function(attrs) {
1061 if (attrs.errorBars) {
1062 this.errorBars_ = attrs.errorBars;
1064 if (attrs.customBars) {
1065 this.customBars_ = attrs.customBars;
1067 if (attrs.strokeWidth) {
1068 this.strokeWidth_ = attrs.strokeWidth;
1070 if (attrs.rollPeriod) {
1071 this.rollPeriod_ = attrs.rollPeriod;
1073 if (attrs.dateWindow) {
1074 this.dateWindow_ = attrs.dateWindow;
1076 if (attrs.valueRange) {
1077 this.valueRange_ = attrs.valueRange;
1079 if (attrs.minTickSize) {
1080 this.minTickSize_ = attrs.minTickSize;
1082 if (typeof(attrs.labels) != 'undefined') {
1083 this.labels_ = attrs.labels;
1084 this.labelsFromCSV_ = (attrs.labels == null);
1086 this.layout_.updateOptions({ 'errorBars': this.errorBars_ });
1087 if (attrs['file'] && attrs['file'] != this.file_) {
1088 this.file_ = attrs['file'];
1091 this.drawGraph_(this.rawData_);
1096 * Adjusts the number of days in the rolling average. Updates the graph to
1097 * reflect the new averaging period.
1098 * @param {Number} length Number of days over which to average the data.
1100 DateGraph.prototype.adjustRoll = function(length) {
1101 this.rollPeriod_ = length;
1102 this.drawGraph_(this.rawData_);