Skip to content

Commit

Permalink
Merge pull request #6589 from plotly/legend-positioning
Browse files Browse the repository at this point in the history
Add `xref` and `yref` to legends
  • Loading branch information
hannahker committed May 12, 2023
2 parents 39c2f06 + 5a9e953 commit cc88162
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 39 deletions.
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.
45 changes: 35 additions & 10 deletions src/components/legend/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,13 +159,26 @@ module.exports = {
},
x: {
valType: 'number',
min: -2,
max: 3,
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*.',
'and between *-2* and *3* if `xref` is *paper*.'
].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 @@ -184,14 +197,26 @@ module.exports = {
},
y: {
valType: 'number',
min: -2,
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*',
'and between *-2* and *3* if `yref` is *paper*.'
].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
56 changes: 48 additions & 8 deletions src/components/legend/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,28 +101,70 @@ 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';

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';
}
}

Lib.coerce(containerIn, containerOut, {
x: {
valType: 'number',
editType: 'legend',
min: isPaperX ? -2 : 0,
max: isPaperX ? 3 : 1,
dflt: defaultX,
}
}, 'x');

Lib.coerce(containerIn, containerOut, {
y: {
valType: 'number',
editType: 'legend',
min: isPaperY ? -2 : 0,
max: isPaperY ? 3 : 1,
dflt: defaultY,
}
}, 'y');

coerce('traceorder', defaultOrder);
if(helpers.isGrouped(layoutOut.legend)) coerce('tracegroupgap');

Expand All @@ -135,9 +177,7 @@ function groupDefaults(legendId, layoutIn, layoutOut, fullData) {
coerce('itemdoubleclick');
coerce('groupclick');

coerce('x', defaultX);
coerce('xanchor');
coerce('y', defaultY);
coerce('xanchor', defaultXAnchor);
coerce('yanchor', defaultYAnchor);
coerce('valign');
Lib.noneOrAll(containerIn, containerOut, ['x', 'y']);
Expand Down
68 changes: 53 additions & 15 deletions src/components/legend/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,24 +154,37 @@ 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 {
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 {
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 +889,45 @@ 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];
} else if(isPaperY) {
gd._fullLayout._reservedMargin[legendId][sideX] = possibleReservedMargins[sideX];
} else {
if(legendObj.orientation === 'v') {
gd._fullLayout._reservedMargin[legendId][sideX] = possibleReservedMargins[sideX];
} else {
gd._fullLayout._reservedMargin[legendId][sideY] = possibleReservedMargins[sideY];
}
}
}

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
}
}
28 changes: 22 additions & 6 deletions test/plot-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2980,10 +2980,8 @@
"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*. and between *-2* and *3* if `xref` is *paper*.",
"editType": "legend",
"max": 3,
"min": -2,
"valType": "number"
},
"xanchor": {
Expand All @@ -2998,11 +2996,19 @@
"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* and between *-2* and *3* if `yref` is *paper*.",
"editType": "legend",
"max": 3,
"min": -2,
"valType": "number"
},
"yanchor": {
Expand All @@ -3015,6 +3021,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

0 comments on commit cc88162

Please sign in to comment.