Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stacked Area Charts #2960

Merged
merged 14 commits into from
Sep 7, 2018
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ lib.sorterAsc = searchModule.sorterAsc;
lib.sorterDes = searchModule.sorterDes;
lib.distinctVals = searchModule.distinctVals;
lib.roundUp = searchModule.roundUp;
lib.sort = searchModule.sort;

var statsModule = require('./stats');
lib.aggNums = statsModule.aggNums;
Expand Down
41 changes: 41 additions & 0 deletions src/lib/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,44 @@ exports.roundUp = function(val, arrayIn, reverse) {
}
return arrayIn[low];
};

/**
* Tweak to Array.sort(sortFn) that improves performance for pre-sorted arrays
*
* Motivation: sometimes we need to sort arrays but the input is likely to
* already be sorted. Browsers don't seem to pick up on pre-sorted arrays,
* and in fact Chrome is actually *slower* sorting pre-sorted arrays than purely
* random arrays. FF is at least faster if the array is pre-sorted, but still
* not as fast as it could be.
* Here's how this plays out sorting a length-1e6 array:
*
* Calls to Sort FN | Chrome bare | FF bare | Chrome tweak | FF tweak
etpinard marked this conversation as resolved.
Show resolved Hide resolved
* ------------------+---------------+-----------+----------------+------------
* ordered | 30.4e6 | 10.1e6 | 1e6 | 1e6
* reversed | 29.4e6 | 9.9e6 | 1e6 + reverse | 1e6 + reverse
* random | ~21e6 | ~18.7e6 | ~21e6 | ~18.7e6
*
* So this is a substantial win for pre-sorted (ordered or exactly reversed)
* arrays. Including this wrapper on an unsorted array adds a penalty that will
* in general be only a few calls to the sort function. The only case this
* penalty will be significant is if the array is mostly sorted but there are
* a few unsorted items near the end, but the penalty is still at most N calls
* out of (for N=1e6) ~20N total calls
*
* @param {Array} array: the array, to be sorted in place
* @param {function} sortFn: As in Array.sort, function(a, b) that puts
* item a before item b if the return is negative, a after b if positive,
* and no change if zero.
* @return {Array}: the original array, sorted in place.
*/
exports.sort = function(array, sortFn) {
var notOrdered = 0;
var notReversed = 0;
for(var i = 1; i < array.length; i++) {
var pairOrder = sortFn(array[i], array[i - 1]);
if(pairOrder < 0) notOrdered = 1;
else if(pairOrder > 0) notReversed = 1;
if(notOrdered && notReversed) return array.sort(sortFn);
}
return notReversed ? array : array.reverse();
};
112 changes: 63 additions & 49 deletions src/plots/cartesian/autorange.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ function getAutoRange(gd, ax) {
ax.autorange = true;
}

var rangeMode = ax.rangemode;
var toZero = rangeMode === 'tozero';
var nonNegative = rangeMode === 'nonnegative';
var axLen = ax._length;
// don't allow padding to reduce the data to < 10% of the length
var minSpan = axLen / 10;

var mbest = 0;
var minpt, maxpt, minbest, maxbest, dp, dv;

Expand All @@ -95,76 +102,83 @@ function getAutoRange(gd, ax) {
for(j = 0; j < maxArray.length; j++) {
maxpt = maxArray[j];
dv = maxpt.val - minpt.val;
dp = ax._length - getPad(minpt) - getPad(maxpt);
if(dv > 0 && dp > 0 && dv / dp > mbest) {
minbest = minpt;
maxbest = maxpt;
mbest = dv / dp;
if(dv > 0) {
dp = axLen - getPad(minpt) - getPad(maxpt);
if(dp > minSpan) {
if(dv / dp > mbest) {
minbest = minpt;
maxbest = maxpt;
mbest = dv / dp;
}
}
else if(dv / axLen > mbest) {
// in case of padding longer than the axis
// at least include the unpadded data values.
minbest = {val: minpt.val, pad: 0};
maxbest = {val: maxpt.val, pad: 0};
mbest = dv / axLen;
etpinard marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}

function getMaxPad(prev, pt) {
return Math.max(prev, getPad(pt));
}

if(minmin === maxmax) {
var lower = minmin - 1;
var upper = minmin + 1;
if(ax.rangemode === 'tozero') {
newRange = minmin < 0 ? [lower, 0] : [0, upper];
} else if(ax.rangemode === 'nonnegative') {
newRange = [Math.max(0, lower), Math.max(0, upper)];
if(toZero) {
if(minmin === 0) {
// The only value we have on this axis is 0, and we want to
// autorange so zero is one end.
// In principle this could be [0, 1] or [-1, 0] but usually
// 'tozero' pins 0 to the low end, so follow that.
newRange = [0, 1];
}
else {
var maxPad = (minmin > 0 ? maxArray : minArray).reduce(getMaxPad, 0);
// we're pushing a single value away from the edge due to its
// padding, with the other end clamped at zero
// 0.5 means don't push it farther than the center.
var rangeEnd = minmin / (1 - Math.min(0.5, maxPad / axLen));
newRange = minmin > 0 ? [0, rangeEnd] : [rangeEnd, 0];
}
} else if(nonNegative) {
newRange = [Math.max(0, lower), Math.max(1, upper)];
} else {
newRange = [lower, upper];
}
}
else if(mbest) {
if(ax.type === 'linear' || ax.type === '-') {
if(ax.rangemode === 'tozero') {
if(minbest.val >= 0) {
minbest = {val: 0, pad: 0};
}
if(maxbest.val <= 0) {
maxbest = {val: 0, pad: 0};
}
else {
if(toZero) {
if(minbest.val >= 0) {
minbest = {val: 0, pad: 0};
}
else if(ax.rangemode === 'nonnegative') {
if(minbest.val - mbest * getPad(minbest) < 0) {
minbest = {val: 0, pad: 0};
}
if(maxbest.val < 0) {
maxbest = {val: 1, pad: 0};
}
if(maxbest.val <= 0) {
maxbest = {val: 0, pad: 0};
}
}
else if(nonNegative) {
if(minbest.val - mbest * getPad(minbest) < 0) {
minbest = {val: 0, pad: 0};
}
if(maxbest.val <= 0) {
maxbest = {val: 1, pad: 0};
}

// in case it changed again...
mbest = (maxbest.val - minbest.val) /
(ax._length - getPad(minbest) - getPad(maxbest));

}

// in case it changed again...
mbest = (maxbest.val - minbest.val) /
(axLen - getPad(minbest) - getPad(maxbest));

newRange = [
minbest.val - mbest * getPad(minbest),
maxbest.val + mbest * getPad(maxbest)
];
}

// don't let axis have zero size, while still respecting tozero and nonnegative
if(newRange[0] === newRange[1]) {
etpinard marked this conversation as resolved.
Show resolved Hide resolved
if(ax.rangemode === 'tozero') {
if(newRange[0] < 0) {
newRange = [newRange[0], 0];
} else if(newRange[0] > 0) {
newRange = [0, newRange[0]];
} else {
newRange = [0, 1];
}
}
else {
newRange = [newRange[0] - 1, newRange[0] + 1];
if(ax.rangemode === 'nonnegative') {
newRange[0] = Math.max(0, newRange[0]);
}
}
}

// maintain reversal
if(axReverse) newRange.reverse();

Expand Down
2 changes: 1 addition & 1 deletion src/plots/polar/layout_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ function handleDefaults(contIn, contOut, coerce, opts) {
case 'radialaxis':
var autoRange = coerceAxis('autorange', !axOut.isValidRange(axIn.range));
axIn.autorange = autoRange;
if(autoRange) coerceAxis('rangemode');
if(autoRange && (axType === 'linear' || axType === '-')) coerceAxis('rangemode');
if(autoRange === 'reversed') axOut._m = -1;

coerceAxis('range');
Expand Down
Binary file modified test/image/baselines/autorange-tozero-rangemode.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/image/baselines/axes_range_type.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/image/baselines/contour_log.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 0 additions & 2 deletions test/image/mocks/scatter_fill_corner_cases.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@
{
"x": [1.5],
"y": [1.25],
"fill": "tonexty",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh this change I think is actually required due to a change in the stacked area commit be38e93#diff-33c02cd37e7a4c951059a3c93221ac4eR175 - we were accidentally treating a length-1 trace as filling to itself (since its start and end points are the same!) but we shouldn't do that... therefore this trace, since it's the first on its subplot, should interpret 'tonexty' as 'tozeroy'.

"showlegend": false,
"yaxis": "y2"
},
Expand Down Expand Up @@ -111,7 +110,6 @@
{
"x": [1.5],
"y": [1.25],
"fill": "tonexty",
"line": {"shape": "spline"},
"xaxis": "x2",
"showlegend": false,
Expand Down
94 changes: 88 additions & 6 deletions test/jasmine/tests/axes_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1558,7 +1558,7 @@ describe('Test axes', function() {
expect(getAutoRange(gd, ax)).toEqual([7.5, 0]);
});

it('expands empty positive range to something including 0 with rangemode tozero', function() {
it('expands empty positive range to include 0 with rangemode tozero', function() {
gd = mockGd([
{val: 5, pad: 0}
], [
Expand All @@ -1567,7 +1567,7 @@ describe('Test axes', function() {
ax = mockAx();
ax.rangemode = 'tozero';

expect(getAutoRange(gd, ax)).toEqual([0, 6]);
expect(getAutoRange(gd, ax)).toEqual([0, 5]);
});

it('expands empty negative range to something including 0 with rangemode tozero', function() {
Expand All @@ -1579,7 +1579,63 @@ describe('Test axes', function() {
ax = mockAx();
ax.rangemode = 'tozero';

expect(getAutoRange(gd, ax)).toEqual([-6, 0]);
expect(getAutoRange(gd, ax)).toEqual([-5, 0]);
});

it('pads an empty range, but not past center, with rangemode tozero', function() {
gd = mockGd([
{val: 5, pad: 50} // this min pad gets ignored
], [
{val: 5, pad: 20}
]);
ax = mockAx();
ax.rangemode = 'tozero';

expect(getAutoRange(gd, ax)).toBeCloseToArray([0, 6.25], 0.01);

gd = mockGd([
{val: -5, pad: 80}
], [
{val: -5, pad: 0}
]);
ax = mockAx();
ax.rangemode = 'tozero';

expect(getAutoRange(gd, ax)).toBeCloseToArray([-10, 0], 0.01);
});

it('shows the data even if it cannot show the padding', function() {
gd = mockGd([
{val: 0, pad: 44}
], [
{val: 1, pad: 44}
]);
ax = mockAx();

// this one is *just* on the allowed side of padding
// ie data span is just over 10% of the axis
expect(getAutoRange(gd, ax)).toBeCloseToArray([-3.67, 4.67]);

gd = mockGd([
{val: 0, pad: 46}
], [
{val: 1, pad: 46}
]);
ax = mockAx();

// this one the padded data span would be too small, so we delete
// the padding
expect(getAutoRange(gd, ax)).toEqual([0, 1]);

gd = mockGd([
{val: 0, pad: 400}
], [
{val: 1, pad: 0}
]);
ax = mockAx();

// this one the padding is simply impossible to accept!
expect(getAutoRange(gd, ax)).toEqual([0, 1]);
});

it('never returns a negative range when rangemode nonnegative is set with positive and negative points', function() {
Expand Down Expand Up @@ -1614,17 +1670,43 @@ describe('Test axes', function() {
expect(getAutoRange(gd, ax)).toEqual([0, 1]);
});

it('expands empty range to something nonnegative with rangemode nonnegative', function() {
it('never returns a negative range when rangemode nonnegative is set with only nonpositive points', function() {
gd = mockGd([
{val: -5, pad: 0}
{val: -10, pad: 20},
{val: -8, pad: 0},
{val: -9, pad: 10}
], [
{val: -5, pad: 0}
{val: -5, pad: 20},
{val: 0, pad: 0},
{val: -6, pad: 10}
]);
ax = mockAx();
ax.rangemode = 'nonnegative';

expect(getAutoRange(gd, ax)).toEqual([0, 1]);
});

it('expands empty range to something nonnegative with rangemode nonnegative', function() {
[
[-5, [0, 1]],
[0, [0, 1]],
[0.5, [0, 1.5]],
[1, [0, 2]],
[5, [4, 6]]
].forEach(function(testCase) {
var val = testCase[0];
var expected = testCase[1];
gd = mockGd([
{val: val, pad: 0}
], [
{val: val, pad: 0}
]);
ax = mockAx();
ax.rangemode = 'nonnegative';

expect(getAutoRange(gd, ax)).toEqual(expected, val);
});
});
});

describe('findExtremes', function() {
Expand Down
Loading