Skip to content

Fix axis line width, length, and positioning for coupled subplots #1854

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 13 commits into from
Jul 14, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
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/components/colorbar/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ module.exports = function draw(gd, id) {
ticksuffix: opts.ticksuffix,
title: opts.title,
titlefont: opts.titlefont,
showline: true,
anchor: 'free',
position: 1
},
Expand Down
107 changes: 97 additions & 10 deletions src/plot_api/subroutines.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,29 +174,116 @@ exports.lsInner = function(gd) {
(ax.mirrors && ax.mirrors[counterAx._id + side]);
}

var showFreeX = xa.anchor === 'free' && !freeFinished[xa._id];
var xIsFree = xa.anchor === 'free';
var showFreeX = xIsFree && !freeFinished[xa._id];
var showBottom = shouldShowLine(xa, ya, 'bottom');
var showTop = shouldShowLine(xa, ya, 'top');

var showFreeY = ya.anchor === 'free' && !freeFinished[ya._id];
var yIsFree = ya.anchor === 'free';
var showFreeY = yIsFree && !freeFinished[ya._id];
var showLeft = shouldShowLine(ya, xa, 'left');
var showRight = shouldShowLine(ya, xa, 'right');

var xlw = Drawing.crispRound(gd, xa.linewidth, 1);
var ylw = Drawing.crispRound(gd, ya.linewidth, 1);

// TODO: this gets more complicated with multiple x and y axes
var xLinesXLeft = -pad - ylw;
var xLinesXRight = xa._length + pad + ylw;
function findMainAxis(ax) {
return ax.overlaying ? Plotly.Axes.getFromId(gd, ax.overlaying) : ax;
Copy link
Contributor

Choose a reason for hiding this comment

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

Non- ⛔ : might be nice to stash references to overlaying and anchor axes in plotinfo during makeSubplotData and 🔪 those Axes.getFromId

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

good idea. Actually makeSubplotData didn't really seem the place for this, it duplicated a lot of effort that should have just been done once per axis. I put this all into plots.linkSubplots. 274526a

}

function findCounterAxes(ax) {
Copy link
Contributor

@etpinard etpinard Jul 13, 2017

Choose a reason for hiding this comment

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

Would it be too much to ask to move those new find* functions outside of the subplotSelection.each scope? I don't think compilers can optimize that still meaning these functions are redefined for every subplot.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ah, I actually meant to un-nest these, thanks for reminding me. b31e60d

var counterAxes = [];
var anchorAx = Plotly.Axes.getFromId(gd, ax.anchor);
if(anchorAx) {
var counterMain = findMainAxis(anchorAx);
if(counterAxes.indexOf(counterMain) === -1) {
counterAxes.push(counterMain);
}
for(var i = 0; i < axList.length; i++) {
if(axList[i].overlaying === counterMain._id &&
counterAxes.indexOf(axList[i]) === -1
) {
counterAxes.push(axList[i]);
}
}
}
return counterAxes;
}

function findLineWidth(axes, side) {
for(var i = 0; i < axes.length; i++) {
var ax = axes[i];
if(ax.anchor !== 'free' && shouldShowLine(ax, {_id: ax.anchor}, side)) {
return Drawing.crispRound(gd, ax.linewidth);
}
}
}

function findCounterAxisLineWidth(ax, subplotCounterLineWidth,
subplotCounterIsShown, side) {
if(subplotCounterIsShown) return subplotCounterLineWidth;

var i;

// find all counteraxes for this one, then of these, find the
// first one that has a visible line on this side
var mainAxis = findMainAxis(ax);
var counterAxes = findCounterAxes(mainAxis);

var lineWidth = findLineWidth(counterAxes, side);
if(lineWidth) return lineWidth;

for(i = 0; i < axList.length; i++) {
if(axList[i].overlaying === mainAxis._id) {
counterAxes = findCounterAxes(axList[i]);
lineWidth = findLineWidth(counterAxes, side);
if(lineWidth) return lineWidth;
}
}
return 0;
}

/*
* x lines get longer where they meet y lines, to make a crisp corner
* free x lines are not excluded - they don't necessarily *meet* the
* y lines, but it still looks good if the x line reaches to the ends
* of the y lines, especially in the case of a free axis parallel to
* an anchored axis, like this:
Copy link
Contributor

Choose a reason for hiding this comment

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

I agree 100%. Thanks for documenting.

*
* |
* |
* +-----
* x1
* ------
* ^ x2
*/
var xLinesXLeft = -pad - findCounterAxisLineWidth(xa, ylw, showLeft, 'left');
var xLinesXRight = xa._length + pad + findCounterAxisLineWidth(xa, ylw, showRight, 'right');
var xLinesYFree = gs.h * (1 - (xa.position || 0)) + ((xlw / 2) % 1);
var xLinesYBottom = ya._length + pad + xlw / 2;
var xLinesYTop = -pad - xlw / 2;

// shorten y axis lines so they don't overlap x axis lines
// except where there's no x line
// TODO: this gets more complicated with multiple x and y axes
var yLinesYBottom = ya._length + (showBottom ? 0 : xlw) + pad;
var yLinesYTop = (showTop ? 0 : -xlw) - pad;
/*
* y lines do not get longer when they meet x axes, because the
* x axis already filled that space and we don't want to double-fill.
* BUT they get longer if they're free axes, for the same reason as
* we do not exclude x axes:
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

Copy link
Contributor

Choose a reason for hiding this comment

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

mock 20 looks amazing.

*
* | |
* y2| y1|
* | |
* >| +-----
*
* arguably if the free y axis is over top of the anchored x axis,
* we don't want to do this... but that's a weird edge case, doesn't
* seem worth adding a lot of complexity for.
*/
var yLinesYBottom = ya._length + pad + (yIsFree ?
findCounterAxisLineWidth(ya, xlw, showBottom, 'bottom') :
0);
var yLinesYTop = -pad - (yIsFree ?
findCounterAxisLineWidth(ya, xlw, showTop, 'top') :
0);
var yLinesXFree = gs.w * (ya.position || 0) + ((ylw / 2) % 1);
var yLinesXLeft = -pad - ylw / 2;
var yLinesXRight = xa._length + pad + ylw / 2;
Expand Down
42 changes: 29 additions & 13 deletions src/plots/cartesian/axes.js
Original file line number Diff line number Diff line change
Expand Up @@ -1684,8 +1684,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
gcls = axid + 'grid',
zcls = axid + 'zl',
pad = (ax.linewidth || 1) / 2,
labelStandoff =
(ax.ticks === 'outside' ? ax.ticklen : 1) + (ax.linewidth || 0),
labelStandoff = (ax.ticks === 'outside' ? ax.ticklen : 0),
labelShift = 0,
gridWidth = Drawing.crispRound(gd, ax.gridwidth, 1),
zeroLineWidth = Drawing.crispRound(gd, ax.zerolinewidth, gridWidth),
Expand All @@ -1695,10 +1694,14 @@ axes.doTicks = function(gd, axid, skipTitle) {

if(ax._counterangle && ax.ticks === 'outside') {
var caRad = ax._counterangle * Math.PI / 180;
labelStandoff = ax.ticklen * Math.cos(caRad) + (ax.linewidth || 0);
labelStandoff = ax.ticklen * Math.cos(caRad) + 1;
labelShift = ax.ticklen * Math.sin(caRad);
}

if(ax.ticks === 'outside' || ax.showline) {
labelStandoff += 0.2 * ax.tickfont.size;
}

// positioning arguments for x vs y axes
if(axLetter === 'x') {
sides = ['bottom', 'top'];
Expand Down Expand Up @@ -1781,7 +1784,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
labelpos0 = position + (labelStandoff + pad) * flipit;
labely = function(d) {
return d.dy + labelpos0 + d.fontSize *
((axside === 'bottom') ? 1 : -0.5);
((axside === 'bottom') ? 1 : -0.2);
};
labelanchor = function(angle) {
if(!isNumeric(angle) || angle === 0 || angle === 180) {
Expand All @@ -1792,7 +1795,9 @@ axes.doTicks = function(gd, axid, skipTitle) {
}
else {
flipit = (axside === 'right') ? 1 : -1;
labely = function(d) { return d.dy + d.fontSize / 2 - labelShift * flipit; };
labely = function(d) {
return d.dy + d.fontSize * 0.35 - labelShift * flipit;
Copy link
Contributor

@etpinard etpinard Jul 13, 2017

Choose a reason for hiding this comment

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

Non- ⛔ (might be better suited for #1434 - the part where we proposed making it configurable) but this 0.35 would look really nice side-by-side LINE_SPACING.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

hah, I found the 0.35 value empirically, but polar had the same one, and it turns out there's a rational basis for exactly this value. 16bae9c

Copy link
Contributor

Choose a reason for hiding this comment

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

indeed! just out of curiosity, would alignment-baseline: central be an alternative, or there's something that would be lost with it? (tweenability comes to mind, smooth with pixels but not nice with discrete levels, though probably tweening isn't applicable here)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

would alignment-baseline: central be an alternative

Quite possibly! I recall some baseline properties that I tried to use in the past not having sufficient cross-browser support, so we would need to test it. Also I'm not sure how this would work with pseudo-html (subscript/superscript, changing font sizes, multiple lines...) so I'm not going to do it now but we could investigate later.

Copy link
Contributor

Choose a reason for hiding this comment

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

If you run into such a thing I'm interested as the upcoming table uses it. IIRC dominant-baseline had some inconsistencies but don't now recall an issue with alignment-baseline. We'll see, I just bought a machine for real-life testing which as MS Edge on it.

};
labelx = function(d) {
return d.dx + position + (labelStandoff + pad +
((Math.abs(ax.tickangle) === 90) ? d.fontSize / 2 : 0)) * flipit;
Expand Down Expand Up @@ -2019,16 +2024,23 @@ axes.doTicks = function(gd, axid, skipTitle) {
avoid.offsetTop = translation.y;
}

var titleStandoff = 10 + fontSize * offsetBase +
(ax.linewidth ? ax.linewidth - 1 : 0);

if(axLetter === 'x') {
counterAxis = (ax.anchor === 'free') ?
{_offset: gs.t + (1 - (ax.position || 0)) * gs.h, _length: 0} :
axisIds.getFromId(gd, ax.anchor);

x = ax._offset + ax._length / 2;
y = counterAxis._offset + ((ax.side === 'top') ?
-10 - fontSize * (offsetBase + (ax.showticklabels ? 1 : 0)) :
counterAxis._length + 10 +
fontSize * (offsetBase + (ax.showticklabels ? 1.5 : 0.5)));
if(ax.side === 'top') {
y = -titleStandoff - fontSize * (ax.showticklabels ? 1 : 0);
}
else {
y = counterAxis._length + titleStandoff +
fontSize * (ax.showticklabels ? 1.5 : 0.5);
}
y += counterAxis._offset;

if(ax.rangeslider && ax.rangeslider.visible && ax._boundingBox) {
y += (fullLayout.height - fullLayout.margin.b - fullLayout.margin.t) *
Expand All @@ -2043,10 +2055,14 @@ axes.doTicks = function(gd, axid, skipTitle) {
axisIds.getFromId(gd, ax.anchor);

y = ax._offset + ax._length / 2;
x = counterAxis._offset + ((ax.side === 'right') ?
counterAxis._length + 10 +
fontSize * (offsetBase + (ax.showticklabels ? 1 : 0.5)) :
-10 - fontSize * (offsetBase + (ax.showticklabels ? 0.5 : 0)));
if(ax.side === 'right') {
x = counterAxis._length + titleStandoff +
fontSize * (ax.showticklabels ? 1 : 0.5);
}
else {
x = -titleStandoff - fontSize * (ax.showticklabels ? 0.5 : 0);
}
x += counterAxis._offset;

transform = {rotate: '-90', offset: 0};
if(!avoid.side) avoid.side = 'left';
Expand Down
Binary file modified test/image/baselines/20.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
86 changes: 82 additions & 4 deletions test/image/mocks/20.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,31 @@
"name": "yaxis6 data",
"yaxis": "y6",
"type": "scatter"
},
{
"x": [1, 2, 3],
"y": [1, 2, 3],
"xaxis": "x2",
"yaxis": "y7",
"name": "x2y7"
},
{
"x": [2, 3, 4],
"y": [2, 3, 1],
"xaxis": "x3",
"yaxis": "y8",
"name": "x3y8"
},
{
"x": [3, 4, 5],
"y": [3, 1, 2],
"xaxis": "x4",
"yaxis": "y9",
"name": "x4y9"
}
],
"layout": {
"title": "multiple y-axes example",
"title": "multiple axes example",
"xaxis": {
"domain": [
0.3,
Expand All @@ -104,6 +125,7 @@
"linewidth": 10
},
"yaxis": {
"domain": [0.5, 1],
"title": "yaxis title",
"titlefont": {
"color": "#1f77b4"
Expand All @@ -118,8 +140,8 @@
"linewidth": 20
},
"legend": {
"x": 1.1,
"y": 1,
"x": 1,
"y": 0,
"bordercolor": "#000",
"borderwidth": 1
},
Expand Down Expand Up @@ -211,8 +233,64 @@
"position": 1,
"overlaying": "y"
},
"xaxis2": {
"title": "xaxis2 title",
"domain": [0.2, 0.8],
"anchor": "y7",
"color": "rgba(100,100,0,0.6)",
"showline": true,
"ticks": "outside",
"linewidth": 3
},
"xaxis3": {
"title": "xaxis3 title",
"overlaying": "x2",
"anchor": "y7",
"color": "rgba(100,0,100,0.6)",
"showline": true,
"ticks": "outside",
"linewidth": 6,
"side": "top"
},
"xaxis4": {
"title": "xaxis4 title",
"overlaying": "x2",
"color": "rgba(0,0,0,0.6)",
"showline": true,
"ticks": "outside",
"position": 0,
"anchor": "free"
},
"yaxis7": {
"title": "yaxis7 title",
"domain": [0.15, 0.3],
"color": "rgba(0,0,0,0.6)",
"anchor": "x2",
"showline": true,
"ticks": "outside",
"linewidth": 4
},
"yaxis8": {
"title": "yaxis8 title",
"overlaying": "y7",
"color": "rgba(0,0,0,0.6)",
"anchor": "x2",
"side": "right",
"showline": true,
"ticks": "outside",
"linewidth": 8
},
"yaxis9": {
"title": "yaxis9 title",
"overlaying": "y7",
"color": "rgba(0,0,0,0.6)",
"anchor": "free",
"position": 0,
"showline": true,
"ticks": "outside"
},
"width": 750,
"height": 400,
"height": 600,
"margin": {"r": 200, "l": 50, "t": 50, "b": 50}
}
}