var g = new Dygraph(graph, data, opts);
};
+pathologicalCasesTestCase.prototype.testCombinations = function() {
+ var dataSets = {
+ empty: [],
+ onePoint: [[10, 2]],
+ nanPoint: [[10, NaN]],
+ nanPoints: [[10, NaN], [20, NaN]],
+ multiNan1: [[10, NaN, 2], [20, 3, NaN]],
+ multiNan2: [[10, NaN, 2], [20, NaN, 4]],
+ multiNan3: [[10, NaN, NaN], [20, 3, 4], [30, NaN, NaN]],
+ atZero: [[0, 0]],
+ atZero2: [[0, 0, 0]],
+ negative: [[-10, -1]],
+ acrossZero: [[-10, 1], [10, 2]],
+ normal: [[0,1,9], [10,3,5], [20,2,7], [30,4,3]]
+ };
+
+ var baseOpts = {
+ lines: {},
+ stacked: {
+ stackedGraph: true
+ }
+ };
+
+ var variantOpts = {
+ none: {},
+ avoidMinZero: {
+ avoidMinZero: true,
+ includeZero: true
+ },
+ padded: {
+ includeZero: true,
+ drawAxesAtZero: true,
+ xRangePad: 0.02,
+ yRangePad: 0.04
+ }
+ };
+
+ for (var baseName in baseOpts) {
+ var base = baseOpts[baseName];
+ for (var variantName in variantOpts) {
+ var variant = variantOpts[variantName];
+
+ var opts = {
+ width: 300,
+ height: 150,
+ labelsDivWidth: 100,
+ pointSize: 10
+ };
+ for (var key in base) {
+ if (base.hasOwnProperty(key)) opts[key] = base[key];
+ }
+ for (var key in variant) {
+ if (variant.hasOwnProperty(key)) opts[key] = variant[key];
+ }
+
+ var h = document.createElement('h3');
+ h.appendChild(document.createTextNode(baseName + ' ' + variantName));
+ document.body.appendChild(h);
+ for (var dataName in dataSets) {
+ var data = dataSets[dataName];
+
+ var box = document.createElement('fieldset');
+ box.style.display = 'inline-block';
+ var legend = document.createElement('legend');
+ legend.appendChild(document.createTextNode(dataName));
+ box.appendChild(legend);
+ var gdiv = document.createElement('div');
+ gdiv.style.display = 'inline-block';
+ box.appendChild(gdiv);
+ document.body.appendChild(box);
+
+ var cols = data && data[0] ? data[0].length : 0;
+ opts.labels = ['X', 'A', 'B', 'C'].slice(0, cols);
+
+ var g = new Dygraph(gdiv, data, opts);
+ }
+ }
+ }
+};
+
pathologicalCasesTestCase.prototype.testNullLegend = function() {
var opts = {
width: 480,
document.body.innerHTML = "<div id='graph'></div>";
};
-RangeTestCase.prototype.createGraph = function(opts) {
+RangeTestCase.prototype.createGraph = function(opts, data, expectRangeX, expectRangeY) {
+ if (data === undefined) data = ZERO_TO_FIFTY_STEPS;
+ if (expectRangeX === undefined) expectRangeX = [10, 20];
+ if (expectRangeY === undefined) expectRangeY = [0, 55];
var graph = document.getElementById("graph");
- var g = new Dygraph(graph, ZERO_TO_FIFTY_STEPS, opts);
+ var g = new Dygraph(graph, data, opts);
- assertEquals([10, 20], g.xAxisRange());
- assertEquals([0, 55], g.yAxisRange(0));
+ assertEqualsDelta(expectRangeX, g.xAxisRange(), 0.01);
+ assertEqualsDelta(expectRangeY, g.yAxisRange(0), 0.01);
return g;
};
assertEqualsDelta(1, -1e229 / g.yAxisRange(0)[0], 0.001);
assertEqualsDelta(1, 1.1e230 / g.yAxisRange(0)[1], 0.001);
}
+
+/**
+ * Verify old-style avoidMinZero option.
+ */
+RangeTestCase.prototype.testAvoidMinZero = function() {
+ var g = this.createGraph({
+ avoidMinZero: true,
+ }, ZERO_TO_FIFTY_STEPS, [10, 20], [-5, 55]);
+};
+
+/**
+ * Verify ranges with user-specified padding, implicit avoidMinZero.
+ */
+RangeTestCase.prototype.testPaddingAuto = function() {
+ var g = this.createGraph({
+ xRangePad: 42,
+ yRangePad: 30
+ }, ZERO_TO_FIFTY_STEPS, [9, 21], [-5, 55]);
+};
+
+/**
+ * Verify auto range with drawAxesAtZero.
+ */
+RangeTestCase.prototype.testPaddingAutoAxisAtZero = function() {
+ var g = this.createGraph({
+ drawAxesAtZero: true,
+ }, ZERO_TO_FIFTY_STEPS, [10, 20], [0, 55]);
+};
+
+/**
+ * Verify user-specified range with padding and drawAxesAtZero options.
+ * Try explicit range matching the auto range, should have identical results.
+ */
+RangeTestCase.prototype.testPaddingRange1 = function() {
+ var g = this.createGraph({
+ valueRange: [0, 50],
+ xRangePad: 42,
+ yRangePad: 30,
+ drawAxesAtZero: true
+ }, ZERO_TO_FIFTY_STEPS, [9, 21], [-5, 55]);
+};
+
+/**
+ * Verify user-specified range with padding and drawAxesAtZero options.
+ * User-supplied range differs from the auto range.
+ */
+RangeTestCase.prototype.testPaddingRange2 = function() {
+ var g = this.createGraph({
+ valueRange: [10, 60],
+ xRangePad: 42,
+ yRangePad: 30,
+ drawAxesAtZero: true,
+ }, ZERO_TO_FIFTY_STEPS, [9, 21], [5, 65]);
+};
+
+/**
+ * Verify drawAxesAtZero and includeZero.
+ */
+RangeTestCase.prototype.testPaddingYAtZero = function() {
+ var g = this.createGraph({
+ includeZero: true,
+ xRangePad: 42,
+ yRangePad: 30,
+ drawAxesAtZero: true,
+ }, [
+ [-10, 10],
+ [10, 20],
+ [30, 50]
+ ], [-14, 34], [-5, 55]);
+};
+
+/**
+ * Verify logscale, compat mode.
+ */
+RangeTestCase.prototype.testLogscaleCompat = function() {
+ var g = this.createGraph({
+ logscale: true
+ },
+ [[-10, 10], [10, 10], [30, 1000]],
+ [-10, 30], [10, 1099]);
+};
+
+/**
+ * Verify logscale, new mode.
+ */
+RangeTestCase.prototype.testLogscalePad = function() {
+ var g = this.createGraph({
+ logscale: true,
+ yRangePad: 30
+ },
+ [[-10, 10], [10, 10], [30, 1000]],
+ [-10, 30], [5.01691, 1993.25801]);
+};
};
DygraphLayout.prototype._evaluateLimits = function() {
- this.minxval = this.maxxval = null;
- if (this.dateWindow_) {
- this.minxval = this.dateWindow_[0];
- this.maxxval = this.dateWindow_[1];
- } else {
- 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;
-
- var x2 = series[series.length - 1][0];
- if (!this.maxxval || x2 > this.maxxval) this.maxxval = x2;
- }
- }
- }
- this.xrange = this.maxxval - this.minxval;
- this.xscale = (this.xrange !== 0 ? 1/this.xrange : 1.0);
+ var xlimits = this.dygraph_.xAxisRange();
+ this.minxval = xlimits[0];
+ this.maxxval = xlimits[1];
+ var xrange = xlimits[1] - xlimits[0];
+ this.xscale = (xrange !== 0 ? 1 / xrange : 1.0);
for (var i = 0; i < this.yAxes_.length; i++) {
var axis = this.yAxes_[i];
},
"avoidMinZero": {
"default": "false",
- "labels": ["Axis display"],
+ "labels": ["Deprecated"],
"type": "boolean",
- "description": "When set, the heuristic that fixes the Y axis at zero for a data set with the minimum Y value of zero is disabled. \nThis is particularly useful for data sets that contain many zero values, especially for step plots which may otherwise have lines not visible running along the bottom axis."
+ "description": "Deprecated, please use yRangePad instead. When set, the heuristic that fixes the Y axis at zero for a data set with the minimum Y value of zero is disabled. \nThis is particularly useful for data sets that contain many zero values, especially for step plots which may otherwise have lines not visible running along the bottom axis."
},
"drawAxesAtZero": {
"default": "false",
"type": "boolean",
"description": "When set, draw the X axis at the Y=0 position and the Y axis at the X=0 position if those positions are inside the graph's visible area. Otherwise, draw the axes at the bottom or left graph edge as usual."
},
+ "xRangePad": {
+ "default": "0",
+ "labels": ["Axis display"],
+ "type": "float",
+ "description": "Add the specified amount of extra space (in pixels) around the X-axis value range to ensure points at the edges remain visible."
+ },
+ "yRangePad": {
+ "default": "null",
+ "labels": ["Axis display"],
+ "type": "float",
+ "description": "If set, add the specified amount of extra space (in pixels) around the Y-axis value range to ensure points at the edges remain visible. If unset, use the traditional Y padding algorithm."
+ },
"xAxisLabelFormatter": {
"default": "",
"labels": ["Deprecated"],
stepPlot: false,
avoidMinZero: false,
+ xRangePad: 0,
+ yRangePad: null,
drawAxesAtZero: false,
// Sizes of the various chart labels.
* data set.
*/
Dygraph.prototype.xAxisExtremes = function() {
+ var pad = this.attr_('xRangePad') / this.plotter_.area.w;
+ if (!this.numRows() > 0) {
+ return [0 - pad, 1 + pad];
+ }
var left = this.rawData_[0][0];
var right = this.rawData_[this.rawData_.length - 1][0];
+ if (pad) {
+ // Must keep this in sync with dygraph-layout _evaluateLimits()
+ var range = right - left;
+ left -= range * pad;
+ right += range * pad;
+ }
return [left, right];
};
* @return { Integer } The number of columns.
*/
Dygraph.prototype.numColumns = function() {
+ if (!this.rawData_) return 0;
return this.rawData_[0] ? this.rawData_[0].length : this.attr_("labels").length;
};
* @return { Integer } The number of rows, less any header.
*/
Dygraph.prototype.numRows = function() {
+ if (!this.rawData_) return 0;
return this.rawData_.length;
};
* @private
*/
Dygraph.prototype.fullXRange_ = function() {
- if (this.numRows() > 0) {
- return [this.rawData_[0][0], this.rawData_[this.numRows() - 1][0]];
- } else {
- return [0, 1];
- }
+ return this.xAxisExtremes();
};
/**
maxY = Math.max(extremeMaxY, maxY);
}
}
- if (includeZero && minY > 0) minY = 0;
+
+ // Include zero if requested by the user.
+ if (includeZero && !logscale) {
+ if (minY > 0) minY = 0;
+ if (maxY < 0) maxY = 0;
+ }
// Ensure we have a valid scale, otherwise default to [0, 1] for safety.
if (minY == Infinity) minY = 0;
if (maxY == -Infinity) maxY = 1;
- // Add some padding and round up to an integer to be human-friendly.
var span = maxY - minY;
- // special case: if we have no sense of scale, use +/-10% of the sole value.
- if (span === 0) { span = maxY; }
+ // special case: if we have no sense of scale, center on the sole value.
+ if (span === 0) {
+ if (maxY !== 0) {
+ span = Math.abs(maxY);
+ } else {
+ // ... and if the sole value is zero, use range 0-1.
+ maxY = 1;
+ span = 1;
+ }
+ }
+
+ // Add some padding. This supports two Y padding operation modes:
+ //
+ // - backwards compatible (yRangePad not set):
+ // 10% padding for automatic Y ranges, but not for user-supplied
+ // ranges, and move a close-to-zero edge to zero except if
+ // avoidMinZero is set, since drawing at the edge results in
+ // invisible lines. Unfortunately lines drawn at the edge of a
+ // user-supplied range will still be invisible. If logscale is
+ // set, add a variable amount of padding at the top but
+ // none at the bottom.
+ //
+ // - new-style (yRangePad set by the user):
+ // always add the specified Y padding.
+ //
+ var ypadCompat = true;
+ var ypad = 0.1; // add 10%
+ if (this.attr_('yRangePad') !== null) {
+ ypadCompat = false;
+ // Convert pixel padding to ratio
+ ypad = this.attr_('yRangePad') / this.plotter_.area.h;
+ }
var maxAxisY, minAxisY;
if (logscale) {
- maxAxisY = maxY + 0.1 * span;
- minAxisY = minY;
+ if (ypadCompat) {
+ maxAxisY = maxY + ypad * span;
+ minAxisY = minY;
+ } else {
+ var logpad = Math.exp(Math.log(span) * ypad);
+ maxAxisY = maxY * logpad;
+ minAxisY = minY / logpad;
+ }
} else {
- maxAxisY = maxY + 0.1 * span;
- minAxisY = minY - 0.1 * span;
+ maxAxisY = maxY + ypad * span;
+ minAxisY = minY - ypad * span;
- // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
- if (!this.attr_("avoidMinZero")) {
+ // Backwards-compatible behavior: Move the span to start or end at zero if it's
+ // close to zero, but not if avoidMinZero is set.
+ if (ypadCompat && !this.attr_("avoidMinZero")) {
if (minAxisY < 0 && minY >= 0) minAxisY = 0;
if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0;
}
-
- if (this.attr_("includeZero")) {
- if (maxY < 0) maxAxisY = 0;
- if (minY > 0) minAxisY = 0;
- }
}
axis.extremeRange = [minAxisY, maxAxisY];
}
axis.computedValueRange = [axis.valueWindow[0], axis.valueWindow[1]];
} else if (axis.valueRange) {
// This is a user-set value range for this axis.
- axis.computedValueRange = [
- isNullUndefinedOrNaN(axis.valueRange[0]) ? axis.extremeRange[0] : axis.valueRange[0],
- isNullUndefinedOrNaN(axis.valueRange[1]) ? axis.extremeRange[1] : axis.valueRange[1]
- ];
+ var y0 = isNullUndefinedOrNaN(axis.valueRange[0]) ? axis.extremeRange[0] : axis.valueRange[0];
+ var y1 = isNullUndefinedOrNaN(axis.valueRange[1]) ? axis.extremeRange[1] : axis.valueRange[1];
+ if (!ypadCompat) {
+ if (axis.logscale) {
+ var logpad = Math.exp(Math.log(span) * ypad);
+ y0 *= logpad;
+ y1 /= logpad;
+ } else {
+ var span = y1 - y0;
+ y0 -= span * ypad;
+ y1 += span * ypad;
+ }
+ }
+ axis.computedValueRange = [y0, y1];
} else {
axis.computedValueRange = axis.extremeRange;
}
--- /dev/null
+Gallery.register(
+ 'edge-padding',
+ {
+ name: 'Edge Padding',
+ title: 'Graph edge padding and axis position',
+ setup: function(parent) {
+ parent.innerHTML = (
+ "<p>" +
+ " <b>Mode:</b>" +
+ " <input type='radio' name='mode'>use {x,y}RangePad</input>" +
+ " <input type='radio' name='mode'>original</input>" +
+ " <br /><b>Settings:</b>" +
+ " <input type='checkbox' id='yrange'>valueRange=[-2,2]</input>" +
+ "</p>" +
+ "<div id='demodiv'></div>"
+ );
+ },
+ run: function() {
+ var parent = document.getElementById("demodiv");
+
+ var graphs = [];
+ var nrows = 50;
+
+ for (var oy = -2; oy <= 2; ++oy) {
+ for (var ox = -1; ox <= 1; ++ox) {
+ var gdiv = document.createElement('div');
+ gdiv.style.display = 'inline-block';
+ gdiv.style.margin = '2px';
+ parent.appendChild(gdiv);
+
+ var data = [];
+ for (var row = 0; row < nrows; ++row) {
+ var x = row * 5 / (nrows - 1);
+ data.push([ox * 2.5 + x - 2.5,
+ oy + Math.sin(x),
+ oy + Math.round(Math.cos(x))]);
+ }
+
+ var g = new Dygraph(gdiv, data, {
+ labels: ['x', 'A', 'B'],
+ labelDivWidth: 100,
+ gridLineColor: '#ccc',
+ includeZero: true,
+ width: 250,
+ height: 130
+ });
+ graphs.push(g);
+ }
+ parent.appendChild(document.createElement('br'));
+ }
+
+ var updateGraphOpts = function(opts) {
+ for (var i = 0; i < graphs.length; ++i) {
+ graphs[i].updateOptions(opts);
+ }
+ };
+
+ var mode = document.getElementsByName('mode');
+ mode[0].onchange = function() {
+ updateGraphOpts({
+ avoidMinZero: false,
+ xRangePad: 3,
+ yRangePad: 10,
+ drawAxesAtZero: true})};
+ mode[1].onchange = function() {
+ updateGraphOpts({
+ avoidMinZero: true,
+ xRangePad: 0,
+ yRangePad: null,
+ drawAxesAtZero: false})};
+ mode[0].checked = true;
+ mode[0].onchange();
+
+ var yrange = document.getElementById('yrange');
+ yrange.onchange = function(ev) {
+ updateGraphOpts({
+ valueRange: ev.target.checked ? [-2, 2] : null});
+ };
+
+ }
+ });
#workarea #highlighted-series .few .dygraph-legend > span.highlight { border: 1px solid grey; }
#workarea #highlighted-series .many .dygraph-legend > span { display: none; }
#workarea #highlighted-series .many .dygraph-legend > span.highlight { display: inline; }
+
+#workarea #edge-padding fieldset { display: inline-block; vertical-align: top; }
<script src="temperature-sf-ny.js"></script>
<script src="interaction.js"></script>
<script src="linear-regression.js"></script>
+ <script src="edge-padding.js"></script>
<!-- These might not remain in the gallery
<script src="dygraph-simple.js"></script>