Skip to content

Commit 7dd2a88

Browse files
authored
Merge pull request #6428 from plotly/automargin-title
Adding automargin support to plot titles
2 parents 967bbaa + ad7c6f0 commit 7dd2a88

18 files changed

+457
-23
lines changed

Diff for: draftlogs/6428_add.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Add `title.automargin` to enable automatic margining for both container and paper referenced titles [[#6428](https://github.com/plotly/plotly.js/pull/6428)], with thanks to [Gamma Technologies](https://www.gtisoft.com/) for sponsoring the related development.

Diff for: src/components/titles/index.js

+14-4
Original file line numberDiff line numberDiff line change
@@ -167,11 +167,21 @@ function draw(gd, titleClass, options) {
167167
var pad = isNumeric(avoid.pad) ? avoid.pad : 2;
168168

169169
var titlebb = Drawing.bBox(titleGroup.node());
170+
171+
// Account for reservedMargins
172+
var reservedMargins = {t: 0, b: 0, l: 0, r: 0};
173+
var margins = gd._fullLayout._reservedMargin;
174+
for(var key in margins) {
175+
for(var side in margins[key]) {
176+
var val = margins[key][side];
177+
reservedMargins[side] = Math.max(reservedMargins[side], val);
178+
}
179+
}
170180
var paperbb = {
171-
left: 0,
172-
top: 0,
173-
right: fullLayout.width,
174-
bottom: fullLayout.height
181+
left: reservedMargins.l,
182+
top: reservedMargins.t,
183+
right: fullLayout.width - reservedMargins.r,
184+
bottom: fullLayout.height - reservedMargins.b
175185
};
176186

177187
var maxshift = avoid.maxShift ||

Diff for: src/plot_api/plot_api.js

+1
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ function _doPlot(gd, data, layout, config) {
277277

278278
subroutines.drawMarginPushers(gd);
279279
Axes.allowAutoMargin(gd);
280+
if(gd._fullLayout.title.text && gd._fullLayout.title.automargin) Plots.allowAutoMargin(gd, 'title.automargin');
280281

281282
// TODO can this be moved elsewhere?
282283
if(fullLayout._has('pie')) {

Diff for: src/plot_api/subroutines.js

+112-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ var Registry = require('../registry');
55
var Plots = require('../plots/plots');
66

77
var Lib = require('../lib');
8+
var svgTextUtils = require('../lib/svg_text_utils');
89
var clearGlCanvases = require('../lib/clear_gl_canvases');
910

1011
var Color = require('../components/color');
@@ -397,24 +398,120 @@ function findCounterAxisLineWidth(ax, side, counterAx, axList) {
397398
}
398399

399400
exports.drawMainTitle = function(gd) {
401+
var title = gd._fullLayout.title;
400402
var fullLayout = gd._fullLayout;
401-
402403
var textAnchor = getMainTitleTextAnchor(fullLayout);
403404
var dy = getMainTitleDy(fullLayout);
405+
var y = getMainTitleY(fullLayout, dy);
406+
var x = getMainTitleX(fullLayout, textAnchor);
404407

405408
Titles.draw(gd, 'gtitle', {
406409
propContainer: fullLayout,
407410
propName: 'title.text',
408411
placeholder: fullLayout._dfltTitle.plot,
409-
attributes: {
410-
x: getMainTitleX(fullLayout, textAnchor),
411-
y: getMainTitleY(fullLayout, dy),
412+
attributes: ({
413+
x: x,
414+
y: y,
412415
'text-anchor': textAnchor,
413416
dy: dy
414-
}
417+
})
415418
});
419+
420+
if(title.text && title.automargin) {
421+
var titleObj = d3.selectAll('.gtitle');
422+
var titleHeight = Drawing.bBox(titleObj.node()).height;
423+
var pushMargin = needsMarginPush(gd, title, titleHeight);
424+
if(pushMargin > 0) {
425+
applyTitleAutoMargin(gd, y, pushMargin, titleHeight);
426+
// Re-position the title once we know where it needs to be
427+
titleObj.attr({
428+
x: x,
429+
y: y,
430+
'text-anchor': textAnchor,
431+
dy: getMainTitleDyAdj(title.yanchor)
432+
}).call(svgTextUtils.positionText, x, y);
433+
}
434+
}
416435
};
417436

437+
438+
function isOutsideContainer(gd, title, position, y, titleHeight) {
439+
var plotHeight = title.yref === 'paper' ? gd._fullLayout._size.h : gd._fullLayout.height;
440+
var yPosTop = Lib.isTopAnchor(title) ? y : y - titleHeight; // Standardize to the top of the title
441+
var yPosRel = position === 'b' ? plotHeight - yPosTop : yPosTop; // Position relative to the top or bottom of plot
442+
if((Lib.isTopAnchor(title) && position === 't') || Lib.isBottomAnchor(title) && position === 'b') {
443+
return false;
444+
} else {
445+
return yPosRel < titleHeight;
446+
}
447+
}
448+
449+
function containerPushVal(position, titleY, titleYanchor, height, titleDepth) {
450+
var push = 0;
451+
if(titleYanchor === 'middle') {
452+
push += titleDepth / 2;
453+
}
454+
if(position === 't') {
455+
if(titleYanchor === 'top') {
456+
push += titleDepth;
457+
}
458+
push += (height - titleY * height);
459+
} else {
460+
if(titleYanchor === 'bottom') {
461+
push += titleDepth;
462+
}
463+
push += titleY * height;
464+
}
465+
return push;
466+
}
467+
468+
function needsMarginPush(gd, title, titleHeight) {
469+
var titleY = title.y;
470+
var titleYanchor = title.yanchor;
471+
var position = titleY > 0.5 ? 't' : 'b';
472+
var curMargin = gd._fullLayout.margin[position];
473+
var pushMargin = 0;
474+
if(title.yref === 'paper') {
475+
pushMargin = (
476+
titleHeight +
477+
title.pad.t +
478+
title.pad.b
479+
);
480+
} else if(title.yref === 'container') {
481+
pushMargin = (
482+
containerPushVal(position, titleY, titleYanchor, gd._fullLayout.height, titleHeight) +
483+
title.pad.t +
484+
title.pad.b
485+
);
486+
}
487+
if(pushMargin > curMargin) {
488+
return pushMargin;
489+
}
490+
return 0;
491+
}
492+
493+
function applyTitleAutoMargin(gd, y, pushMargin, titleHeight) {
494+
var titleID = 'title.automargin';
495+
var title = gd._fullLayout.title;
496+
var position = title.y > 0.5 ? 't' : 'b';
497+
var push = {
498+
x: title.x,
499+
y: title.y,
500+
t: 0,
501+
b: 0
502+
};
503+
var reservedPush = {};
504+
505+
if(title.yref === 'paper' && isOutsideContainer(gd, title, position, y, titleHeight)) {
506+
push[position] = pushMargin;
507+
} else if(title.yref === 'container') {
508+
reservedPush[position] = pushMargin;
509+
gd._fullLayout._reservedMargin[titleID] = reservedPush;
510+
}
511+
Plots.allowAutoMargin(gd, titleID);
512+
Plots.autoMargin(gd, titleID, push);
513+
}
514+
418515
function getMainTitleX(fullLayout, textAnchor) {
419516
var title = fullLayout.title;
420517
var gs = fullLayout._size;
@@ -439,7 +536,6 @@ function getMainTitleY(fullLayout, dy) {
439536
var title = fullLayout.title;
440537
var gs = fullLayout._size;
441538
var vPadShift = 0;
442-
443539
if(dy === '0em' || !dy) {
444540
vPadShift = -title.pad.b;
445541
} else if(dy === alignmentConstants.CAP_SHIFT + 'em') {
@@ -459,6 +555,16 @@ function getMainTitleY(fullLayout, dy) {
459555
}
460556
}
461557

558+
function getMainTitleDyAdj(yanchor) {
559+
if(yanchor === 'top') {
560+
return alignmentConstants.CAP_SHIFT + 0.3 + 'em';
561+
} else if(yanchor === 'bottom') {
562+
return '-0.3em';
563+
} else {
564+
return alignmentConstants.MID_SHIFT + 'em';
565+
}
566+
}
567+
462568
function getMainTitleTextAnchor(fullLayout) {
463569
var title = fullLayout.title;
464570

Diff for: src/plots/layout_attributes.js

+15
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,21 @@ module.exports = {
125125
'Padding is muted if the respective anchor value is *middle*/*center*.'
126126
].join(' ')
127127
}),
128+
automargin: {
129+
valType: 'boolean',
130+
dflt: false,
131+
editType: 'plot',
132+
description: [
133+
'Determines whether the title can automatically push the figure margins.',
134+
'If `yref=\'paper\'` then the margin will expand to ensure that the title doesn\’t',
135+
'overlap with the edges of the container. If `yref=\'container\'` then the margins',
136+
'will ensure that the title doesn\’t overlap with the plot area, tick labels,',
137+
'and axis titles. If `automargin=true` and the margins need to be expanded,',
138+
'then y will be set to a default 1 and yanchor will be set to an appropriate',
139+
'default to ensure that minimal margin space is needed. Note that when `yref=\'paper\'`,',
140+
'only 1 or 0 are allowed y values. Invalid values will be reset to the default 1.'
141+
].join(' ')
142+
},
128143
editType: 'layoutstyle'
129144
},
130145
uniformtext: {

Diff for: src/plots/plots.js

+51-13
Original file line numberDiff line numberDiff line change
@@ -1480,15 +1480,41 @@ plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut, formatObj) {
14801480

14811481
coerce('title.text', layoutOut._dfltTitle.plot);
14821482
coerce('title.xref');
1483-
coerce('title.yref');
1484-
coerce('title.x');
1485-
coerce('title.y');
1486-
coerce('title.xanchor');
1487-
coerce('title.yanchor');
1483+
var titleYref = coerce('title.yref');
14881484
coerce('title.pad.t');
14891485
coerce('title.pad.r');
14901486
coerce('title.pad.b');
14911487
coerce('title.pad.l');
1488+
var titleAutomargin = coerce('title.automargin');
1489+
1490+
coerce('title.x');
1491+
coerce('title.xanchor');
1492+
coerce('title.y');
1493+
coerce('title.yanchor');
1494+
1495+
if(titleAutomargin) {
1496+
// when automargin=true
1497+
// title.y is 1 or 0 if paper ref
1498+
// 'auto' is not supported for either title.y or title.yanchor
1499+
1500+
// TODO: mention this smart default in the title.y and title.yanchor descriptions
1501+
1502+
if(titleYref === 'paper') {
1503+
if(layoutOut.title.y !== 0) layoutOut.title.y = 1;
1504+
1505+
if(layoutOut.title.yanchor === 'auto') {
1506+
layoutOut.title.yanchor = layoutOut.title.y === 0 ? 'top' : 'bottom';
1507+
}
1508+
}
1509+
1510+
if(titleYref === 'container') {
1511+
if(layoutOut.title.y === 'auto') layoutOut.title.y = 1;
1512+
1513+
if(layoutOut.title.yanchor === 'auto') {
1514+
layoutOut.title.yanchor = layoutOut.title.y < 0.5 ? 'bottom' : 'top';
1515+
}
1516+
}
1517+
}
14921518

14931519
var uniformtextMode = coerce('uniformtext.mode');
14941520
if(uniformtextMode) {
@@ -1862,6 +1888,7 @@ function initMargins(fullLayout) {
18621888
}
18631889
if(!fullLayout._pushmargin) fullLayout._pushmargin = {};
18641890
if(!fullLayout._pushmarginIds) fullLayout._pushmarginIds = {};
1891+
if(!fullLayout._reservedMargin) fullLayout._reservedMargin = {};
18651892
}
18661893

18671894
// non-negotiable - this is the smallest height we will allow users to specify via explicit margins
@@ -1979,8 +2006,16 @@ plots.doAutoMargin = function(gd) {
19792006

19802007
var gs = fullLayout._size;
19812008
var margin = fullLayout.margin;
2009+
var reservedMargins = {t: 0, b: 0, l: 0, r: 0};
19822010
var oldMargins = Lib.extendFlat({}, gs);
19832011

2012+
var margins = gd._fullLayout._reservedMargin;
2013+
for(var key in margins) {
2014+
for(var side in margins[key]) {
2015+
var val = margins[key][side];
2016+
reservedMargins[side] = Math.max(reservedMargins[side], val);
2017+
}
2018+
}
19842019
// adjust margins for outside components
19852020
// fullLayout.margin is the requested margin,
19862021
// fullLayout._size has margins and plotsize after adjustment
@@ -2016,14 +2051,16 @@ plots.doAutoMargin = function(gd) {
20162051
var pl = pushleft.size;
20172052
var fb = pushbottom.val;
20182053
var pb = pushbottom.size;
2054+
var availableWidth = width - reservedMargins.r - reservedMargins.l;
2055+
var availableHeight = height - reservedMargins.t - reservedMargins.b;
20192056

20202057
for(var k2 in pushMargin) {
20212058
if(isNumeric(pl) && pushMargin[k2].r) {
20222059
var fr = pushMargin[k2].r.val;
20232060
var pr = pushMargin[k2].r.size;
20242061
if(fr > fl) {
2025-
var newL = (pl * fr + (pr - width) * fl) / (fr - fl);
2026-
var newR = (pr * (1 - fl) + (pl - width) * (1 - fr)) / (fr - fl);
2062+
var newL = (pl * fr + (pr - availableWidth) * fl) / (fr - fl);
2063+
var newR = (pr * (1 - fl) + (pl - availableWidth) * (1 - fr)) / (fr - fl);
20272064
if(newL + newR > ml + mr) {
20282065
ml = newL;
20292066
mr = newR;
@@ -2035,8 +2072,8 @@ plots.doAutoMargin = function(gd) {
20352072
var ft = pushMargin[k2].t.val;
20362073
var pt = pushMargin[k2].t.size;
20372074
if(ft > fb) {
2038-
var newB = (pb * ft + (pt - height) * fb) / (ft - fb);
2039-
var newT = (pt * (1 - fb) + (pb - height) * (1 - ft)) / (ft - fb);
2075+
var newB = (pb * ft + (pt - availableHeight) * fb) / (ft - fb);
2076+
var newT = (pt * (1 - fb) + (pb - availableHeight) * (1 - ft)) / (ft - fb);
20402077
if(newB + newT > mb + mt) {
20412078
mb = newB;
20422079
mt = newT;
@@ -2078,10 +2115,11 @@ plots.doAutoMargin = function(gd) {
20782115
}
20792116
}
20802117

2081-
gs.l = Math.round(ml);
2082-
gs.r = Math.round(mr);
2083-
gs.t = Math.round(mt);
2084-
gs.b = Math.round(mb);
2118+
2119+
gs.l = Math.round(ml) + reservedMargins.l;
2120+
gs.r = Math.round(mr) + reservedMargins.r;
2121+
gs.t = Math.round(mt) + reservedMargins.t;
2122+
gs.b = Math.round(mb) + reservedMargins.b;
20852123
gs.p = Math.round(margin.pad);
20862124
gs.w = Math.round(width) - gs.l - gs.r;
20872125
gs.h = Math.round(height) - gs.t - gs.b;
13.7 KB
Loading
13.9 KB
Loading
13.8 KB
Loading
10.8 KB
Loading

Diff for: test/image/baselines/zzz-automargin-title-paper.png

13.7 KB
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"data": [
3+
{
4+
"showlegend": false,
5+
"type": "scatter",
6+
"x": [
7+
1,
8+
2,
9+
3
10+
],
11+
"y": [
12+
4,
13+
5,
14+
6
15+
]
16+
}],
17+
"layout": {
18+
"height": 300,
19+
"width": 400,
20+
"margin": {"t":0, "b": 0, "l": 0, "r": 0},
21+
"xaxis": {"automargin": true, "title": {"text": "x-axis title"}},
22+
"title": {
23+
"automargin": true,
24+
"text": "Container | pad | y=0",
25+
"pad": {"t": 15, "b": 10},
26+
"y": 0,
27+
"yref": "container"
28+
}
29+
}
30+
}

Diff for: test/image/mocks/zzz-automargin-title-container.json

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"data": [
3+
{
4+
"showlegend": false,
5+
"type": "scatter",
6+
"x": [
7+
1,
8+
2,
9+
3
10+
],
11+
"y": [
12+
4,
13+
5,
14+
6
15+
]
16+
}],
17+
"layout": {
18+
"height": 300,
19+
"width": 400,
20+
"margin": {"t":0, "b": 0, "l": 0, "r": 0},
21+
"xaxis": {"automargin": true, "title": {"text": "x-axis title"}},
22+
"title": {
23+
"automargin": true,
24+
"text": "Container | no-pad | y=1",
25+
"pad": {"t": 0, "b": 0},
26+
"yref": "container"
27+
}
28+
}
29+
}

0 commit comments

Comments
 (0)