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