Skip to content

Commit

Permalink
Merge pull request #6428 from plotly/automargin-title
Browse files Browse the repository at this point in the history
Adding automargin support to plot titles
  • Loading branch information
hannahker authored Mar 15, 2023
2 parents 967bbaa + ad7c6f0 commit 7dd2a88
Show file tree
Hide file tree
Showing 18 changed files with 457 additions and 23 deletions.
1 change: 1 addition & 0 deletions draftlogs/6428_add.md
Original file line number Diff line number Diff line change
@@ -0,0 +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.
18 changes: 14 additions & 4 deletions src/components/titles/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,21 @@ function draw(gd, titleClass, options) {
var pad = isNumeric(avoid.pad) ? avoid.pad : 2;

var titlebb = Drawing.bBox(titleGroup.node());

// Account for reservedMargins
var reservedMargins = {t: 0, b: 0, l: 0, r: 0};
var margins = gd._fullLayout._reservedMargin;
for(var key in margins) {
for(var side in margins[key]) {
var val = margins[key][side];
reservedMargins[side] = Math.max(reservedMargins[side], val);
}
}
var paperbb = {
left: 0,
top: 0,
right: fullLayout.width,
bottom: fullLayout.height
left: reservedMargins.l,
top: reservedMargins.t,
right: fullLayout.width - reservedMargins.r,
bottom: fullLayout.height - reservedMargins.b
};

var maxshift = avoid.maxShift ||
Expand Down
1 change: 1 addition & 0 deletions src/plot_api/plot_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ function _doPlot(gd, data, layout, config) {

subroutines.drawMarginPushers(gd);
Axes.allowAutoMargin(gd);
if(gd._fullLayout.title.text && gd._fullLayout.title.automargin) Plots.allowAutoMargin(gd, 'title.automargin');

// TODO can this be moved elsewhere?
if(fullLayout._has('pie')) {
Expand Down
118 changes: 112 additions & 6 deletions src/plot_api/subroutines.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ var Registry = require('../registry');
var Plots = require('../plots/plots');

var Lib = require('../lib');
var svgTextUtils = require('../lib/svg_text_utils');
var clearGlCanvases = require('../lib/clear_gl_canvases');

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

exports.drawMainTitle = function(gd) {
var title = gd._fullLayout.title;
var fullLayout = gd._fullLayout;

var textAnchor = getMainTitleTextAnchor(fullLayout);
var dy = getMainTitleDy(fullLayout);
var y = getMainTitleY(fullLayout, dy);
var x = getMainTitleX(fullLayout, textAnchor);

Titles.draw(gd, 'gtitle', {
propContainer: fullLayout,
propName: 'title.text',
placeholder: fullLayout._dfltTitle.plot,
attributes: {
x: getMainTitleX(fullLayout, textAnchor),
y: getMainTitleY(fullLayout, dy),
attributes: ({
x: x,
y: y,
'text-anchor': textAnchor,
dy: dy
}
})
});

if(title.text && title.automargin) {
var titleObj = d3.selectAll('.gtitle');
var titleHeight = Drawing.bBox(titleObj.node()).height;
var pushMargin = needsMarginPush(gd, title, titleHeight);
if(pushMargin > 0) {
applyTitleAutoMargin(gd, y, pushMargin, titleHeight);
// Re-position the title once we know where it needs to be
titleObj.attr({
x: x,
y: y,
'text-anchor': textAnchor,
dy: getMainTitleDyAdj(title.yanchor)
}).call(svgTextUtils.positionText, x, y);
}
}
};


function isOutsideContainer(gd, title, position, y, titleHeight) {
var plotHeight = title.yref === 'paper' ? gd._fullLayout._size.h : gd._fullLayout.height;
var yPosTop = Lib.isTopAnchor(title) ? y : y - titleHeight; // Standardize to the top of the title
var yPosRel = position === 'b' ? plotHeight - yPosTop : yPosTop; // Position relative to the top or bottom of plot
if((Lib.isTopAnchor(title) && position === 't') || Lib.isBottomAnchor(title) && position === 'b') {
return false;
} else {
return yPosRel < titleHeight;
}
}

function containerPushVal(position, titleY, titleYanchor, height, titleDepth) {
var push = 0;
if(titleYanchor === 'middle') {
push += titleDepth / 2;
}
if(position === 't') {
if(titleYanchor === 'top') {
push += titleDepth;
}
push += (height - titleY * height);
} else {
if(titleYanchor === 'bottom') {
push += titleDepth;
}
push += titleY * height;
}
return push;
}

function needsMarginPush(gd, title, titleHeight) {
var titleY = title.y;
var titleYanchor = title.yanchor;
var position = titleY > 0.5 ? 't' : 'b';
var curMargin = gd._fullLayout.margin[position];
var pushMargin = 0;
if(title.yref === 'paper') {
pushMargin = (
titleHeight +
title.pad.t +
title.pad.b
);
} else if(title.yref === 'container') {
pushMargin = (
containerPushVal(position, titleY, titleYanchor, gd._fullLayout.height, titleHeight) +
title.pad.t +
title.pad.b
);
}
if(pushMargin > curMargin) {
return pushMargin;
}
return 0;
}

function applyTitleAutoMargin(gd, y, pushMargin, titleHeight) {
var titleID = 'title.automargin';
var title = gd._fullLayout.title;
var position = title.y > 0.5 ? 't' : 'b';
var push = {
x: title.x,
y: title.y,
t: 0,
b: 0
};
var reservedPush = {};

if(title.yref === 'paper' && isOutsideContainer(gd, title, position, y, titleHeight)) {
push[position] = pushMargin;
} else if(title.yref === 'container') {
reservedPush[position] = pushMargin;
gd._fullLayout._reservedMargin[titleID] = reservedPush;
}
Plots.allowAutoMargin(gd, titleID);
Plots.autoMargin(gd, titleID, push);
}

function getMainTitleX(fullLayout, textAnchor) {
var title = fullLayout.title;
var gs = fullLayout._size;
Expand All @@ -439,7 +536,6 @@ function getMainTitleY(fullLayout, dy) {
var title = fullLayout.title;
var gs = fullLayout._size;
var vPadShift = 0;

if(dy === '0em' || !dy) {
vPadShift = -title.pad.b;
} else if(dy === alignmentConstants.CAP_SHIFT + 'em') {
Expand All @@ -459,6 +555,16 @@ function getMainTitleY(fullLayout, dy) {
}
}

function getMainTitleDyAdj(yanchor) {
if(yanchor === 'top') {
return alignmentConstants.CAP_SHIFT + 0.3 + 'em';
} else if(yanchor === 'bottom') {
return '-0.3em';
} else {
return alignmentConstants.MID_SHIFT + 'em';
}
}

function getMainTitleTextAnchor(fullLayout) {
var title = fullLayout.title;

Expand Down
15 changes: 15 additions & 0 deletions src/plots/layout_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,21 @@ module.exports = {
'Padding is muted if the respective anchor value is *middle*/*center*.'
].join(' ')
}),
automargin: {
valType: 'boolean',
dflt: false,
editType: 'plot',
description: [
'Determines whether the title can automatically push the figure margins.',
'If `yref=\'paper\'` then the margin will expand to ensure that the title doesn\’t',
'overlap with the edges of the container. If `yref=\'container\'` then the margins',
'will ensure that the title doesn\’t overlap with the plot area, tick labels,',
'and axis titles. If `automargin=true` and the margins need to be expanded,',
'then y will be set to a default 1 and yanchor will be set to an appropriate',
'default to ensure that minimal margin space is needed. Note that when `yref=\'paper\'`,',
'only 1 or 0 are allowed y values. Invalid values will be reset to the default 1.'
].join(' ')
},
editType: 'layoutstyle'
},
uniformtext: {
Expand Down
64 changes: 51 additions & 13 deletions src/plots/plots.js
Original file line number Diff line number Diff line change
Expand Up @@ -1480,15 +1480,41 @@ plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut, formatObj) {

coerce('title.text', layoutOut._dfltTitle.plot);
coerce('title.xref');
coerce('title.yref');
coerce('title.x');
coerce('title.y');
coerce('title.xanchor');
coerce('title.yanchor');
var titleYref = coerce('title.yref');
coerce('title.pad.t');
coerce('title.pad.r');
coerce('title.pad.b');
coerce('title.pad.l');
var titleAutomargin = coerce('title.automargin');

coerce('title.x');
coerce('title.xanchor');
coerce('title.y');
coerce('title.yanchor');

if(titleAutomargin) {
// when automargin=true
// title.y is 1 or 0 if paper ref
// 'auto' is not supported for either title.y or title.yanchor

// TODO: mention this smart default in the title.y and title.yanchor descriptions

if(titleYref === 'paper') {
if(layoutOut.title.y !== 0) layoutOut.title.y = 1;

if(layoutOut.title.yanchor === 'auto') {
layoutOut.title.yanchor = layoutOut.title.y === 0 ? 'top' : 'bottom';
}
}

if(titleYref === 'container') {
if(layoutOut.title.y === 'auto') layoutOut.title.y = 1;

if(layoutOut.title.yanchor === 'auto') {
layoutOut.title.yanchor = layoutOut.title.y < 0.5 ? 'bottom' : 'top';
}
}
}

var uniformtextMode = coerce('uniformtext.mode');
if(uniformtextMode) {
Expand Down Expand Up @@ -1862,6 +1888,7 @@ function initMargins(fullLayout) {
}
if(!fullLayout._pushmargin) fullLayout._pushmargin = {};
if(!fullLayout._pushmarginIds) fullLayout._pushmarginIds = {};
if(!fullLayout._reservedMargin) fullLayout._reservedMargin = {};
}

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

var gs = fullLayout._size;
var margin = fullLayout.margin;
var reservedMargins = {t: 0, b: 0, l: 0, r: 0};
var oldMargins = Lib.extendFlat({}, gs);

var margins = gd._fullLayout._reservedMargin;
for(var key in margins) {
for(var side in margins[key]) {
var val = margins[key][side];
reservedMargins[side] = Math.max(reservedMargins[side], val);
}
}
// adjust margins for outside components
// fullLayout.margin is the requested margin,
// fullLayout._size has margins and plotsize after adjustment
Expand Down Expand Up @@ -2016,14 +2051,16 @@ plots.doAutoMargin = function(gd) {
var pl = pushleft.size;
var fb = pushbottom.val;
var pb = pushbottom.size;
var availableWidth = width - reservedMargins.r - reservedMargins.l;
var availableHeight = height - reservedMargins.t - reservedMargins.b;

for(var k2 in pushMargin) {
if(isNumeric(pl) && pushMargin[k2].r) {
var fr = pushMargin[k2].r.val;
var pr = pushMargin[k2].r.size;
if(fr > fl) {
var newL = (pl * fr + (pr - width) * fl) / (fr - fl);
var newR = (pr * (1 - fl) + (pl - width) * (1 - fr)) / (fr - fl);
var newL = (pl * fr + (pr - availableWidth) * fl) / (fr - fl);
var newR = (pr * (1 - fl) + (pl - availableWidth) * (1 - fr)) / (fr - fl);
if(newL + newR > ml + mr) {
ml = newL;
mr = newR;
Expand All @@ -2035,8 +2072,8 @@ plots.doAutoMargin = function(gd) {
var ft = pushMargin[k2].t.val;
var pt = pushMargin[k2].t.size;
if(ft > fb) {
var newB = (pb * ft + (pt - height) * fb) / (ft - fb);
var newT = (pt * (1 - fb) + (pb - height) * (1 - ft)) / (ft - fb);
var newB = (pb * ft + (pt - availableHeight) * fb) / (ft - fb);
var newT = (pt * (1 - fb) + (pb - availableHeight) * (1 - ft)) / (ft - fb);
if(newB + newT > mb + mt) {
mb = newB;
mt = newT;
Expand Down Expand Up @@ -2078,10 +2115,11 @@ plots.doAutoMargin = function(gd) {
}
}

gs.l = Math.round(ml);
gs.r = Math.round(mr);
gs.t = Math.round(mt);
gs.b = Math.round(mb);

gs.l = Math.round(ml) + reservedMargins.l;
gs.r = Math.round(mr) + reservedMargins.r;
gs.t = Math.round(mt) + reservedMargins.t;
gs.b = Math.round(mb) + reservedMargins.b;
gs.p = Math.round(margin.pad);
gs.w = Math.round(width) - gs.l - gs.r;
gs.h = Math.round(height) - gs.t - gs.b;
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions test/image/mocks/zzz-automargin-title-container-bottom.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"data": [
{
"showlegend": false,
"type": "scatter",
"x": [
1,
2,
3
],
"y": [
4,
5,
6
]
}],
"layout": {
"height": 300,
"width": 400,
"margin": {"t":0, "b": 0, "l": 0, "r": 0},
"xaxis": {"automargin": true, "title": {"text": "x-axis title"}},
"title": {
"automargin": true,
"text": "Container | pad | y=0",
"pad": {"t": 15, "b": 10},
"y": 0,
"yref": "container"
}
}
}
29 changes: 29 additions & 0 deletions test/image/mocks/zzz-automargin-title-container.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"data": [
{
"showlegend": false,
"type": "scatter",
"x": [
1,
2,
3
],
"y": [
4,
5,
6
]
}],
"layout": {
"height": 300,
"width": 400,
"margin": {"t":0, "b": 0, "l": 0, "r": 0},
"xaxis": {"automargin": true, "title": {"text": "x-axis title"}},
"title": {
"automargin": true,
"text": "Container | no-pad | y=1",
"pad": {"t": 0, "b": 0},
"yref": "container"
}
}
}
Loading

0 comments on commit 7dd2a88

Please sign in to comment.