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