Skip to content

Introduce cartesian axis breaks #4614

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

Merged
merged 24 commits into from
Mar 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4f6ea7e
introduce axis breaks attributes
etpinard Mar 3, 2020
e9cfe04
add axis breaks default logic
etpinard Mar 3, 2020
baf753a
add ax.maskBreaks method
etpinard Mar 3, 2020
259eafa
implement axis breaks setConvert logic
etpinard Mar 3, 2020
eca3da7
adapt autorange routine for axis breaks
etpinard Mar 3, 2020
6bec94d
adapt calcTicks for axis breaks
etpinard Mar 3, 2020
fe80cad
adapt dragbox logic for axis breaks
etpinard Mar 3, 2020
d425373
do not show zeroline when it falls inside an axis break
etpinard Mar 3, 2020
5f2fbe0
add axis breaks mocks
etpinard Mar 3, 2020
b5aaf92
add TODO for better "first tick" algo on date axes
etpinard Mar 3, 2020
187c93a
fix typo in comment
etpinard Mar 3, 2020
ebca01b
fix axis breaks + rangeslider behavior
etpinard Mar 4, 2020
3957d95
during l2p(v) when v falls into breaks, pick offset closest to it
etpinard Mar 4, 2020
76a265e
fix typo in break `bounds` description
etpinard Mar 5, 2020
e00af90
Handle breaks on date axes only for now
etpinard Mar 5, 2020
53196e5
simplify logic - breaks are on date axes only
archmoj Mar 9, 2020
493bb4e
Handle axis breaks on reversed ranges
etpinard Mar 5, 2020
7080f90
replace 'spread' -> 'size' in attr descriptions
etpinard Mar 10, 2020
dcceb76
fix %H maskBreaks for values greater but on the same hour
etpinard Mar 10, 2020
28c328d
increase dtick on axes with breaks ...
etpinard Mar 11, 2020
432f0d0
Merge branch 'master' into axis-breaks
etpinard Mar 12, 2020
40d57fa
fix scale when panning on breaks
archmoj Mar 12, 2020
1b55b42
fix 'increase dtick' lgoic for cases with dtick value starting 'M'
etpinard Mar 12, 2020
49b4053
set scattergl and splom traces to visible:false on axis with breaks
etpinard Mar 12, 2020
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
71 changes: 65 additions & 6 deletions src/components/rangeslider/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,19 +123,70 @@ module.exports = function(gd) {

// update data <--> pixel coordinate conversion methods

var range0 = axisOpts.r2l(opts.range[0]);
var range1 = axisOpts.r2l(opts.range[1]);
var dist = range1 - range0;
opts._rl = Lib.simpleMap(opts.range, axisOpts.r2l);
var rl0 = opts._rl[0];
var rl1 = opts._rl[1];
var drl = rl1 - rl0;

opts.p2d = function(v) {
return (v / opts._width) * dist + range0;
return (v / opts._width) * drl + rl0;
};

opts.d2p = function(v) {
return (v - range0) / dist * opts._width;
return (v - rl0) / drl * opts._width;
};

opts._rl = [range0, range1];
if(axisOpts.breaks) {
var rsBreaks = axisOpts.locateBreaks(rl0, rl1);

if(rsBreaks.length) {
var j, brk;

var lBreaks = 0;
for(j = 0; j < rsBreaks.length; j++) {
brk = rsBreaks[j];
lBreaks += (brk.max - brk.min);
}

// TODO fix for reversed-range axes !!!

// compute slope and piecewise offsets
var m2 = opts._width / (rl1 - rl0 - lBreaks);
var _B = [-m2 * rl0];
for(j = 0; j < rsBreaks.length; j++) {
brk = rsBreaks[j];
_B.push(_B[_B.length - 1] - m2 * (brk.max - brk.min));
}

opts.d2p = function(v) {
var b = _B[0];
for(var j = 0; j < rsBreaks.length; j++) {
var brk = rsBreaks[j];
if(v >= brk.max) b = _B[j + 1];
else if(v < brk.min) break;
}
return b + m2 * v;
};

// fill pixel (i.e. 'p') min/max here,
// to not have to loop through the _breaks twice during `p2d`
for(j = 0; j < rsBreaks.length; j++) {
brk = rsBreaks[j];
brk.pmin = opts.d2p(brk.min);
brk.pmax = opts.d2p(brk.max);
}

opts.p2d = function(v) {
var b = _B[0];
for(var j = 0; j < rsBreaks.length; j++) {
var brk = rsBreaks[j];
if(v >= brk.pmax) b = _B[j + 1];
else if(v < brk.pmin) break;
}
return (v - b) / m2;
};
}
}

if(oppAxisRangeOpts.rangemode !== 'match') {
var range0OppAxis = oppAxisOpts.r2l(oppAxisRangeOpts.range[0]);
Expand Down Expand Up @@ -404,13 +455,21 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) {
_context: gd._context
};

if(axisOpts.breaks) {
mockFigure.layout.xaxis.breaks = axisOpts.breaks;
}

mockFigure.layout[oppAxisName] = {
type: oppAxisOpts.type,
domain: [0, 1],
range: oppAxisRangeOpts.rangemode !== 'match' ? oppAxisRangeOpts.range.slice() : oppAxisOpts.range.slice(),
calendar: oppAxisOpts.calendar
};

if(oppAxisOpts.breaks) {
mockFigure.layout[oppAxisName].breaks = oppAxisOpts.breaks;
}

Plots.supplyDefaults(mockFigure);

var xa = mockFigure._fullLayout.xaxis;
Expand Down
16 changes: 14 additions & 2 deletions src/plots/cartesian/autorange.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,26 @@ function getAutoRange(gd, ax) {
// don't allow padding to reduce the data to < 10% of the length
var minSpan = axLen / 10;

// find axis breaks in [v0,v1] and compute its length in value space
var calcBreaksLength = function(v0, v1) {
var lBreaks = 0;
if(ax.breaks) {
var breaksOut = ax.locateBreaks(v0, v1);
for(var i = 0; i < breaksOut.length; i++) {
lBreaks += (breaksOut[i].max - breaksOut[i].min);
}
}
return lBreaks;
};

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

for(i = 0; i < minArray.length; i++) {
minpt = minArray[i];
for(j = 0; j < maxArray.length; j++) {
maxpt = maxArray[j];
dv = maxpt.val - minpt.val;
dv = maxpt.val - minpt.val - calcBreaksLength(minpt.val, maxpt.val);
if(dv > 0) {
dp = axLen - getPad(minpt) - getPad(maxpt);
if(dp > minSpan) {
Expand Down Expand Up @@ -167,7 +179,7 @@ function getAutoRange(gd, ax) {
}

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

newRange = [
Expand Down
80 changes: 62 additions & 18 deletions src/plots/cartesian/axes.js
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,10 @@ axes.prepTicks = function(ax) {
// have explicit tickvals without tick text
if(ax.tickmode === 'array') nt *= 100;

axes.autoTicks(ax, Math.abs(rng[1] - rng[0]) / nt);

ax._roughDTick = (Math.abs(rng[1] - rng[0]) - (ax._lBreaks || 0)) / nt;
axes.autoTicks(ax, ax._roughDTick);

// check for a forced minimum dtick
if(ax._minDtick > 0 && ax.dtick < ax._minDtick * 2) {
ax.dtick = ax._minDtick;
Expand Down Expand Up @@ -573,32 +576,65 @@ axes.calcTicks = function calcTicks(ax) {
if((ax._tmin < startTick) !== axrev) return [];

// return the full set of tick vals
var tickVals = [];
if(ax.type === 'category' || ax.type === 'multicategory') {
endTick = (axrev) ? Math.max(-0.5, endTick) :
Math.min(ax._categories.length - 0.5, endTick);
}

var isDLog = (ax.type === 'log') && !(isNumeric(ax.dtick) || ax.dtick.charAt(0) === 'L');

var xPrevious = null;
var maxTicks = Math.max(1000, ax._length || 0);
for(var x = ax._tmin;
(axrev) ? (x >= endTick) : (x <= endTick);
x = axes.tickIncrement(x, ax.dtick, axrev, ax.calendar)) {
// prevent infinite loops - no more than one tick per pixel,
// and make sure each value is different from the previous
if(tickVals.length > maxTicks || x === xPrevious) break;
xPrevious = x;
var tickVals;
function generateTicks() {
var xPrevious = null;
var maxTicks = Math.max(1000, ax._length || 0);
tickVals = [];
for(var x = ax._tmin;
(axrev) ? (x >= endTick) : (x <= endTick);
x = axes.tickIncrement(x, ax.dtick, axrev, ax.calendar)) {
// prevent infinite loops - no more than one tick per pixel,
// and make sure each value is different from the previous
if(tickVals.length > maxTicks || x === xPrevious) break;
xPrevious = x;

var minor = false;
if(isDLog && (x !== (x | 0))) {
minor = true;
}

tickVals.push({
minor: minor,
value: x
});
}
}

generateTicks();

if(ax.breaks) {
var nTicksBefore = tickVals.length;

// remove ticks falling inside breaks
tickVals = tickVals.filter(function(d) {
return ax.maskBreaks(d.value) !== BADNUM;
});

var minor = false;
if(isDLog && (x !== (x | 0))) {
minor = true;
// if 'numerous' ticks get placed into breaks,
// increase dtick to generate more ticks,
// so that some hopefully fall between breaks
if(ax.tickmode === 'auto' && tickVals.length < nTicksBefore / 6) {
axes.autoTicks(ax, ax._roughDTick / 3);
autoTickRound(ax);
ax._tmin = axes.tickFirst(ax);
generateTicks();
tickVals = tickVals.filter(function(d) {
return ax.maskBreaks(d.value) !== BADNUM;
});
}

tickVals.push({
minor: minor,
value: x
// remove "overlapping" ticks (e.g. on either side of a break)
var tf2 = ax.tickfont ? 1.5 * ax.tickfont.size : 0;
tickVals = tickVals.filter(function(d, i, self) {
return !(i && Math.abs(ax.c2p(d.value) - ax.c2p(self[i - 1].value)) < tf2);
});
}

Expand Down Expand Up @@ -670,6 +706,13 @@ function arrayTicks(ax) {

if(j < vals.length) ticksOut.splice(j, vals.length - j);

if(ax.breaks) {
// remove ticks falling inside breaks
ticksOut = ticksOut.filter(function(d) {
return ax.maskBreaks(d.x) !== BADNUM;
});
}

return ticksOut;
}

Expand Down Expand Up @@ -966,7 +1009,7 @@ axes.tickText = function(ax, x, hover, noSuffixPrefix) {

if(arrayMode && Array.isArray(ax.ticktext)) {
var rng = Lib.simpleMap(ax.range, ax.r2l);
var minDiff = Math.abs(rng[1] - rng[0]) / 10000;
var minDiff = (Math.abs(rng[1] - rng[0]) - (ax._lBreaks || 0)) / 10000;

for(i = 0; i < ax.ticktext.length; i++) {
if(Math.abs(x - tickVal2l(ax.tickvals[i])) < minDiff) break;
Expand Down Expand Up @@ -2828,6 +2871,7 @@ axes.shouldShowZeroLine = function(gd, ax, counterAxis) {
(rng[0] * rng[1] <= 0) &&
ax.zeroline &&
(ax.type === 'linear' || ax.type === '-') &&
!(ax.breaks && ax.maskBreaks(0) === BADNUM) &&
(
clipEnds(ax, 0) ||
!anyCounterAxLineAtZero(gd, ax, counterAxis, rng) ||
Expand Down
72 changes: 72 additions & 0 deletions src/plots/cartesian/axis_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
var Registry = require('../../registry');
var Lib = require('../../lib');

var handleArrayContainerDefaults = require('../array_container_defaults');

var layoutAttributes = require('./layout_attributes');
var handleTickValueDefaults = require('./tick_value_defaults');
var handleTickMarkDefaults = require('./tick_mark_defaults');
Expand Down Expand Up @@ -117,5 +119,75 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,
}
}

if(containerOut.type === 'date') {
var breaks = containerIn.breaks;
if(Array.isArray(breaks) && breaks.length) {
handleArrayContainerDefaults(containerIn, containerOut, {
name: 'breaks',
inclusionAttr: 'enabled',
handleItemDefaults: breaksDefaults
});
setConvert(containerOut, layoutOut);

if(layoutOut._has('scattergl') || layoutOut._has('splom')) {
for(var i = 0; i < options.data.length; i++) {
var trace = options.data[i];
if(trace.type === 'scattergl' || trace.type === 'splom') {
trace.visible = false;
Lib.warn(trace.type +
' traces do not work on axes with breaks.' +
' Setting trace ' + trace.index + ' to `visible: false`.');
}
}
}
}
}

return containerOut;
};

function breaksDefaults(itemIn, itemOut, containerOut) {
function coerce(attr, dflt) {
return Lib.coerce(itemIn, itemOut, layoutAttributes.breaks, attr, dflt);
}

var enabled = coerce('enabled');

if(enabled) {
var bnds = coerce('bounds');

if(bnds && bnds.length >= 2) {
if(bnds.length > 2) {
itemOut.bounds = itemOut.bounds.slice(0, 2);
}

if(containerOut.autorange === false) {
var rng = containerOut.range;

// if bounds are bigger than the (set) range, disable break
if(rng[0] < rng[1]) {
if(bnds[0] < rng[0] && bnds[1] > rng[1]) {
itemOut.enabled = false;
return;
}
} else if(bnds[0] > rng[0] && bnds[1] < rng[1]) {
itemOut.enabled = false;
return;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

What about handling autorange: 'reversed' case?
In the following demos I tried to flip the axes which it didn't work.
https://codepen.io/MojtabaSamimi/pen/yLNzLra?editors=0010
https://codepen.io/MojtabaSamimi/pen/bGdoGyG?editors=0010
https://codepen.io/MojtabaSamimi/pen/QWbqWRj?editors=0010

Copy link
Contributor Author

@etpinard etpinard Mar 4, 2020

Choose a reason for hiding this comment

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

Ah right, I forgot to test those. They won't be handled here as this logic has to do with set (i.e. autorange: false) ranges, but yeah I'll get this fixed. Thanks!


coerce('pattern');
} else {
var values = coerce('values');

if(values && values.length) {
coerce('dvalue');
} else {
itemOut.enabled = false;
return;
}
}

coerce('operation');
}
}
39 changes: 31 additions & 8 deletions src/plots/cartesian/dragbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -986,10 +986,20 @@ function zoomAxRanges(axList, r0Fraction, r1Fraction, updates, linkedAxes) {
var axi = axList[i];
if(axi.fixedrange) continue;

var axRangeLinear0 = axi._rl[0];
var axRangeLinearSpan = axi._rl[1] - axRangeLinear0;
updates[axi._name + '.range[0]'] = axi.l2r(axRangeLinear0 + axRangeLinearSpan * r0Fraction);
updates[axi._name + '.range[1]'] = axi.l2r(axRangeLinear0 + axRangeLinearSpan * r1Fraction);
if(axi.breaks) {
if(axi._id.charAt(0) === 'y') {
updates[axi._name + '.range[0]'] = axi.l2r(axi.p2l((1 - r0Fraction) * axi._length));
updates[axi._name + '.range[1]'] = axi.l2r(axi.p2l((1 - r1Fraction) * axi._length));
} else {
updates[axi._name + '.range[0]'] = axi.l2r(axi.p2l(r0Fraction * axi._length));
updates[axi._name + '.range[1]'] = axi.l2r(axi.p2l(r1Fraction * axi._length));
}
} else {
var axRangeLinear0 = axi._rl[0];
var axRangeLinearSpan = axi._rl[1] - axRangeLinear0;
updates[axi._name + '.range[0]'] = axi.l2r(axRangeLinear0 + axRangeLinearSpan * r0Fraction);
updates[axi._name + '.range[1]'] = axi.l2r(axRangeLinear0 + axRangeLinearSpan * r1Fraction);
}
}

// zoom linked axes about their centers
Expand All @@ -1003,10 +1013,23 @@ function dragAxList(axList, pix) {
for(var i = 0; i < axList.length; i++) {
var axi = axList[i];
if(!axi.fixedrange) {
axi.range = [
axi.l2r(axi._rl[0] - pix / axi._m),
axi.l2r(axi._rl[1] - pix / axi._m)
];
if(axi.breaks) {
var p0 = 0;
var p1 = axi._length;
var d0 = axi.p2l(p0 + pix) - axi.p2l(p0);
var d1 = axi.p2l(p1 + pix) - axi.p2l(p1);
var delta = (d0 + d1) / 2;

axi.range = [
axi.l2r(axi._rl[0] - delta),
axi.l2r(axi._rl[1] - delta)
];
} else {
axi.range = [
axi.l2r(axi._rl[0] - pix / axi._m),
axi.l2r(axi._rl[1] - pix / axi._m)
];
}
}
}
}
Expand Down
Loading