Skip to content

Commit 700c088

Browse files
authored
Merge pull request #1854 from plotly/axis-lines
Fix axis line width, length, and positioning for coupled subplots
2 parents f17c0af + 739c998 commit 700c088

File tree

317 files changed

+366
-151
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

317 files changed

+366
-151
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/constants/alignment.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,13 @@ module.exports = {
3030
top: 0
3131
},
3232
// multiple of fontSize to get the vertical offset between lines
33-
LINE_SPACING: 1.3
33+
LINE_SPACING: 1.3,
34+
35+
// multiple of fontSize to shift from the baseline to the midline
36+
// (to use when we don't calculate this shift from Drawing.bBox)
37+
// To be precise this should be half the cap height (capital letter)
38+
// of the font, and according to wikipedia:
39+
// an "average" font might have a cap height of 70% of the em
40+
// https://en.wikipedia.org/wiki/Em_(typography)#History
41+
MID_SHIFT: 0.35
3442
};

src/plot_api/subroutines.js

+185-96
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,16 @@ function overlappingDomain(xDomain, yDomain, domains) {
4242
}
4343

4444
exports.lsInner = function(gd) {
45-
var fullLayout = gd._fullLayout,
46-
gs = fullLayout._size,
47-
axList = Plotly.Axes.list(gd),
48-
i;
45+
var fullLayout = gd._fullLayout;
46+
var gs = fullLayout._size;
47+
var pad = gs.p;
48+
var axList = Plotly.Axes.list(gd);
49+
50+
// _has('cartesian') means SVG specifically, not GL2D - but GL2D
51+
// can still get here because it makes some of the SVG structure
52+
// for shared features like selections.
53+
var hasSVGCartesian = fullLayout._has('cartesian');
54+
var i;
4955

5056
// clear axis line positions, to be set in the subplot loop below
5157
for(i = 0; i < axList.length; i++) axList[i]._linepositions = {};
@@ -80,11 +86,9 @@ exports.lsInner = function(gd) {
8086
return;
8187
}
8288

83-
var xa = Plotly.Axes.getFromId(gd, subplot, 'x'),
84-
ya = Plotly.Axes.getFromId(gd, subplot, 'y'),
85-
xDomain = xa.domain,
86-
yDomain = ya.domain,
87-
plotgroupBgData = [];
89+
var xDomain = plotinfo.xaxis.domain;
90+
var yDomain = plotinfo.yaxis.domain;
91+
var plotgroupBgData = [];
8892

8993
if(overlappingDomain(xDomain, yDomain, lowerDomains)) {
9094
plotgroupBgData = [0];
@@ -125,22 +129,21 @@ exports.lsInner = function(gd) {
125129
fullLayout._plots[subplot].bg = d3.select(this);
126130
});
127131

128-
var freefinished = [];
132+
var freeFinished = {};
129133
subplotSelection.each(function(subplot) {
130134
var plotinfo = fullLayout._plots[subplot];
131-
132-
var xa = Plotly.Axes.getFromId(gd, subplot, 'x'),
133-
ya = Plotly.Axes.getFromId(gd, subplot, 'y');
135+
var xa = plotinfo.xaxis;
136+
var ya = plotinfo.yaxis;
134137

135138
// reset scale in case the margins have changed
136139
xa.setScale();
137140
ya.setScale();
138141

139-
if(plotinfo.bg && fullLayout._has('cartesian')) {
142+
if(plotinfo.bg && hasSVGCartesian) {
140143
plotinfo.bg
141144
.call(Drawing.setRect,
142-
xa._offset - gs.p, ya._offset - gs.p,
143-
xa._length + 2 * gs.p, ya._length + 2 * gs.p)
145+
xa._offset - pad, ya._offset - pad,
146+
xa._length + 2 * pad, ya._length + 2 * pad)
144147
.call(Color.fill, fullLayout.plot_bgcolor)
145148
.style('stroke-width', 0);
146149
}
@@ -165,7 +168,7 @@ exports.lsInner = function(gd) {
165168
'height': ya._length
166169
});
167170

168-
plotinfo.plot.call(Drawing.setTranslate, xa._offset, ya._offset);
171+
Drawing.setTranslate(plotinfo.plot, xa._offset, ya._offset);
169172

170173
var plotClipId;
171174
var layerClipId;
@@ -191,126 +194,153 @@ exports.lsInner = function(gd) {
191194
// to DRY up Drawing.setClipUrl calls downstream
192195
plotinfo.layerClipId = layerClipId;
193196

194-
var xlw = Drawing.crispRound(gd, xa.linewidth, 1),
195-
ylw = Drawing.crispRound(gd, ya.linewidth, 1),
196-
xp = gs.p + ylw,
197-
xpathPrefix = 'M' + (-xp) + ',',
198-
xpathSuffix = 'h' + (xa._length + 2 * xp),
199-
showfreex = xa.anchor === 'free' &&
200-
freefinished.indexOf(xa._id) === -1,
201-
freeposx = gs.h * (1 - (xa.position||0)) + ((xlw / 2) % 1),
202-
showbottom =
203-
(xa.anchor === ya._id && (xa.mirror || xa.side !== 'top')) ||
204-
xa.mirror === 'all' || xa.mirror === 'allticks' ||
205-
(xa.mirrors && xa.mirrors[ya._id + 'bottom']),
206-
bottompos = ya._length + gs.p + xlw / 2,
207-
showtop =
208-
(xa.anchor === ya._id && (xa.mirror || xa.side === 'top')) ||
209-
xa.mirror === 'all' || xa.mirror === 'allticks' ||
210-
(xa.mirrors && xa.mirrors[ya._id + 'top']),
211-
toppos = -gs.p - xlw / 2,
212-
213-
// shorten y axis lines so they don't overlap x axis lines
214-
yp = gs.p,
215-
// except where there's no x line
216-
// TODO: this gets more complicated with multiple x and y axes
217-
ypbottom = showbottom ? 0 : xlw,
218-
yptop = showtop ? 0 : xlw,
219-
ypathSuffix = ',' + (-yp - yptop) +
220-
'v' + (ya._length + 2 * yp + yptop + ypbottom),
221-
showfreey = ya.anchor === 'free' &&
222-
freefinished.indexOf(ya._id) === -1,
223-
freeposy = gs.w * (ya.position||0) + ((ylw / 2) % 1),
224-
showleft =
225-
(ya.anchor === xa._id && (ya.mirror || ya.side !== 'right')) ||
226-
ya.mirror === 'all' || ya.mirror === 'allticks' ||
227-
(ya.mirrors && ya.mirrors[xa._id + 'left']),
228-
leftpos = -gs.p - ylw / 2,
229-
showright =
230-
(ya.anchor === xa._id && (ya.mirror || ya.side === 'right')) ||
231-
ya.mirror === 'all' || ya.mirror === 'allticks' ||
232-
(ya.mirrors && ya.mirrors[xa._id + 'right']),
233-
rightpos = xa._length + gs.p + ylw / 2;
197+
var xIsFree = !xa._anchorAxis;
198+
var showFreeX = xIsFree && !freeFinished[xa._id];
199+
var showBottom = shouldShowLine(xa, ya, 'bottom');
200+
var showTop = shouldShowLine(xa, ya, 'top');
201+
202+
var yIsFree = !ya._anchorAxis;
203+
var showFreeY = yIsFree && !freeFinished[ya._id];
204+
var showLeft = shouldShowLine(ya, xa, 'left');
205+
var showRight = shouldShowLine(ya, xa, 'right');
206+
207+
var xlw = Drawing.crispRound(gd, xa.linewidth, 1);
208+
var ylw = Drawing.crispRound(gd, ya.linewidth, 1);
209+
210+
/*
211+
* x lines get longer where they meet y lines, to make a crisp corner.
212+
* The x lines get the padding (margin.pad) plus the y line width to
213+
* fill up the corner nicely. Free x lines are excluded - they always
214+
* span exactly the data area of the plot
215+
*
216+
* | XXXXX
217+
* | XXXXX
218+
* |
219+
* +------
220+
* x1
221+
* -----
222+
* x2
223+
*/
224+
var leftYLineWidth = findCounterAxisLineWidth(gd, xa, ylw, showLeft, 'left', axList);
225+
var xLinesXLeft = (!xIsFree && leftYLineWidth) ?
226+
(-pad - leftYLineWidth) : 0;
227+
var rightYLineWidth = findCounterAxisLineWidth(gd, xa, ylw, showRight, 'right', axList);
228+
var xLinesXRight = xa._length + ((!xIsFree && rightYLineWidth) ?
229+
(pad + rightYLineWidth) : 0);
230+
var xLinesYFree = gs.h * (1 - (xa.position || 0)) + ((xlw / 2) % 1);
231+
var xLinesYBottom = ya._length + pad + xlw / 2;
232+
var xLinesYTop = -pad - xlw / 2;
233+
234+
/*
235+
* y lines that meet x axes get longer only by margin.pad, because
236+
* the x axes fill in the corner space. Free y axes, like free x axes,
237+
* always span exactly the data area of the plot
238+
*
239+
* | | XXXX
240+
* y2| y1| XXXX
241+
* | | XXXX
242+
* |
243+
* +-----
244+
*/
245+
var connectYBottom = !yIsFree && findCounterAxisLineWidth(
246+
gd, ya, xlw, showBottom, 'bottom', axList);
247+
var yLinesYBottom = ya._length + (connectYBottom ? pad : 0);
248+
var connectYTop = !yIsFree && findCounterAxisLineWidth(
249+
gd, ya, xlw, showTop, 'top', axList);
250+
var yLinesYTop = connectYTop ? -pad : 0;
251+
var yLinesXFree = gs.w * (ya.position || 0) + ((ylw / 2) % 1);
252+
var yLinesXLeft = -pad - ylw / 2;
253+
var yLinesXRight = xa._length + pad + ylw / 2;
254+
255+
function xLinePath(y, showThis) {
256+
if(!showThis) return '';
257+
return 'M' + xLinesXLeft + ',' + y + 'H' + xLinesXRight;
258+
}
259+
260+
function yLinePath(x, showThis) {
261+
if(!showThis) return '';
262+
return 'M' + x + ',' + yLinesYTop + 'V' + yLinesYBottom;
263+
}
234264

235265
// save axis line positions for ticks, draggers, etc to reference
236266
// each subplot gets an entry:
237267
// [left or bottom, right or top, free, main]
238268
// main is the position at which to draw labels and draggers, if any
239269
xa._linepositions[subplot] = [
240-
showbottom ? bottompos : undefined,
241-
showtop ? toppos : undefined,
242-
showfreex ? freeposx : undefined
270+
showBottom ? xLinesYBottom : undefined,
271+
showTop ? xLinesYTop : undefined,
272+
showFreeX ? xLinesYFree : undefined
243273
];
244-
if(xa.anchor === ya._id) {
274+
if(xa._anchorAxis === ya) {
245275
xa._linepositions[subplot][3] = xa.side === 'top' ?
246-
toppos : bottompos;
276+
xLinesYTop : xLinesYBottom;
247277
}
248-
else if(showfreex) {
249-
xa._linepositions[subplot][3] = freeposx;
278+
else if(showFreeX) {
279+
xa._linepositions[subplot][3] = xLinesYFree;
250280
}
251281

252282
ya._linepositions[subplot] = [
253-
showleft ? leftpos : undefined,
254-
showright ? rightpos : undefined,
255-
showfreey ? freeposy : undefined
283+
showLeft ? yLinesXLeft : undefined,
284+
showRight ? yLinesXRight : undefined,
285+
showFreeY ? yLinesXFree : undefined
256286
];
257-
if(ya.anchor === xa._id) {
287+
if(ya._anchorAxis === xa) {
258288
ya._linepositions[subplot][3] = ya.side === 'right' ?
259-
rightpos : leftpos;
289+
yLinesXRight : yLinesXLeft;
260290
}
261-
else if(showfreey) {
262-
ya._linepositions[subplot][3] = freeposy;
291+
else if(showFreeY) {
292+
ya._linepositions[subplot][3] = yLinesXFree;
263293
}
264294

265295
// translate all the extra stuff to have the
266296
// same origin as the plot area or axes
267-
var origin = 'translate(' + xa._offset + ',' + ya._offset + ')',
268-
originx = origin,
269-
originy = origin;
270-
if(showfreex) {
271-
originx = 'translate(' + xa._offset + ',' + gs.t + ')';
272-
toppos += ya._offset - gs.t;
273-
bottompos += ya._offset - gs.t;
297+
var origin = 'translate(' + xa._offset + ',' + ya._offset + ')';
298+
var originX = origin;
299+
var originY = origin;
300+
if(showFreeX) {
301+
originX = 'translate(' + xa._offset + ',' + gs.t + ')';
302+
xLinesYTop += ya._offset - gs.t;
303+
xLinesYBottom += ya._offset - gs.t;
274304
}
275-
if(showfreey) {
276-
originy = 'translate(' + gs.l + ',' + ya._offset + ')';
277-
leftpos += xa._offset - gs.l;
278-
rightpos += xa._offset - gs.l;
305+
if(showFreeY) {
306+
originY = 'translate(' + gs.l + ',' + ya._offset + ')';
307+
yLinesXLeft += xa._offset - gs.l;
308+
yLinesXRight += xa._offset - gs.l;
279309
}
280310

281-
if(fullLayout._has('cartesian')) {
311+
if(hasSVGCartesian) {
282312
plotinfo.xlines
283-
.attr('transform', originx)
313+
.attr('transform', originX)
284314
.attr('d', (
285-
(showbottom ? (xpathPrefix + bottompos + xpathSuffix) : '') +
286-
(showtop ? (xpathPrefix + toppos + xpathSuffix) : '') +
287-
(showfreex ? (xpathPrefix + freeposx + xpathSuffix) : '')) ||
315+
xLinePath(xLinesYBottom, showBottom) +
316+
xLinePath(xLinesYTop, showTop) +
317+
xLinePath(xLinesYFree, showFreeX)) ||
288318
// so it doesn't barf with no lines shown
289319
'M0,0')
290320
.style('stroke-width', xlw + 'px')
291321
.call(Color.stroke, xa.showline ?
292322
xa.linecolor : 'rgba(0,0,0,0)');
293323
plotinfo.ylines
294-
.attr('transform', originy)
324+
.attr('transform', originY)
295325
.attr('d', (
296-
(showleft ? ('M' + leftpos + ypathSuffix) : '') +
297-
(showright ? ('M' + rightpos + ypathSuffix) : '') +
298-
(showfreey ? ('M' + freeposy + ypathSuffix) : '')) ||
326+
yLinePath(yLinesXLeft, showLeft) +
327+
yLinePath(yLinesXRight, showRight) +
328+
yLinePath(yLinesXFree, showFreeY)) ||
299329
'M0,0')
300-
.attr('stroke-width', ylw + 'px')
330+
.style('stroke-width', ylw + 'px')
301331
.call(Color.stroke, ya.showline ?
302332
ya.linecolor : 'rgba(0,0,0,0)');
303333
}
304334

305-
plotinfo.xaxislayer.attr('transform', originx);
306-
plotinfo.yaxislayer.attr('transform', originy);
335+
plotinfo.xaxislayer.attr('transform', originX);
336+
plotinfo.yaxislayer.attr('transform', originY);
307337
plotinfo.gridlayer.attr('transform', origin);
308338
plotinfo.zerolinelayer.attr('transform', origin);
309339
plotinfo.draglayer.attr('transform', origin);
310340

311341
// mark free axes as displayed, so we don't draw them again
312-
if(showfreex) { freefinished.push(xa._id); }
313-
if(showfreey) { freefinished.push(ya._id); }
342+
if(showFreeX) freeFinished[xa._id] = 1;
343+
if(showFreeY) freeFinished[ya._id] = 1;
314344
});
315345

316346
Plotly.Axes.makeClipPaths(gd);
@@ -320,6 +350,65 @@ exports.lsInner = function(gd) {
320350
return gd._promises.length && Promise.all(gd._promises);
321351
};
322352

353+
function shouldShowLine(ax, counterAx, side) {
354+
return (ax._anchorAxis === counterAx && (ax.mirror || ax.side === side)) ||
355+
ax.mirror === 'all' || ax.mirror === 'allticks' ||
356+
(ax.mirrors && ax.mirrors[counterAx._id + side]);
357+
}
358+
359+
function findCounterAxes(gd, ax, axList) {
360+
var counterAxes = [];
361+
var anchorAx = ax._anchorAxis;
362+
if(anchorAx) {
363+
var counterMain = anchorAx._mainAxis;
364+
if(counterAxes.indexOf(counterMain) === -1) {
365+
counterAxes.push(counterMain);
366+
for(var i = 0; i < axList.length; i++) {
367+
if(axList[i].overlaying === counterMain._id &&
368+
counterAxes.indexOf(axList[i]) === -1
369+
) {
370+
counterAxes.push(axList[i]);
371+
}
372+
}
373+
}
374+
}
375+
return counterAxes;
376+
}
377+
378+
function findLineWidth(gd, axes, side) {
379+
for(var i = 0; i < axes.length; i++) {
380+
var ax = axes[i];
381+
var anchorAx = ax._anchorAxis;
382+
if(anchorAx && shouldShowLine(ax, anchorAx, side)) {
383+
return Drawing.crispRound(gd, ax.linewidth);
384+
}
385+
}
386+
}
387+
388+
function findCounterAxisLineWidth(gd, ax, subplotCounterLineWidth,
389+
subplotCounterIsShown, side, axList) {
390+
if(subplotCounterIsShown) return subplotCounterLineWidth;
391+
392+
var i;
393+
394+
// find all counteraxes for this one, then of these, find the
395+
// first one that has a visible line on this side
396+
var mainAxis = ax._mainAxis;
397+
var counterAxes = findCounterAxes(gd, mainAxis, axList);
398+
399+
var lineWidth = findLineWidth(gd, counterAxes, side);
400+
if(lineWidth) return lineWidth;
401+
402+
for(i = 0; i < axList.length; i++) {
403+
if(axList[i].overlaying === mainAxis._id) {
404+
counterAxes = findCounterAxes(gd, axList[i], axList);
405+
lineWidth = findLineWidth(gd, counterAxes, side);
406+
if(lineWidth) return lineWidth;
407+
}
408+
}
409+
return 0;
410+
}
411+
323412
exports.drawMainTitle = function(gd) {
324413
var fullLayout = gd._fullLayout;
325414

0 commit comments

Comments
 (0)