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

Introduce cartesian axis breaks #4614

Merged
merged 24 commits into from Mar 12, 2020
Merged
Show file tree
Hide file tree
Changes from 14 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
69 changes: 63 additions & 6 deletions src/components/rangeslider/draw.js
Expand Up @@ -123,19 +123,68 @@ 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);
}

// 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 +453,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
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
31 changes: 29 additions & 2 deletions src/plots/cartesian/axes.js
Expand Up @@ -526,7 +526,8 @@ 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);
axes.autoTicks(ax, (Math.abs(rng[1] - rng[0]) - (ax._lBreaks || 0)) / nt);

// check for a forced minimum dtick
if(ax._minDtick > 0 && ax.dtick < ax._minDtick * 2) {
ax.dtick = ax._minDtick;
Expand Down Expand Up @@ -608,6 +609,19 @@ axes.calcTicks = function calcTicks(ax) {
tickVals.pop();
}

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

// TODO what to do with "overlapping" ticks?
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);
});
}

// save the last tick as well as first, so we can
// show the exponent only on the last one
ax._tmax = (tickVals[tickVals.length - 1] || {}).value;
Expand Down Expand Up @@ -670,6 +684,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 @@ -718,6 +739,8 @@ axes.autoTicks = function(ax, roughDTick) {
// being > half of the final unit - so precalculate twice the rough val
var roughX2 = 2 * roughDTick;

// TODO find way to have 'better' first tick on axes with breaks

if(roughX2 > ONEAVGYEAR) {
roughDTick /= ONEAVGYEAR;
base = getBase(10);
Expand Down Expand Up @@ -776,6 +799,9 @@ axes.autoTicks = function(ax, roughDTick) {
ax.tick0 = 0;
base = getBase(10);
ax.dtick = roundDTick(roughDTick, base, roundBase10);

// TODO having tick0 = 0 being insider a breaks does not seem
// to matter ...
}

// prevent infinite loops
Expand Down Expand Up @@ -966,7 +992,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 +2854,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
67 changes: 67 additions & 0 deletions src/plots/cartesian/axis_defaults.js
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 All @@ -19,6 +21,8 @@ var handleCategoryOrderDefaults = require('./category_order_defaults');
var handleLineGridDefaults = require('./line_grid_defaults');
var setConvert = require('./set_convert');

var ONEDAY = require('../../constants/numerical').ONEDAY;

/**
* options: object containing:
*
Expand Down Expand Up @@ -117,5 +121,68 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,
}
}

// TODO
// - does this make sense for 'log', 'category' and 'multicategory' axis types ??

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

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 isDateAxis = containerOut.type === 'date';

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!


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

if(values && values.length) {
coerce('dvalue', isDateAxis ? ONEDAY : 1);
} else {
itemOut.enabled = false;
return;
}
}

coerce('operation');
}
}
33 changes: 25 additions & 8 deletions src/plots/cartesian/dragbox.js
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,17 @@ 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) {
axi.range = [
axi.l2r(axi._rl[0] - (axi.p2l(pix) - axi.p2l(0))),
axi.l2r(axi._rl[1] - (axi.p2l(axi._length + pix) - axi.p2l(axi._length)))
];
} else {
axi.range = [
axi.l2r(axi._rl[0] - pix / axi._m),
axi.l2r(axi._rl[1] - pix / axi._m)
];
}
}
}
}
Expand Down