--- /dev/null
+// Copyright 2011 Google Inc. All Rights Reserved.
+
+/**
+ * @fileoverview Regression tests for range selector.
+ * @author paul.eric.felix@gmail.com (Paul Felix)
+ */
+var RangeSelectorTestCase = TestCase("range-selector");
+
+RangeSelectorTestCase.prototype.setUp = function() {
+ document.body.innerHTML = "<div id='graph'></div>";
+};
+
+RangeSelectorTestCase.prototype.tearDown = function() {
+};
+
+RangeSelectorTestCase.prototype.testRangeSelector = function() {
+ var opts = {
+ width: 480,
+ height: 320,
+ showRangeSelector: true
+ };
+ var data = [
+ [1, 10],
+ [2, 15],
+ [3, 10],
+ [4, 15],
+ [5, 10],
+ [6, 15],
+ [7, 10],
+ [8, 15],
+ [9, 10]
+ ];
+ var graph = document.getElementById("graph");
+ var g = new Dygraph(graph, data, opts);
+ this.assertGraphExistence(g, graph);
+};
+
+RangeSelectorTestCase.prototype.testRangeSelectorWithErrorBars = function() {
+ var opts = {
+ width: 480,
+ height: 320,
+ errorBars: true,
+ showRangeSelector: true
+ };
+ var data = [
+ [1, [10, 10]],
+ [2, [15, 10]],
+ [3, [10, 10]],
+ [4, [15, 10]],
+ [5, [10, 10]],
+ [6, [15, 20]],
+ [7, [10, 20]],
+ [8, [15, 20]],
+ [9, [10, 20]]
+ ];
+ var graph = document.getElementById("graph");
+ var g = new Dygraph(graph, data, opts);
+ this.assertGraphExistence(g, graph);
+};
+
+RangeSelectorTestCase.prototype.testRangeSelectorWithCustomBars = function() {
+ var opts = {
+ width: 480,
+ height: 320,
+ customBars: true,
+ showRangeSelector: true
+ };
+ var data = [
+ [1, [10, 10, 100]],
+ [2, [15, 20, 110]],
+ [3, [10, 30, 100]],
+ [4, [15, 40, 110]],
+ [5, [10, 120, 100]],
+ [6, [15, 50, 110]],
+ [7, [10, 70, 100]],
+ [8, [15, 90, 110]],
+ [9, [10, 50, 100]]
+ ];
+ var graph = document.getElementById("graph");
+ var g = new Dygraph(graph, data, opts);
+ this.assertGraphExistence(g, graph);
+};
+
+RangeSelectorTestCase.prototype.testRangeSelectorWithLogScale = function() {
+ var opts = {
+ width: 480,
+ height: 320,
+ logscale: true,
+ showRangeSelector: true
+ };
+ var data = [
+ [1, 10],
+ [2, 15],
+ [3, 10],
+ [4, 15],
+ [5, 10],
+ [6, 15],
+ [7, 10],
+ [8, 15],
+ [9, 10]
+ ];
+ var graph = document.getElementById("graph");
+ var g = new Dygraph(graph, data, opts);
+ this.assertGraphExistence(g, graph);
+};
+
+RangeSelectorTestCase.prototype.testRangeSelectorOptions = function() {
+ var opts = {
+ width: 480,
+ height: 320,
+ showRangeSelector: true,
+ rangeSelectorHeight: 30,
+ rangeSelectorPlotFillColor: 'lightyellow',
+ rangeSelectorPlotStyleColor: 'yellow'
+ };
+ var data = [
+ [1, 10],
+ [2, 15],
+ [3, 10],
+ [4, 15],
+ [5, 10],
+ [6, 15],
+ [7, 10],
+ [8, 15],
+ [9, 10]
+ ];
+ var graph = document.getElementById("graph");
+ var g = new Dygraph(graph, data, opts);
+ this.assertGraphExistence(g, graph);
+};
+
+RangeSelectorTestCase.prototype.assertGraphExistence = function(g, graph) {
+ assertNotNull(g);
+ var zoomhandles = graph.getElementsByClassName('dygraph_rangesel_zoomhandle');
+ assertEquals(2, zoomhandles.length);
+ var bgcanvas = graph.getElementsByClassName('dygraph_rangesel_bgcanvas');
+ assertEquals(1, bgcanvas.length);
+ var fgcanvas = graph.getElementsByClassName('dygraph_rangesel_fgcanvas');
+ assertEquals(1, fgcanvas.length);
+}
assertEquals([{"v":1167627600000,"label":"Jan 07"},{"v":1175400000000,"label":"Apr 07"},{"v":1183262400000,"label":"Jul 07"},{"v":1191211200000,"label":"Oct 07"}], Dygraph.dateTicker(1167627600000, 1199077200000, 480, this.createOptionsViewForAxis('x')));
assertEquals([{"v":1167627600000,"label":"Jan 07"},{"v":1175400000000,"label":"Apr 07"},{"v":1183262400000,"label":"Jul 07"},{"v":1191211200000,"label":"Oct 07"}], Dygraph.dateTicker(1167627600000, 1199077200000, 600, this.createOptionsViewForAxis('x')));
// assertEquals([{"v":1167627600000,"label":"Jan 07"},{"v":1170306000000,"label":"Feb 07"},{"v":1172725200000,"label":"Mar 07"},{"v":1175400000000,"label":"Apr 07"},{"v":1177992000000,"label":"May 07"},{"v":1180670400000,"label":"Jun 07"},{"v":1183262400000,"label":"Jul 07"},{"v":1185940800000,"label":"Aug 07"},{"v":1188619200000,"label":"Sep 07"},{"v":1191211200000,"label":"Oct 07"},{"v":1193889600000,"label":"Nov 07"},{"v":1196485200000,"label":"Dec 07"},{"v":null,"label":"undefined NaN"}], Dygraph.dateTicker(1167627600000, 1199077200000, 800, this.createOptionsViewForAxis('x')));
+
+ // Test monthly for time span starting Dec 31, 2010.
+ assertEquals([{"v":1293858000000,"label":"Jan 11"},{"v":1296536400000,"label":"Feb 11"},{"v":1298955600000,"label":"Mar 11"},{"v":1301630400000,"label":"Apr 11"},{"v":1304222400000,"label":"May 11"},{"v":1306900800000,"label":"Jun 11"},{"v":1309492800000,"label":"Jul 11"},{"v":1312171200000,"label":"Aug 11"}], Dygraph.dateTicker(1293771600000, 1312862400000, 727, this.createOptionsViewForAxis('x')));
+
assertEquals([{"v":1201842000000,"label":"01Feb"},{"v":1201928400000,"label":"02Feb"},{"v":1202014800000,"label":"03Feb"},{"v":1202101200000,"label":"04Feb"},{"v":1202187600000,"label":"05Feb"},{"v":1202274000000,"label":"06Feb"}], Dygraph.dateTicker(1201842000000, 1202274000000, 700, this.createOptionsViewForAxis('x')));
assertEquals([{"v":1210132800000,"label":"07May"},{"v":1210154400000,"label":"06:00"},{"v":1210176000000,"label":"12:00"},{"v":1210197600000,"label":"18:00"},{"v":1210219200000,"label":"08May"},{"v":1210240800000,"label":"06:00"},{"v":1210262400000,"label":"12:00"},{"v":1210284000000,"label":"18:00"},{"v":1210305600000,"label":"09May"}], Dygraph.dateTicker(1210132800000, 1210305600000, 480, this.createOptionsViewForAxis('x')));
assertEquals([{"v":1210132800000,"label":"07May"},{"v":1210219200000,"label":"08May"},{"v":1210305600000,"label":"09May"},{"v":1210392000000,"label":"10May"},{"v":1210478400000,"label":"11May"}], Dygraph.dateTicker(1210132800000, 1210478400000, 480, this.createOptionsViewForAxis('x')));
this.annotations = new Array();
this.chartLabels = {};
- this.area = this.computeArea_();
+ this.area = layout.plotArea;
this.container.style.position = "relative";
this.container.style.width = this.width + "px";
return this.dygraph_.attr_(x);
};
-// Compute the box which the chart should be drawn in. This is the canvas's
-// box, less space needed for axis and chart labels.
-// TODO(danvk): this belongs in DygraphLayout.
-DygraphCanvasRenderer.prototype.computeArea_ = function() {
- var area = {
- // TODO(danvk): per-axis setting.
- x: 0,
- y: 0
- };
- if (this.attr_('drawYAxis')) {
- area.x = this.attr_('yAxisLabelWidth') + 2 * this.attr_('axisTickSize');
- }
-
- area.w = this.width - area.x - this.attr_('rightGap');
- area.h = this.height;
- if (this.attr_('drawXAxis')) {
- if (this.attr_('xAxisHeight')) {
- area.h -= this.attr_('xAxisHeight');
- } else {
- area.h -= this.attr_('axisLabelFontSize') + 2 * this.attr_('axisTickSize');
- }
- }
-
- // Shrink the drawing area to accomodate additional y-axes.
- if (this.dygraph_.numAxes() == 2) {
- // TODO(danvk): per-axis setting.
- area.w -= (this.attr_('yAxisLabelWidth') + 2 * this.attr_('axisTickSize'));
- } else if (this.dygraph_.numAxes() > 2) {
- this.dygraph_.error("Only two y-axes are supported at this time. (Trying " +
- "to use " + this.dygraph_.numAxes() + ")");
- }
-
- // Add space for chart labels: title, xlabel and ylabel.
- if (this.attr_('title')) {
- area.h -= this.attr_('titleHeight');
- area.y += this.attr_('titleHeight');
- }
- if (this.attr_('xlabel')) {
- area.h -= this.attr_('xLabelHeight');
- }
- if (this.attr_('ylabel')) {
- // It would make sense to shift the chart here to make room for the y-axis
- // label, but the default yAxisLabelWidth is large enough that this results
- // in overly-padded charts. The y-axis label should fit fine. If it
- // doesn't, the yAxisLabelWidth option can be increased.
- }
-
- return area;
-};
-
DygraphCanvasRenderer.prototype.clear = function() {
if (this.isIE) {
// VML takes a while to start up, so we just poll every this.IEDelay
// Do the ordinary rendering, as before
this._renderLineChart();
this._renderAxis();
- this._renderChartLabels();
+ this._renderChartLabels();
this._renderAnnotations();
};
color: this.attr_('axisLabelColor'),
width: this.attr_('axisLabelWidth') + "px",
// height: this.attr_('axisLabelFontSize') + 2 + "px",
+ lineHeight: "normal", // Something other than "normal" line-height screws up label positioning.
overflow: "hidden"
};
var makeDiv = function(txt, axis, prec_axis) {
var isNullOrNaN = function(x) {
return (x === null || isNaN(x));
};
-
+
// TODO(danvk): use this.attr_ for many of these.
var context = this.elementContext;
var fillAlpha = this.attr_('fillAlpha');
"dygraph-utils.js",
"dygraph-gviz.js",
"dygraph-interaction-model.js",
- "dygraph-options-reference.js", // Shouldn't be included in generate-combined.sh
- "dygraph-tickers.js"
+ "dygraph-range-selector.js",
+ "dygraph-tickers.js",
+ "dygraph-options-reference.js" // Shouldn't be included in generate-combined.sh
];
for (var i = 0; i < source_files.length; i++) {
this.datasets = new Array();
this.annotations = new Array();
this.yAxes_ = null;
+ this.plotArea = this.computePlotArea_();
// TODO(danvk): it's odd that xTicks_ and yTicks_ are inputs, but xticks and
// yticks are outputs. Clean this up.
this.datasets[setname] = set_xy;
};
+// Compute the box which the chart should be drawn in. This is the canvas's
+// box, less space needed for axis and chart labels.
+DygraphLayout.prototype.computePlotArea_ = function() {
+ var area = {
+ // TODO(danvk): per-axis setting.
+ x: 0,
+ y: 0
+ };
+ if (this.attr_('drawYAxis')) {
+ area.x = this.attr_('yAxisLabelWidth') + 2 * this.attr_('axisTickSize');
+ }
+
+ area.w = this.dygraph_.width_ - area.x - this.attr_('rightGap');
+ area.h = this.dygraph_.height_;
+ if (this.attr_('drawXAxis')) {
+ if (this.attr_('xAxisHeight')) {
+ area.h -= this.attr_('xAxisHeight');
+ } else {
+ area.h -= this.attr_('axisLabelFontSize') + 2 * this.attr_('axisTickSize');
+ }
+ }
+
+ // Shrink the drawing area to accomodate additional y-axes.
+ if (this.dygraph_.numAxes() == 2) {
+ // TODO(danvk): per-axis setting.
+ area.w -= (this.attr_('yAxisLabelWidth') + 2 * this.attr_('axisTickSize'));
+ } else if (this.dygraph_.numAxes() > 2) {
+ this.dygraph_.error("Only two y-axes are supported at this time. (Trying " +
+ "to use " + this.dygraph_.numAxes() + ")");
+ }
+
+ // Add space for chart labels: title, xlabel and ylabel.
+ if (this.attr_('title')) {
+ area.h -= this.attr_('titleHeight');
+ area.y += this.attr_('titleHeight');
+ }
+ if (this.attr_('xlabel')) {
+ area.h -= this.attr_('xLabelHeight');
+ }
+ if (this.attr_('ylabel')) {
+ // It would make sense to shift the chart here to make room for the y-axis
+ // label, but the default yAxisLabelWidth is large enough that this results
+ // in overly-padded charts. The y-axis label should fit fine. If it
+ // doesn't, the yAxisLabelWidth option can be increased.
+ }
+
+ // Add space for range selector, if needed.
+ if (this.attr_('showRangeSelector')) {
+ area.h -= this.attr_('rangeSelectorHeight') + 4;
+ }
+
+ return area;
+};
+
DygraphLayout.prototype.setAnnotations = function(ann) {
// The Dygraph object's annotations aren't parsed. We parse them here and
// save a copy. If there is no parser, then the user must be using raw format.
if (series.length > 1) {
var x1 = series[0][0];
if (!this.minxval || x1 < this.minxval) this.minxval = x1;
-
+
var x2 = series[series.length - 1][0];
if (!this.maxxval || x2 > this.maxxval) this.maxxval = x2;
}
if (!this.annotations || !this.annotations.length) {
return;
}
-
+
// TODO(antrob): loop through annotations not points.
for (var i = 0; i < this.points.length; i++) {
var p = this.points[i];
*/
DygraphLayout.prototype.unstackPointAtIndex = function(idx) {
var point = this.points[idx];
-
+
// Clone the point since we modify it
- var unstackedPoint = {};
+ var unstackedPoint = {};
for (var i in point) {
unstackedPoint[i] = point[i];
}
-
+
if (!this.attr_("stackedGraph")) {
return unstackedPoint;
}
-
- // The unstacked yval is equal to the current yval minus the yval of the
+
+ // The unstacked yval is equal to the current yval minus the yval of the
// next point at the same xval.
for (var i = idx+1; i < this.points.length; i++) {
if (this.points[i].xval == point.xval) {
- unstackedPoint.yval -= this.points[i].yval;
+ unstackedPoint.yval -= this.points[i].yval;
break;
}
}
-
+
return unstackedPoint;
-}
+}
"labels": [ "Debugging" ],
"type": "string",
"description": "Set this option to log timing information. The value of the option will be logged along with the timimg, so that you can distinguish multiple dygraphs on the same page."
+ },
+ "showRangeSelector": {
+ "default": "false",
+ "labels": ["Interactive Elements"],
+ "type": "boolean",
+ "description": "Show the range selector widget. This option can only be specified at Dygraph creation time."
+ },
+ "rangeSelectorHeight": {
+ "default": "40",
+ "labels": ["Interactive Elements"],
+ "type": "integer",
+ "description": "Height, in pixels, of the range selector widget. This option can only be specified at Dygraph creation time."
+ },
+ "rangeSelectorPlotStrokeColor": {
+ "default": "#808FAB",
+ "labels": ["Interactive Elements"],
+ "type": "string",
+ "description": "The range selector mini plot stroke color. This can be of the form \"#AABBCC\" or \"rgb(255,100,200)\" or \"yellow\". You can also specify null or \"\" to turn off stroke."
+ },
+ "rangeSelectorPlotFillColor": {
+ "default": "#A7B1C4",
+ "labels": ["Interactive Elements"],
+ "type": "string",
+ "description": "The range selector mini plot fill color. This can be of the form \"#AABBCC\" or \"rgb(255,100,200)\" or \"yellow\". You can also specify null or \"\" to turn off fill."
}
}
; // </JSON>
(function() {
var warn = function(msg) { if (console) console.warn(msg); };
var flds = ['type', 'default', 'description'];
- var valid_cats = [
+ var valid_cats = [
'Annotations',
'Axis display',
'Chart labels',
--- /dev/null
+// Copyright 2011 Paul Felix (paul.eric.felix@gmail.com)
+// All Rights Reserved.
+
+/**
+ * @fileoverview This file contains the DygraphRangeSelector class used to provide
+ * a timeline range selector widget for dygraphs.
+ */
+
+/**
+ * The DygraphRangeSelector class provides a timeline range selector widget.
+ * @param {Dygraph} dygraph The dygraph object
+ * @constructor
+ */
+DygraphRangeSelector = function(dygraph) {
+ this.isIE_ = /MSIE/.test(navigator.userAgent) && !window.opera;
+ this.isUsingExcanvas = typeof(G_vmlCanvasManager) != 'undefined';
+ this.dygraph_ = dygraph;
+ this.createCanvases_();
+ this.createZoomHandles_();
+ this.initInteraction_();
+};
+
+/**
+ * Adds the range selector to the dygraph.
+ * @param {Object} graphDiv The container div for the range selector.
+ * @param {DygraphLayout} layout The DygraphLayout object for this graph.
+ */
+DygraphRangeSelector.prototype.addToGraph = function(graphDiv, layout) {
+ this.layout_ = layout;
+ this.resize_();
+ graphDiv.appendChild(this.bgcanvas_);
+ graphDiv.appendChild(this.fgcanvas_);
+ graphDiv.appendChild(this.leftZoomHandle_);
+ graphDiv.appendChild(this.rightZoomHandle_);
+};
+
+/**
+ * Renders the static background portion of the range selector.
+ */
+DygraphRangeSelector.prototype.renderStaticLayer = function() {
+ this.resize_();
+ this.drawStaticLayer_();
+};
+
+/**
+ * Renders the interactive foreground portion of the range selector.
+ */
+DygraphRangeSelector.prototype.renderInteractiveLayer = function() {
+ if (this.isChangingRange_) {
+ return;
+ }
+
+ // The zoom handle image may not be loaded yet. May need to try again later.
+ if (this.leftZoomHandle_.height == 0 && this.leftZoomHandle_.retryCount != 5) {
+ var self = this;
+ setTimeout(function() { self.renderInteractiveLayer(); }, 300);
+ var retryCount = this.leftZoomHandle_.retryCount;
+ this.leftZoomHandle_.retryCount = retryCount == undefined ? 1 : retryCount+1;
+ return;
+ }
+
+ this.placeZoomHandles_();
+ this.drawInteractiveLayer_();
+};
+
+/**
+ * @private
+ * Resizes the range selector.
+ */
+DygraphRangeSelector.prototype.resize_ = function() {
+ function setCanvasRect(canvas, rect) {
+ canvas.style.top = rect.y + 'px';
+ canvas.style.left = rect.x + 'px';
+ canvas.width = rect.w;
+ canvas.height = rect.h;
+ canvas.style.width = canvas.width + 'px'; // for IE
+ canvas.style.height = canvas.height + 'px'; // for IE
+ };
+
+ var plotArea = this.layout_.plotArea;
+ var xAxisLabelHeight = this.attr_('axisLabelFontSize') + 2 * this.attr_('axisTickSize');
+ this.canvasRect_ = {
+ x: plotArea.x,
+ y: plotArea.y + plotArea.h + xAxisLabelHeight + 4,
+ w: plotArea.w,
+ h: this.attr_('rangeSelectorHeight')
+ };
+
+ setCanvasRect(this.bgcanvas_, this.canvasRect_);
+ setCanvasRect(this.fgcanvas_, this.canvasRect_);
+};
+
+DygraphRangeSelector.prototype.attr_ = function(name) {
+ return this.dygraph_.attr_(name);
+};
+
+/**
+ * @private
+ * Creates the background and foreground canvases.
+ */
+DygraphRangeSelector.prototype.createCanvases_ = function() {
+ this.bgcanvas_ = Dygraph.createCanvas();
+ this.bgcanvas_.className = 'dygraph_rangesel_bgcanvas';
+ this.bgcanvas_.style.position = 'absolute';
+ this.bgcanvas_ctx_ = Dygraph.getContext(this.bgcanvas_);
+
+ this.fgcanvas_ = Dygraph.createCanvas();
+ this.fgcanvas_.className = 'dygraph_rangesel_fgcanvas';
+ this.fgcanvas_.style.position = 'absolute';
+ this.fgcanvas_.style.cursor = 'default';
+ this.fgcanvas_ctx_ = Dygraph.getContext(this.fgcanvas_);
+};
+
+/**
+ * @private
+ * Creates the zoom handle elements.
+ */
+DygraphRangeSelector.prototype.createZoomHandles_ = function() {
+ var img = new Image();
+ img.className = 'dygraph_rangesel_zoomhandle';
+ img.style.position = 'absolute';
+ img.style.visibility = 'hidden'; // Initially hidden so they don't show up in the wrong place.
+ img.style.cursor = 'col-resize';
+ img.src = 'data:image/png;base64,\
+iVBORw0KGgoAAAANSUhEUgAAAAkAAAAQCAYAAADESFVDAAAAAXNSR0IArs4c6QAAAAZiS0dEANAA\
+zwDP4Z7KegAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAAd0SU1FB9sHGw0cMqdt1UwAAAAZdEVYdENv\
+bW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAaElEQVQoz+3SsRFAQBCF4Z9WJM8KCDVwownl\
+6YXsTmCUsyKGkZzcl7zkz3YLkypgAnreFmDEpHkIwVOMfpdi9CEEN2nGpFdwD03yEqDtOgCaun7s\
+qSTDH32I1pQA2Pb9sZecAxc5r3IAb21d6878xsAAAAAASUVORK5CYII=';
+
+ this.leftZoomHandle_ = img;
+ this.rightZoomHandle_ = img.cloneNode(false);
+};
+
+/**
+ * @private
+ * Sets up the interaction for the range selector.
+ */
+DygraphRangeSelector.prototype.initInteraction_ = function() {
+ var self = this;
+ var topElem = this.isIE_ ? document : window;
+ var xLast = 0;
+ var handle = null;
+ var isZooming = false;
+ var isPanning = false;
+
+ function toXDataWindow(zoomHandleStatus) {
+ var xDataLimits = self.dygraph_.xAxisExtremes();
+ var fact = (xDataLimits[1] - xDataLimits[0])/self.canvasRect_.w;
+ var xDataMin = xDataLimits[0] + (zoomHandleStatus.leftHandlePos - self.canvasRect_.x)*fact;
+ var xDataMax = xDataLimits[0] + (zoomHandleStatus.rightHandlePos - self.canvasRect_.x)*fact;
+ return [xDataMin, xDataMax];
+ };
+
+ function onZoomStart(e) {
+ Dygraph.cancelEvent(e);
+ isZooming = true;
+ xLast = e.screenX;
+ handle = e.target ? e.target : e.srcElement;
+ Dygraph.addEvent(topElem, 'mousemove', onZoom);
+ Dygraph.addEvent(topElem, 'mouseup', onZoomEnd);
+ self.fgcanvas_.style.cursor = 'col-resize';
+ };
+
+ function onZoom(e) {
+ if (!isZooming) {
+ return;
+ }
+ var delX = e.screenX - xLast;
+ if (Math.abs(delX) < 4) {
+ return;
+ }
+ xLast = e.screenX;
+ var zoomHandleStatus = self.getZoomHandleStatus_();
+ var halfHandleWidth = Math.round(handle.width/2);
+ if (handle == self.leftZoomHandle_) {
+ var newPos = zoomHandleStatus.leftHandlePos + delX;
+ newPos = Math.min(newPos, zoomHandleStatus.rightHandlePos - handle.width - 3);
+ newPos = Math.max(newPos, self.canvasRect_.x);
+ } else {
+ var newPos = zoomHandleStatus.rightHandlePos + delX;
+ newPos = Math.min(newPos, self.canvasRect_.x + self.canvasRect_.w);
+ newPos = Math.max(newPos, zoomHandleStatus.leftHandlePos + handle.width + 3);
+ }
+ handle.style.left = (newPos - halfHandleWidth) + 'px';
+ self.drawInteractiveLayer_();
+
+ // Zoom on the fly (if not using excanvas).
+ if (!self.isUsingExcanvas) {
+ doZoom();
+ }
+ };
+
+ function onZoomEnd(e) {
+ if (!isZooming) {
+ return;
+ }
+ isZooming = false;
+ Dygraph.removeEvent(topElem, 'mousemove', onZoom);
+ Dygraph.removeEvent(topElem, 'mouseup', onZoomEnd);
+ self.fgcanvas_.style.cursor = 'default';
+
+ // If using excanvas, Zoom now.
+ if (self.isUsingExcanvas) {
+ doZoom();
+ }
+ };
+
+ function doZoom() {
+ try {
+ var zoomHandleStatus = self.getZoomHandleStatus_();
+ self.isChangingRange_ = true;
+ if (!zoomHandleStatus.isZoomed) {
+ self.dygraph_.doUnzoom_();
+ } else {
+ var xDataWindow = toXDataWindow(zoomHandleStatus);
+ self.dygraph_.doZoomXDates_(xDataWindow[0], xDataWindow[1]);
+ }
+ } finally {
+ self.isChangingRange_ = false;
+ }
+ };
+
+ function isMouseInPanZone(e) {
+ // Getting clientX directly from the event is not accurate enough :(
+ var clientX = self.canvasRect_.x + (e.layerX != undefined ? e.layerX : e.offsetX);
+ var zoomHandleStatus = self.getZoomHandleStatus_();
+ return (clientX > zoomHandleStatus.leftHandlePos && clientX < zoomHandleStatus.rightHandlePos);
+ };
+
+ function onPanStart(e) {
+ if (!isPanning && isMouseInPanZone(e) && self.getZoomHandleStatus_().isZoomed) {
+ Dygraph.cancelEvent(e);
+ isPanning = true;
+ xLast = e.screenX;
+ Dygraph.addEvent(topElem, 'mousemove', onPan);
+ Dygraph.addEvent(topElem, 'mouseup', onPanEnd);
+ }
+ };
+
+ function onPan(e) {
+ if (!isPanning) {
+ return;
+ }
+
+ var delX = e.screenX - xLast;
+ if (Math.abs(delX) < 4) {
+ return;
+ }
+ xLast = e.screenX;
+
+ // Move range view
+ var zoomHandleStatus = self.getZoomHandleStatus_();
+ var leftHandlePos = zoomHandleStatus.leftHandlePos;
+ var rightHandlePos = zoomHandleStatus.rightHandlePos;
+ var rangeSize = rightHandlePos - leftHandlePos;
+ if (leftHandlePos + delX <= self.canvasRect_.x) {
+ leftHandlePos = self.canvasRect_.x;
+ rightHandlePos = leftHandlePos + rangeSize;
+ } else if (rightHandlePos + delX >= self.canvasRect_.x + self.canvasRect_.w) {
+ rightHandlePos = self.canvasRect_.x + self.canvasRect_.w;
+ leftHandlePos = rightHandlePos - rangeSize;
+ } else {
+ leftHandlePos += delX;
+ rightHandlePos += delX;
+ }
+ var halfHandleWidth = Math.round(self.leftZoomHandle_.width/2);
+ self.leftZoomHandle_.style.left = (leftHandlePos - halfHandleWidth) + 'px';
+ self.rightZoomHandle_.style.left = (rightHandlePos - halfHandleWidth) + 'px';
+ self.drawInteractiveLayer_();
+
+ // Do pan on the fly (if not using excanvas).
+ if (!self.isUsingExcanvas) {
+ doPan();
+ }
+ };
+
+ function onPanEnd(e) {
+ if (!isPanning) {
+ return;
+ }
+ isPanning = false;
+ Dygraph.removeEvent(topElem, 'mousemove', onPan);
+ Dygraph.removeEvent(topElem, 'mouseup', onPanEnd);
+ // If using excanvas, do pan now.
+ if (self.isUsingExcanvas) {
+ doPan();
+ }
+ };
+
+ function doPan() {
+ try {
+ self.isChangingRange_ = true;
+ self.dygraph_.dateWindow_ = toXDataWindow(self.getZoomHandleStatus_());
+ self.dygraph_.drawGraph_(false);
+ } finally {
+ self.isChangingRange_ = false;
+ }
+ };
+
+ function onCanvasMouseMove(e) {
+ if (isZooming || isPanning) {
+ return;
+ }
+ var cursor = isMouseInPanZone(e) ? 'move' : 'default';
+ if (cursor != self.fgcanvas_.style.cursor) {
+ self.fgcanvas_.style.cursor = cursor;
+ }
+ };
+
+ var interactionModel = {
+ mousedown: function(event, g, context) {
+ context.initializeMouseDown(event, g, context);
+ Dygraph.startPan(event, g, context);
+ },
+ mousemove: function(event, g, context) {
+ if (context.isPanning) {
+ Dygraph.movePan(event, g, context);
+ }
+ },
+ mouseup: function(event, g, context) {
+ if (context.isPanning) {
+ Dygraph.endPan(event, g, context);
+ }
+ }
+ };
+
+ this.dygraph_.attrs_.interactionModel = interactionModel;
+ this.dygraph_.attrs_.panEdgeFraction = .0001;
+
+ Dygraph.addEvent(this.leftZoomHandle_, 'dragstart', onZoomStart);
+ Dygraph.addEvent(this.rightZoomHandle_, 'dragstart', onZoomStart);
+ Dygraph.addEvent(this.fgcanvas_, 'mousedown', onPanStart);
+ Dygraph.addEvent(this.fgcanvas_, 'mousemove', onCanvasMouseMove);
+};
+
+/**
+ * @private
+ * Draws the static layer in the background canvas.
+ */
+DygraphRangeSelector.prototype.drawStaticLayer_ = function() {
+ var ctx = this.bgcanvas_ctx_;
+ ctx.clearRect(0, 0, this.canvasRect_.w, this.canvasRect_.h);
+ var margin = .5;
+ try {
+ this.drawMiniPlot_();
+ } catch(ex) {
+ }
+ ctx.strokeStyle = 'lightgray';
+ if (false) {
+ ctx.strokeRect(margin, margin, this.canvasRect_.w-margin, this.canvasRect_.h-margin);
+ } else {
+ ctx.beginPath();
+ ctx.moveTo(margin, margin);
+ ctx.lineTo(margin, this.canvasRect_.h-margin);
+ ctx.lineTo(this.canvasRect_.w-margin, this.canvasRect_.h-margin);
+ ctx.lineTo(this.canvasRect_.w-margin, margin);
+ ctx.stroke();
+ }
+};
+
+
+/**
+ * @private
+ * Draws the mini plot in the background canvas.
+ */
+DygraphRangeSelector.prototype.drawMiniPlot_ = function() {
+ var fillStyle = this.attr_('rangeSelectorPlotFillColor');
+ var strokeStyle = this.attr_('rangeSelectorPlotStrokeColor');
+ if (!fillStyle && !strokeStyle) {
+ return;
+ }
+
+ var combinedSeriesData = this.computeCombinedSeriesAndLimits_();
+ var yRange = combinedSeriesData.yMax - combinedSeriesData.yMin;
+
+ // Draw the mini plot.
+ var ctx = this.bgcanvas_ctx_;
+ var margin = .5;
+
+ var xExtremes = this.dygraph_.xAxisExtremes();
+ var xRange = Math.max(xExtremes[1] - xExtremes[0], 1.e-30);
+ var xFact = (this.canvasRect_.w - margin)/xRange;
+ var yFact = (this.canvasRect_.h - margin)/yRange;
+ var canvasWidth = this.canvasRect_.w - margin;
+ var canvasHeight = this.canvasRect_.h - margin;
+
+ ctx.beginPath();
+ ctx.moveTo(margin, canvasHeight);
+ for (var i = 0; i < combinedSeriesData.data.length; i++) {
+ var dataPoint = combinedSeriesData.data[i];
+ var x = (dataPoint[0] - xExtremes[0])*xFact;
+ var y = canvasHeight - (dataPoint[1] - combinedSeriesData.yMin)*yFact;
+ if (isFinite(x) && isFinite(y)) {
+ ctx.lineTo(x, y);
+ }
+ }
+ ctx.lineTo(canvasWidth, canvasHeight);
+ ctx.closePath();
+
+ if (fillStyle) {
+ var lingrad = this.bgcanvas_ctx_.createLinearGradient(0, 0, 0, canvasHeight);
+ lingrad.addColorStop(0, 'white');
+ lingrad.addColorStop(1, fillStyle);
+ this.bgcanvas_ctx_.fillStyle = lingrad;
+ ctx.fill();
+ }
+
+ if (strokeStyle) {
+ this.bgcanvas_ctx_.strokeStyle = strokeStyle;
+ this.bgcanvas_ctx_.lineWidth = 1.5;
+ ctx.stroke();
+ }
+};
+
+/**
+ * @private
+ * Computes and returns the combinded series data along with min/max for the mini plot.
+ * @return {Object} An object containing combinded series array, ymin, ymax.
+ */
+DygraphRangeSelector.prototype.computeCombinedSeriesAndLimits_ = function() {
+ var data = this.dygraph_.rawData_;
+ var logscale = this.attr_('logscale');
+
+ // Create a combined series (average of all series values).
+ var combinedSeries = [];
+ var sum;
+ var count;
+ var mutipleValues = typeof data[0][1] != 'number';
+
+ if (mutipleValues) {
+ sum = [];
+ count = [];
+ for (var k = 0; k < data[0][1].length; k++) {
+ sum.push(0);
+ count.push(0);
+ }
+ mutipleValues = true;
+ }
+
+ for (var i = 0; i < data.length; i++) {
+ var dataPoint = data[i];
+ var xVal = dataPoint[0];
+ var yVal;
+
+ if (mutipleValues) {
+ for (var k = 0; k < sum.length; k++) {
+ sum[k] = count[k] = 0;
+ }
+ } else {
+ sum = count = 0;
+ }
+
+ for (var j = 1; j < dataPoint.length; j++) {
+ if (this.dygraph_.visibility()[j-1]) {
+ if (mutipleValues) {
+ for (var k = 0; k < sum.length; k++) {
+ var y = dataPoint[j][k];
+ if (y == null || isNaN(y)) continue;
+ sum[k] += y;
+ count[k]++;
+ }
+ } else {
+ var y = dataPoint[j];
+ if (y == null || isNaN(y)) continue;
+ sum += y;
+ count++;
+ }
+ }
+ }
+
+ if (mutipleValues) {
+ for (var k = 0; k < sum.length; k++) {
+ sum[k] /= count[k];
+ }
+ yVal = sum.slice(0);
+ } else {
+ yVal = sum/count;
+ }
+
+ combinedSeries.push([xVal, yVal]);
+ }
+
+ // Account for roll period, fractions.
+ combinedSeries = this.dygraph_.rollingAverage(combinedSeries, this.dygraph_.rollPeriod_);
+
+ if (typeof combinedSeries[0][1] != 'number') {
+ for (var i = 0; i < combinedSeries.length; i++) {
+ var yVal = combinedSeries[i][1];
+ combinedSeries[i][1] = yVal[0];
+ }
+ }
+
+ // Compute the y range.
+ var yMin = Number.MAX_VALUE;
+ var yMax = -Number.MAX_VALUE;
+ for (var i = 0; i < combinedSeries.length; i++) {
+ var yVal = combinedSeries[i][1];
+ if (!logscale || yVal > 0) {
+ yMin = Math.min(yMin, yVal);
+ yMax = Math.max(yMax, yVal);
+ }
+ }
+
+ // Convert Y data to log scale if needed.
+ // Also, expand the Y range to compress the mini plot a little.
+ var extraPercent = .25;
+ if (logscale) {
+ yMax = Dygraph.log10(yMax);
+ yMax += yMax*extraPercent;
+ yMin = Dygraph.log10(yMin);
+ for (var i = 0; i < combinedSeries.length; i++) {
+ combinedSeries[i][1] = Dygraph.log10(combinedSeries[i][1]);
+ }
+ } else {
+ var yExtra;
+ yRange = yMax - yMin;
+ if (yRange <= Number.MIN_VALUE) {
+ yExtra = yMax*extraPercent;
+ } else {
+ yExtra = yRange*extraPercent;
+ }
+ yMax += yExtra;
+ yMin -= yExtra;
+ }
+
+ return {data: combinedSeries, yMin: yMin, yMax: yMax};
+};
+
+/**
+ * @private
+ * Places the zoom handles in the proper position based on the current X data window.
+ */
+DygraphRangeSelector.prototype.placeZoomHandles_ = function() {
+ var xExtremes = this.dygraph_.xAxisExtremes();
+ var xWindowLimits = this.dygraph_.xAxisRange();
+ var xRange = xExtremes[1] - xExtremes[0];
+ var leftPercent = Math.max(0, (xWindowLimits[0] - xExtremes[0])/xRange);
+ var rightPercent = Math.max(0, (xExtremes[1] - xWindowLimits[1])/xRange);
+ var leftCoord = this.canvasRect_.x + this.canvasRect_.w*leftPercent;
+ var rightCoord = this.canvasRect_.x + this.canvasRect_.w*(1 - rightPercent);
+ var handleTop = Math.round(Math.max(this.canvasRect_.y, this.canvasRect_.y + (this.canvasRect_.h - this.leftZoomHandle_.height)/2));
+ var halfHandleWidth = Math.round(this.leftZoomHandle_.width/2);
+ this.leftZoomHandle_.style.left = Math.round(leftCoord - halfHandleWidth) + 'px';
+ this.leftZoomHandle_.style.top = handleTop + 'px';
+ this.rightZoomHandle_.style.left = Math.round(rightCoord - halfHandleWidth) + 'px';
+ this.rightZoomHandle_.style.top = this.leftZoomHandle_.style.top;
+
+ this.leftZoomHandle_.style.visibility = 'visible';
+ this.rightZoomHandle_.style.visibility = 'visible';
+};
+
+/**
+ * @private
+ * Draws the interactive layer in the foreground canvas.
+ */
+DygraphRangeSelector.prototype.drawInteractiveLayer_ = function() {
+ var ctx = this.fgcanvas_ctx_;
+ ctx.clearRect(0, 0, this.canvasRect_.w, this.canvasRect_.h);
+ var margin = 1;
+ var width = this.canvasRect_.w - margin;
+ var height = this.canvasRect_.h - margin;
+ var zoomHandleStatus = this.getZoomHandleStatus_();
+
+ ctx.strokeStyle = 'black';
+ if (!zoomHandleStatus.isZoomed) {
+ ctx.beginPath();
+ ctx.moveTo(margin, margin);
+ ctx.lineTo(margin, height);
+ ctx.lineTo(width, height);
+ ctx.lineTo(width, margin);
+ ctx.stroke();
+ } else {
+ leftHandleCanvasPos = Math.max(margin, zoomHandleStatus.leftHandlePos - this.canvasRect_.x);
+ rightHandleCanvasPos = Math.min(width, zoomHandleStatus.rightHandlePos - this.canvasRect_.x);
+
+ ctx.fillStyle = 'rgba(240, 240, 240, 0.6)';
+ ctx.fillRect(margin, margin, leftHandleCanvasPos, height - margin);
+ ctx.fillRect(rightHandleCanvasPos, margin, width - rightHandleCanvasPos, height - margin);
+
+ ctx.beginPath();
+ ctx.moveTo(margin, margin);
+ ctx.lineTo(leftHandleCanvasPos, margin);
+ ctx.lineTo(leftHandleCanvasPos, height);
+ ctx.lineTo(rightHandleCanvasPos, height);
+ ctx.lineTo(rightHandleCanvasPos, margin);
+ ctx.lineTo(width, margin);
+ ctx.stroke();
+ }
+};
+
+/**
+ * @private
+ * Returns the current zoom handle position information.
+ * @return {Object} The zoom handle status.
+ */
+DygraphRangeSelector.prototype.getZoomHandleStatus_ = function() {
+ var halfHandleWidth = Math.round(this.leftZoomHandle_.width/2);
+ var leftHandlePos = parseInt(this.leftZoomHandle_.style.left) + halfHandleWidth;
+ var rightHandlePos = parseInt(this.rightZoomHandle_.style.left) + halfHandleWidth;
+ return {
+ leftHandlePos: leftHandlePos,
+ rightHandlePos: rightHandlePos,
+ isZoomed: (leftHandlePos - 1 > this.canvasRect_.x || rightHandlePos + 1 < this.canvasRect_.x+this.canvasRect_.w)
+ };
+};
var year_mod = 1; // e.g. to only print one point every 10 years.
if (granularity == Dygraph.MONTHLY) {
- months = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
+ months = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ];
} else if (granularity == Dygraph.QUARTERLY) {
months = [ 0, 3, 6, 9 ];
} else if (granularity == Dygraph.BIANNUAL) {
* @private
* Add an event handler. This smooths a difference between IE and the rest of
* the world.
- * @param { DOM element } el The element to add the event to.
- * @param { String } evt The name of the event, e.g. 'click' or 'mousemove'.
+ * @param { DOM element } elem The element to add the event to.
+ * @param { String } type The type of the event, e.g. 'click' or 'mousemove'.
* @param { Function } fn The function to call on the event. The function takes
* one parameter: the event object.
*/
-Dygraph.addEvent = function(el, evt, fn) {
- var normed_fn = function(e) {
- if (!e) var e = window.event;
- fn(e);
- };
- if (window.addEventListener) { // Mozilla, Netscape, Firefox
- el.addEventListener(evt, normed_fn, false);
- } else { // IE
- el.attachEvent('on' + evt, normed_fn);
+Dygraph.addEvent = function addEvent(elem, type, fn) {
+ if (elem.addEventListener) {
+ elem.addEventListener(type, fn, false);
+ } else {
+ elem[type+fn] = function(){fn(window.event);};
+ elem.attachEvent('on'+type, elem[type+fn]);
+ }
+};
+
+/**
+ * @private
+ * Remove an event handler. This smooths a difference between IE and the rest of
+ * the world.
+ * @param { DOM element } elem The element to add the event to.
+ * @param { String } type The type of the event, e.g. 'click' or 'mousemove'.
+ * @param { Function } fn The function to call on the event. The function takes
+ * one parameter: the event object.
+ */
+Dygraph.removeEvent = function addEvent(elem, type, fn) {
+ if (elem.removeEventListener) {
+ elem.removeEventListener(type, fn, false);
+ } else {
+ elem.detachEvent('on'+type, elem[type+fn]);
+ elem[type+fn] = null;
}
};
'pixelsPerYLabel': true,
'pointClickCallback': true,
'pointSize': true,
+ 'rangeSelectorPlotFillColor': true,
+ 'rangeSelectorPlotStrokeColor': true,
'showLabelsOnHighlight': true,
'showRoller': true,
'sigFigs': true,
'yAxisLabelFormatter': true,
'yValueFormatter': true,
'zoomCallback': true
- };
+ };
// Assume that we do not require new points.
// This will change to true if we actually do need new points.
// If this was not a series specific option list, check if its a pixel changing property.
} else if (!pixelSafeOptions[property]) {
requiresNewPoints = true;
- }
+ }
}
}
interactionModel: null, // will be set to Dygraph.Interaction.defaultModel
+ // Range selector options
+ showRangeSelector: false,
+ rangeSelectorHeight: 40,
+ rangeSelectorPlotStrokeColor: "#808FAB",
+ rangeSelectorPlotFillColor: "#A7B1C4",
+
// per-axis options
axes: {
x: {
document.readyState != 'complete') {
var self = this;
setTimeout(function() { self.__init__(div, file, attrs) }, 100);
+ return;
}
// Support two-argument constructor
if (div.style.height == '' && attrs.height) {
div.style.height = attrs.height + "px";
}
- if (div.style.height == '' && div.offsetHeight == 0) {
+ if (div.style.height == '' && div.clientHeight == 0) {
div.style.height = Dygraph.DEFAULT_HEIGHT + "px";
if (div.style.width == '') {
div.style.width = Dygraph.DEFAULT_WIDTH + "px";
}
}
// these will be zero if the dygraph's div is hidden.
- this.width_ = div.offsetWidth;
- this.height_ = div.offsetHeight;
+ this.width_ = div.clientWidth;
+ this.height_ = div.clientHeight;
// TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
if (attrs['stackedGraph']) {
this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
this.hidden_ctx_ = Dygraph.getContext(this.hidden_);
+ if (this.attr_('showRangeSelector')) {
+ // The range selector must be created here so that its canvases and contexts get created here.
+ // For some reason, if the canvases and contexts don't get created here, things don't work in IE.
+ // The range selector also sets xAxisHeight in order to reserve space.
+ this.rangeSelector_ = new DygraphRangeSelector(this);
+ }
+
// The interactive parts of the graph are drawn on top of the chart.
this.graphDiv.appendChild(this.hidden_);
this.graphDiv.appendChild(this.canvas_);
this.mouseEventElement_ = this.canvas_;
+ // Create the grapher
+ this.layout_ = new DygraphLayout(this);
+
+ if (this.rangeSelector_) {
+ // This needs to happen after the graph canvases are added to the div and the layout object is created.
+ this.rangeSelector_.addToGraph(this.graphDiv, this.layout_);
+ }
+
var dygraph = this;
Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(e) {
dygraph.mouseMove_(e);
dygraph.mouseOut_(e);
});
- // Create the grapher
- this.layout_ = new DygraphLayout(this);
-
this.createStatusMessage_();
this.createDragInterface_();
* up any previous zoom rectangles that were drawn. This could be optimized to
* avoid extra redrawing, but it's tricky to avoid interactions with the status
* dots.
- *
+ *
* @param {Number} direction the direction of the zoom rectangle. Acceptable
* values are Dygraph.HORIZONTAL and Dygraph.VERTICAL.
* @param {Number} startX The X position where the drag started, in canvas
for (var i in this.layout_.datasets) {
if (row < this.layout_.datasets[i].length) {
var point = this.layout_.points[pos+row];
-
+
if (this.attr_("stackedGraph")) {
point = this.layout_.unstackPointAtIndex(pos+row);
}
-
+
this.selPoints_.push(point);
}
pos += this.layout_.datasets[i].length;
// edge of the div, if we have two y-axes.
this.positionLabelsDiv_();
+ if (this.rangeSelector_) {
+ this.rangeSelector_.renderStaticLayer();
+ }
+
// If the data or options have changed, then we'd better redraw.
this.drawGraph_();
}
}
+ if (this.rangeSelector_) {
+ this.rangeSelector_.renderInteractiveLayer();
+ }
+
if (this.attr_("drawCallback") !== null) {
this.attr_("drawCallback")(this, is_initial_draw);
}
this.width_ = width;
this.height_ = height;
} else {
- this.width_ = this.maindiv_.offsetWidth;
- this.height_ = this.maindiv_.offsetHeight;
+ this.width_ = this.maindiv_.clientWidth;
+ this.height_ = this.maindiv_.clientHeight;
}
if (old_width != this.width_ || old_height != this.height_) {
dygraph-utils.js \
dygraph-gviz.js \
dygraph-interaction-model.js \
+dygraph-range-selector.js \
dygraph-tickers.js \
rgbcolor/rgbcolor.js \
strftime/strftime-min.js \
- dygraph-gviz.js
- dygraph-interaction-model.js
- dygraph-options-reference.js
+ - dygraph-range-selector.js
- dygraph-tickers.js
- dygraph-dev.js
- excanvas.js
--- /dev/null
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7; IE=EmulateIE9">
+ <title>Temperatures with Range Selector</title>
+ <!--[if IE]>
+ <script type="text/javascript" src="../excanvas.js"></script>
+ <![endif]-->
+ <!--
+ For production (minified) code, use:
+ <script type="text/javascript" src="dygraph-combined.js"></script>
+ -->
+ <script type="text/javascript" src="../dygraph-dev.js"></script>
+
+ <script type="text/javascript" src="data.js"></script>
+ <style type="text/css">
+ #bordered {
+ border: 1px solid red;
+ }
+ </style>
+ </head>
+ <body>
+ <p>Demo of a graph with the range selector.</p>
+ <p>
+ No roll period.
+ </p>
+ <div id="noroll" style="width:800px; height:320px;"></div>
+ <p>
+ Roll period of 14 timesteps, custom range selector height and plot color.
+ </p>
+ <div id="roll14" style="width:800px; height:320px;"></div>
+ <script type="text/javascript">
+ g1 = new Dygraph(
+ document.getElementById("noroll"),
+ data_temp,
+ {
+ customBars: true,
+ title: 'Daily Temperatures in New York vs. San Francisco',
+ ylabel: 'Temperature (F)',
+ legend: 'always',
+ labelsDivStyles: { 'textAlign': 'right' },
+ showRangeSelector: true
+ }
+ );
+ g2 = new Dygraph(
+ document.getElementById("roll14"),
+ data_temp,
+ {
+ rollPeriod: 14,
+ showRoller: true,
+ customBars: true,
+ title: 'Daily Temperatures in New York vs. San Francisco',
+ ylabel: 'Temperature (F)',
+ legend: 'always',
+ labelsDivStyles: { 'textAlign': 'right' },
+ showRangeSelector: true,
+ rangeSelectorHeight: 30,
+ rangeSelectorPlotStrokeColor: 'yellow',
+ rangeSelectorPlotFillColor: 'lightyellow'
+ }
+ );
+ </script>
+ </body>
+</html>