Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add xref and yref to legends #6589

Merged
merged 14 commits into from
May 12, 2023
1 change: 1 addition & 0 deletions draftlogs/6589_add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add `legend.xref` and `legend.yref` to enable container-referenced positioning for plot legends [[#6589](https://github.com/plotly/plotly.js/pull/6589)], with thanks to [Gamma Technologies](https://www.gtisoft.com/) for sponsoring the related development.
39 changes: 33 additions & 6 deletions src/components/legend/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,23 @@ module.exports = {
max: 3,
hannahker marked this conversation as resolved.
Show resolved Hide resolved
editType: 'legend',
description: [
'Sets the x position (in normalized coordinates) of the legend.',
'Defaults to *1.02* for vertical legends and',
'defaults to *0* for horizontal legends.'
'Sets the x position with respect to `xref` (in normalized coordinates) of the legend.',
'When `xref` is *paper*, defaults to *1.02* for vertical legends and',
'defaults to *0* for horizontal legends.',
'When `xref` is *container*, defaults to *1* for vertical legends and',
'defaults to *0* for horizontal legends.',
'Must be between *0* and *1* if `xref` is *container*.'
].join(' ')
},
xref: {
valType: 'enumerated',
dflt: 'paper',
values: ['container', 'paper'],
editType: 'layoutstyle',
description: [
'Sets the container `x` refers to.',
'*container* spans the entire `width` of the plot.',
'*paper* refers to the width of the plotting area only.'
].join(' ')
},
xanchor: {
Expand All @@ -188,10 +202,23 @@ module.exports = {
max: 3,
editType: 'legend',
description: [
'Sets the y position (in normalized coordinates) of the legend.',
'Defaults to *1* for vertical legends,',
'Sets the y position with respect to `yref` (in normalized coordinates) of the legend.',
'When `yref` is *paper*, defaults to *1* for vertical legends,',
'defaults to *-0.1* for horizontal legends on graphs w/o range sliders and',
'defaults to *1.1* for horizontal legends on graph with one or multiple range sliders.'
'defaults to *1.1* for horizontal legends on graph with one or multiple range sliders.',
'When `yref` is *container*, defaults to *1*.',
'Must be between *0* and *1* if `yref` is *container*.'
].join(' ')
},
yref: {
valType: 'enumerated',
dflt: 'paper',
values: ['container', 'paper'],
editType: 'layoutstyle',
description: [
'Sets the container `y` refers to.',
'*container* spans the entire `height` of the plot.',
'*paper* refers to the height of the plotting area only.'
].join(' ')
},
yanchor: {
Expand Down
36 changes: 30 additions & 6 deletions src/components/legend/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,26 +101,50 @@ function groupDefaults(legendId, layoutIn, layoutOut, fullData) {
coerce('borderwidth');

var orientation = coerce('orientation');

var yref = coerce('yref');
var xref = coerce('xref');

var isHorizontal = orientation === 'h';
var isPaperY = yref === 'paper';
var isPaperX = xref === 'paper';
var defaultX, defaultY, defaultYAnchor;
var defaultXAnchor = 'left';

// TODO: Adjust default xanchor if needed for container ref?
// TODO: Constrain x or y if container ref to be within 0-1
hannahker marked this conversation as resolved.
Show resolved Hide resolved
if(isHorizontal) {
defaultX = 0;

if(Registry.getComponentMethod('rangeslider', 'isVisible')(layoutIn.xaxis)) {
defaultY = 1.1;
defaultYAnchor = 'bottom';
if(isPaperY) {
defaultY = 1.1;
defaultYAnchor = 'bottom';
} else {
defaultY = 1;
defaultYAnchor = 'top';
}
} else {
// maybe use y=1.1 / yanchor=bottom as above
// to avoid https://github.com/plotly/plotly.js/issues/1199
// in v3
defaultY = -0.1;
defaultYAnchor = 'top';
if(isPaperY) {
defaultY = -0.1;
defaultYAnchor = 'top';
} else {
defaultY = 0;
defaultYAnchor = 'bottom';
}
}
} else {
defaultX = 1.02;
defaultY = 1;
defaultYAnchor = 'auto';
if(isPaperX) {
defaultX = 1.02;
} else {
defaultX = 1;
defaultXAnchor = 'right';
}
}

coerce('traceorder', defaultOrder);
Expand All @@ -136,7 +160,7 @@ function groupDefaults(legendId, layoutIn, layoutOut, fullData) {
coerce('groupclick');

coerce('x', defaultX);
coerce('xanchor');
coerce('xanchor', defaultXAnchor);
coerce('y', defaultY);
hannahker marked this conversation as resolved.
Show resolved Hide resolved
coerce('yanchor', defaultYAnchor);
coerce('valign');
Expand Down
73 changes: 58 additions & 15 deletions src/components/legend/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,24 +154,39 @@ function drawOne(gd, opts) {
function() {
var gs = fullLayout._size;
var bw = legendObj.borderwidth;
var isPaperX = legendObj.xref === 'paper';
var isPaperY = legendObj.yref === 'paper';

if(!inHover) {
var expMargin = expandMargin(gd, legendId);
var lx, ly;

if(isPaperX) {
lx = gs.l + gs.w * legendObj.x - FROM_TL[getXanchor(legendObj)] * legendObj._width;
} else {
legendObj.x = Lib.constrain(legendObj.x, 0, 1); // TODO: Move this to defaults setting?
hannahker marked this conversation as resolved.
Show resolved Hide resolved
lx = fullLayout.width * legendObj.x - FROM_TL[getXanchor(legendObj)] * legendObj._width;
}

if(isPaperY) {
ly = gs.t + gs.h * (1 - legendObj.y) - FROM_TL[getYanchor(legendObj)] * legendObj._effHeight;
} else {
legendObj.y = Lib.constrain(legendObj.y, 0, 1); // TODO: Move this to defaults setting?
ly = fullLayout.height * (1 - legendObj.y) - FROM_TL[getYanchor(legendObj)] * legendObj._effHeight;
}

var expMargin = expandMargin(gd, legendId, lx, ly);

// IF expandMargin return a Promise (which is truthy),
// we're under a doAutoMargin redraw, so we don't have to
// draw the remaining pieces below
if(expMargin) return;

var lx = gs.l + gs.w * legendObj.x - FROM_TL[getXanchor(legendObj)] * legendObj._width;
var ly = gs.t + gs.h * (1 - legendObj.y) - FROM_TL[getYanchor(legendObj)] * legendObj._effHeight;

if(fullLayout.margin.autoexpand) {
var lx0 = lx;
var ly0 = ly;

lx = Lib.constrain(lx, 0, fullLayout.width - legendObj._width);
ly = Lib.constrain(ly, 0, fullLayout.height - legendObj._effHeight);
lx = isPaperX ? Lib.constrain(lx, 0, fullLayout.width - legendObj._width) : lx0;
ly = isPaperY ? Lib.constrain(ly, 0, fullLayout.height - legendObj._effHeight) : ly0;

if(lx !== lx0) {
Lib.log('Constrain ' + legendId + '.x to make legend fit inside graph');
Expand Down Expand Up @@ -876,20 +891,48 @@ function computeLegendDimensions(gd, groups, traces, legendObj) {
});
}

function expandMargin(gd, legendId) {
function expandMargin(gd, legendId, lx, ly) {
var fullLayout = gd._fullLayout;
var legendObj = fullLayout[legendId];
var xanchor = getXanchor(legendObj);
var yanchor = getYanchor(legendObj);

return Plots.autoMargin(gd, legendId, {
x: legendObj.x,
y: legendObj.y,
l: legendObj._width * (FROM_TL[xanchor]),
r: legendObj._width * (FROM_BR[xanchor]),
b: legendObj._effHeight * (FROM_BR[yanchor]),
t: legendObj._effHeight * (FROM_TL[yanchor])
});
var isPaperX = legendObj.xref === 'paper';
var isPaperY = legendObj.yref === 'paper';

gd._fullLayout._reservedMargin[legendId] = {};
var sideY = legendObj.y < 0.5 ? 'b' : 't';
var sideX = legendObj.x < 0.5 ? 'l' : 'r';
var possibleReservedMargins = {
r: (fullLayout.width - lx),
l: lx + legendObj._width,
b: (fullLayout.height - ly),
t: ly + legendObj._effHeight
};

if(isPaperX && isPaperY) {
return Plots.autoMargin(gd, legendId, {
x: legendObj.x,
y: legendObj.y,
l: legendObj._width * (FROM_TL[xanchor]),
r: legendObj._width * (FROM_BR[xanchor]),
b: legendObj._effHeight * (FROM_BR[yanchor]),
t: legendObj._effHeight * (FROM_TL[yanchor])
});
} else if(isPaperX) {
gd._fullLayout._reservedMargin[legendId][sideY] = possibleReservedMargins[sideY];
return;
hannahker marked this conversation as resolved.
Show resolved Hide resolved
} else if(isPaperY) {
gd._fullLayout._reservedMargin[legendId][sideX] = possibleReservedMargins[sideX];
return;
hannahker marked this conversation as resolved.
Show resolved Hide resolved
} else {
if(legendObj.orientation === 'v') {
gd._fullLayout._reservedMargin[legendId][sideX] = possibleReservedMargins[sideX];
} else {
gd._fullLayout._reservedMargin[legendId][sideY] = possibleReservedMargins[sideY];
}
return;
hannahker marked this conversation as resolved.
Show resolved Hide resolved
}
}

function getXanchor(legendObj) {
Expand Down
Binary file added test/image/baselines/zz-container-legend.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
75 changes: 75 additions & 0 deletions test/image/mocks/zz-container-legend.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"data": [
{
"y": [0]
},
{
"y": [1]
},
{
"y": [2]
},
{
"y": [3],
"legend": "legend2"
},
{
"y": [4],
"legend": "legend3"
},
{
"y": [5],
"legend": "legend3"
}
],
"layout": {
"margin": {"t": 0, "b": 0, "r": 0, "l": 0},
"title": {
"text": "Multiple legends | Legends 1 & 2 with container ref",
"automargin": true,
"yref": "container"
},
"width": 500,
"height": 500,
"yaxis": {
"autorange": "reversed",
"title": {"text": "Long axis title with standoff", "font": {"size": 24}, "standoff": 25},
"side": "right",
"automargin": true
},
"xaxis": {"title": {"text": "Xaxis title"}, "automargin": true},
"legend": {
"bgcolor": "lightgray",
"xref": "container",
"title": {
"text": "Legend"
}
},
"legend2": {
"x": 0,
"y": 0.5,
"xanchor": "right",
"yanchor": "top",
"bgcolor": "lightblue",
"title": {
"text": "Legend 2"
}
},
"legend3": {
"y": 0,
"x": 0.5,
"orientation": "h",
"yref": "container",
"xref": "container",
"xanchor": "center",
"bgcolor": "yellow",
"title": {
"text": "Legend 3"
}
},
"hovermode": "x unified"
},
"config": {
"editable": true
}
}
24 changes: 22 additions & 2 deletions test/plot-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2964,7 +2964,7 @@
"valType": "boolean"
},
"x": {
"description": "Sets the x position (in normalized coordinates) of the legend. Defaults to *1.02* for vertical legends and defaults to *0* for horizontal legends.",
"description": "Sets the x position with respect to `xref` (in normalized coordinates) of the legend. When `xref` is *paper*, defaults to *1.02* for vertical legends and defaults to *0* for horizontal legends. When `xref` is *container*, defaults to *1* for vertical legends and defaults to *0* for horizontal legends. Must be between *0* and *1* if `xref` is *container*.",
"editType": "legend",
"max": 3,
"min": -2,
Expand All @@ -2982,8 +2982,18 @@
"right"
]
},
"xref": {
"description": "Sets the container `x` refers to. *container* spans the entire `width` of the plot. *paper* refers to the width of the plotting area only.",
"dflt": "paper",
"editType": "layoutstyle",
"valType": "enumerated",
"values": [
"container",
"paper"
]
},
"y": {
"description": "Sets the y position (in normalized coordinates) of the legend. Defaults to *1* for vertical legends, defaults to *-0.1* for horizontal legends on graphs w/o range sliders and defaults to *1.1* for horizontal legends on graph with one or multiple range sliders.",
"description": "Sets the y position with respect to `yref` (in normalized coordinates) of the legend. When `yref` is *paper*, defaults to *1* for vertical legends, defaults to *-0.1* for horizontal legends on graphs w/o range sliders and defaults to *1.1* for horizontal legends on graph with one or multiple range sliders. When `yref` is *container*, defaults to *1*. Must be between *0* and *1* if `yref` is *container*.",
"editType": "legend",
"max": 3,
"min": -2,
Expand All @@ -2999,6 +3009,16 @@
"middle",
"bottom"
]
},
"yref": {
"description": "Sets the container `y` refers to. *container* spans the entire `height` of the plot. *paper* refers to the height of the plotting area only.",
"dflt": "paper",
"editType": "layoutstyle",
"valType": "enumerated",
"values": [
"container",
"paper"
]
}
},
"mapbox": {
Expand Down