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