diff --git a/draftlogs/7072-fix.md b/draftlogs/7072-fix.md new file mode 100644 index 00000000000..f363f589e40 --- /dev/null +++ b/draftlogs/7072-fix.md @@ -0,0 +1,2 @@ + - Fix maximum dimensions for legend that is anchored to container [[#7072](https://github.com/plotly/plotly.js/pull/7072)], + with thanks to @attatrol for the contribution! \ No newline at end of file diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 6a8106bbcc1..80119a8ec2d 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -751,6 +751,9 @@ function computeLegendDimensions(gd, groups, traces, legendObj) { legendObj = fullLayout[legendId]; } var gs = fullLayout._size; + const plotBottom = gs.b; + const plotTop = gs.b + gs.h; + const plotHeight = gs.h; var isVertical = helpers.isVertical(legendObj); var isGrouped = helpers.isGrouped(legendObj); @@ -763,19 +766,58 @@ function computeLegendDimensions(gd, groups, traces, legendObj) { var endPad = 2 * (bw + itemGap); var yanchor = getYanchor(legendObj); - var isBelowPlotArea = legendObj.y < 0 || (legendObj.y === 0 && yanchor === 'top'); - var isAbovePlotArea = legendObj.y > 1 || (legendObj.y === 1 && yanchor === 'bottom'); + let isBelowPlotArea, isAbovePlotArea; var traceGroupGap = legendObj.tracegroupgap; var legendGroupWidths = {}; - const { orientation, yref } = legendObj; - let { maxheight } = legendObj; - const useFullLayoutHeight = isBelowPlotArea || isAbovePlotArea || orientation !== "v" || yref !== "paper" - // Set default maxheight here since it depends on values passed in by user - maxheight ||= useFullLayoutHeight ? 0.5 : 1; - const heightToBeScaled = useFullLayoutHeight ? fullLayout.height : gs.h; - legendObj._maxHeight = Math.max(maxheight > 1 ? maxheight : maxheight * heightToBeScaled, 30); + const { orientation, yref, y } = legendObj; + let { maxheight, maxwidth } = legendObj; + let yPixels; + let referenceHeight; + let useFullLayoutHeight; + + if (yref === 'paper') { + // Calculate pixel value of y position + yPixels = (y * plotHeight) + plotBottom; + // Check if legend anchor point is below or above plot area + isBelowPlotArea = yPixels < plotBottom || (yPixels === plotBottom && yanchor === 'top'); + isAbovePlotArea = yPixels > plotTop || (yPixels === plotTop && yanchor === 'bottom'); + useFullLayoutHeight = isBelowPlotArea || isAbovePlotArea || orientation !== "v"; + // Use the appropriate reference height + referenceHeight = useFullLayoutHeight ? fullLayout.height : plotHeight; + } + else { + // Calculate pixel value of y position + yPixels = y * fullLayout.height; + // Check if legend anchor point is below or above plot area + isBelowPlotArea = yPixels < plotBottom || (yPixels === plotBottom && yanchor === 'top'); + isAbovePlotArea = yPixels > plotTop || (yPixels === plotTop && yanchor === 'bottom'); + // Use the container height as the reference height + useFullLayoutHeight = false; + referenceHeight = fullLayout.height; + } + + // Set default maxheight if not provided by user + maxheight ||= (useFullLayoutHeight) ? 0.5 : 1; + // Convert maxheight to pixels if it's a ratio (≤1), otherwise use as-is + if (maxheight <= 1) { + maxheight = maxheight * referenceHeight; + } + + // Calculate the maximum available height based on the anchor point + let maxAvailableHeight; + if (yanchor === 'top') { + maxAvailableHeight = yPixels; + } else if (yanchor === 'bottom') { + maxAvailableHeight = fullLayout.height - yPixels; + } else { + // If yanchor is 'middle' + maxAvailableHeight = 2 * Math.min(yPixels, fullLayout.height - yPixels); + } + + maxheight = Math.min(maxheight, maxAvailableHeight); + legendObj._maxHeight = Math.max(maxheight, 30); var toggleRectWidth = 0; legendObj._width = 0; @@ -805,19 +847,30 @@ function computeLegendDimensions(gd, groups, traces, legendObj) { } } else { var xanchor = getXanchor(legendObj); - var isLeftOfPlotArea = legendObj.x < 0 || (legendObj.x === 0 && xanchor === 'right'); - var isRightOfPlotArea = legendObj.x > 1 || (legendObj.x === 1 && xanchor === 'left'); - var isBeyondPlotAreaY = isAbovePlotArea || isBelowPlotArea; - var hw = fullLayout.width / 2; - - // - if placed within x-margins, extend the width of the plot area - // - else if below/above plot area and anchored in the margin, extend to opposite margin, - // - otherwise give it the maximum potential margin-push value - legendObj._maxWidth = Math.max( - isLeftOfPlotArea ? ((isBeyondPlotAreaY && xanchor === 'left') ? gs.l + gs.w : hw) : - isRightOfPlotArea ? ((isBeyondPlotAreaY && xanchor === 'right') ? gs.r + gs.w : hw) : - gs.w, - 2 * textGap); + if(legendObj.xref === 'paper') { + var isLeftOfPlotArea = legendObj.x < 0 || (legendObj.x === 0 && xanchor === 'right'); + var isRightOfPlotArea = legendObj.x > 1 || (legendObj.x === 1 && xanchor === 'left'); + var isBeyondPlotAreaY = isAbovePlotArea || isBelowPlotArea; + var hw = fullLayout.width / 2; + + // - if placed within x-margins, extend the width of the plot area + // - else if below/above plot area and anchored in the margin, extend to opposite margin, + // - otherwise give it the maximum potential margin-push value + legendObj._maxWidth = Math.max( + isLeftOfPlotArea ? ((isBeyondPlotAreaY && xanchor === 'left') ? gs.l + gs.w : hw) : + isRightOfPlotArea ? ((isBeyondPlotAreaY && xanchor === 'right') ? gs.r + gs.w : hw) : + gs.w, + 2 * textGap); + } else { + if(xanchor === 'right') + maxwidth = legendObj.x * fullLayout.width; + else if(xanchor === 'left') + maxwidth = (1 - legendObj.x) * fullLayout.width; + else // if (xanchor === 'center') + maxwidth = 2 * Math.min(1 - legendObj.x, legendObj.x) * fullLayout.width; + legendObj._maxWidth = Math.max(maxwidth, 2 * textGap); + } + var maxItemWidth = 0; var combinedItemWidth = 0; traces.each(function(d) { diff --git a/test/image/baselines/sunburst_coffee.png b/test/image/baselines/sunburst_coffee.png index 523744ad90d..590edb08ba9 100644 Binary files a/test/image/baselines/sunburst_coffee.png and b/test/image/baselines/sunburst_coffee.png differ diff --git a/test/image/baselines/treemap_sunburst_marker_colors.png b/test/image/baselines/treemap_sunburst_marker_colors.png index beebd418791..209427c1b2a 100644 Binary files a/test/image/baselines/treemap_sunburst_marker_colors.png and b/test/image/baselines/treemap_sunburst_marker_colors.png differ diff --git a/test/image/baselines/treemap_textfit.png b/test/image/baselines/treemap_textfit.png index b9b0a41090d..c5822e62271 100644 Binary files a/test/image/baselines/treemap_textfit.png and b/test/image/baselines/treemap_textfit.png differ diff --git a/test/jasmine/tests/legend_test.js b/test/jasmine/tests/legend_test.js index 9fc21aabb47..b002a398943 100644 --- a/test/jasmine/tests/legend_test.js +++ b/test/jasmine/tests/legend_test.js @@ -1381,6 +1381,50 @@ describe('legend relayout update', function() { }) .then(done, done.fail); }); + + it('should constrain legend height when yref is container', function(done) { + var fig = { + data: Array.from({length: 20}, () => ({ + x: [1, 2, 3], + y: [1, 2, 3] + })), + layout: { + legend: { + xref: 'container', + yref: 'container', + x:0.9 + } + } + } + + function _assert(msg, xy, wh) { + return function() { + var fullLayout = gd._fullLayout; + var legend3 = d3Select('g.legend'); + var bg3 = legend3.select('rect.bg'); + var translate = Drawing.getTranslate(legend3); + var x = translate.x; + var y = translate.y; + var w = +bg3.attr('width'); + var h = +bg3.attr('height'); + + expect([x, y]).toBeWithinArray(xy, 25, msg + '| legend x,y'); + expect([w, h]).toBeWithinArray(wh, 25, msg + '| legend w,h'); + expect(x + w <= fullLayout.width).toBe(true, msg + '| fits in x'); + expect(y + h <= fullLayout.height).toBe(true, msg + '| fits in y'); + }; + } + + Plotly.newPlot(gd, fig) + .then(_assert('base', [539, 0], [91, 390])) + .then(function() { return Plotly.relayout(gd, 'legend.y', 0.6); }) + .then(function() { return Plotly.relayout(gd, 'legend.yanchor', 'bottom'); }) + .then(_assert('after moving legend towards the top edge', [539, 0], [101, 180])) + .then(function() { return Plotly.relayout(gd, 'legend.y', 0.4); }) + .then(function() { return Plotly.relayout(gd, 'legend.yanchor', 'top'); }) + .then(_assert('after moving legend towards the bottom edge', [539, 270], [101, 180])) + .then(done, done.fail); + }); }); describe('legend orientation change:', function() {