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 6 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
286 changes: 140 additions & 146 deletions src/components/fx/hover.js

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions src/constants/numerical.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ module.exports = {
*/
ALMOST_EQUAL: 1 - 1e-6,

/*
* If we're asked to clip a non-positive log value, how far off-screen
* do we put it?
*/
LOG_CLIP: 10,

/*
* not a number, but for displaying numbers: the "minus sign" symbol is
* wider than the regular ascii dash "-"
Expand Down
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
13 changes: 8 additions & 5 deletions src/plots/cartesian/axes.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ var Titles = require('../../components/titles');
var Color = require('../../components/color');
var Drawing = require('../../components/drawing');

var axAttrs = require('./layout_attributes');

var constants = require('../../constants/numerical');
var ONEAVGYEAR = constants.ONEAVGYEAR;
var ONEAVGMONTH = constants.ONEAVGMONTH;
Expand Down Expand Up @@ -2411,11 +2413,12 @@ function swapAxisGroup(gd, xIds, yIds) {
for(i = 0; i < xIds.length; i++) xFullAxes.push(axes.getFromId(gd, xIds[i]));
for(i = 0; i < yIds.length; i++) yFullAxes.push(axes.getFromId(gd, yIds[i]));

var allAxKeys = Object.keys(xFullAxes[0]),
noSwapAttrs = [
'anchor', 'domain', 'overlaying', 'position', 'side', 'tickangle'
],
numericTypes = ['linear', 'log'];
var allAxKeys = Object.keys(axAttrs);

var noSwapAttrs = [
'anchor', 'domain', 'overlaying', 'position', 'side', 'tickangle', 'editType'
etpinard marked this conversation as resolved.
Show resolved Hide resolved
];
var numericTypes = ['linear', 'log'];

for(i = 0; i < allAxKeys.length; i++) {
var keyi = allAxKeys[i],
Expand Down
2 changes: 1 addition & 1 deletion src/plots/cartesian/axis_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,
setConvert(containerOut, layoutOut);

var autoRange = coerce('autorange', !containerOut.isValidRange(containerIn.range));
if(autoRange) coerce('rangemode');
if(autoRange && (axType === 'linear' || axType === '-')) coerce('rangemode');

coerce('range');
containerOut.cleanRange();
Expand Down
3 changes: 2 additions & 1 deletion src/plots/cartesian/layout_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ module.exports = {
'If *tozero*`, the range extends to 0,',
'regardless of the input data',
'If *nonnegative*, the range is non-negative,',
'regardless of the input data.'
'regardless of the input data.',
'Applies only to linear axes.'
].join(' ')
},
range: {
Expand Down
10 changes: 3 additions & 7 deletions src/plots/cartesian/set_convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ var ensureNumber = Lib.ensureNumber;
var numConstants = require('../../constants/numerical');
var FP_SAFE = numConstants.FP_SAFE;
var BADNUM = numConstants.BADNUM;
var LOG_CLIP = numConstants.LOG_CLIP;

var constants = require('./constants');
var axisIds = require('./axis_ids');
Expand Down Expand Up @@ -59,20 +60,15 @@ module.exports = function setConvert(ax, fullLayout) {

var axLetter = (ax._id || 'x').charAt(0);

// clipMult: how many axis lengths past the edge do we render?
// for panning, 1-2 would suffice, but for zooming more is nice.
// also, clipping can affect the direction of lines off the edge...
var clipMult = 10;

function toLog(v, clip) {
if(v > 0) return Math.log(v) / Math.LN10;

else if(v <= 0 && clip && ax.range && ax.range.length === 2) {
// clip NaN (ie past negative infinity) to clipMult axis
// clip NaN (ie past negative infinity) to LOG_CLIP axis
// length past the negative edge
var r0 = ax.range[0],
r1 = ax.range[1];
return 0.5 * (r0 + r1 - 3 * clipMult * Math.abs(r0 - r1));
return 0.5 * (r0 + r1 - 2 * LOG_CLIP * Math.abs(r0 - r1));
}

else return BADNUM;
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
48 changes: 37 additions & 11 deletions src/traces/scatter/line_points.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@

'use strict';

var BADNUM = require('../../constants/numerical').BADNUM;
var numConstants = require('../../constants/numerical');
var BADNUM = numConstants.BADNUM;
var LOG_CLIP = numConstants.LOG_CLIP;
var LOG_CLIP_PLUS = LOG_CLIP + 0.5;
var LOG_CLIP_MINUS = LOG_CLIP - 0.5;
var Lib = require('../../lib');
var segmentsIntersect = Lib.segmentsIntersect;
var constrain = Lib.constrain;
Expand All @@ -19,6 +23,10 @@ var constants = require('./constants');
module.exports = function linePoints(d, opts) {
var xa = opts.xaxis;
var ya = opts.yaxis;
var xLog = xa.type === 'log';
var yLog = ya.type === 'log';
var xLen = xa._length;
var yLen = ya._length;
var connectGaps = opts.connectGaps;
var baseTolerance = opts.baseTolerance;
var shape = opts.shape;
Expand Down Expand Up @@ -59,7 +67,25 @@ module.exports = function linePoints(d, opts) {
if(!di) return false;
var x = xa.c2p(di.x);
var y = ya.c2p(di.y);
if(x === BADNUM || y === BADNUM) return false;

// if non-positive log values, set them VERY far off-screen
// so the line looks essentially straight from the previous point.
if(x === BADNUM) {
if(xLog) x = xa.c2p(di.x, true);
if(x === BADNUM) return false;
// If BOTH were bad log values, make the line follow a constant
// exponent rather than a constant slope
if(yLog && y === BADNUM) {
x *= Math.abs(xa._m * yLen * (xa._m > 0 ? LOG_CLIP_PLUS : LOG_CLIP_MINUS) /
(ya._m * xLen * (ya._m > 0 ? LOG_CLIP_PLUS : LOG_CLIP_MINUS)));
}
x *= 1000;
}
if(y === BADNUM) {
if(yLog) y = ya.c2p(di.y, true);
if(y === BADNUM) return false;
y *= 1000;
}
etpinard marked this conversation as resolved.
Show resolved Hide resolved
return [x, y];
}

Expand All @@ -79,16 +105,16 @@ module.exports = function linePoints(d, opts) {
var latestXFrac, latestYFrac;
// if we're off-screen, increase tolerance over baseTolerance
function getTolerance(pt, nextPt) {
var xFrac = pt[0] / xa._length;
var yFrac = pt[1] / ya._length;
var xFrac = pt[0] / xLen;
var yFrac = pt[1] / yLen;
var offScreenFraction = Math.max(0, -xFrac, xFrac - 1, -yFrac, yFrac - 1);
if(offScreenFraction && (latestXFrac !== undefined) &&
crossesViewport(xFrac, yFrac, latestXFrac, latestYFrac)
) {
offScreenFraction = 0;
}
if(offScreenFraction && nextPt &&
crossesViewport(xFrac, yFrac, nextPt[0] / xa._length, nextPt[1] / ya._length)
crossesViewport(xFrac, yFrac, nextPt[0] / xLen, nextPt[1] / yLen)
) {
offScreenFraction = 0;
}
Expand All @@ -114,10 +140,10 @@ module.exports = function linePoints(d, opts) {
// if both are outside there will be 0 or 2 intersections
// (or 1 if it's right at a corner - we'll treat that like 0)
// returns an array of intersection pts
var xEdge0 = -xa._length * maxScreensAway;
var xEdge1 = xa._length * (1 + maxScreensAway);
var yEdge0 = -ya._length * maxScreensAway;
var yEdge1 = ya._length * (1 + maxScreensAway);
var xEdge0 = -xLen * maxScreensAway;
var xEdge1 = xLen * (1 + maxScreensAway);
var yEdge0 = -yLen * maxScreensAway;
var yEdge1 = yLen * (1 + maxScreensAway);
var edges = [
[xEdge0, yEdge0, xEdge1, yEdge0],
[xEdge1, yEdge0, xEdge1, yEdge1],
Expand Down Expand Up @@ -261,8 +287,8 @@ module.exports = function linePoints(d, opts) {
}

function addPt(pt) {
latestXFrac = pt[0] / xa._length;
latestYFrac = pt[1] / ya._length;
latestXFrac = pt[0] / xLen;
latestYFrac = pt[1] / yLen;
// Are we more than maxScreensAway off-screen any direction?
// if so, clip to this box, but in such a way that on-screen
// drawing is unchanged
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.
Binary file added test/image/baselines/log_lines_fills.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading