<script type="text/javascript" src="../tests/update_options.js"></script>
<script type="text/javascript" src="../tests/utils_test.js"></script>
<script type="text/javascript" src="../tests/multiple_axes.js"></script>
+ <script type="text/javascript" src="../tests/callback.js"></script>
<script type="text/javascript">
g.setSelection(9);
assertEquals("vf9: y:vf18", getLegend());
};
+
+AxisLabelsTestCase.prototype.testSeriesOrder = function() {
+ var opts = {
+ width: 480,
+ height: 320
+ };
+ var data = "x,00,01,10,11\n" +
+ "0,101,201,301,401\n" +
+ "1,102,202,302,402\n" +
+ "2,103,203,303,403\n" +
+ "3,104,204,304,404\n"
+ ;
+
+ var graph = document.getElementById("graph");
+ var g = new Dygraph(graph, data, opts);
+
+ g.setSelection(2);
+ assertEquals('2: 00:103 01:203 10:303 11:403', getLegend());
+
+ // Sanity checks for indexFromSetName
+ assertEquals(0, g.indexFromSetName("x"));
+ assertEquals(1, g.indexFromSetName("00"));
+ assertEquals(null, g.indexFromSetName("abcde"));
+
+ // Verify that we get the label list back in the right order
+ assertEquals(["x", "00", "01", "10", "11"], g.getLabels());
+};
--- /dev/null
+/**
+ * @fileoverview Test cases for the callbacks.
+ *
+ * @author uemit.seren@gmail.com (Ümit Seren)
+ */
+
+var CallbackTestCase = TestCase("callback");
+
+CallbackTestCase.prototype.setUp = function() {
+ document.body.innerHTML = "<div id='graph'></div>";
+};
+
+CallbackTestCase.prototype.tearDown = function() {
+};
+
+ var data = "X,a\,b,c\n" +
+ "10,-1,1,2\n" +
+ "11,0,3,1\n" +
+ "12,1,4,2\n" +
+ "13,0,2,3\n";
+
+
+ /**
+ * This tests that when the function idxToRow_ returns the proper row and the onHiglightCallback
+ * is properly called when the first series is hidden (setVisibility = false)
+ *
+ */
+ CallbackTestCase.prototype.testHighlightCallbackIsCalled = function() {
+ var h_row;
+ var h_pts;
+
+ var highlightCallback = function(e, x, pts, row) {
+ h_row = row;
+ h_pts = pts;
+ };
+
+
+
+ var graph = document.getElementById("graph");
+ var g = new Dygraph(graph, data,
+ {
+ width: 100,
+ height : 100,
+ visibility: [false, true, true],
+ highlightCallback : highlightCallback,
+ });
+
+ DygraphOps.dispatchMouseMove(g, 13, 10);
+
+ //check correct row is returned
+ assertEquals(3, h_row);
+ //check there are only two points (because first series is hidden)
+ assertEquals(2, h_pts.length);
+ };
assertTrue(g.isZoomed("x"));
assertTrue(g.isZoomed("y"));
};
+
+
+InteractionModelTestCase.prototype.testCorrectAxisValueRangeAfterUnzoom = function() {
+ var g = new Dygraph(document.getElementById("graph"), data2, {valueRange:[1,50],dateRange:[1,9],animatedZooms:false});
+
+ // Zoom x axis
+ DygraphOps.dispatchMouseDown_Point(g, 10, 10);
+ DygraphOps.dispatchMouseMove_Point(g, 30, 10);
+ DygraphOps.dispatchMouseUp_Point(g, 30, 10);
+
+ // Zoom y axis
+ DygraphOps.dispatchMouseDown_Point(g, 10, 10);
+ DygraphOps.dispatchMouseMove_Point(g, 10, 30);
+ DygraphOps.dispatchMouseUp_Point(g, 10, 30);
+ currentYAxisRange = g.yAxisRange();
+ currentXAxisRange = g.xAxisRange();
+
+ //check that the range for the axis has changed
+ assertNotEquals(1,currentXAxisRange[0]);
+ assertNotEquals(10,currentXAxisRange[1]);
+ assertNotEquals(1,currentYAxisRange[0]);
+ assertNotEquals(50,currentYAxisRange[1]);
+
+ // unzoom by doubleclick
+ DygraphOps.dispatchDoubleClick(g, null);
+
+ // check if range for y-axis was reset to original value
+ // TODO check if range for x-axis is correct.
+ // Currently not possible because dateRange is set to null and extremes are returned
+ newYAxisRange = g.yAxisRange();
+ assertEquals(1,newYAxisRange[0]);
+ assertEquals(50,newYAxisRange[1]);
+};
assertEquals(["y-axis"], getClassTexts("dygraph-ylabel"));
assertEquals([], getClassTexts("dygraph-y2label"));
};
+
+MultipleAxesTestCase.prototype.testValueRangePerAxisOptions = function() {
+ var data = MultipleAxesTestCase.getData();
+
+ g = new Dygraph(
+ document.getElementById("graph"),
+ data,
+ {
+ labels: [ 'Date', 'Y1', 'Y2', 'Y3', 'Y4' ],
+ 'Y3': {
+ axis: {
+ }
+ },
+ 'Y4': {
+ axis: 'Y3' // use the same y-axis as series Y3
+ },
+ axes: {
+ y: {
+ valueRange: [40, 70]
+ },
+ y2: {
+ // set axis-related properties here
+ labelsKMB: true
+ }
+ },
+ ylabel: 'Primary y-axis',
+ y2label: 'Secondary y-axis',
+ yAxisLabelWidth: 60
+ }
+ );
+ assertEquals(["40", "45", "50", "55", "60", "65"], getYLabelsForAxis("1"));
+ assertEquals(["900K","1.1M","1.3M","1.5M","1.7M","1.9M"], getYLabelsForAxis("2"));
+
+ g.updateOptions(
+ {
+ axes: {
+ y: {
+ valueRange: [40, 80]
+ },
+ y2: {
+ valueRange: [1e6, 1.2e6]
+ }
+ }
+ }
+ );
+ assertEquals(["40", "45", "50", "55", "60", "65", "70", "75"], getYLabelsForAxis("1"));
+ assertEquals(["1M", "1.02M", "1.05M", "1.08M", "1.1M", "1.13M", "1.15M", "1.18M"], getYLabelsForAxis("2"));
+};
\ No newline at end of file
g.updateOptions({ });
assertEquals([12, 18], g.xAxisRange());
assertEquals([10, 40], g.yAxisRange(0));
+
+ g.updateOptions({valueRange : null, axes: {y:{valueRange : [15, 20]}}});
+ assertEquals([12, 18], g.xAxisRange());
+ assertEquals([15, 20], g.yAxisRange(0));
- g.updateOptions({ dateWindow : null, valueRange : null });
+ g.updateOptions({ dateWindow : null, valueRange : null, axes: null });
assertEquals([10, 20], g.xAxisRange());
assertEquals([0, 55], g.yAxisRange(0));
};
var graph = document.getElementById("graph");
var g = new Dygraph(graph, ZERO_TO_FIFTY, { valueRange: [0,50] });
assertEquals([0, 50], g.yAxisRange(0));
+ g.updateOptions({valueRange: null, axes: {y: {valueRange: [10, 40]}}});
+ assertEquals([10, 40], g.yAxisRange(0));
};
/**
assertEquals(50, g.toDomYCoord(0));
assertEquals(0, g.toDomYCoord(50));
-
+
+ for (var x = 0; x <= 50; x++) {
+ assertEqualsDelta(50 - x, g.toDomYCoord(x), 0.00001);
+ }
+ g.updateOptions({valueRange: null, axes: {y: {valueRange: [0, 50]}}});
+
+ assertEquals(50, g.toDomYCoord(0));
+ assertEquals(0, g.toDomYCoord(50));
+
for (var x = 0; x <= 50; x++) {
assertEqualsDelta(50 - x, g.toDomYCoord(x), 0.00001);
}
lineWidth: 1
});
}
+
+/**
+ * Tests that it is drawing dashes, and it remember the dash history between
+ * points.
+ */
+SimpleDrawingTestCase.prototype.testDrawSimpleDash = function() {
+ var opts = {
+ drawXGrid: false,
+ drawYGrid: false,
+ drawXAxis: false,
+ drawYAxis: false,
+ 'Y1': {strokePattern: [25, 7, 7, 7]},
+ colors: ['#ff0000']
+ };
+
+ var graph = document.getElementById("graph");
+ // Set the dims so we pass if default changes.
+ graph.style.width='480px';
+ graph.style.height='320px';
+ var g = new Dygraph(graph, [[1, 4], [2, 5], [3, 3], [4, 7], [5, 9]], opts);
+ htx = g.hidden_ctx_;
+
+ assertEquals(29, CanvasAssertions.numLinesDrawn(htx, "#ff0000"));
+};
<p>dygraphs has also found use in other organizations:</p>
<ul class='padded-list'>
+ <li><a
+ href="http://iswa.ccmc.gsfc.nasa.gov:8080/IswaSystemWebApp/index.jsp?i_1=388&l_1=99&t_1=316&w_1=800&h_1=400&s_1=0!3!0!ACE.B_x!ACE.B_y!ACE.B_z!">Integrated
+ Space Weather Analysis System</a> (NASA)<br/>
+ <span class="desc">“We use [dygraphs] in the Integrated Space Weather
+ Analysis System available from the Space Weather Laboratory at NASA Goddard
+ Space Flight Center. It works quite well for time series data from various
+ missions and simulations that we store.”</span></li>
+
+
+ <li><a href="http://www.eutelsat.fr">Eutelsat</a><br/>
+ <span class="desc">“Eutelsat uses dygraphs for charting spacecraft
+ telemetry for a fleet of 25 geostationary satellites. The spacecraft
+ engineers are very happy with it. All satellite combined are producing
+ about 200 millions unique data points per day so we really appreciate the
+ excellent performance of dygraphs.”</span></li>
+
+ <li><a href="http://www.10gen.com/mongodb-monitoring-service">10gen MongoDB
+ Monitoring Service</a><br/>
+ <span class="desc">A free monitoring service for MongoDB from 10gen (the
+ creators of MongoDB). Used by thousands of servers and users. Makes use of
+ <a href="tests/synchronize.html">synchronized charts</a> to display many
+ quantities simultaneously.</span></li>
+
<li><a href="http://toolserver.org/~dartar/moodbar/">Wikimedia Foundation - Moodbar data dashboard</a><br/>
<span class="desc">dygraphs is used internally at Wikimedia as a handy solution to monitor the
results of a bunch of small experiments.</span></li>
+ <li><a href="http://code.google.com/p/quadrant-framework/">quadrant-framework</a> (MySQL Load Testing Framework)<br/>
+ <span class="desc">A user friendly framework for creating and visualizing
+ MySQL database load test jobs. For more information on its use of dygraphs,
+ see <a href="http://themattreid.com/wordpress/2011/05/20/quadrant-framework-rev7-update-adds-dygraphs-support/">this post</a>.</span></li>
+
+ <li><a href="http://spinwave.wordpress.com/2011/03/28/spinwave-systems-enables-energy-efficiency-case-studies/">Spinwave Systems</a> (Home energy monitoring)<br/>
+ <span class="desc">dygraphs is used to chart energy usage over time.</span></li>
+
+
<li><a href="http://www.socib.es/jwebchart/?file=http://thredds.socib.es/thredds/dodsC/mooring/weather_station/mobims_calamillor-scb_met001/L1/dep0001_mobims-calamillor_scb-met001_L1_latest.nc">Jwebchart</a><br/>
<span class="desc">
jWebChart is a stand-alone and Thredds' embedded plotting system for
/*global Dygraph:false,RGBColor:false */
"use strict";
+
var DygraphCanvasRenderer = function(dygraph, element, elementContext, layout) {
this.dygraph_ = dygraph;
inner_div.className = 'dygraph-axis-label' +
' dygraph-axis-label-' + axis +
(prec_axis ? ' dygraph-axis-label-' + prec_axis : '');
- inner_div.appendChild(document.createTextNode(txt));
+ inner_div.innerHTML=txt;
div.appendChild(inner_div);
return div;
};
var pointsLength = points.length;
var point, i, j, prevX, prevY, prevYs, color, setName, newYs, err_color, rgb, yscale, axis;
- var setNames = [];
- for (var name in this.layout.datasets) {
- if (this.layout.datasets.hasOwnProperty(name)) {
- setNames.push(name);
- }
- }
+ var setNames = this.layout.setNames;
var setCount = setNames.length;
// TODO(danvk): Move this mapping into Dygraph and get it out of here.
var afterLastIndexInSet = 0;
var setLength = 0;
for (i = 0; i < setCount; i += 1) {
+ firstIndexInSet = this.layout.setPointsOffsets[i];
setLength = this.layout.setPointsLengths[i];
- afterLastIndexInSet += setLength;
+ afterLastIndexInSet = firstIndexInSet + setLength;
setName = setNames[i];
color = this.colors[setName];
var strokeWidth = this.dygraph_.attr_("strokeWidth", setName);
drawPointCallback = Dygraph.Circles.DEFAULT;
}
var pointsOnLine = []; // Array of [canvasx, canvasy] pairs.
+ var strokePattern = this.dygraph_.attr_("strokePattern", setName);
+ if (!Dygraph.isArrayLike(strokePattern)) {
+ strokePattern = null;
+ }
for (j = firstIndexInSet; j < afterLastIndexInSet; j++) {
point = points[j];
if (isNullOrNaN(point.canvasy)) {
ctx.beginPath();
ctx.strokeStyle = color;
ctx.lineWidth = this.attr_('strokeWidth');
- ctx.moveTo(prevX, prevY);
- ctx.lineTo(point.canvasx, prevY);
+ this._dashedLine(ctx, prevX, prevY, point.canvasx, prevY, strokePattern);
ctx.stroke();
}
// this will make us move to the next point, not draw a line to it.
ctx.beginPath();
ctx.strokeStyle = color;
ctx.lineWidth = strokeWidth;
- ctx.moveTo(prevX, prevY);
if (stepPlot) {
- ctx.lineTo(point.canvasx, prevY);
+ this._dashedLine(ctx, prevX, prevY, point.canvasx, prevY, strokePattern);
}
+ this._dashedLine(ctx, prevX, prevY, point.canvasx, point.canvasy, strokePattern);
prevX = point.canvasx;
prevY = point.canvasy;
- ctx.lineTo(prevX, prevY);
ctx.stroke();
}
}
context.restore();
};
+
+/**
+ * This does dashed lines onto a canvas for a given pattern. You must call
+ * ctx.stroke() after to actually draw it, much line ctx.lineTo(). It remembers
+ * the state of the line in regards to where we left off on drawing the pattern.
+ * You can draw a dashed line in several function calls and the pattern will be
+ * continous as long as you didn't call this function with a different pattern
+ * in between.
+ * @param ctx The canvas 2d context to draw on.
+ * @param x The start of the line's x coordinate.
+ * @param y The start of the line's y coordinate.
+ * @param x2 The end of the line's x coordinate.
+ * @param y2 The end of the line's y coordinate.
+ * @param pattern The dash pattern to draw, an array of integers where even
+ * index is drawn and odd index is not drawn (Ex. [10, 2, 5, 2], 10 is drawn 5
+ * is drawn, 2 is the space between.). A null pattern, array of length one, or
+ * empty array will do just a solid line.
+ * @private
+ */
+DygraphCanvasRenderer.prototype._dashedLine = function(ctx, x, y, x2, y2, pattern) {
+ // Original version http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
+ // Modified by Russell Valentine to keep line history and continue the pattern
+ // where it left off.
+ var dx, dy, len, rot, patternIndex, segment;
+
+ // If we don't have a pattern or it is an empty array or of size one just
+ // do a solid line.
+ if (!pattern || pattern.length <= 1) {
+ ctx.moveTo(x, y);
+ ctx.lineTo(x2, y2);
+ return;
+ }
+
+ // If we have a different dash pattern than the last time this was called we
+ // reset our dash history and start the pattern from the begging
+ // regardless of state of the last pattern.
+ if (!Dygraph.compareArrays(pattern, this._dashedLineToHistoryPattern)) {
+ this._dashedLineToHistoryPattern = pattern;
+ this._dashedLineToHistory = [0, 0];
+ }
+ ctx.save();
+
+ // Calculate transformation parameters
+ dx = (x2-x);
+ dy = (y2-y);
+ len = Math.sqrt(dx*dx + dy*dy);
+ rot = Math.atan2(dy, dx);
+
+ // Set transformation
+ ctx.translate(x, y);
+ ctx.moveTo(0, 0);
+ ctx.rotate(rot);
+
+ // Set last pattern index we used for this pattern.
+ patternIndex = this._dashedLineToHistory[0];
+ x = 0;
+ while (len > x) {
+ // Get the length of the pattern segment we are dealing with.
+ segment = pattern[patternIndex];
+ // If our last draw didn't complete the pattern segment all the way we
+ // will try to finish it. Otherwise we will try to do the whole segment.
+ if (this._dashedLineToHistory[1]) {
+ x += this._dashedLineToHistory[1];
+ } else {
+ x += segment;
+ }
+ if (x > len) {
+ // We were unable to complete this pattern index all the way, keep
+ // where we are the history so our next draw continues where we left off
+ // in the pattern.
+ this._dashedLineToHistory = [patternIndex, x-len];
+ x = len;
+ } else {
+ // We completed this patternIndex, we put in the history that we are on
+ // the beginning of the next segment.
+ this._dashedLineToHistory = [(patternIndex+1)%pattern.length, 0];
+ }
+
+ // We do a line on a even pattern index and just move on a odd pattern index.
+ // The move is the empty space in the dash.
+ if(patternIndex % 2 === 0) {
+ ctx.lineTo(x, 0);
+ } else {
+ ctx.moveTo(x, 0);
+ }
+ // If we are not done, next loop process the next pattern segment, or the
+ // first segment again if we are at the end of the pattern.
+ patternIndex = (patternIndex+1) % pattern.length;
+ }
+ ctx.restore();
+};
if (row < 0) return selection;
- var col = 1;
var datasets = this.date_graph.layout_.datasets;
- for (var k in datasets) {
- if (!datasets.hasOwnProperty(k)) continue;
- selection.push({row: row, column: col});
- col++;
+ for (var setIdx = 0; setIdx < datasets.length; ++setIdx) {
+ selection.push({row: row, column: setIdx + 1});
}
return selection;
};
-
}
}
};
+
+// Default interaction model when using the range selector.
+Dygraph.Interaction.dragIsPanInteractionModel = {
+ 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);
+ }
+ }
+};
var DygraphLayout = function(dygraph) {
this.dygraph_ = dygraph;
this.datasets = [];
+ this.setNames = [];
this.annotations = [];
this.yAxes_ = null;
};
DygraphLayout.prototype.addDataset = function(setname, set_xy) {
- this.datasets[setname] = set_xy;
+ this.datasets.push(set_xy);
+ this.setNames.push(setname);
};
DygraphLayout.prototype.getPlotArea = function() {
this.minxval = this.dateWindow_[0];
this.maxxval = this.dateWindow_[1];
} else {
- for (var name in this.datasets) {
- if (!this.datasets.hasOwnProperty(name)) continue;
- var series = this.datasets[name];
+ for (var setIdx = 0; setIdx < this.datasets.length; ++setIdx) {
+ var series = this.datasets[setIdx];
if (series.length > 1) {
var x1 = series[0][0];
if (!this.minxval || x1 < this.minxval) this.minxval = x1;
// for every data set since the points are added in order of the sets in
// datasets.
this.setPointsLengths = [];
+ this.setPointsOffsets = [];
- for (var setName in this.datasets) {
- if (!this.datasets.hasOwnProperty(setName)) continue;
-
- var dataset = this.datasets[setName];
+ for (var setIdx = 0; setIdx < this.datasets.length; ++setIdx) {
+ var dataset = this.datasets[setIdx];
+ var setName = this.setNames[setIdx];
var axis = this.dygraph_.axisPropertiesForSeries(setName);
+ this.setPointsOffsets.push(this.points.length);
var setPointsLength = 0;
for (var j = 0; j < dataset.length; j++) {
// Copy over the error terms
var i = 0; // index in this.points
- for (var setName in this.datasets) {
- if (!this.datasets.hasOwnProperty(setName)) continue;
+ for (var setIdx = 0; setIdx < this.datasets.length; ++setIdx) {
var j = 0;
- var dataset = this.datasets[setName];
+ var dataset = this.datasets[setIdx];
+ var setName = this.setNames[setIdx];
var axis = this.dygraph_.axisPropertiesForSeries(setName);
for (j = 0; j < dataset.length; j++, i++) {
var item = dataset[j];
*/
DygraphLayout.prototype.removeAllDatasets = function() {
delete this.datasets;
+ delete this.setNames;
+ delete this.setPointsLengths;
+ delete this.setPointsOffsets;
this.datasets = [];
+ this.setNames = [];
+ this.setPointsLengths = [];
+ this.setPointsOffsets = [];
};
/**
"example": "0.5, 2.0",
"description": "The width of the lines connecting data points. This can be used to increase the contrast or some graphs."
},
+ "strokePattern": {
+ "default": "null",
+ "labels": ["Data Line display"],
+ "type": "array<integer>",
+ "example": "[10, 2, 5, 2]",
+ "description": "A custom pattern array where the even index is a draw and odd is a space in pixels. If null then it draws a solid line. The array should have a even length as any odd lengthed array could be expressed as a smaller even length array."
+ },
"wilsonInterval": {
"default": "true",
"labels": ["Error Bars"],
"labels": ["Axis display"],
"type": "Array of two numbers",
"example": "[10, 110]",
- "description": "Explicitly set the vertical range of the graph to [low, high]."
+ "description": "Explicitly set the vertical range of the graph to [low, high]. This may be set on a per-axis basis to define each y-axis separately."
},
"labelsDivWidth": {
"default": "250",
return e.srcElement == self.iePanOverlay_;
} else {
// Getting clientX directly from the event is not accurate enough :(
- var clientX = self.canvasRect_.x + (e.layerX !== undefined ? e.layerX : e.offsetX);
+ var clientX;
+ if (e.offsetX != undefined) {
+ clientX = self.canvasRect_.x + e.offsetX;
+ } else {
+ clientX = e.clientX;
+ }
var zoomHandleStatus = self.getZoomHandleStatus_();
return (clientX > zoomHandleStatus.leftHandlePos && clientX < zoomHandleStatus.rightHandlePos);
}
}
};
- 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_.interactionModel =
+ Dygraph.Interaction.dragIsPanInteractionModel;
this.dygraph_.attrs_.panEdgeFraction = 0.0001;
var dragStartEvent = window.opera ? 'mousedown' : 'dragstart';
// https://github.com/eriwen/javascript-stacktrace
Dygraph.LOG_STACK_TRACES = false;
+/** A dotted line stroke pattern. */
+Dygraph.DOTTED_LINE = [2, 2];
+/** A dashed line stroke pattern. */
+Dygraph.DASHED_LINE = [7, 3];
+/** A dot dash stroke pattern. */
+Dygraph.DOT_DASH_LINE = [7, 2, 2, 2];
+
/**
* @private
* Log an error on the JS console at the given severity.
return requiresNewPoints;
};
+/**
+ * Compares two arrays to see if they are equal. If either parameter is not an
+ * array it will return false. Does a shallow compare
+ * Dygraph.compareArrays([[1,2], [3, 4]], [[1,2], [3,4]]) === false.
+ * @param array1 first array
+ * @param array2 second array
+ * @return True if both parameters are arrays, and contents are equal.
+ */
+Dygraph.compareArrays = function(array1, array2) {
+ if (!Dygraph.isArrayLike(array1) || !Dygraph.isArrayLike(array2)) {
+ return false;
+ }
+ if (array1.length !== array2.length) {
+ return false;
+ }
+ for (var i = 0; i < array1.length; i++) {
+ if (array1[i] !== array2[i]) {
+ return false;
+ }
+ }
+ return true;
+};
+
Dygraph.RegularConvex = function(sides, rotation) {
this.sides = sides;
Dygraph.updateDeep(this.attrs_, Dygraph.DEFAULT_ATTRS);
this.boundaryIds_ = [];
+ this.setIndexByName_ = {};
// Create the containing DIV and other interactive elements
this.createInterface_();
this.createStatusMessage_();
this.createDragInterface_();
+ this.resizeHandler = function(e) {
+ dygraph.resize();
+ }
+
// Update when the window is resized.
// TODO(danvk): drop frames depending on complexity of the chart.
- Dygraph.addEvent(window, 'resize', function(e) {
- dygraph.resize();
- });
+ Dygraph.addEvent(window, 'resize', this.resizeHandler);
};
/**
}
}
};
-
+ // remove event handlers
+ Dygraph.removeEvent(window,'resize',this.resizeHandler);
+ this.resizeHandler = null;
// These may not all be necessary, but it can't hurt...
nullOut(this.layout_);
nullOut(this.plotter_);
}
for (var i = 0; i < this.axes_.length; i++) {
- if (this.axes_[i].valueWindow !== null) {
+ if (typeof(this.axes_[i].valueWindow) !== 'undefined' && this.axes_[i].valueWindow !== null) {
dirty = true;
dirtyY = true;
}
newValueRanges = [];
for (i = 0; i < this.axes_.length; i++) {
- newValueRanges.push(this.axes_[i].extremeRange);
+ var axis = this.axes_[i];
+ newValueRanges.push(axis.valueRange != null ? axis.valueRange : axis.extremeRange);
}
}
Dygraph.prototype.idxToRow_ = function(idx) {
if (idx < 0) return -1;
- for (var i in this.layout_.datasets) {
- if (idx < this.layout_.datasets[i].length) {
- return this.boundaryIds_[0][0]+idx;
+ // make sure that you get the boundaryIds record which is also defined (see bug #236)
+ var boundaryIdx = -1;
+ for (var i = 0; i < this.boundaryIds_.length; i++) {
+ if (this.boundaryIds_[i] !== undefined) {
+ boundaryIdx = i;
+ break;
+ }
+ }
+ if (boundaryIdx < 0) return -1;
+ for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) {
+ var set = this.layout_.datasets[setIdx];
+ if (idx < set.length) {
+ return this.boundaryIds_[boundaryIdx][0] + idx;
}
- idx -= this.layout_.datasets[i].length;
+ idx -= set.length;
}
return -1;
};
/**
* @private
+ * Generates legend html dash for any stroke pattern. It will try to scale the
+ * pattern to fit in 1em width. Or if small enough repeat the partern for 1em
+ * width.
+ * @param strokePattern The pattern
+ * @param color The color of the series.
+ * @param oneEmWidth The width in pixels of 1em in the legend.
+ */
+Dygraph.prototype.generateLegendDashHTML_ = function(strokePattern, color, oneEmWidth) {
+ var dash = "";
+ var i, j, paddingLeft, marginRight;
+ var strokePixelLength = 0, segmentLoop = 0;
+ var normalizedPattern = [];
+ var loop;
+ // IE 7,8 fail at these divs, so they get boring legend, have not tested 9.
+ var isIE = (/MSIE/.test(navigator.userAgent) && !window.opera);
+ if(isIE) {
+ return "—";
+ }
+ if (!strokePattern || strokePattern.length <= 1) {
+ // Solid line
+ dash = "<div style=\"display: inline-block; position: relative; " +
+ "bottom: .5ex; padding-left: 1em; height: 1px; " +
+ "border-bottom: 2px solid " + color + ";\"></div>";
+ } else {
+ // Compute the length of the pixels including the first segment twice,
+ // since we repeat it.
+ for (i = 0; i <= strokePattern.length; i++) {
+ strokePixelLength += strokePattern[i%strokePattern.length];
+ }
+
+ // See if we can loop the pattern by itself at least twice.
+ loop = Math.floor(oneEmWidth/(strokePixelLength-strokePattern[0]));
+ if (loop > 1) {
+ // This pattern fits at least two times, no scaling just convert to em;
+ for (i = 0; i < strokePattern.length; i++) {
+ normalizedPattern[i] = strokePattern[i]/oneEmWidth;
+ }
+ // Since we are repeating the pattern, we don't worry about repeating the
+ // first segment in one draw.
+ segmentLoop = normalizedPattern.length;
+ } else {
+ // If the pattern doesn't fit in the legend we scale it to fit.
+ loop = 1;
+ for (i = 0; i < strokePattern.length; i++) {
+ normalizedPattern[i] = strokePattern[i]/strokePixelLength;
+ }
+ // For the scaled patterns we do redraw the first segment.
+ segmentLoop = normalizedPattern.length+1;
+ }
+ // Now make the pattern.
+ for (j = 0; j < loop; j++) {
+ for (i = 0; i < segmentLoop; i+=2) {
+ // The padding is the drawn segment.
+ paddingLeft = normalizedPattern[i%normalizedPattern.length];
+ if (i < strokePattern.length) {
+ // The margin is the space segment.
+ marginRight = normalizedPattern[(i+1)%normalizedPattern.length];
+ } else {
+ // The repeated first segment has no right margin.
+ marginRight = 0;
+ }
+ dash += "<div style=\"display: inline-block; position: relative; " +
+ "bottom: .5ex; margin-right: " + marginRight + "em; padding-left: " +
+ paddingLeft + "em; height: 1px; border-bottom: 2px solid " + color +
+ ";\"></div>";
+ }
+ }
+ }
+ return dash;
+};
+
+/**
+ * @private
* Generates HTML for the legend which is displayed when hovering over the
* chart. If no selected points are specified, a default legend is returned
* (this may just be the empty string).
* @param { Number } [x] The x-value of the selected points.
* @param { [Object] } [sel_points] List of selected points for the given
* x-value. Should have properties like 'name', 'yval' and 'canvasy'.
+ * @param { Number } [oneEmWidth] The pixel width for 1em in the legend.
*/
-Dygraph.prototype.generateLegendHTML_ = function(x, sel_points) {
+Dygraph.prototype.generateLegendHTML_ = function(x, sel_points, oneEmWidth) {
// If no points are selected, we display a default legend. Traditionally,
// this has been blank. But a better default would be a conventional legend,
// which provides essential information for a non-interactive chart.
- var html, sepLines, i, c;
+ var html, sepLines, i, c, dash, strokePattern;
if (typeof(x) === 'undefined') {
if (this.attr_('legend') != 'always') return '';
if (!this.visibility()[i - 1]) continue;
c = this.plotter_.colors[labels[i]];
if (html !== '') html += (sepLines ? '<br/>' : ' ');
- html += "<b><span style='color: " + c + ";'>—" + labels[i] +
- "</span></b>";
+ strokePattern = this.attr_("strokePattern", labels[i]);
+ dash = this.generateLegendDashHTML_(strokePattern, c, oneEmWidth);
+ html += "<span style='font-weight: bold; color: " + c + ";'>" + dash +
+ " " + labels[i] + "</span>";
}
return html;
}
* x-value. Should have properties like 'name', 'yval' and 'canvasy'.
*/
Dygraph.prototype.setLegendHTML_ = function(x, sel_points) {
- var html = this.generateLegendHTML_(x, sel_points);
var labelsDiv = this.attr_("labelsDiv");
+ var sizeSpan = document.createElement('span');
+ // Calculates the width of 1em in pixels for the legend.
+ sizeSpan.setAttribute('style', 'margin: 0; padding: 0 0 0 1em; border: 0;');
+ labelsDiv.appendChild(sizeSpan);
+ var oneEmWidth=sizeSpan.offsetWidth;
+
+ var html = this.generateLegendHTML_(x, sel_points, oneEmWidth);
if (labelsDiv !== null) {
labelsDiv.innerHTML = html;
} else {
}
if (row !== false && row >= 0) {
- for (var i in this.layout_.datasets) {
- if (row < this.layout_.datasets[i].length) {
+ for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) {
+ var set = this.layout_.datasets[setIdx];
+ if (row < set.length) {
var point = this.layout_.points[pos+row];
if (this.attr_("stackedGraph")) {
this.selPoints_.push(point);
}
- pos += this.layout_.datasets[i].length;
+ pos += set.length;
}
}
var extremes = packed[1];
this.boundaryIds_ = packed[2];
+ this.setIndexByName_ = {};
+ var labels = this.attr_("labels");
+ if (labels.length > 0) {
+ this.setIndexByName_[labels[0]] = 0;
+ }
for (var i = 1; i < datasets.length; i++) {
+ this.setIndexByName_[labels[i]] = i;
if (!this.visibility()[i - 1]) continue;
- this.layout_.addDataset(this.attr_("labels")[i], datasets[i]);
+ this.layout_.addDataset(labels[i], datasets[i]);
}
this.computeYAxisRanges_(extremes);
Dygraph.prototype.computeYAxes_ = function() {
// Preserve valueWindow settings if they exist, and if the user hasn't
// specified a new valueRange.
- var i, valueWindows, seriesName, axis, index;
+ var i, valueWindows, seriesName, axis, index, opts, v;
if (this.axes_ !== undefined && this.user_attrs_.hasOwnProperty("valueRange") === false) {
valueWindows = [];
for (index = 0; index < this.axes_.length; index++) {
// Copy global axis options over to the first axis.
for (i = 0; i < axisOptions.length; i++) {
var k = axisOptions[i];
- var v = this.attr_(k);
+ v = this.attr_(k);
if (v) this.axes_[0][k] = v;
}
}
if (typeof(axis) == 'object') {
// Add a new axis, making a copy of its per-axis options.
- var opts = {};
+ opts = {};
Dygraph.update(opts, this.axes_[0]);
Dygraph.update(opts, { valueRange: null }); // shouldn't inherit this.
var yAxisId = this.axes_.length;
this.axes_[index].valueWindow = valueWindows[index];
}
}
+
+ // New axes options
+ for (axis = 0; axis < this.axes_.length; axis++) {
+ if (axis === 0) {
+ opts = this.optionsViewForAxis_('y' + (axis ? '2' : ''));
+ v = opts("valueRange");
+ if (v) this.axes_[axis].valueRange = v;
+ } else { // To keep old behavior
+ var axes = this.user_attrs_.axes;
+ if (axes && axes.y2) {
+ v = axes.y2.valueRange;
+ if (v) this.axes_[axis].valueRange = v;
+ }
+ }
+ }
+
};
/**
* @private
*/
Dygraph.prototype.parseDataTable_ = function(data) {
+ var shortTextForAnnotationNum = function(num) {
+ // converts [0-9]+ [A-Z][a-z]*
+ // example: 0=A, 1=B, 25=Z, 26=Aa, 27=Ab
+ // and continues like.. Ba Bb .. Za .. Zz..Aaa...Zzz Aaaa Zzzz
+ var shortText = String.fromCharCode(65 /* A */ + num % 26);
+ num = Math.floor(num / 26);
+ while ( num > 0 ) {
+ shortText = String.fromCharCode(65 /* A */ + (num - 1) % 26 ) + shortText.toLowerCase();
+ num = Math.floor((num - 1) / 26);
+ }
+ return shortText;
+ }
+
var cols = data.getNumberOfColumns();
var rows = data.getNumberOfRows();
var ann = {};
ann.series = data.getColumnLabel(col);
ann.xval = row[0];
- ann.shortText = String.fromCharCode(65 /* A */ + annotations.length);
+ ann.shortText = shortTextForAnnotationNum(annotations.length);
ann.text = '';
for (var k = 0; k < annotationCols[col].length; k++) {
if (k) ann.text += "\n";
};
/**
+ * Get the list of label names for this graph. The first column is the
+ * x-axis, so the data series names start at index 1.
+ */
+Dygraph.prototype.getLabels = function(name) {
+ return this.attr_("labels").slice();
+};
+
+/**
* Get the index of a series (column) given its name. The first column is the
* x-axis, so the data series start with index 1.
*/
Dygraph.prototype.indexFromSetName = function(name) {
- var labels = this.attr_("labels");
- for (var i = 0; i < labels.length; i++) {
- if (labels[i] == name) return i;
- }
- return null;
+ return this.setIndexByName_[name];
};
/**
function dblClickV3(event, g, context) {
// Reducing by 20% makes it 80% the original size, which means
// to restore to original size it must grow by 25%
+
+ if (!(event.offsetX && event.offsetY)){
+ event.offsetX = event.layerX - event.target.offsetLeft;
+ event.offsetY = event.layerY - event.target.offsetTop;
+ }
+
var percentages = offsetToPercentage(g, event.offsetX, event.offsetY);
var xPct = percentages[0];
var yPct = percentages[1];
// that verbatim, it would be a 7.5%.
var percentage = normal / 50;
+ if (!(event.offsetX && event.offsetY)){
+ event.offsetX = event.layerX - event.target.offsetLeft;
+ event.offsetY = event.layerY - event.target.offsetTop;
+ }
+
var percentages = offsetToPercentage(g, event.offsetX, event.offsetY);
var xPct = percentages[0];
var yPct = percentages[1];
./generate-jsdoc.sh
# Copy everything to the site.
- scp -r gallery tests jsdoc experimental $site \
+ scp -r gallery common tests jsdoc experimental $site \
&& \
scp dygraph*.js gadget.xml excanvas.js thumbnail.png screenshot.png docs/* $site/
else
data.addColumn('string', 'title2');
data.addColumn('string', 'text2');
data.addRows([
- [new Date(2008, 1 ,1), 30000, undefined, undefined, 40645, undefined, undefined],
- [new Date(2008, 1 ,2), 14045, undefined, undefined, 20374, undefined, undefined],
- [new Date(2008, 1 ,3), 55022, undefined, undefined, 50766, undefined, undefined],
- [new Date(2008, 1 ,4), 75284, undefined, undefined, 14334, 'Out of Stock','Ran out of stock on pens at 4pm'],
- [new Date(2008, 1 ,5), 41476, 'Bought Pens','Bought 200k pens', 66467, undefined, undefined],
- [new Date(2008, 1 ,6), 33322, undefined, undefined, 39463, undefined, undefined]
+ [new Date(2008, 11 ,1), 30000, undefined, undefined, 40645, undefined, undefined],
+ [new Date(2008, 11 ,2), 14045, undefined, undefined, 20374, undefined, undefined],
+ [new Date(2008, 11 ,3), 55022, undefined, undefined, 50766, undefined, undefined],
+ [new Date(2008, 11 ,4), 75284, undefined, undefined, 14334, 'Out of Stock','Ran out of stock on pens at 4pm'],
+ [new Date(2008, 11 ,5), 41476, 'Bought Pens','Bought 200k pens', 66467, undefined, undefined],
+ [new Date(2008, 11 ,6), 33322, undefined, undefined, 39463, undefined, undefined]
]);
+ for (var i = 1; i < 14; i++) {
+ data.addRows([
+ [new Date(2008, 11 , 6 + i), i * 1000, 'title1-' + i, 'text1 ' + (i * 1000 - i), (i * 2000 + i), 'title2-' + i, 'text2' + i * 1000],
+ ]);
+ }
+
var chart = new google.visualization.AnnotatedTimeLine(document.getElementById('gviz_div'));
chart.draw(data, {displayAnnotations: true});
<div id='dg_div' style='width: 700px; height: 240px;'></div>
</body>
</html>
-
<body>
<p>Plot which can be easily generated with different numbers of points for
benchmarking/profiling and improving performance of dygraphs.</p>
- <p>Data to plot:
- <input type="radio" id="sine" name="group1" value="sine"
- onclick="clickedRadioButton(this);" checked> sinusoid function
- <input type="radio" id="rand" name="group1" value="rand"
- onclick="clickedRadioButton(this);"> random points <br></p>
- <p>Number of points:
- <input type="text" id="points" size="20"></p>
- <p>Number of series:
- <input type="text" id="series" size="20"></p>
- <p>Roll period (in points):
- <input type="text" id="rollPeriod" size="20"></p>
- <p>Repetitions:
- <input type="text" id="repetitions" size="20"></p>
-
- <input type="button" value="Go!" onclick="updatePlot();">
+ <div id='parameters'>
+ <p>Data to plot:
+ <input type="radio" id="sine" name="group1" value="sine"
+ onclick="clickedRadioButton(this);" checked> sinusoid function
+ <input type="radio" id="rand" name="group1" value="rand"
+ onclick="clickedRadioButton(this);"> random points <br></p>
+ <p>Number of points:
+ <input type="text" id="points" size="20"></p>
+ <p>Number of series:
+ <input type="text" id="series" size="20"></p>
+ <p>Roll period (in points):
+ <input type="text" id="rollPeriod" size="20"></p>
+ <p>Repetitions:
+ <input type="text" id="repetitions" size="20"></p>
+ <input type="button" value="Go!" onclick="updatePlot();">
+ </div>
<br>
<br>
<div id="plot"></div>
<div id="metaperformance"></div>
<script type="text/javascript">
- var plot;
+ var graph = null;
+ var metrics = null;
var dataType = "sine";
var durations = [];
var opts = {labels: labels, rollPeriod: rollPeriod, timingName: "x"};
var millisecondss = [];
for (var i = 0; i < repetitions; i++) {
+ if (graph != null) {
+ graph.destroy(); // release memory from prior graph.
+ }
var start = new Date();
- plot = new Dygraph(plotDiv, data, opts);
+ graph = new Dygraph(plotDiv, data, opts);
var end = new Date();
durations.push([start, end - start]);
millisecondss.push(end - start);
if (durations.length > 0) {
var start2 = new Date();
- new Dygraph(
- document.getElementById('metrics'),
- durations,
- {
- highlightCircleSize: 4,
- labels: [ "Date", "ms" ]
- });
+ if (!metrics) {
+ metrics = new Dygraph(
+ document.getElementById('metrics'),
+ durations,
+ {
+ highlightCircleSize: 4,
+ labels: [ "Date", "ms" ]
+ });
+ } else {
+ metrics.updateOptions({file: durations});
+ }
var end2 = new Date();
document.getElementById("metaperformance").innerHTML =
"completed in " + (end2 - start2) + " milliseconds.";
function dblClickV3(event, g, context) {
// Reducing by 20% makes it 80% the original size, which means
// to restore to original size it must grow by 25%
+
+ if (!(event.offsetX && event.offsetY)){
+ event.offsetX = event.layerX - event.target.offsetLeft;
+ event.offsetY = event.layerY - event.target.offsetTop;
+ }
+
var percentages = offsetToPercentage(g, event.offsetX, event.offsetY);
var xPct = percentages[0];
var yPct = percentages[1];
// that verbatim, it would be a 7.5%.
var percentage = normal / 50;
+ if (!(event.offsetX && event.offsetY)){
+ event.offsetX = event.layerX - event.target.offsetLeft;
+ event.offsetY = event.layerY - event.target.offsetTop;
+ }
+
var percentages = offsetToPercentage(g, event.offsetX, event.offsetY);
var xPct = percentages[0];
var yPct = percentages[1];
}
function dblClickV4(event, g, context) {
- restorePositioning(g4);
+ restorePositioning(g);
}
function drawV4(x, y) {
<body>
<h2>Chart with per-series properties</h2>
<div id="demodiv"></div>
-
+ <h2>Chart with per-series properties with legend.</h2>
+ <div id="demodiv2"></div>
<script type="text/javascript">
+ data = function() {
+ var zp = function(x) { if (x < 10) return "0"+x; else return x; };
+ var r = "date,parabola,line,another line,sine wave,sine wave2\n";
+ for (var i=1; i<=31; i++) {
+ r += "200610" + zp(i);
+ r += "," + 10*(i*(31-i));
+ r += "," + 10*(8*i);
+ r += "," + 10*(250 - 8*i);
+ r += "," + 10*(125 + 125 * Math.sin(0.3*i));
+ r += "," + 10*(125 + 125 * Math.sin(0.3*i+Math.PI));
+ r += "\n";
+ }
+ return r;
+ };
g = new Dygraph(
document.getElementById("demodiv"),
- function() {
- var zp = function(x) { if (x < 10) return "0"+x; else return x; };
- var r = "date,parabola,line,another line,sine wave\n";
- for (var i=1; i<=31; i++) {
- r += "200610" + zp(i);
- r += "," + 10*(i*(31-i));
- r += "," + 10*(8*i);
- r += "," + 10*(250 - 8*i);
- r += "," + 10*(125 + 125 * Math.sin(0.3*i));
- r += "\n";
- }
- return r;
- },
+ data,
{
strokeWidth: 2,
'parabola': {
'sine wave': {
strokeWidth: 3,
highlightCircleSize: 10
+ },
+ 'sine wave2': {
+ strokePattern: [10, 2, 5, 2],
+ strokeWidth: 2,
+ highlightCircleSize: 3
+ }
+ }
+ );
+ g2 = new Dygraph(
+ document.getElementById("demodiv2"),
+ data,
+ {
+ legend: 'always',
+ strokeWidth: 2,
+ 'parabola': {
+ strokePattern: null,
+ drawPoints: true,
+ pointSize: 4,
+ highlightCircleSize: 6
+ },
+ 'line': {
+ strokePattern: Dygraph.DASHED_LINE,
+ strokeWidth: 1.0,
+ drawPoints: true,
+ pointSize: 1.5
+ },
+ 'another line': {
+ strokePattern: [25, 5]
+ },
+ 'sine wave': {
+ strokePattern: Dygraph.DOTTED_LINE,
+ strokeWidth: 3,
+ highlightCircleSize: 10
+ },
+ 'sine wave2': {
+ strokePattern: Dygraph.DOT_DASH_LINE,
+ strokeWidth: 2,
+ highlightCircleSize: 3
}
}
);
+
</script>
</body>
</html>
--- /dev/null
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7; IE=EmulateIE9">
+ <title>Multiple y-axes with valueRange</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>
+
+ </head>
+ <body>
+ <h2>Multiple y-axes with valueRange</h2>
+ <p>The same data with both different valueRanges. Two-axis old y[40, 70] valueRange:</p>
+ <div id="demodiv" style="width: 640; height: 350; border: 1px solid black"></div>
+ <p>Two-axis new valueRange y[40, 80] set:</p>
+ <div id="demodiv_one" style="width: 640; height: 350; border: 1px solid black"></div>
+ <p>Two-axis new valueRange y[40, 80] & y2[1e6, 1.2e6] set:</p>
+ <div id="demodiv_two" style="width: 640; height: 350; border: 1px solid black"></div>
+ <script type="text/javascript">
+ var data = [];
+ for (var i = 1; i <= 100; i++) {
+ var m = "01", d = i;
+ if (d > 31) { m = "02"; d -= 31; }
+ if (m == "02" && d > 28) { m = "03"; d -= 28; }
+ if (m == "03" && d > 31) { m = "04"; d -= 31; }
+ if (d < 10) d = "0" + d;
+ // two series, one with range 1-100, one with range 1-2M
+ data.push([new Date("2010/" + m + "/" + d),
+ i,
+ 100 - i,
+ 1e6 * (1 + i * (100 - i) / (50 * 50)),
+ 1e6 * (2 - i * (100 - i) / (50 * 50))]);
+ }
+
+ g = new Dygraph(
+ document.getElementById("demodiv"),
+ data,
+ {
+ labels: [ 'Date', 'Y1', 'Y2', 'Y3', 'Y4' ],
+ 'Y3': {
+ axis: {
+ }
+ },
+ 'Y4': {
+ axis: 'Y3' // use the same y-axis as series Y3
+ },
+ valueRange: [40, 70],
+ axes: {
+ y2: {
+ // set axis-related properties here
+ labelsKMB: true
+ }
+ },
+ ylabel: 'Primary y-axis',
+ y2label: 'Secondary y-axis',
+ yAxisLabelWidth: 60
+ }
+ );
+
+ g2 = new Dygraph(
+ document.getElementById("demodiv_one"),
+ data,
+ {
+ labels: [ 'Date', 'Y1', 'Y2', 'Y3', 'Y4' ],
+ 'Y3': {
+ axis: {
+ }
+ },
+ 'Y4': {
+ axis: 'Y3' // use the same y-axis as series Y3
+ },
+ axes: {
+ y: {
+ valueRange: [40, 80]
+ },
+ y2: {
+ // set axis-related properties here
+ labelsKMB: true
+ }
+ },
+ ylabel: 'Primary y-axis',
+ y2label: 'Secondary y-axis',
+ yAxisLabelWidth: 60
+ }
+ );
+
+ g2 = new Dygraph(
+ document.getElementById("demodiv_two"),
+ data,
+ {
+ labels: [ 'Date', 'Y1', 'Y2', 'Y3', 'Y4' ],
+ 'Y3': {
+ axis: {
+ }
+ },
+ 'Y4': {
+ axis: 'Y3' // use the same y-axis as series Y3
+ },
+ axes: {
+ y: {
+ valueRange: [40, 80]
+ },
+ y2: {
+ // set axis-related properties here
+ valueRange: [1e6, 1.2e6],
+ labelsKMB: true
+ }
+ },
+ ylabel: 'Primary y-axis',
+ y2label: 'Secondary y-axis',
+ yAxisLabelWidth: 60
+ }
+ );
+
+
+ </script>
+</body>
+</html>