Skip to content

Commit eb48a9d

Browse files
committed
fix axis line length logic and tweak label positioning
1 parent 543d6b0 commit eb48a9d

File tree

5 files changed

+209
-27
lines changed

5 files changed

+209
-27
lines changed

src/components/colorbar/draw.js

+1
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ module.exports = function draw(gd, id) {
169169
ticksuffix: opts.ticksuffix,
170170
title: opts.title,
171171
titlefont: opts.titlefont,
172+
showline: true,
172173
anchor: 'free',
173174
position: 1
174175
},

src/plot_api/subroutines.js

+97-10
Original file line numberDiff line numberDiff line change
@@ -174,29 +174,116 @@ exports.lsInner = function(gd) {
174174
(ax.mirrors && ax.mirrors[counterAx._id + side]);
175175
}
176176

177-
var showFreeX = xa.anchor === 'free' && !freeFinished[xa._id];
177+
var xIsFree = xa.anchor === 'free';
178+
var showFreeX = xIsFree && !freeFinished[xa._id];
178179
var showBottom = shouldShowLine(xa, ya, 'bottom');
179180
var showTop = shouldShowLine(xa, ya, 'top');
180181

181-
var showFreeY = ya.anchor === 'free' && !freeFinished[ya._id];
182+
var yIsFree = ya.anchor === 'free';
183+
var showFreeY = yIsFree && !freeFinished[ya._id];
182184
var showLeft = shouldShowLine(ya, xa, 'left');
183185
var showRight = shouldShowLine(ya, xa, 'right');
184186

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

188-
// TODO: this gets more complicated with multiple x and y axes
189-
var xLinesXLeft = -pad - ylw;
190-
var xLinesXRight = xa._length + pad + ylw;
190+
function findMainAxis(ax) {
191+
return ax.overlaying ? Plotly.Axes.getFromId(gd, ax.overlaying) : ax;
192+
}
193+
194+
function findCounterAxes(ax) {
195+
var counterAxes = [];
196+
var anchorAx = Plotly.Axes.getFromId(gd, ax.anchor);
197+
if(anchorAx) {
198+
var counterMain = findMainAxis(anchorAx);
199+
if(counterAxes.indexOf(counterMain) === -1) {
200+
counterAxes.push(counterMain);
201+
}
202+
for(var i = 0; i < axList.length; i++) {
203+
if(axList[i].overlaying === counterMain._id &&
204+
counterAxes.indexOf(axList[i]) === -1
205+
) {
206+
counterAxes.push(axList[i]);
207+
}
208+
}
209+
}
210+
return counterAxes;
211+
}
212+
213+
function findLineWidth(axes, side) {
214+
for(var i = 0; i < axes.length; i++) {
215+
var ax = axes[i];
216+
if(ax.anchor !== 'free' && shouldShowLine(ax, {_id: ax.anchor}, side)) {
217+
return Drawing.crispRound(gd, ax.linewidth);
218+
}
219+
}
220+
}
221+
222+
function findCounterAxisLineWidth(ax, subplotCounterLineWidth,
223+
subplotCounterIsShown, side) {
224+
if(subplotCounterIsShown) return subplotCounterLineWidth;
225+
226+
var i;
227+
228+
// find all counteraxes for this one, then of these, find the
229+
// first one that has a visible line on this side
230+
var mainAxis = findMainAxis(ax);
231+
var counterAxes = findCounterAxes(mainAxis);
232+
233+
var lineWidth = findLineWidth(counterAxes, side);
234+
if(lineWidth) return lineWidth;
235+
236+
for(i = 0; i < axList.length; i++) {
237+
if(axList[i].overlaying === mainAxis._id) {
238+
counterAxes = findCounterAxes(axList[i]);
239+
lineWidth = findLineWidth(counterAxes, side);
240+
if(lineWidth) return lineWidth;
241+
}
242+
}
243+
return 0;
244+
}
245+
246+
/*
247+
* x lines get longer where they meet y lines, to make a crisp corner
248+
* free x lines are not excluded - they don't necessarily *meet* the
249+
* y lines, but it still looks good if the x line reaches to the ends
250+
* of the y lines, especially in the case of a free axis parallel to
251+
* an anchored axis, like this:
252+
*
253+
* |
254+
* |
255+
* +-----
256+
* x1
257+
* ------
258+
* ^ x2
259+
*/
260+
var xLinesXLeft = -pad - findCounterAxisLineWidth(xa, ylw, showLeft, 'left');
261+
var xLinesXRight = xa._length + pad + findCounterAxisLineWidth(xa, ylw, showRight, 'right');
191262
var xLinesYFree = gs.h * (1 - (xa.position || 0)) + ((xlw / 2) % 1);
192263
var xLinesYBottom = ya._length + pad + xlw / 2;
193264
var xLinesYTop = -pad - xlw / 2;
194265

195-
// shorten y axis lines so they don't overlap x axis lines
196-
// except where there's no x line
197-
// TODO: this gets more complicated with multiple x and y axes
198-
var yLinesYBottom = ya._length + (showBottom ? 0 : xlw) + pad;
199-
var yLinesYTop = (showTop ? 0 : -xlw) - pad;
266+
/*
267+
* y lines do not get longer when they meet x axes, because the
268+
* x axis already filled that space and we don't want to double-fill.
269+
* BUT they get longer if they're free axes, for the same reason as
270+
* we do not exclude x axes:
271+
*
272+
* | |
273+
* y2| y1|
274+
* | |
275+
* >| +-----
276+
*
277+
* arguably if the free y axis is over top of the anchored x axis,
278+
* we don't want to do this... but that's a weird edge case, doesn't
279+
* seem worth adding a lot of complexity for.
280+
*/
281+
var yLinesYBottom = ya._length + pad + (yIsFree ?
282+
findCounterAxisLineWidth(ya, xlw, showBottom, 'bottom') :
283+
0);
284+
var yLinesYTop = -pad - (yIsFree ?
285+
findCounterAxisLineWidth(ya, xlw, showTop, 'top') :
286+
0);
200287
var yLinesXFree = gs.w * (ya.position || 0) + ((ylw / 2) % 1);
201288
var yLinesXLeft = -pad - ylw / 2;
202289
var yLinesXRight = xa._length + pad + ylw / 2;

src/plots/cartesian/axes.js

+29-13
Original file line numberDiff line numberDiff line change
@@ -1684,8 +1684,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
16841684
gcls = axid + 'grid',
16851685
zcls = axid + 'zl',
16861686
pad = (ax.linewidth || 1) / 2,
1687-
labelStandoff =
1688-
(ax.ticks === 'outside' ? ax.ticklen : 1) + (ax.linewidth || 0),
1687+
labelStandoff = (ax.ticks === 'outside' ? ax.ticklen : 0),
16891688
labelShift = 0,
16901689
gridWidth = Drawing.crispRound(gd, ax.gridwidth, 1),
16911690
zeroLineWidth = Drawing.crispRound(gd, ax.zerolinewidth, gridWidth),
@@ -1695,10 +1694,14 @@ axes.doTicks = function(gd, axid, skipTitle) {
16951694

16961695
if(ax._counterangle && ax.ticks === 'outside') {
16971696
var caRad = ax._counterangle * Math.PI / 180;
1698-
labelStandoff = ax.ticklen * Math.cos(caRad) + (ax.linewidth || 0);
1697+
labelStandoff = ax.ticklen * Math.cos(caRad) + 1;
16991698
labelShift = ax.ticklen * Math.sin(caRad);
17001699
}
17011700

1701+
if(ax.ticks === 'outside' || ax.showline) {
1702+
labelStandoff += 0.2 * ax.tickfont.size;
1703+
}
1704+
17021705
// positioning arguments for x vs y axes
17031706
if(axLetter === 'x') {
17041707
sides = ['bottom', 'top'];
@@ -1781,7 +1784,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
17811784
labelpos0 = position + (labelStandoff + pad) * flipit;
17821785
labely = function(d) {
17831786
return d.dy + labelpos0 + d.fontSize *
1784-
((axside === 'bottom') ? 1 : -0.5);
1787+
((axside === 'bottom') ? 1 : -0.2);
17851788
};
17861789
labelanchor = function(angle) {
17871790
if(!isNumeric(angle) || angle === 0 || angle === 180) {
@@ -1792,7 +1795,9 @@ axes.doTicks = function(gd, axid, skipTitle) {
17921795
}
17931796
else {
17941797
flipit = (axside === 'right') ? 1 : -1;
1795-
labely = function(d) { return d.dy + d.fontSize / 2 - labelShift * flipit; };
1798+
labely = function(d) {
1799+
return d.dy + d.fontSize * 0.35 - labelShift * flipit;
1800+
};
17961801
labelx = function(d) {
17971802
return d.dx + position + (labelStandoff + pad +
17981803
((Math.abs(ax.tickangle) === 90) ? d.fontSize / 2 : 0)) * flipit;
@@ -2019,16 +2024,23 @@ axes.doTicks = function(gd, axid, skipTitle) {
20192024
avoid.offsetTop = translation.y;
20202025
}
20212026

2027+
var titleStandoff = 10 + fontSize * offsetBase +
2028+
(ax.linewidth ? ax.linewidth - 1 : 0);
2029+
20222030
if(axLetter === 'x') {
20232031
counterAxis = (ax.anchor === 'free') ?
20242032
{_offset: gs.t + (1 - (ax.position || 0)) * gs.h, _length: 0} :
20252033
axisIds.getFromId(gd, ax.anchor);
20262034

20272035
x = ax._offset + ax._length / 2;
2028-
y = counterAxis._offset + ((ax.side === 'top') ?
2029-
-10 - fontSize * (offsetBase + (ax.showticklabels ? 1 : 0)) :
2030-
counterAxis._length + 10 +
2031-
fontSize * (offsetBase + (ax.showticklabels ? 1.5 : 0.5)));
2036+
if(ax.side === 'top') {
2037+
y = -titleStandoff - fontSize * (ax.showticklabels ? 1 : 0);
2038+
}
2039+
else {
2040+
y = counterAxis._length + titleStandoff +
2041+
fontSize * (ax.showticklabels ? 1.5 : 0.5);
2042+
}
2043+
y += counterAxis._offset;
20322044

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

20452057
y = ax._offset + ax._length / 2;
2046-
x = counterAxis._offset + ((ax.side === 'right') ?
2047-
counterAxis._length + 10 +
2048-
fontSize * (offsetBase + (ax.showticklabels ? 1 : 0.5)) :
2049-
-10 - fontSize * (offsetBase + (ax.showticklabels ? 0.5 : 0)));
2058+
if(ax.side === 'right') {
2059+
x = counterAxis._length + titleStandoff +
2060+
fontSize * (ax.showticklabels ? 1 : 0.5);
2061+
}
2062+
else {
2063+
x = -titleStandoff - fontSize * (ax.showticklabels ? 0.5 : 0);
2064+
}
2065+
x += counterAxis._offset;
20502066

20512067
transform = {rotate: '-90', offset: 0};
20522068
if(!avoid.side) avoid.side = 'left';

test/image/baselines/20.png

15.4 KB
Loading

test/image/mocks/20.json

+82-4
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,31 @@
8888
"name": "yaxis6 data",
8989
"yaxis": "y6",
9090
"type": "scatter"
91+
},
92+
{
93+
"x": [1, 2, 3],
94+
"y": [1, 2, 3],
95+
"xaxis": "x2",
96+
"yaxis": "y7",
97+
"name": "x2y7"
98+
},
99+
{
100+
"x": [2, 3, 4],
101+
"y": [2, 3, 1],
102+
"xaxis": "x3",
103+
"yaxis": "y8",
104+
"name": "x3y8"
105+
},
106+
{
107+
"x": [3, 4, 5],
108+
"y": [3, 1, 2],
109+
"xaxis": "x4",
110+
"yaxis": "y9",
111+
"name": "x4y9"
91112
}
92113
],
93114
"layout": {
94-
"title": "multiple y-axes example",
115+
"title": "multiple axes example",
95116
"xaxis": {
96117
"domain": [
97118
0.3,
@@ -104,6 +125,7 @@
104125
"linewidth": 10
105126
},
106127
"yaxis": {
128+
"domain": [0.5, 1],
107129
"title": "yaxis title",
108130
"titlefont": {
109131
"color": "#1f77b4"
@@ -118,8 +140,8 @@
118140
"linewidth": 20
119141
},
120142
"legend": {
121-
"x": 1.1,
122-
"y": 1,
143+
"x": 1,
144+
"y": 0,
123145
"bordercolor": "#000",
124146
"borderwidth": 1
125147
},
@@ -211,8 +233,64 @@
211233
"position": 1,
212234
"overlaying": "y"
213235
},
236+
"xaxis2": {
237+
"title": "xaxis2 title",
238+
"domain": [0.2, 0.8],
239+
"anchor": "y7",
240+
"color": "rgba(100,100,0,0.6)",
241+
"showline": true,
242+
"ticks": "outside",
243+
"linewidth": 3
244+
},
245+
"xaxis3": {
246+
"title": "xaxis3 title",
247+
"overlaying": "x2",
248+
"anchor": "y7",
249+
"color": "rgba(100,0,100,0.6)",
250+
"showline": true,
251+
"ticks": "outside",
252+
"linewidth": 6,
253+
"side": "top"
254+
},
255+
"xaxis4": {
256+
"title": "xaxis4 title",
257+
"overlaying": "x2",
258+
"color": "rgba(0,0,0,0.6)",
259+
"showline": true,
260+
"ticks": "outside",
261+
"position": 0,
262+
"anchor": "free"
263+
},
264+
"yaxis7": {
265+
"title": "yaxis7 title",
266+
"domain": [0.15, 0.3],
267+
"color": "rgba(0,0,0,0.6)",
268+
"anchor": "x2",
269+
"showline": true,
270+
"ticks": "outside",
271+
"linewidth": 4
272+
},
273+
"yaxis8": {
274+
"title": "yaxis8 title",
275+
"overlaying": "y7",
276+
"color": "rgba(0,0,0,0.6)",
277+
"anchor": "x2",
278+
"side": "right",
279+
"showline": true,
280+
"ticks": "outside",
281+
"linewidth": 8
282+
},
283+
"yaxis9": {
284+
"title": "yaxis9 title",
285+
"overlaying": "y7",
286+
"color": "rgba(0,0,0,0.6)",
287+
"anchor": "free",
288+
"position": 0,
289+
"showline": true,
290+
"ticks": "outside"
291+
},
214292
"width": 750,
215-
"height": 400,
293+
"height": 600,
216294
"margin": {"r": 200, "l": 50, "t": 50, "b": 50}
217295
}
218296
}

0 commit comments

Comments
 (0)