4f5eca58b49459a6c266538ea16d9f2d75369e9a
[dygraphs.git] / dygraph.js
1 // Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
2 // All Rights Reserved.
3
4 /**
5 * @fileoverview Creates an interactive, zoomable graph based on a CSV file or
6 * string. 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)
10
11 Usage:
12 <div id="graphdiv" style="width:800px; height:500px;"></div>
13 <script type="text/javascript">
14 new DateGraph(document.getElementById("graphdiv"),
15 "datafile.csv",
16 ["Series 1", "Series 2"],
17 { }); // options
18 </script>
19
20 The CSV file is of the form
21
22 YYYYMMDD,A1,B1,C1
23 YYYYMMDD,A2,B2,C2
24
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.
27
28 If the 'errorBars' option is set in the constructor, the input should be of
29 the form
30
31 YYYYMMDD,A1,sigmaA1,B1,sigmaB1,...
32 YYYYMMDD,A2,sigmaA2,B2,sigmaB2,...
33
34 If the 'fractions' option is set, the input should be of the form:
35
36 YYYYMMDD,A1/B1,A2/B2,...
37 YYYYMMDD,A1/B1,A2/B2,...
38
39 And error bars will be calculated automatically using a binomial distribution.
40
41 For further documentation and examples, see http://www/~danvk/dg/
42
43 */
44
45 /**
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.
54 */
55 DateGraph = function(div, file, labels, attrs) {
56 if (arguments.length > 0)
57 this.__init__(div, file, labels, attrs);
58 };
59
60 DateGraph.NAME = "DateGraph";
61 DateGraph.VERSION = "1.1";
62 DateGraph.__repr__ = function() {
63 return "[" + this.NAME + " " + this.VERSION + "]";
64 };
65 DateGraph.toString = function() {
66 return this.__repr__();
67 };
68
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;
75
76 // Default attribute values.
77 DateGraph.DEFAULT_ATTRS = {
78 highlightCircleSize: 3,
79 pixelsPerXLabel: 60,
80 labelsDivWidth: 250,
81 labelsDivStyles: {
82 // TODO(danvk): move defaults from createStatusMessage_ here.
83 }
84
85 // TODO(danvk): default padding
86 };
87
88 /**
89 * Initializes the DateGraph. This creates a new DIV and constructs the PlotKit
90 * and interaction &lt;canvas&gt; inside of it. See the constructor for details
91 * on the parameters.
92 * @param {String | Function} file Source data
93 * @param {Array.<String>} labels Names of the data series
94 * @param {Object} attrs Miscellaneous other options
95 * @private
96 */
97 DateGraph.prototype.__init__ = function(div, file, labels, attrs) {
98 // Copy the important bits into the object
99 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
100 this.maindiv_ = div;
101 this.labels_ = labels;
102 this.file_ = file;
103 this.rollPeriod_ = attrs.rollPeriod || DateGraph.DEFAULT_ROLL_PERIOD;
104 this.previousVerticalX_ = -1;
105 this.width_ = parseInt(div.style.width, 10);
106 this.height_ = parseInt(div.style.height, 10);
107 this.errorBars_ = attrs.errorBars || false;
108 this.fractions_ = attrs.fractions || false;
109 this.strokeWidth_ = attrs.strokeWidth || DateGraph.DEFAULT_STROKE_WIDTH;
110 this.dateWindow_ = attrs.dateWindow || null;
111 this.valueRange_ = attrs.valueRange || null;
112 this.labelsSeparateLines = attrs.labelsSeparateLines || false;
113 this.labelsDiv_ = attrs.labelsDiv || null;
114 this.labelsKMB_ = attrs.labelsKMB || false;
115 this.minTickSize_ = attrs.minTickSize || 0;
116 this.xValueParser_ = attrs.xValueParser || DateGraph.prototype.dateParser;
117 this.xValueFormatter_ = attrs.xValueFormatter ||
118 DateGraph.prototype.dateString_;
119 this.xTicker_ = attrs.xTicker || DateGraph.prototype.dateTicker;
120 this.sigma_ = attrs.sigma || 2.0;
121 this.wilsonInterval_ = attrs.wilsonInterval || true;
122 this.customBars_ = attrs.customBars || false;
123
124 this.attrs_ = DateGraph.DEFAULT_ATTRS;
125 MochiKit.Base.update(this.attrs_, attrs);
126
127 if (typeof this.attrs_.pixelsPerXLabel == 'undefined') {
128 this.attrs_.pixelsPerXLabel = 60;
129 }
130
131 // Make a note of whether labels will be pulled from the CSV file.
132 this.labelsFromCSV_ = (this.labels_ == null);
133 if (this.labels_ == null)
134 this.labels_ = [];
135
136 // Prototype of the callback is "void clickCallback(event, date)"
137 this.clickCallback_ = attrs.clickCallback || null;
138
139 // Prototype of zoom callback is "void dragCallback(minDate, maxDate)"
140 this.zoomCallback_ = attrs.zoomCallback || null;
141
142 // Create the containing DIV and other interactive elements
143 this.createInterface_();
144
145 // Create the PlotKit grapher
146 this.layoutOptions_ = { 'errorBars': (this.errorBars_ || this.customBars_),
147 'xOriginIsZero': false };
148 MochiKit.Base.update(this.layoutOptions_, attrs);
149 this.setColors_(attrs);
150
151 this.layout_ = new DateGraphLayout(this.layoutOptions_);
152
153 this.renderOptions_ = { colorScheme: this.colors_,
154 strokeColor: null,
155 strokeWidth: this.strokeWidth_,
156 axisLabelFontSize: 14,
157 axisLineWidth: DateGraph.AXIS_LINE_WIDTH };
158 MochiKit.Base.update(this.renderOptions_, attrs);
159 this.plotter_ = new DateGraphCanvasRenderer(this.hidden_, this.layout_,
160 this.renderOptions_);
161
162 this.createStatusMessage_();
163 this.createRollInterface_();
164 this.createDragInterface_();
165
166 // connect(window, 'onload', this, function(e) { this.start_(); });
167 this.start_();
168 };
169
170 /**
171 * Returns the current rolling period, as set by the user or an option.
172 * @return {Number} The number of days in the rolling window
173 */
174 DateGraph.prototype.rollPeriod = function() {
175 return this.rollPeriod_;
176 }
177
178 /**
179 * Generates interface elements for the DateGraph: a containing div, a div to
180 * display the current point, and a textbox to adjust the rolling average
181 * period.
182 * @private
183 */
184 DateGraph.prototype.createInterface_ = function() {
185 // Create the all-enclosing graph div
186 var enclosing = this.maindiv_;
187
188 this.graphDiv = MochiKit.DOM.DIV( { style: { 'width': this.width_ + "px",
189 'height': this.height_ + "px"
190 }});
191 appendChildNodes(enclosing, this.graphDiv);
192
193 // Create the canvas to store
194 var canvas = MochiKit.DOM.CANVAS;
195 this.canvas_ = canvas( { style: { 'position': 'absolute' },
196 width: this.width_,
197 height: this.height_});
198 appendChildNodes(this.graphDiv, this.canvas_);
199
200 this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
201 connect(this.hidden_, 'onmousemove', this, function(e) { this.mouseMove_(e) });
202 connect(this.hidden_, 'onmouseout', this, function(e) { this.mouseOut_(e) });
203 }
204
205 /**
206 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
207 * this particular canvas. All DateGraph work is done on this.canvas_.
208 * @param {Object} canvas The DateGraph canvas to over which to overlay the plot
209 * @return {Object} The newly-created canvas
210 * @private
211 */
212 DateGraph.prototype.createPlotKitCanvas_ = function(canvas) {
213 var h = document.createElement("canvas");
214 h.style.position = "absolute";
215 h.style.top = canvas.style.top;
216 h.style.left = canvas.style.left;
217 h.width = this.width_;
218 h.height = this.height_;
219 MochiKit.DOM.appendChildNodes(this.graphDiv, h);
220 return h;
221 };
222
223 /**
224 * Generate a set of distinct colors for the data series. This is done with a
225 * color wheel. Saturation/Value are customizable, and the hue is
226 * equally-spaced around the color wheel. If a custom set of colors is
227 * specified, that is used instead.
228 * @param {Object} attrs Various attributes, e.g. saturation and value
229 * @private
230 */
231 DateGraph.prototype.setColors_ = function(attrs) {
232 var num = this.labels_.length;
233 this.colors_ = [];
234 if (!attrs.colors) {
235 var sat = attrs.colorSaturation || 1.0;
236 var val = attrs.colorValue || 0.5;
237 for (var i = 1; i <= num; i++) {
238 var hue = (1.0*i/(1+num));
239 this.colors_.push( MochiKit.Color.Color.fromHSV(hue, sat, val) );
240 }
241 } else {
242 for (var i = 0; i < num; i++) {
243 var colorStr = attrs.colors[i % attrs.colors.length];
244 this.colors_.push( MochiKit.Color.Color.fromString(colorStr) );
245 }
246 }
247 }
248
249 /**
250 * Create the div that contains information on the selected point(s)
251 * This goes in the top right of the canvas, unless an external div has already
252 * been specified.
253 * @private
254 */
255 DateGraph.prototype.createStatusMessage_ = function(){
256 if (!this.labelsDiv_) {
257 var divWidth = this.attrs_.labelsDivWidth;
258 var messagestyle = { "style": {
259 "position": "absolute",
260 "fontSize": "14px",
261 "zIndex": 10,
262 "width": divWidth + "px",
263 "top": "0px",
264 "left": this.width_ - divWidth + "px",
265 "background": "white",
266 "textAlign": "left",
267 "overflow": "hidden"}};
268 MochiKit.Base.update(messagestyle["style"], this.attrs_.labelsDivStyles);
269 this.labelsDiv_ = MochiKit.DOM.DIV(messagestyle);
270 MochiKit.DOM.appendChildNodes(this.graphDiv, this.labelsDiv_);
271 }
272 };
273
274 /**
275 * Create the text box to adjust the averaging period
276 * @return {Object} The newly-created text box
277 * @private
278 */
279 DateGraph.prototype.createRollInterface_ = function() {
280 var padding = this.plotter_.options.padding;
281 if (typeof this.attrs_.showRoller == 'undefined') {
282 this.attrs_.showRoller = false;
283 }
284 var display = this.attrs_.showRoller ? "block" : "none";
285 var textAttr = { "type": "text",
286 "size": "2",
287 "value": this.rollPeriod_,
288 "style": { "position": "absolute",
289 "zIndex": 10,
290 "top": (this.height_ - 25 - padding.bottom) + "px",
291 "left": (padding.left+1) + "px",
292 "display": display }
293 };
294 var roller = MochiKit.DOM.INPUT(textAttr);
295 var pa = this.graphDiv;
296 MochiKit.DOM.appendChildNodes(pa, roller);
297 connect(roller, 'onchange', this,
298 function() { this.adjustRoll(roller.value); });
299 return roller;
300 }
301
302 /**
303 * Set up all the mouse handlers needed to capture dragging behavior for zoom
304 * events. Uses MochiKit.Signal to attach all the event handlers.
305 * @private
306 */
307 DateGraph.prototype.createDragInterface_ = function() {
308 var self = this;
309
310 // Tracks whether the mouse is down right now
311 var mouseDown = false;
312 var dragStartX = null;
313 var dragStartY = null;
314 var dragEndX = null;
315 var dragEndY = null;
316 var prevEndX = null;
317
318 // Utility function to convert page-wide coordinates to canvas coords
319 var px = 0;
320 var py = 0;
321 var getX = function(e) { return e.mouse().page.x - px };
322 var getY = function(e) { return e.mouse().page.y - py };
323
324 // Draw zoom rectangles when the mouse is down and the user moves around
325 connect(this.hidden_, 'onmousemove', function(event) {
326 if (mouseDown) {
327 dragEndX = getX(event);
328 dragEndY = getY(event);
329
330 self.drawZoomRect_(dragStartX, dragEndX, prevEndX);
331 prevEndX = dragEndX;
332 }
333 });
334
335 // Track the beginning of drag events
336 connect(this.hidden_, 'onmousedown', function(event) {
337 mouseDown = true;
338 px = PlotKit.Base.findPosX(self.canvas_);
339 py = PlotKit.Base.findPosY(self.canvas_);
340 dragStartX = getX(event);
341 dragStartY = getY(event);
342 });
343
344 // If the user releases the mouse button during a drag, but not over the
345 // canvas, then it doesn't count as a zooming action.
346 connect(document, 'onmouseup', this, function(event) {
347 if (mouseDown) {
348 mouseDown = false;
349 dragStartX = null;
350 dragStartY = null;
351 }
352 });
353
354 // Temporarily cancel the dragging event when the mouse leaves the graph
355 connect(this.hidden_, 'onmouseout', this, function(event) {
356 if (mouseDown) {
357 dragEndX = null;
358 dragEndY = null;
359 }
360 });
361
362 // If the mouse is released on the canvas during a drag event, then it's a
363 // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
364 connect(this.hidden_, 'onmouseup', this, function(event) {
365 if (mouseDown) {
366 mouseDown = false;
367 dragEndX = getX(event);
368 dragEndY = getY(event);
369 var regionWidth = Math.abs(dragEndX - dragStartX);
370 var regionHeight = Math.abs(dragEndY - dragStartY);
371
372 if (regionWidth < 2 && regionHeight < 2 &&
373 self.clickCallback_ != null &&
374 self.lastx_ != undefined) {
375 self.clickCallback_(event, new Date(self.lastx_));
376 }
377
378 if (regionWidth >= 10) {
379 self.doZoom_(Math.min(dragStartX, dragEndX),
380 Math.max(dragStartX, dragEndX));
381 } else {
382 self.canvas_.getContext("2d").clearRect(0, 0,
383 self.canvas_.width,
384 self.canvas_.height);
385 }
386
387 dragStartX = null;
388 dragStartY = null;
389 }
390 });
391
392 // Double-clicking zooms back out
393 connect(this.hidden_, 'ondblclick', this, function(event) {
394 self.dateWindow_ = null;
395 self.drawGraph_(self.rawData_);
396 var minDate = self.rawData_[0][0];
397 var maxDate = self.rawData_[self.rawData_.length - 1][0];
398 if (self.zoomCallback_) {
399 self.zoomCallback_(minDate, maxDate);
400 }
401 });
402 };
403
404 /**
405 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
406 * up any previous zoom rectangles that were drawn. This could be optimized to
407 * avoid extra redrawing, but it's tricky to avoid interactions with the status
408 * dots.
409 * @param {Number} startX The X position where the drag started, in canvas
410 * coordinates.
411 * @param {Number} endX The current X position of the drag, in canvas coords.
412 * @param {Number} prevEndX The value of endX on the previous call to this
413 * function. Used to avoid excess redrawing
414 * @private
415 */
416 DateGraph.prototype.drawZoomRect_ = function(startX, endX, prevEndX) {
417 var ctx = this.canvas_.getContext("2d");
418
419 // Clean up from the previous rect if necessary
420 if (prevEndX) {
421 ctx.clearRect(Math.min(startX, prevEndX), 0,
422 Math.abs(startX - prevEndX), this.height_);
423 }
424
425 // Draw a light-grey rectangle to show the new viewing area
426 if (endX && startX) {
427 ctx.fillStyle = "rgba(128,128,128,0.33)";
428 ctx.fillRect(Math.min(startX, endX), 0,
429 Math.abs(endX - startX), this.height_);
430 }
431 };
432
433 /**
434 * Zoom to something containing [lowX, highX]. These are pixel coordinates
435 * in the canvas. The exact zoom window may be slightly larger if there are no
436 * data points near lowX or highX. This function redraws the graph.
437 * @param {Number} lowX The leftmost pixel value that should be visible.
438 * @param {Number} highX The rightmost pixel value that should be visible.
439 * @private
440 */
441 DateGraph.prototype.doZoom_ = function(lowX, highX) {
442 // Find the earliest and latest dates contained in this canvasx range.
443 var points = this.layout_.points;
444 var minDate = null;
445 var maxDate = null;
446 // Find the nearest [minDate, maxDate] that contains [lowX, highX]
447 for (var i = 0; i < points.length; i++) {
448 var cx = points[i].canvasx;
449 var x = points[i].xval;
450 if (cx < lowX && (minDate == null || x > minDate)) minDate = x;
451 if (cx > highX && (maxDate == null || x < maxDate)) maxDate = x;
452 }
453 // Use the extremes if either is missing
454 if (minDate == null) minDate = points[0].xval;
455 if (maxDate == null) maxDate = points[points.length-1].xval;
456
457 this.dateWindow_ = [minDate, maxDate];
458 this.drawGraph_(this.rawData_);
459 if (this.zoomCallback_) {
460 this.zoomCallback_(minDate, maxDate);
461 }
462 };
463
464 /**
465 * When the mouse moves in the canvas, display information about a nearby data
466 * point and draw dots over those points in the data series. This function
467 * takes care of cleanup of previously-drawn dots.
468 * @param {Object} event The mousemove event from the browser.
469 * @private
470 */
471 DateGraph.prototype.mouseMove_ = function(event) {
472 var canvasx = event.mouse().page.x - PlotKit.Base.findPosX(this.hidden_);
473 var points = this.layout_.points;
474
475 var lastx = -1;
476 var lasty = -1;
477
478 // Loop through all the points and find the date nearest to our current
479 // location.
480 var minDist = 1e+100;
481 var idx = -1;
482 for (var i = 0; i < points.length; i++) {
483 var dist = Math.abs(points[i].canvasx - canvasx);
484 if (dist > minDist) break;
485 minDist = dist;
486 idx = i;
487 }
488 if (idx >= 0) lastx = points[idx].xval;
489 // Check that you can really highlight the last day's data
490 if (canvasx > points[points.length-1].canvasx)
491 lastx = points[points.length-1].xval;
492
493 // Extract the points we've selected
494 var selPoints = [];
495 for (var i = 0; i < points.length; i++) {
496 if (points[i].xval == lastx) {
497 selPoints.push(points[i]);
498 }
499 }
500
501 // Clear the previously drawn vertical, if there is one
502 var circleSize = this.attrs_.highlightCircleSize;
503 var ctx = this.canvas_.getContext("2d");
504 if (this.previousVerticalX_ >= 0) {
505 var px = this.previousVerticalX_;
506 ctx.clearRect(px - circleSize - 1, 0, 2 * circleSize + 2, this.height_);
507 }
508
509 if (selPoints.length > 0) {
510 var canvasx = selPoints[0].canvasx;
511
512 // Set the status message to indicate the selected point(s)
513 var replace = this.xValueFormatter_(lastx) + ":";
514 var clen = this.colors_.length;
515 for (var i = 0; i < selPoints.length; i++) {
516 if (this.labelsSeparateLines) {
517 replace += "<br/>";
518 }
519 var point = selPoints[i];
520 replace += " <b><font color='" + this.colors_[i%clen].toHexString() + "'>"
521 + point.name + "</font></b>:"
522 + this.round_(point.yval, 2);
523 }
524 this.labelsDiv_.innerHTML = replace;
525
526 // Save last x position for callbacks.
527 this.lastx_ = lastx;
528
529 // Draw colored circles over the center of each selected point
530 ctx.save()
531 for (var i = 0; i < selPoints.length; i++) {
532 ctx.beginPath();
533 ctx.fillStyle = this.colors_[i%clen].toRGBString();
534 ctx.arc(canvasx, selPoints[i%clen].canvasy, circleSize, 0, 360, false);
535 ctx.fill();
536 }
537 ctx.restore();
538
539 this.previousVerticalX_ = canvasx;
540 }
541 };
542
543 /**
544 * The mouse has left the canvas. Clear out whatever artifacts remain
545 * @param {Object} event the mouseout event from the browser.
546 * @private
547 */
548 DateGraph.prototype.mouseOut_ = function(event) {
549 // Get rid of the overlay data
550 var ctx = this.canvas_.getContext("2d");
551 ctx.clearRect(0, 0, this.width_, this.height_);
552 this.labelsDiv_.innerHTML = "";
553 };
554
555 DateGraph.zeropad = function(x) {
556 if (x < 10) return "0" + x; else return "" + x;
557 }
558
559 /**
560 * Return a string version of the hours, minutes and seconds portion of a date.
561 * @param {Number} date The JavaScript date (ms since epoch)
562 * @return {String} A time of the form "HH:MM:SS"
563 * @private
564 */
565 DateGraph.prototype.hmsString_ = function(date) {
566 var zeropad = DateGraph.zeropad;
567 var d = new Date(date);
568 if (d.getSeconds()) {
569 return zeropad(d.getHours()) + ":" +
570 zeropad(d.getMinutes()) + ":" +
571 zeropad(d.getSeconds());
572 } else if (d.getMinutes()) {
573 return zeropad(d.getHours()) + ":" + zeropad(d.getMinutes());
574 } else {
575 return zeropad(d.getHours());
576 }
577 }
578
579 /**
580 * Convert a JS date (millis since epoch) to YYYY/MM/DD
581 * @param {Number} date The JavaScript date (ms since epoch)
582 * @return {String} A date of the form "YYYY/MM/DD"
583 * @private
584 */
585 DateGraph.prototype.dateString_ = function(date) {
586 var zeropad = DateGraph.zeropad;
587 var d = new Date(date);
588
589 // Get the year:
590 var year = "" + d.getFullYear();
591 // Get a 0 padded month string
592 var month = zeropad(d.getMonth() + 1); //months are 0-offset, sigh
593 // Get a 0 padded day string
594 var day = zeropad(d.getDate());
595
596 var ret = "";
597 var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
598 if (frac) ret = " " + this.hmsString_(date);
599
600 return year + "/" + month + "/" + day + ret;
601 };
602
603 /**
604 * Round a number to the specified number of digits past the decimal point.
605 * @param {Number} num The number to round
606 * @param {Number} places The number of decimals to which to round
607 * @return {Number} The rounded number
608 * @private
609 */
610 DateGraph.prototype.round_ = function(num, places) {
611 var shift = Math.pow(10, places);
612 return Math.round(num * shift)/shift;
613 };
614
615 /**
616 * Fires when there's data available to be graphed.
617 * @param {String} data Raw CSV data to be plotted
618 * @private
619 */
620 DateGraph.prototype.loadedEvent_ = function(data) {
621 this.rawData_ = this.parseCSV_(data);
622 this.drawGraph_(this.rawData_);
623 };
624
625 DateGraph.prototype.months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
626 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
627 DateGraph.prototype.quarters = ["Jan", "Apr", "Jul", "Oct"];
628
629 /**
630 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
631 * @private
632 */
633 DateGraph.prototype.addXTicks_ = function() {
634 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
635 var startDate, endDate;
636 if (this.dateWindow_) {
637 startDate = this.dateWindow_[0];
638 endDate = this.dateWindow_[1];
639 } else {
640 startDate = this.rawData_[0][0];
641 endDate = this.rawData_[this.rawData_.length - 1][0];
642 }
643
644 var xTicks = this.xTicker_(startDate, endDate);
645 this.layout_.updateOptions({xTicks: xTicks});
646 };
647
648 // Time granularity enumeration
649 DateGraph.SECONDLY = 0;
650 DateGraph.MINUTELY = 1;
651 DateGraph.HOURLY = 2;
652 DateGraph.DAILY = 3;
653 DateGraph.WEEKLY = 4;
654 DateGraph.MONTHLY = 5;
655 DateGraph.QUARTERLY = 6;
656 DateGraph.BIANNUAL = 7;
657 DateGraph.ANNUAL = 8;
658 DateGraph.DECADAL = 9;
659 DateGraph.NUM_GRANULARITIES = 10;
660
661 DateGraph.SHORT_SPACINGS = [];
662 DateGraph.SHORT_SPACINGS[DateGraph.SECONDLY] = 1000 * 1;
663 DateGraph.SHORT_SPACINGS[DateGraph.MINUTELY] = 1000 * 60;
664 DateGraph.SHORT_SPACINGS[DateGraph.HOURLY] = 1000 * 3600;
665 DateGraph.SHORT_SPACINGS[DateGraph.DAILY] = 1000 * 86400;
666 DateGraph.SHORT_SPACINGS[DateGraph.WEEKLY] = 1000 * 604800;
667
668 // NumXTicks()
669 //
670 // If we used this time granularity, how many ticks would there be?
671 // This is only an approximation, but it's generally good enough.
672 //
673 DateGraph.prototype.NumXTicks = function(start_time, end_time, granularity) {
674 if (granularity < DateGraph.MONTHLY) {
675 // Generate one tick mark for every fixed interval of time.
676 var spacing = DateGraph.SHORT_SPACINGS[granularity];
677 return Math.floor(0.5 + 1.0 * (end_time - start_time) / spacing);
678 } else {
679 var year_mod = 1; // e.g. to only print one point every 10 years.
680 var num_months = 12;
681 if (granularity == DateGraph.QUARTERLY) num_months = 3;
682 if (granularity == DateGraph.BIANNUAL) num_months = 2;
683 if (granularity == DateGraph.ANNUAL) num_months = 1;
684 if (granularity == DateGraph.DECADAL) { num_months = 1; year_mod = 10; }
685
686 var msInYear = 365.2524 * 24 * 3600 * 1000;
687 var num_years = 1.0 * (end_time - start_time) / msInYear;
688 return Math.floor(0.5 + 1.0 * num_years * num_months / year_mod);
689 }
690 };
691
692 // GetXAxis()
693 //
694 // Construct an x-axis of nicely-formatted times on meaningful boundaries
695 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
696 //
697 // Returns an array containing {v: millis, label: label} dictionaries.
698 //
699 DateGraph.prototype.GetXAxis = function(start_time, end_time, granularity) {
700 var ticks = [];
701 if (granularity < DateGraph.MONTHLY) {
702 // Generate one tick mark for every fixed interval of time.
703 var spacing = DateGraph.SHORT_SPACINGS[granularity];
704 var format = '%d%b'; // e.g. "1 Jan"
705 for (var t = start_time; t <= end_time; t += spacing) {
706 var d = new Date(t);
707 var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
708 if (frac == 0 || granularity >= DateGraph.DAILY) {
709 // the extra hour covers DST problems.
710 ticks.push({ v:t, label: new Date(t + 3600*1000).strftime(format) });
711 } else {
712 ticks.push({ v:t, label: this.hmsString_(t) });
713 }
714 }
715 } else {
716 // Display a tick mark on the first of a set of months of each year.
717 // Years get a tick mark iff y % year_mod == 0. This is useful for
718 // displaying a tick mark once every 10 years, say, on long time scales.
719 var months;
720 var year_mod = 1; // e.g. to only print one point every 10 years.
721
722 // TODO(danvk): use CachingRoundTime where appropriate to get boundaries.
723 if (granularity == DateGraph.MONTHLY) {
724 months = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
725 } else if (granularity == DateGraph.QUARTERLY) {
726 months = [ 0, 3, 6, 9 ];
727 } else if (granularity == DateGraph.BIANNUAL) {
728 months = [ 0, 6 ];
729 } else if (granularity == DateGraph.ANNUAL) {
730 months = [ 0 ];
731 } else if (granularity == DateGraph.DECADAL) {
732 months = [ 0 ];
733 year_mod = 10;
734 }
735
736 var start_year = new Date(start_time).getFullYear();
737 var end_year = new Date(end_time).getFullYear();
738 var zeropad = DateGraph.zeropad;
739 for (var i = start_year; i <= end_year; i++) {
740 if (i % year_mod != 0) continue;
741 for (var j = 0; j < months.length; j++) {
742 var date_str = i + "/" + zeropad(1 + months[j]) + "/01";
743 var t = Date.parse(date_str);
744 if (t < start_time || t > end_time) continue;
745 ticks.push({ v:t, label: new Date(t).strftime('%b %y') });
746 }
747 }
748 }
749
750 return ticks;
751 };
752
753
754 /**
755 * Add ticks to the x-axis based on a date range.
756 * @param {Number} startDate Start of the date window (millis since epoch)
757 * @param {Number} endDate End of the date window (millis since epoch)
758 * @return {Array.<Object>} Array of {label, value} tuples.
759 * @public
760 */
761 DateGraph.prototype.dateTicker = function(startDate, endDate) {
762 var chosen = -1;
763 for (var i = 0; i < DateGraph.NUM_GRANULARITIES; i++) {
764 var num_ticks = this.NumXTicks(startDate, endDate, i);
765 if (this.width_ / num_ticks >= this.attrs_.pixelsPerXLabel) {
766 chosen = i;
767 break;
768 }
769 }
770
771 if (chosen >= 0) {
772 return this.GetXAxis(startDate, endDate, chosen);
773 } else {
774 // TODO(danvk): signal error.
775 }
776 };
777
778 /**
779 * Add ticks when the x axis has numbers on it (instead of dates)
780 * @param {Number} startDate Start of the date window (millis since epoch)
781 * @param {Number} endDate End of the date window (millis since epoch)
782 * @return {Array.<Object>} Array of {label, value} tuples.
783 * @public
784 */
785 DateGraph.prototype.numericTicks = function(minV, maxV) {
786 var scale;
787 if (maxV <= 0.0) {
788 scale = 1.0;
789 } else {
790 scale = Math.pow( 10, Math.floor(Math.log(maxV)/Math.log(10.0)) );
791 }
792
793 // Add a smallish number of ticks at human-friendly points
794 var nTicks = (maxV - minV) / scale;
795 while (2 * nTicks < 20) {
796 nTicks *= 2;
797 }
798 if ((maxV - minV) / nTicks < this.minTickSize_) {
799 nTicks = this.round_((maxV - minV) / this.minTickSize_, 1);
800 }
801
802 // Construct labels for the ticks
803 var ticks = [];
804 for (var i = 0; i <= nTicks; i++) {
805 var tickV = minV + i * (maxV - minV) / nTicks;
806 var label = this.round_(tickV, 2);
807 if (this.labelsKMB_) {
808 var k = 1000;
809 if (tickV >= k*k*k) {
810 label = this.round_(tickV/(k*k*k), 1) + "B";
811 } else if (tickV >= k*k) {
812 label = this.round_(tickV/(k*k), 1) + "M";
813 } else if (tickV >= k) {
814 label = this.round_(tickV/k, 1) + "K";
815 }
816 }
817 ticks.push( {label: label, v: tickV} );
818 }
819 return ticks;
820 };
821
822 /**
823 * Adds appropriate ticks on the y-axis
824 * @param {Number} minY The minimum Y value in the data set
825 * @param {Number} maxY The maximum Y value in the data set
826 * @private
827 */
828 DateGraph.prototype.addYTicks_ = function(minY, maxY) {
829 // Set the number of ticks so that the labels are human-friendly.
830 var ticks = this.numericTicks(minY, maxY);
831 this.layout_.updateOptions( { yAxis: [minY, maxY],
832 yTicks: ticks } );
833 };
834
835 /**
836 * Update the graph with new data. Data is in the format
837 * [ [date1, val1, val2, ...], [date2, val1, val2, ...] if errorBars=false
838 * or, if errorBars=true,
839 * [ [date1, [val1,stddev1], [val2,stddev2], ...], [date2, ...], ...]
840 * @param {Array.<Object>} data The data (see above)
841 * @private
842 */
843 DateGraph.prototype.drawGraph_ = function(data) {
844 var maxY = null;
845 this.layout_.removeAllDatasets();
846 // Loop over all fields in the dataset
847 for (var i = 1; i < data[0].length; i++) {
848 var series = [];
849 for (var j = 0; j < data.length; j++) {
850 var date = data[j][0];
851 series[j] = [date, data[j][i]];
852 }
853 series = this.rollingAverage(series, this.rollPeriod_);
854
855 // Prune down to the desired range, if necessary (for zooming)
856 var bars = this.errorBars_ || this.customBars_;
857 if (this.dateWindow_) {
858 var low = this.dateWindow_[0];
859 var high= this.dateWindow_[1];
860 var pruned = [];
861 for (var k = 0; k < series.length; k++) {
862 if (series[k][0] >= low && series[k][0] <= high) {
863 pruned.push(series[k]);
864 var y = bars ? series[k][1][0] : series[k][1];
865 if (maxY == null || y > maxY) maxY = y;
866 }
867 }
868 series = pruned;
869 } else {
870 for (var j = 0; j < series.length; j++) {
871 var y = bars ? series[j][1][0] : series[j][1];
872 if (maxY == null || y > maxY) {
873 maxY = bars ? y + series[j][1][1] : y;
874 }
875 }
876 }
877
878 if (bars) {
879 var vals = [];
880 for (var j=0; j<series.length; j++)
881 vals[j] = [series[j][0],
882 series[j][1][0], series[j][1][1], series[j][1][2]];
883 this.layout_.addDataset(this.labels_[i - 1], vals);
884 } else {
885 this.layout_.addDataset(this.labels_[i - 1], series);
886 }
887 }
888
889 // Use some heuristics to come up with a good maxY value, unless it's been
890 // set explicitly by the user.
891 if (this.valueRange_ != null) {
892 this.addYTicks_(this.valueRange_[0], this.valueRange_[1]);
893 } else {
894 // Add some padding and round up to an integer to be human-friendly.
895 maxY *= 1.1;
896 if (maxY <= 0.0) maxY = 1.0;
897 else {
898 var scale = Math.pow(10, Math.floor(Math.log(maxY) / Math.log(10.0)));
899 maxY = scale * Math.ceil(maxY / scale);
900 }
901 this.addYTicks_(0, maxY);
902 }
903
904 this.addXTicks_();
905
906 // Tell PlotKit to use this new data and render itself
907 this.layout_.evaluateWithError();
908 this.plotter_.clear();
909 this.plotter_.render();
910 this.canvas_.getContext('2d').clearRect(0, 0,
911 this.canvas_.width, this.canvas_.height);
912 };
913
914 /**
915 * Calculates the rolling average of a data set.
916 * If originalData is [label, val], rolls the average of those.
917 * If originalData is [label, [, it's interpreted as [value, stddev]
918 * and the roll is returned in the same form, with appropriately reduced
919 * stddev for each value.
920 * Note that this is where fractional input (i.e. '5/10') is converted into
921 * decimal values.
922 * @param {Array} originalData The data in the appropriate format (see above)
923 * @param {Number} rollPeriod The number of days over which to average the data
924 */
925 DateGraph.prototype.rollingAverage = function(originalData, rollPeriod) {
926 if (originalData.length < 2)
927 return originalData;
928 var rollPeriod = Math.min(rollPeriod, originalData.length - 1);
929 var rollingData = [];
930 var sigma = this.sigma_;
931
932 if (this.fractions_) {
933 var num = 0;
934 var den = 0; // numerator/denominator
935 var mult = 100.0;
936 for (var i = 0; i < originalData.length; i++) {
937 num += originalData[i][1][0];
938 den += originalData[i][1][1];
939 if (i - rollPeriod >= 0) {
940 num -= originalData[i - rollPeriod][1][0];
941 den -= originalData[i - rollPeriod][1][1];
942 }
943
944 var date = originalData[i][0];
945 var value = den ? num / den : 0.0;
946 if (this.errorBars_) {
947 if (this.wilsonInterval_) {
948 // For more details on this confidence interval, see:
949 // http://en.wikipedia.org/wiki/Binomial_confidence_interval
950 if (den) {
951 var p = value < 0 ? 0 : value, n = den;
952 var pm = sigma * Math.sqrt(p*(1-p)/n + sigma*sigma/(4*n*n));
953 var denom = 1 + sigma * sigma / den;
954 var low = (p + sigma * sigma / (2 * den) - pm) / denom;
955 var high = (p + sigma * sigma / (2 * den) + pm) / denom;
956 rollingData[i] = [date,
957 [p * mult, (p - low) * mult, (high - p) * mult]];
958 } else {
959 rollingData[i] = [date, [0, 0, 0]];
960 }
961 } else {
962 var stddev = den ? sigma * Math.sqrt(value * (1 - value) / den) : 1.0;
963 rollingData[i] = [date, [mult * value, mult * stddev, mult * stddev]];
964 }
965 } else {
966 rollingData[i] = [date, mult * value];
967 }
968 }
969 } else if (this.customBars_) {
970 var low = 0;
971 var mid = 0;
972 var high = 0;
973 var count = 0;
974 for (var i = 0; i < originalData.length; i++) {
975 var data = originalData[i][1];
976 var y = data[1];
977 rollingData[i] = [originalData[i][0], [y, y - data[0], data[2] - y]];
978
979 low += data[0];
980 mid += y;
981 high += data[2];
982 count += 1;
983 if (i - rollPeriod >= 0) {
984 var prev = originalData[i - rollPeriod];
985 low -= prev[1][0];
986 mid -= prev[1][1];
987 high -= prev[1][2];
988 count -= 1;
989 }
990 rollingData[i] = [originalData[i][0], [ 1.0 * mid / count,
991 1.0 * (mid - low) / count,
992 1.0 * (high - mid) / count ]];
993 }
994 } else {
995 // Calculate the rolling average for the first rollPeriod - 1 points where
996 // there is not enough data to roll over the full number of days
997 var num_init_points = Math.min(rollPeriod - 1, originalData.length - 2);
998 if (!this.errorBars_){
999 for (var i = 0; i < num_init_points; i++) {
1000 var sum = 0;
1001 for (var j = 0; j < i + 1; j++)
1002 sum += originalData[j][1];
1003 rollingData[i] = [originalData[i][0], sum / (i + 1)];
1004 }
1005 // Calculate the rolling average for the remaining points
1006 for (var i = Math.min(rollPeriod - 1, originalData.length - 2);
1007 i < originalData.length;
1008 i++) {
1009 var sum = 0;
1010 for (var j = i - rollPeriod + 1; j < i + 1; j++)
1011 sum += originalData[j][1];
1012 rollingData[i] = [originalData[i][0], sum / rollPeriod];
1013 }
1014 } else {
1015 for (var i = 0; i < num_init_points; i++) {
1016 var sum = 0;
1017 var variance = 0;
1018 for (var j = 0; j < i + 1; j++) {
1019 sum += originalData[j][1][0];
1020 variance += Math.pow(originalData[j][1][1], 2);
1021 }
1022 var stddev = Math.sqrt(variance)/(i+1);
1023 rollingData[i] = [originalData[i][0],
1024 [sum/(i+1), sigma * stddev, sigma * stddev]];
1025 }
1026 // Calculate the rolling average for the remaining points
1027 for (var i = Math.min(rollPeriod - 1, originalData.length - 2);
1028 i < originalData.length;
1029 i++) {
1030 var sum = 0;
1031 var variance = 0;
1032 for (var j = i - rollPeriod + 1; j < i + 1; j++) {
1033 sum += originalData[j][1][0];
1034 variance += Math.pow(originalData[j][1][1], 2);
1035 }
1036 var stddev = Math.sqrt(variance) / rollPeriod;
1037 rollingData[i] = [originalData[i][0],
1038 [sum / rollPeriod, sigma * stddev, sigma * stddev]];
1039 }
1040 }
1041 }
1042
1043 return rollingData;
1044 };
1045
1046 /**
1047 * Parses a date, returning the number of milliseconds since epoch. This can be
1048 * passed in as an xValueParser in the DateGraph constructor.
1049 * @param {String} A date in YYYYMMDD format.
1050 * @return {Number} Milliseconds since epoch.
1051 * @public
1052 */
1053 DateGraph.prototype.dateParser = function(dateStr) {
1054 var dateStrSlashed;
1055 if (dateStr.length == 10 && dateStr.search("-") != -1) { // e.g. '2009-07-12'
1056 dateStrSlashed = dateStr.replace("-", "/", "g");
1057 while (dateStrSlashed.search("-") != -1) {
1058 dateStrSlashed = dateStrSlashed.replace("-", "/");
1059 }
1060 return Date.parse(dateStrSlashed);
1061 } else if (dateStr.length == 8) { // e.g. '20090712'
1062 dateStrSlashed = dateStr.substr(0,4) + "/" + dateStr.substr(4,2)
1063 + "/" + dateStr.substr(6,2);
1064 return Date.parse(dateStrSlashed);
1065 } else {
1066 // Any format that Date.parse will accept, e.g. "2009/07/12" or
1067 // "2009/07/12 12:34:56"
1068 return Date.parse(dateStr);
1069 }
1070 };
1071
1072 /**
1073 * Parses a string in a special csv format. We expect a csv file where each
1074 * line is a date point, and the first field in each line is the date string.
1075 * We also expect that all remaining fields represent series.
1076 * if this.errorBars_ is set, then interpret the fields as:
1077 * date, series1, stddev1, series2, stddev2, ...
1078 * @param {Array.<Object>} data See above.
1079 * @private
1080 */
1081 DateGraph.prototype.parseCSV_ = function(data) {
1082 var ret = [];
1083 var lines = data.split("\n");
1084 var start = this.labelsFromCSV_ ? 1 : 0;
1085 if (this.labelsFromCSV_) {
1086 var labels = lines[0].split(",");
1087 labels.shift(); // a "date" parameter is assumed.
1088 this.labels_ = labels;
1089 // regenerate automatic colors.
1090 this.setColors_(this.attrs_);
1091 this.renderOptions_.colorScheme = this.colors_;
1092 MochiKit.Base.update(this.plotter_.options, this.renderOptions_);
1093 MochiKit.Base.update(this.layoutOptions_, this.attrs_);
1094 }
1095
1096 for (var i = start; i < lines.length; i++) {
1097 var line = lines[i];
1098 if (line.length == 0) continue; // skip blank lines
1099 var inFields = line.split(',');
1100 if (inFields.length < 2)
1101 continue;
1102
1103 var fields = [];
1104 fields[0] = this.xValueParser_(inFields[0]);
1105
1106 // If fractions are expected, parse the numbers as "A/B"
1107 if (this.fractions_) {
1108 for (var j = 1; j < inFields.length; j++) {
1109 // TODO(danvk): figure out an appropriate way to flag parse errors.
1110 var vals = inFields[j].split("/");
1111 fields[j] = [parseFloat(vals[0]), parseFloat(vals[1])];
1112 }
1113 } else if (this.errorBars_) {
1114 // If there are error bars, values are (value, stddev) pairs
1115 for (var j = 1; j < inFields.length; j += 2)
1116 fields[(j + 1) / 2] = [parseFloat(inFields[j]),
1117 parseFloat(inFields[j + 1])];
1118 } else if (this.customBars_) {
1119 // Bars are a low;center;high tuple
1120 for (var j = 1; j < inFields.length; j++) {
1121 var vals = inFields[j].split(";");
1122 fields[j] = [ parseFloat(vals[0]),
1123 parseFloat(vals[1]),
1124 parseFloat(vals[2]) ];
1125 }
1126 } else {
1127 // Values are just numbers
1128 for (var j = 1; j < inFields.length; j++)
1129 fields[j] = parseFloat(inFields[j]);
1130 }
1131 ret.push(fields);
1132 }
1133 return ret;
1134 };
1135
1136 /**
1137 * Parses a DataTable object from gviz.
1138 * The data is expected to have a first column that is either a date or a
1139 * number. All subsequent columns must be numbers. If there is a clear mismatch
1140 * between this.xValueParser_ and the type of the first column, it will be
1141 * fixed. Returned value is in the same format as return value of parseCSV_.
1142 * @param {Array.<Object>} data See above.
1143 * @private
1144 */
1145 DateGraph.prototype.parseDataTable_ = function(data) {
1146 var cols = data.getNumberOfColumns();
1147 var rows = data.getNumberOfRows();
1148
1149 // Read column labels
1150 var labels = [];
1151 for (var i = 0; i < cols; i++) {
1152 labels.push(data.getColumnLabel(i));
1153 }
1154 labels.shift(); // the x-axis parameter is assumed and unnamed.
1155 this.labels_ = labels;
1156 // regenerate automatic colors.
1157 this.setColors_(this.attrs_);
1158 this.renderOptions_.colorScheme = this.colors_;
1159 MochiKit.Base.update(this.plotter_.options, this.renderOptions_);
1160 MochiKit.Base.update(this.layoutOptions_, this.attrs_);
1161
1162 var indepType = data.getColumnType(0);
1163 if (indepType != 'date' && indepType != 'number') {
1164 // TODO(danvk): standardize error reporting.
1165 alert("only 'date' and 'number' types are supported for column 1" +
1166 "of DataTable input (Got '" + indepType + "')");
1167 return null;
1168 }
1169
1170 var ret = [];
1171 for (var i = 0; i < rows; i++) {
1172 var row = [];
1173 if (indepType == 'date') {
1174 row.push(data.getValue(i, 0).getTime());
1175 } else {
1176 row.push(data.getValue(i, 0));
1177 }
1178 for (var j = 1; j < cols; j++) {
1179 row.push(data.getValue(i, j));
1180 }
1181 ret.push(row);
1182 }
1183 return ret;
1184 }
1185
1186 /**
1187 * Get the CSV data. If it's in a function, call that function. If it's in a
1188 * file, do an XMLHttpRequest to get it.
1189 * @private
1190 */
1191 DateGraph.prototype.start_ = function() {
1192 if (typeof this.file_ == 'function') {
1193 // Stubbed out to allow this to run off a filesystem
1194 this.loadedEvent_(this.file_());
1195 } else if (typeof this.file_ == 'object' &&
1196 typeof this.file_.getColumnRange == 'function') {
1197 // must be a DataTable from gviz.
1198 this.rawData_ = this.parseDataTable_(this.file_);
1199 this.drawGraph_(this.rawData_);
1200 } else {
1201 var req = new XMLHttpRequest();
1202 var caller = this;
1203 req.onreadystatechange = function () {
1204 if (req.readyState == 4) {
1205 if (req.status == 200) {
1206 caller.loadedEvent_(req.responseText);
1207 }
1208 }
1209 };
1210
1211 req.open("GET", this.file_, true);
1212 req.send(null);
1213 }
1214 };
1215
1216 /**
1217 * Changes various properties of the graph. These can include:
1218 * <ul>
1219 * <li>file: changes the source data for the graph</li>
1220 * <li>errorBars: changes whether the data contains stddev</li>
1221 * </ul>
1222 * @param {Object} attrs The new properties and values
1223 */
1224 DateGraph.prototype.updateOptions = function(attrs) {
1225 if (attrs.errorBars) {
1226 this.errorBars_ = attrs.errorBars;
1227 }
1228 if (attrs.customBars) {
1229 this.customBars_ = attrs.customBars;
1230 }
1231 if (attrs.strokeWidth) {
1232 this.strokeWidth_ = attrs.strokeWidth;
1233 }
1234 if (attrs.rollPeriod) {
1235 this.rollPeriod_ = attrs.rollPeriod;
1236 }
1237 if (attrs.dateWindow) {
1238 this.dateWindow_ = attrs.dateWindow;
1239 }
1240 if (attrs.valueRange) {
1241 this.valueRange_ = attrs.valueRange;
1242 }
1243 if (attrs.minTickSize) {
1244 this.minTickSize_ = attrs.minTickSize;
1245 }
1246 if (typeof(attrs.labels) != 'undefined') {
1247 this.labels_ = attrs.labels;
1248 this.labelsFromCSV_ = (attrs.labels == null);
1249 }
1250 this.layout_.updateOptions({ 'errorBars': this.errorBars_ });
1251 if (attrs['file'] && attrs['file'] != this.file_) {
1252 this.file_ = attrs['file'];
1253 this.start_();
1254 } else {
1255 this.drawGraph_(this.rawData_);
1256 }
1257 };
1258
1259 /**
1260 * Adjusts the number of days in the rolling average. Updates the graph to
1261 * reflect the new averaging period.
1262 * @param {Number} length Number of days over which to average the data.
1263 */
1264 DateGraph.prototype.adjustRoll = function(length) {
1265 this.rollPeriod_ = length;
1266 this.drawGraph_(this.rawData_);
1267 };
1268
1269
1270 /**
1271 * A wrapper around DateGraph that implements the gviz API.
1272 * @param {Object} container The DOM object the visualization should live in.
1273 */
1274 DateGraph.GVizChart = function(container) {
1275 this.container = container;
1276 }
1277
1278 DateGraph.GVizChart.prototype.draw = function(data, options) {
1279 this.container.innerHTML = '';
1280 this.date_graph = new DateGraph(this.container, data, null, options || {});
1281 }