Skip to content

Commit 07ef000

Browse files
authored
Merge pull request #4279 from plotly/axis-title-standoff
Add axis title standoff
2 parents 138291e + be3de92 commit 07ef000

12 files changed

+264
-57
lines changed

src/components/titles/index.js

+18-20
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,7 @@ var Color = require('../color');
2020
var svgTextUtils = require('../../lib/svg_text_utils');
2121
var interactConstants = require('../../constants/interactions');
2222

23-
module.exports = {
24-
draw: draw
25-
};
26-
23+
var OPPOSITE_SIDE = require('../../constants/alignment').OPPOSITE_SIDE;
2724
var numStripRE = / [XY][0-9]* /;
2825

2926
/**
@@ -167,29 +164,26 @@ function draw(gd, titleClass, options) {
167164

168165
// move toward avoid.side (= left, right, top, bottom) if needed
169166
// can include pad (pixels, default 2)
170-
var shift = 0;
171-
var backside = {
172-
left: 'right',
173-
right: 'left',
174-
top: 'bottom',
175-
bottom: 'top'
176-
}[avoid.side];
177-
var shiftSign = (['left', 'top'].indexOf(avoid.side) !== -1) ?
178-
-1 : 1;
167+
var backside = OPPOSITE_SIDE[avoid.side];
168+
var shiftSign = (avoid.side === 'left' || avoid.side === 'top') ? -1 : 1;
179169
var pad = isNumeric(avoid.pad) ? avoid.pad : 2;
170+
180171
var titlebb = Drawing.bBox(titleGroup.node());
181172
var paperbb = {
182173
left: 0,
183174
top: 0,
184175
right: fullLayout.width,
185176
bottom: fullLayout.height
186177
};
187-
var maxshift = avoid.maxShift || (
188-
(paperbb[avoid.side] - titlebb[avoid.side]) *
189-
((avoid.side === 'left' || avoid.side === 'top') ? -1 : 1));
178+
179+
var maxshift = avoid.maxShift ||
180+
shiftSign * (paperbb[avoid.side] - titlebb[avoid.side]);
181+
var shift = 0;
182+
190183
// Prevent the title going off the paper
191-
if(maxshift < 0) shift = maxshift;
192-
else {
184+
if(maxshift < 0) {
185+
shift = maxshift;
186+
} else {
193187
// so we don't have to offset each avoided element,
194188
// give the title the opposite offset
195189
var offsetLeft = avoid.offsetLeft || 0;
@@ -211,15 +205,15 @@ function draw(gd, titleClass, options) {
211205
});
212206
shift = Math.min(maxshift, shift);
213207
}
208+
214209
if(shift > 0 || maxshift < 0) {
215210
var shiftTemplate = {
216211
left: [-shift, 0],
217212
right: [shift, 0],
218213
top: [0, -shift],
219214
bottom: [0, shift]
220215
}[avoid.side];
221-
titleGroup.attr('transform',
222-
'translate(' + shiftTemplate + ')');
216+
titleGroup.attr('transform', 'translate(' + shiftTemplate + ')');
223217
}
224218
}
225219
}
@@ -265,3 +259,7 @@ function draw(gd, titleClass, options) {
265259

266260
return group;
267261
}
262+
263+
module.exports = {
264+
draw: draw
265+
};

src/plots/cartesian/axes.js

+83-30
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@ var ONESEC = constants.ONESEC;
3232
var MINUS_SIGN = constants.MINUS_SIGN;
3333
var BADNUM = constants.BADNUM;
3434

35-
var MID_SHIFT = require('../../constants/alignment').MID_SHIFT;
36-
var LINE_SPACING = require('../../constants/alignment').LINE_SPACING;
37-
var OPPOSITE_SIDE = require('../../constants/alignment').OPPOSITE_SIDE;
35+
var alignmentConstants = require('../../constants/alignment');
36+
var MID_SHIFT = alignmentConstants.MID_SHIFT;
37+
var CAP_SHIFT = alignmentConstants.CAP_SHIFT;
38+
var LINE_SPACING = alignmentConstants.LINE_SPACING;
39+
var OPPOSITE_SIDE = alignmentConstants.OPPOSITE_SIDE;
3840

3941
var axes = module.exports = {};
4042

@@ -1831,7 +1833,6 @@ axes.drawOne = function(gd, ax, opts) {
18311833

18321834
if(ax.type === 'multicategory') {
18331835
var pad = {x: 2, y: 10}[axLetter];
1834-
var sgn = {l: -1, t: -1, r: 1, b: 1}[ax.side.charAt(0)];
18351836

18361837
seq.push(function() {
18371838
var bboxKey = {x: 'height', y: 'width'}[axLetter];
@@ -1845,20 +1846,24 @@ axes.drawOne = function(gd, ax, opts) {
18451846
repositionOnUpdate: true,
18461847
secondary: true,
18471848
transFn: transFn,
1848-
labelFns: axes.makeLabelFns(ax, mainLinePosition + standoff * sgn)
1849+
labelFns: axes.makeLabelFns(ax, mainLinePosition + standoff * tickSigns[4])
18491850
});
18501851
});
18511852

18521853
seq.push(function() {
1853-
ax._depth = sgn * (getLabelLevelBbox('tick2')[ax.side] - mainLinePosition);
1854+
ax._depth = tickSigns[4] * (getLabelLevelBbox('tick2')[ax.side] - mainLinePosition);
18541855

18551856
return drawDividers(gd, ax, {
18561857
vals: dividerVals,
18571858
layer: mainAxLayer,
1858-
path: axes.makeTickPath(ax, mainLinePosition, sgn, ax._depth),
1859+
path: axes.makeTickPath(ax, mainLinePosition, tickSigns[4], ax._depth),
18591860
transFn: transFn
18601861
});
18611862
});
1863+
} else if(ax.title.hasOwnProperty('standoff')) {
1864+
seq.push(function() {
1865+
ax._depth = tickSigns[4] * (getLabelLevelBbox()[ax.side] - mainLinePosition);
1866+
});
18621867
}
18631868

18641869
var hasRangeSlider = Registry.getComponentMethod('rangeslider', 'isVisible')(ax);
@@ -1936,10 +1941,7 @@ axes.drawOne = function(gd, ax, opts) {
19361941
ax._anchorAxis.domain[domainIndices[0]];
19371942

19381943
if(ax.title.text !== fullLayout._dfltTitle[axLetter]) {
1939-
var extraLines = (ax.title.text.match(svgTextUtils.BR_TAG_ALL) || []).length;
1940-
push[s] += extraLines ?
1941-
ax.title.font.size * (extraLines + 1) * LINE_SPACING :
1942-
ax.title.font.size;
1944+
push[s] += approxTitleDepth(ax) + (ax.title.standoff || 0);
19431945
}
19441946

19451947
if(ax.mirror && ax.anchor !== 'free') {
@@ -2097,6 +2099,7 @@ function calcLabelLevelBbox(ax, cls) {
20972099
* - [1]: sign for bottom/left ticks (i.e. positive SVG direction)
20982100
* - [2]: sign for ticks corresponding to 'ax.side'
20992101
* - [3]: sign for ticks mirroring 'ax.side'
2102+
* - [4]: sign of arrow starting at axis pointing towards margin
21002103
*/
21012104
axes.getTickSigns = function(ax) {
21022105
var axLetter = ax._id.charAt(0);
@@ -2107,6 +2110,10 @@ axes.getTickSigns = function(ax) {
21072110
if((ax.ticks !== 'inside') === (axLetter === 'x')) {
21082111
out = out.map(function(v) { return -v; });
21092112
}
2113+
// independent of `ticks`; do not flip this one
2114+
if(ax.side) {
2115+
out.push({l: -1, t: -1, r: 1, b: 1}[ax.side.charAt(0)]);
2116+
}
21102117
return out;
21112118
};
21122119

@@ -2699,42 +2706,84 @@ axes.getPxPosition = function(gd, ax) {
26992706
}
27002707
};
27012708

2709+
/**
2710+
* Approximate axis title depth (w/o computing its bounding box)
2711+
*
2712+
* @param {object} ax (full) axis object
2713+
* - {string} title.text
2714+
* - {number} title.font.size
2715+
* - {number} title.standoff
2716+
* @return {number} (in px)
2717+
*/
2718+
function approxTitleDepth(ax) {
2719+
var fontSize = ax.title.font.size;
2720+
var extraLines = (ax.title.text.match(svgTextUtils.BR_TAG_ALL) || []).length;
2721+
if(ax.title.hasOwnProperty('standoff')) {
2722+
return extraLines ?
2723+
fontSize * (CAP_SHIFT + (extraLines * LINE_SPACING)) :
2724+
fontSize * CAP_SHIFT;
2725+
} else {
2726+
return extraLines ?
2727+
fontSize * (extraLines + 1) * LINE_SPACING :
2728+
fontSize;
2729+
}
2730+
}
2731+
2732+
/**
2733+
* Draw axis title, compute default standoff if necessary
2734+
*
2735+
* @param {DOM element} gd
2736+
* @param {object} ax (full) axis object
2737+
* - {string} _id
2738+
* - {string} _name
2739+
* - {string} side
2740+
* - {number} title.font.size
2741+
* - {object} _selections
2742+
*
2743+
* - {number} _depth
2744+
* - {number} title.standoff
2745+
* OR
2746+
* - {number} linewidth
2747+
* - {boolean} showticklabels
2748+
*/
27022749
function drawTitle(gd, ax) {
27032750
var fullLayout = gd._fullLayout;
27042751
var axId = ax._id;
27052752
var axLetter = axId.charAt(0);
27062753
var fontSize = ax.title.font.size;
27072754

27082755
var titleStandoff;
2709-
if(ax.type === 'multicategory') {
2710-
titleStandoff = ax._depth;
2756+
2757+
if(ax.title.hasOwnProperty('standoff')) {
2758+
titleStandoff = ax._depth + ax.title.standoff + approxTitleDepth(ax);
27112759
} else {
2712-
var offsetBase = 1.5;
2713-
titleStandoff = 10 + fontSize * offsetBase + (ax.linewidth ? ax.linewidth - 1 : 0);
2760+
if(ax.type === 'multicategory') {
2761+
titleStandoff = ax._depth;
2762+
} else {
2763+
var offsetBase = 1.5;
2764+
titleStandoff = 10 + fontSize * offsetBase + (ax.linewidth ? ax.linewidth - 1 : 0);
2765+
}
2766+
2767+
if(axLetter === 'x') {
2768+
titleStandoff += ax.side === 'top' ?
2769+
fontSize * (ax.showticklabels ? 1 : 0) :
2770+
fontSize * (ax.showticklabels ? 1.5 : 0.5);
2771+
} else {
2772+
titleStandoff += ax.side === 'right' ?
2773+
fontSize * (ax.showticklabels ? 1 : 0.5) :
2774+
fontSize * (ax.showticklabels ? 0.5 : 0);
2775+
}
27142776
}
27152777

27162778
var pos = axes.getPxPosition(gd, ax);
27172779
var transform, x, y;
27182780

27192781
if(axLetter === 'x') {
27202782
x = ax._offset + ax._length / 2;
2721-
2722-
if(ax.side === 'top') {
2723-
y = -titleStandoff - fontSize * (ax.showticklabels ? 1 : 0);
2724-
} else {
2725-
y = titleStandoff + fontSize * (ax.showticklabels ? 1.5 : 0.5);
2726-
}
2727-
y += pos;
2783+
y = (ax.side === 'top') ? pos - titleStandoff : pos + titleStandoff;
27282784
} else {
27292785
y = ax._offset + ax._length / 2;
2730-
2731-
if(ax.side === 'right') {
2732-
x = titleStandoff + fontSize * (ax.showticklabels ? 1 : 0.5);
2733-
} else {
2734-
x = -titleStandoff - fontSize * (ax.showticklabels ? 0.5 : 0);
2735-
}
2736-
x += pos;
2737-
2786+
x = (ax.side === 'right') ? pos + titleStandoff : pos - titleStandoff;
27382787
transform = {rotate: '-90', offset: 0};
27392788
}
27402789

@@ -2753,6 +2802,10 @@ function drawTitle(gd, ax) {
27532802
avoid.offsetLeft = translation.x;
27542803
avoid.offsetTop = translation.y;
27552804
}
2805+
2806+
if(ax.title.hasOwnProperty('standoff')) {
2807+
avoid.pad = 0;
2808+
}
27562809
}
27572810

27582811
return Titles.draw(gd, axId + 'title', {

src/plots/cartesian/layout_attributes.js

+15
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,21 @@ module.exports = {
6262
'by the now deprecated `titlefont` attribute.'
6363
].join(' ')
6464
}),
65+
standoff: {
66+
valType: 'number',
67+
role: 'info',
68+
min: 0,
69+
editType: 'ticks',
70+
description: [
71+
'Sets the standoff distance (in px) between the axis labels and the title text',
72+
'The default value is a function of the axis tick labels, the title `font.size`',
73+
'and the axis `linewidth`.',
74+
'Note that the axis title position is always constrained within the margins,',
75+
'so the actual standoff distance is always less than the set or default value.',
76+
'By setting `standoff` and turning on `automargin`, plotly.js will push the',
77+
'margins to fit the axis title at given standoff distance.'
78+
].join(' ')
79+
},
6580
editType: 'ticks'
6681
},
6782
type: {

src/plots/cartesian/layout_defaults.js

+2
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,8 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
235235
grid: layoutOut.grid
236236
});
237237

238+
coerce('title.standoff');
239+
238240
axLayoutOut._input = axLayoutIn;
239241
}
240242

src/plots/gl3d/layout/axis_attributes.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,10 @@ module.exports = overrideAll({
7171
color: axesAttrs.color,
7272
categoryorder: axesAttrs.categoryorder,
7373
categoryarray: axesAttrs.categoryarray,
74-
title: axesAttrs.title,
74+
title: {
75+
text: axesAttrs.title.text,
76+
font: axesAttrs.title.font
77+
},
7578
type: extendFlat({}, axesAttrs.type, {
7679
values: ['-', 'linear', 'log', 'date', 'category']
7780
}),

src/plots/polar/layout_attributes.js

+12-5
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,18 @@ var radialAxisAttrs = {
114114
},
115115

116116

117-
title: overrideAll(axesAttrs.title, 'plot', 'from-root'),
118-
// might need a 'titleside' and even 'titledirection' down the road
117+
title: {
118+
// radial title is not gui-editable at the moment,
119+
// so it needs dflt: '', similar to carpet axes.
120+
text: extendFlat({}, axesAttrs.title.text, {editType: 'plot', dflt: ''}),
121+
font: extendFlat({}, axesAttrs.title.font, {editType: 'plot'}),
122+
123+
// TODO
124+
// - might need a 'titleside' and even 'titledirection' down the road
125+
// - what about standoff ??
126+
127+
editType: 'plot'
128+
},
119129

120130
hoverformat: axesAttrs.hoverformat,
121131

@@ -138,9 +148,6 @@ var radialAxisAttrs = {
138148
}
139149
};
140150

141-
// radial title is not gui-editable, so it needs dflt: '', similar to carpet axes.
142-
radialAxisAttrs.title.text.dflt = '';
143-
144151
extendFlat(
145152
radialAxisAttrs,
146153

src/plots/ternary/layout_attributes.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ var overrideAll = require('../../plot_api/edit_types').overrideAll;
1616
var extendFlat = require('../../lib/extend').extendFlat;
1717

1818
var ternaryAxesAttrs = {
19-
title: axesAttrs.title,
19+
title: {
20+
text: axesAttrs.title.text,
21+
font: axesAttrs.title.font
22+
// TODO does standoff here make sense?
23+
},
2024
color: axesAttrs.color,
2125
// ticks
2226
tickmode: axesAttrs.tickmode,

src/traces/carpet/axis_attributes.js

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ module.exports = {
5757
'by the now deprecated `titlefont` attribute.'
5858
].join(' ')
5959
}),
60+
// TODO how is this different than `title.standoff`
6061
offset: {
6162
valType: 'number',
6263
role: 'info',
Loading
34.8 KB
Loading

0 commit comments

Comments
 (0)