diff --git a/src/components/legend/anchor_utils.js b/src/components/legend/anchor_utils.js deleted file mode 100644 index b1bf7e97044..00000000000 --- a/src/components/legend/anchor_utils.js +++ /dev/null @@ -1,47 +0,0 @@ -/** -* Copyright 2012-2019, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - - -'use strict'; - - -/** - * Determine the position anchor property of x/y xanchor/yanchor components. - * - * - values < 1/3 align the low side at that fraction, - * - values [1/3, 2/3] align the center at that fraction, - * - values > 2/3 align the right at that fraction. - */ - -exports.isRightAnchor = function isRightAnchor(opts) { - return ( - opts.xanchor === 'right' || - (opts.xanchor === 'auto' && opts.x >= 2 / 3) - ); -}; - -exports.isCenterAnchor = function isCenterAnchor(opts) { - return ( - opts.xanchor === 'center' || - (opts.xanchor === 'auto' && opts.x > 1 / 3 && opts.x < 2 / 3) - ); -}; - -exports.isBottomAnchor = function isBottomAnchor(opts) { - return ( - opts.yanchor === 'bottom' || - (opts.yanchor === 'auto' && opts.y <= 1 / 3) - ); -}; - -exports.isMiddleAnchor = function isMiddleAnchor(opts) { - return ( - opts.yanchor === 'middle' || - (opts.yanchor === 'auto' && opts.y > 1 / 3 && opts.y < 2 / 3) - ); -}; diff --git a/src/components/legend/attributes.js b/src/components/legend/attributes.js index 2eee7aa05cb..43e45b2faff 100644 --- a/src/components/legend/attributes.js +++ b/src/components/legend/attributes.js @@ -121,10 +121,13 @@ module.exports = { valType: 'number', min: -2, max: 3, - dflt: 1.02, role: 'style', editType: 'legend', - description: 'Sets the x position (in normalized coordinates) of the 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.' + ].join(' ') }, xanchor: { valType: 'enumerated', @@ -135,28 +138,37 @@ module.exports = { description: [ 'Sets the legend\'s horizontal position anchor.', 'This anchor binds the `x` position to the *left*, *center*', - 'or *right* of the legend.' + 'or *right* of the legend.', + 'Value *auto* anchors legends to the right for `x` values greater than or equal to 2/3,', + 'anchors legends to the left for `x` values less than or equal to 1/3 and', + 'anchors legends with respect to their center otherwise.' ].join(' ') }, y: { valType: 'number', min: -2, max: 3, - dflt: 1, role: 'style', editType: 'legend', - description: 'Sets the y position (in normalized coordinates) of the legend.' + 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.' + ].join(' ') }, yanchor: { valType: 'enumerated', values: ['auto', 'top', 'middle', 'bottom'], - dflt: 'auto', role: 'info', editType: 'legend', description: [ 'Sets the legend\'s vertical position anchor', 'This anchor binds the `y` position to the *top*, *middle*', - 'or *bottom* of the legend.' + 'or *bottom* of the legend.', + 'Value *auto* anchors legends at their bottom for `y` values less than or equal to 1/3,', + 'anchors legends to at their top for `y` values greater than or equal to 2/3 and', + 'anchors legends with respect to their middle otherwise.' ].join(' ') }, uirevision: { diff --git a/src/components/legend/constants.js b/src/components/legend/constants.js index 468757a7720..a4fa79dd92a 100644 --- a/src/components/legend/constants.js +++ b/src/components/legend/constants.js @@ -13,5 +13,10 @@ module.exports = { scrollBarMinHeight: 20, scrollBarColor: '#808BA4', scrollBarMargin: 4, - textOffsetX: 40 + scrollBarEnterAttrs: {rx: 20, ry: 3, width: 0, height: 0}, + + // number of px between legend symbol and legend text (always in x direction) + textGap: 40, + // number of px between each legend item (x and/or y direction) + itemGap: 5 }; diff --git a/src/components/legend/defaults.js b/src/components/legend/defaults.js index 955c94f9f53..f73add5cba4 100644 --- a/src/components/legend/defaults.js +++ b/src/components/legend/defaults.js @@ -24,8 +24,6 @@ module.exports = function legendDefaults(layoutIn, layoutOut, fullData) { var legendReallyHasATrace = false; var defaultOrder = 'normal'; - var defaultX, defaultY, defaultXAnchor, defaultYAnchor; - for(var i = 0; i < fullData.length; i++) { var trace = fullData[i]; @@ -82,20 +80,26 @@ module.exports = function legendDefaults(layoutIn, layoutOut, fullData) { coerce('borderwidth'); Lib.coerceFont(coerce, 'font', layoutOut.font); - coerce('orientation'); - if(containerOut.orientation === 'h') { - var xaxis = layoutIn.xaxis; - if(Registry.getComponentMethod('rangeslider', 'isVisible')(xaxis)) { - defaultX = 0; - defaultXAnchor = 'left'; + var orientation = coerce('orientation'); + var defaultX, defaultY, defaultYAnchor; + + if(orientation === 'h') { + defaultX = 0; + + if(Registry.getComponentMethod('rangeslider', 'isVisible')(layoutIn.xaxis)) { defaultY = 1.1; defaultYAnchor = 'bottom'; } else { - defaultX = 0; - defaultXAnchor = 'left'; + // maybe use y=1.1 / yanchor=bottom as above + // to avoid https://github.com/plotly/plotly.js/issues/1199 + // in v2 defaultY = -0.1; defaultYAnchor = 'top'; } + } else { + defaultX = 1.02; + defaultY = 1; + defaultYAnchor = 'auto'; } coerce('traceorder', defaultOrder); @@ -107,7 +111,7 @@ module.exports = function legendDefaults(layoutIn, layoutOut, fullData) { coerce('itemdoubleclick'); coerce('x', defaultX); - coerce('xanchor', defaultXAnchor); + coerce('xanchor'); coerce('y', defaultY); coerce('yanchor', defaultYAnchor); coerce('valign'); diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 159536a7230..23c2b1ce74a 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -45,26 +45,11 @@ module.exports = function draw(gd) { if(!fullLayout.showlegend || !legendData.length) { fullLayout._infolayer.selectAll('.legend').remove(); fullLayout._topdefs.select('#' + clipId).remove(); - - Plots.autoMargin(gd, 'legend'); - return; - } - - var maxLength = 0; - for(var i = 0; i < legendData.length; i++) { - for(var j = 0; j < legendData[i].length; j++) { - var item = legendData[i][j][0]; - var trace = item.trace; - var isPieLike = Registry.traceIs(trace, 'pie-like'); - var name = isPieLike ? item.label : trace.name; - maxLength = Math.max(maxLength, name && name.length || 0); - } + return Plots.autoMargin(gd, 'legend'); } - var firstRender = false; var legend = Lib.ensureSingle(fullLayout._infolayer, 'g', 'legend', function(s) { s.attr('pointer-events', 'all'); - firstRender = true; }); var clipPath = Lib.ensureSingleById(fullLayout._topdefs, 'clipPath', clipId, function(s) { @@ -74,7 +59,6 @@ module.exports = function draw(gd) { var bg = Lib.ensureSingle(legend, 'rect', 'bg', function(s) { s.attr('shape-rendering', 'crispEdges'); }); - bg.call(Color.stroke, opts.bordercolor) .call(Color.fill, opts.bgcolor) .style('stroke-width', opts.borderwidth + 'px'); @@ -82,26 +66,15 @@ module.exports = function draw(gd) { var scrollBox = Lib.ensureSingle(legend, 'g', 'scrollbox'); var scrollBar = Lib.ensureSingle(legend, 'rect', 'scrollbar', function(s) { - s.attr({ - rx: 20, - ry: 3, - width: 0, - height: 0 - }) - .call(Color.fill, '#808BA4'); + s.attr(constants.scrollBarEnterAttrs) + .call(Color.fill, constants.scrollBarColor); }); - var groups = scrollBox.selectAll('g.groups') - .data(legendData); - - groups.enter().append('g') - .attr('class', 'groups'); - + var groups = scrollBox.selectAll('g.groups').data(legendData); + groups.enter().append('g').attr('class', 'groups'); groups.exit().remove(); - var traces = groups.selectAll('g.traces') - .data(Lib.identity); - + var traces = groups.selectAll('g.traces').data(Lib.identity); traces.enter().append('g').attr('class', 'traces'); traces.exit().remove(); @@ -113,84 +86,38 @@ module.exports = function draw(gd) { return trace.visible === 'legendonly' ? 0.5 : 1; } }) - .each(function() { - d3.select(this) - .call(drawTexts, gd, maxLength); - }) + .each(function() { d3.select(this).call(drawTexts, gd); }) .call(style, gd) - .each(function() { - d3.select(this) - .call(setupTraceToggle, gd); - }); + .each(function() { d3.select(this).call(setupTraceToggle, gd); }); - Lib.syncOrAsync([Plots.previousPromises, + Lib.syncOrAsync([ + Plots.previousPromises, + function() { return computeLegendDimensions(gd, groups, traces); }, function() { - if(firstRender) { - computeLegendDimensions(gd, groups, traces); - expandMargin(gd); - } + // 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(expandMargin(gd)) return; - // Position and size the legend - var lxMin = 0; - var lxMax = fullLayout.width; - var lyMin = 0; - var lyMax = fullLayout.height; - - computeLegendDimensions(gd, groups, traces); - - if(opts._height > lyMax) { - // If the legend doesn't fit in the plot area, - // do not expand the vertical margins. - expandHorizontalMargin(gd); - } else { - expandMargin(gd); - } - - // Scroll section must be executed after repositionLegend. - // It requires the legend width, height, x and y to position the scrollbox - // and these values are mutated in repositionLegend. var gs = fullLayout._size; - var lx = gs.l + gs.w * opts.x; - var ly = gs.t + gs.h * (1 - opts.y); - - if(Lib.isRightAnchor(opts)) { - lx -= opts._width; - } else if(Lib.isCenterAnchor(opts)) { - lx -= opts._width / 2; - } - - if(Lib.isBottomAnchor(opts)) { - ly -= opts._height; - } else if(Lib.isMiddleAnchor(opts)) { - ly -= opts._height / 2; - } + var bw = opts.borderwidth; - // Make sure the legend left and right sides are visible - var legendWidth = opts._width; - var legendWidthMax = gs.w; + var lx = gs.l + gs.w * opts.x - FROM_TL[getXanchor(opts)] * opts._width; + var ly = gs.t + gs.h * (1 - opts.y) - FROM_TL[getYanchor(opts)] * opts._effHeight; - if(legendWidth > legendWidthMax) { - lx = gs.l; - legendWidth = legendWidthMax; - } else { - if(lx + legendWidth > lxMax) lx = lxMax - legendWidth; - if(lx < lxMin) lx = lxMin; - legendWidth = Math.min(lxMax - lx, opts._width); - } + if(fullLayout.margin.autoexpand) { + var lx0 = lx; + var ly0 = ly; - // Make sure the legend top and bottom are visible - // (legends with a scroll bar are not allowed to stretch beyond the extended - // margins) - var legendHeight = opts._height; - var legendHeightMax = gs.h; + lx = Lib.constrain(lx, 0, fullLayout.width - opts._width); + ly = Lib.constrain(ly, 0, fullLayout.height - opts._effHeight); - if(legendHeight > legendHeightMax) { - ly = gs.t; - legendHeight = legendHeightMax; - } else { - if(ly + legendHeight > lyMax) ly = lyMax - legendHeight; - if(ly < lyMin) ly = lyMin; - legendHeight = Math.min(lyMax - ly, opts._height); + if(lx !== lx0) { + Lib.log('Constrain legend.x to make legend fit inside graph'); + } + if(ly !== ly0) { + Lib.log('Constrain legend.y to make legend fit inside graph'); + } } // Set size and position of all the elements that make up a legend: @@ -201,22 +128,22 @@ module.exports = function draw(gd) { scrollBar.on('.drag', null); legend.on('wheel', null); - if(opts._height <= legendHeight || gd._context.staticPlot) { + if(opts._height <= opts._maxHeight || gd._context.staticPlot) { // if scrollbar should not be shown. bg.attr({ - width: legendWidth - opts.borderwidth, - height: legendHeight - opts.borderwidth, - x: opts.borderwidth / 2, - y: opts.borderwidth / 2 + width: opts._width - bw, + height: opts._effHeight - bw, + x: bw / 2, + y: bw / 2 }); Drawing.setTranslate(scrollBox, 0, 0); clipPath.select('rect').attr({ - width: legendWidth - 2 * opts.borderwidth, - height: legendHeight - 2 * opts.borderwidth, - x: opts.borderwidth, - y: opts.borderwidth + width: opts._width - 2 * bw, + height: opts._effHeight - 2 * bw, + x: bw, + y: bw }); Drawing.setClipUrl(scrollBox, clipId, gd); @@ -225,11 +152,11 @@ module.exports = function draw(gd) { delete opts._scrollY; } else { var scrollBarHeight = Math.max(constants.scrollBarMinHeight, - legendHeight * legendHeight / opts._height); - var scrollBarYMax = legendHeight - + opts._effHeight * opts._effHeight / opts._height); + var scrollBarYMax = opts._effHeight - scrollBarHeight - 2 * constants.scrollBarMargin; - var scrollBoxYMax = opts._height - legendHeight; + var scrollBoxYMax = opts._height - opts._effHeight; var scrollRatio = scrollBarYMax / scrollBoxYMax; var scrollBoxY = Math.min(opts._scrollY || 0, scrollBoxYMax); @@ -237,23 +164,23 @@ module.exports = function draw(gd) { // increase the background and clip-path width // by the scrollbar width and margin bg.attr({ - width: legendWidth - - 2 * opts.borderwidth + + width: opts._width - + 2 * bw + constants.scrollBarWidth + constants.scrollBarMargin, - height: legendHeight - opts.borderwidth, - x: opts.borderwidth / 2, - y: opts.borderwidth / 2 + height: opts._effHeight - bw, + x: bw / 2, + y: bw / 2 }); clipPath.select('rect').attr({ - width: legendWidth - - 2 * opts.borderwidth + + width: opts._width - + 2 * bw + constants.scrollBarWidth + constants.scrollBarMargin, - height: legendHeight - 2 * opts.borderwidth, - x: opts.borderwidth, - y: opts.borderwidth + scrollBoxY + height: opts._effHeight - 2 * bw, + x: bw, + y: bw + scrollBoxY }); Drawing.setClipUrl(scrollBox, clipId, gd); @@ -291,21 +218,18 @@ module.exports = function draw(gd) { scrollBar.call(drag); } - function scrollHandler(scrollBoxY, scrollBarHeight, scrollRatio) { opts._scrollY = gd._fullLayout.legend._scrollY = scrollBoxY; Drawing.setTranslate(scrollBox, 0, -scrollBoxY); Drawing.setRect( scrollBar, - legendWidth, + opts._width, constants.scrollBarMargin + scrollBoxY * scrollRatio, constants.scrollBarWidth, scrollBarHeight ); - clipPath.select('rect').attr({ - y: opts.borderwidth + scrollBoxY - }); + clipPath.select('rect').attr('y', bw + scrollBoxY); } if(gd._context.edits.legendPosition) { @@ -318,7 +242,6 @@ module.exports = function draw(gd) { gd: gd, prepFn: function() { var transform = Drawing.getTranslate(legend); - x0 = transform.x; y0 = transform.y; }, @@ -391,13 +314,15 @@ function clickOrDoubleClick(gd, legend, legendItem, numClicks, evt) { } } -function drawTexts(g, gd, maxLength) { +function drawTexts(g, gd) { var legendItem = g.data()[0][0]; var fullLayout = gd._fullLayout; + var opts = fullLayout.legend; var trace = legendItem.trace; var isPieLike = Registry.traceIs(trace, 'pie-like'); var traceIndex = trace.index; var isEditable = gd._context.edits.legendText && !isPieLike; + var maxNameLength = opts._maxNameLength; var name = isPieLike ? legendItem.label : trace.name; if(trace._meta) { @@ -409,9 +334,9 @@ function drawTexts(g, gd, maxLength) { textEl.attr('text-anchor', 'start') .classed('user-select-none', true) .call(Drawing.font, fullLayout.legend.font) - .text(isEditable ? ensureLength(name, maxLength) : name); + .text(isEditable ? ensureLength(name, maxNameLength) : name); - svgTextUtils.positionText(textEl, constants.textOffsetX, 0); + svgTextUtils.positionText(textEl, constants.textGap, 0); function textLayout(s) { svgTextUtils.convertToTspans(s, gd, function() { @@ -423,7 +348,7 @@ function drawTexts(g, gd, maxLength) { textEl.call(svgTextUtils.makeEditable, {gd: gd, text: name}) .call(textLayout) .on('edit', function(newName) { - this.text(ensureLength(newName, maxLength)) + this.text(ensureLength(newName, maxNameLength)) .call(textLayout); var fullInput = legendItem.trace._fullInput || {}; @@ -530,7 +455,7 @@ function computeTextDimensions(g, gd) { // approximation to height offset to center the font // to avoid getBoundingClientRect var textY = lineHeight * (0.3 + (1 - textLines) / 2); - svgTextUtils.positionText(text, constants.textOffsetX, textY); + svgTextUtils.positionText(text, constants.textGap, textY); } legendItem.lineHeight = lineHeight; @@ -538,238 +463,197 @@ function computeTextDimensions(g, gd) { legendItem.width = width; } +/* + * Computes in fullLayout.legend: + * + * - _height: legend height including items past scrollbox height + * - _maxHeight: maximum legend height before scrollbox is required + * - _effHeight: legend height w/ or w/o scrollbox + * + * - _width: legend width + * - _maxWidth (for orientation:h only): maximum width before starting new row + */ function computeLegendDimensions(gd, groups, traces) { var fullLayout = gd._fullLayout; var opts = fullLayout.legend; - var borderwidth = opts.borderwidth; + var gs = fullLayout._size; + var isVertical = helpers.isVertical(opts); var isGrouped = helpers.isGrouped(opts); - var extraWidth = 0; - - var traceGap = 5; + var bw = opts.borderwidth; + var bw2 = 2 * bw; + var textGap = constants.textGap; + var itemGap = constants.itemGap; + var endPad = 2 * (bw + itemGap); + + var yanchor = getYanchor(opts); + var isBelowPlotArea = opts.y < 0 || (opts.y === 0 && yanchor === 'top'); + var isAbovePlotArea = opts.y > 1 || (opts.y === 1 && yanchor === 'bottom'); + + // - if below/above plot area, give it the maximum potential margin-push value + // - otherwise, extend the height of the plot area + opts._maxHeight = Math.max( + (isBelowPlotArea || isAbovePlotArea) ? fullLayout.height / 2 : gs.h, + 30 + ); + var toggleRectWidth = 0; opts._width = 0; opts._height = 0; - if(helpers.isVertical(opts)) { + if(isVertical) { + traces.each(function(d) { + var h = d[0].height; + Drawing.setTranslate(this, bw, itemGap + bw + opts._height + h / 2); + opts._height += h; + opts._width = Math.max(opts._width, d[0].width); + }); + + toggleRectWidth = textGap + opts._width; + opts._width += itemGap + textGap + bw2; + opts._height += endPad; + if(isGrouped) { groups.each(function(d, i) { Drawing.setTranslate(this, 0, i * opts.tracegroupgap); }); + opts._height += (opts._lgroupsLength - 1) * opts.tracegroupgap; } - + } else { + var xanchor = getXanchor(opts); + var isLeftOfPlotArea = opts.x < 0 || (opts.x === 0 && xanchor === 'right'); + var isRightOfPlotArea = opts.x > 1 || (opts.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 + opts._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); + var maxItemWidth = 0; + var combinedItemWidth = 0; traces.each(function(d) { - var legendItem = d[0]; - var textHeight = legendItem.height; - var textWidth = legendItem.width; - - Drawing.setTranslate(this, - borderwidth, - (5 + borderwidth + opts._height + textHeight / 2)); - - opts._height += textHeight; - opts._width = Math.max(opts._width, textWidth); + var w = d[0].width + textGap; + maxItemWidth = Math.max(maxItemWidth, w); + combinedItemWidth += w; }); - opts._width += 45 + borderwidth * 2; - opts._height += 10 + borderwidth * 2; + toggleRectWidth = null; + var maxRowWidth = 0; if(isGrouped) { - opts._height += (opts._lgroupsLength - 1) * opts.tracegroupgap; - } - - extraWidth = 40; - } else if(isGrouped) { - var maxHeight = 0; - var maxWidth = 0; - var groupData = groups.data(); - - var maxItems = 0; - - var i; - for(i = 0; i < groupData.length; i++) { - var group = groupData[i]; - var groupWidths = group.map(function(legendItemArray) { - return legendItemArray[0].width; - }); - - var groupWidth = Lib.aggNums(Math.max, null, groupWidths); - var groupHeight = group.reduce(function(a, b) { - return a + b[0].height; - }, 0); - - maxWidth = Math.max(maxWidth, groupWidth); - maxHeight = Math.max(maxHeight, groupHeight); - maxItems = Math.max(maxItems, group.length); - } - - maxWidth += traceGap; - maxWidth += 40; - - var groupXOffsets = [opts._width]; - var groupYOffsets = []; - var rowNum = 0; - for(i = 0; i < groupData.length; i++) { - if(fullLayout._size.w < (borderwidth + opts._width + traceGap + maxWidth)) { - groupXOffsets[groupXOffsets.length - 1] = groupXOffsets[0]; - opts._width = maxWidth; - rowNum++; - } else { - opts._width += maxWidth + borderwidth; - } - - var rowYOffset = (rowNum * maxHeight); - rowYOffset += rowNum > 0 ? opts.tracegroupgap : 0; - - groupYOffsets.push(rowYOffset); - groupXOffsets.push(opts._width); - } - - groups.each(function(d, i) { - Drawing.setTranslate(this, groupXOffsets[i], groupYOffsets[i]); - }); + var maxGroupHeightInRow = 0; + var groupOffsetX = 0; + var groupOffsetY = 0; + groups.each(function() { + var maxWidthInGroup = 0; + var offsetY = 0; + d3.select(this).selectAll('g.traces').each(function(d) { + var h = d[0].height; + Drawing.setTranslate(this, 0, itemGap + bw + h / 2 + offsetY); + offsetY += h; + maxWidthInGroup = Math.max(maxWidthInGroup, textGap + d[0].width); + }); + maxGroupHeightInRow = Math.max(maxGroupHeightInRow, offsetY); - groups.each(function() { - var group = d3.select(this); - var groupTraces = group.selectAll('g.traces'); - var groupHeight = 0; + var next = maxWidthInGroup + itemGap; - groupTraces.each(function(d) { - var legendItem = d[0]; - var textHeight = legendItem.height; + if((next + bw + groupOffsetX) > opts._maxWidth) { + maxRowWidth = Math.max(maxRowWidth, groupOffsetX); + groupOffsetX = 0; + groupOffsetY += maxGroupHeightInRow + opts.tracegroupgap; + maxGroupHeightInRow = offsetY; + } - Drawing.setTranslate(this, - 0, - (5 + borderwidth + groupHeight + textHeight / 2)); + Drawing.setTranslate(this, groupOffsetX, groupOffsetY); - groupHeight += textHeight; + groupOffsetX += next; }); - }); - - var maxYLegend = groupYOffsets[groupYOffsets.length - 1] + maxHeight; - opts._height = 10 + (borderwidth * 2) + maxYLegend; - var maxOffset = Math.max.apply(null, groupXOffsets); - opts._width = maxOffset + maxWidth + 40; - opts._width += borderwidth * 2; - } else { - var rowHeight = 0; - var maxTraceHeight = 0; - var maxTraceWidth = 0; - var offsetX = 0; - var fullTracesWidth = 0; + opts._width = Math.max(maxRowWidth, groupOffsetX) + bw; + opts._height = groupOffsetY + maxGroupHeightInRow + endPad; + } else { + var nTraces = traces.size(); + var oneRowLegend = (combinedItemWidth + bw2 + (nTraces - 1) * itemGap) < opts._maxWidth; + + var maxItemHeightInRow = 0; + var offsetX = 0; + var offsetY = 0; + var rowWidth = 0; + traces.each(function(d) { + var h = d[0].height; + var w = textGap + d[0].width; + var next = (oneRowLegend ? w : maxItemWidth) + itemGap; + + if((next + bw + offsetX) > opts._maxWidth) { + maxRowWidth = Math.max(maxRowWidth, rowWidth); + offsetX = 0; + offsetY += maxItemHeightInRow; + opts._height += maxItemHeightInRow; + maxItemHeightInRow = 0; + } - // calculate largest width for traces and use for width of all legend items - traces.each(function(d) { - maxTraceWidth = Math.max(40 + d[0].width, maxTraceWidth); - fullTracesWidth += 40 + d[0].width + traceGap; - }); + Drawing.setTranslate(this, bw + offsetX, itemGap + bw + h / 2 + offsetY); - // check if legend fits in one row - var oneRowLegend = fullLayout._size.w > borderwidth + fullTracesWidth - traceGap; + rowWidth = offsetX + w + itemGap; + offsetX += next; + maxItemHeightInRow = Math.max(maxItemHeightInRow, h); + }); - traces.each(function(d) { - var legendItem = d[0]; - var traceWidth = oneRowLegend ? 40 + d[0].width : maxTraceWidth; - - if((borderwidth + offsetX + traceGap + traceWidth) > fullLayout._size.w) { - offsetX = 0; - rowHeight += maxTraceHeight; - opts._height += maxTraceHeight; - // reset for next row - maxTraceHeight = 0; + if(oneRowLegend) { + opts._width = offsetX + bw2; + opts._height = maxItemHeightInRow + endPad; + } else { + opts._width = Math.max(maxRowWidth, rowWidth) + bw2; + opts._height += maxItemHeightInRow + endPad; } - - Drawing.setTranslate(this, - (borderwidth + offsetX), - (5 + borderwidth + legendItem.height / 2) + rowHeight); - - opts._width += traceGap + traceWidth; - - // keep track of tallest trace in group - offsetX += traceGap + traceWidth; - maxTraceHeight = Math.max(legendItem.height, maxTraceHeight); - }); - - if(oneRowLegend) { - opts._height = maxTraceHeight; - } else { - opts._height += maxTraceHeight; } - - opts._width += borderwidth * 2; - opts._height += 10 + borderwidth * 2; } - // make sure we're only getting full pixels opts._width = Math.ceil(opts._width); opts._height = Math.ceil(opts._height); - var isEditable = ( - gd._context.edits.legendText || - gd._context.edits.legendPosition - ); + opts._effHeight = Math.min(opts._height, opts._maxHeight); + var edits = gd._context.edits; + var isEditable = edits.legendText || edits.legendPosition; traces.each(function(d) { - var legendItem = d[0]; - var bg = d3.select(this).select('.legendtoggle'); - - Drawing.setRect(bg, - 0, - -legendItem.height / 2, - (isEditable ? 0 : opts._width) + extraWidth, - legendItem.height - ); + var traceToggle = d3.select(this).select('.legendtoggle'); + var h = d[0].height; + var w = isEditable ? textGap : (toggleRectWidth || (textGap + d[0].width)); + if(!isVertical) w += itemGap / 2; + Drawing.setRect(traceToggle, 0, -h / 2, w, h); }); } function expandMargin(gd) { var fullLayout = gd._fullLayout; var opts = fullLayout.legend; + var xanchor = getXanchor(opts); + var yanchor = getYanchor(opts); - var xanchor = 'left'; - if(Lib.isRightAnchor(opts)) { - xanchor = 'right'; - } else if(Lib.isCenterAnchor(opts)) { - xanchor = 'center'; - } - - var yanchor = 'top'; - if(Lib.isBottomAnchor(opts)) { - yanchor = 'bottom'; - } else if(Lib.isMiddleAnchor(opts)) { - yanchor = 'middle'; - } - - // lastly check if the margin auto-expand has changed - Plots.autoMargin(gd, 'legend', { + return Plots.autoMargin(gd, 'legend', { x: opts.x, y: opts.y, l: opts._width * (FROM_TL[xanchor]), r: opts._width * (FROM_BR[xanchor]), - b: opts._height * (FROM_BR[yanchor]), - t: opts._height * (FROM_TL[yanchor]) + b: opts._effHeight * (FROM_BR[yanchor]), + t: opts._effHeight * (FROM_TL[yanchor]) }); } -function expandHorizontalMargin(gd) { - var fullLayout = gd._fullLayout; - var opts = fullLayout.legend; - - var xanchor = 'left'; - if(Lib.isRightAnchor(opts)) { - xanchor = 'right'; - } else if(Lib.isCenterAnchor(opts)) { - xanchor = 'center'; - } +function getXanchor(opts) { + return Lib.isRightAnchor(opts) ? 'right' : + Lib.isCenterAnchor(opts) ? 'center' : + 'left'; +} - // lastly check if the margin auto-expand has changed - Plots.autoMargin(gd, 'legend', { - x: opts.x, - y: 0.5, - l: opts._width * (FROM_TL[xanchor]), - r: opts._width * (FROM_BR[xanchor]), - b: 0, - t: 0 - }); +function getYanchor(opts) { + return Lib.isBottomAnchor(opts) ? 'bottom' : + Lib.isMiddleAnchor(opts) ? 'middle' : + 'top'; } diff --git a/src/components/legend/get_legend_data.js b/src/components/legend/get_legend_data.js index 0e9c4599d63..414ccee21f1 100644 --- a/src/components/legend/get_legend_data.js +++ b/src/components/legend/get_legend_data.js @@ -17,13 +17,14 @@ module.exports = function getLegendData(calcdata, opts) { var hasOneNonBlankGroup = false; var slicesShown = {}; var lgroupi = 0; + var maxNameLength = 0; var i, j; function addOneItem(legendGroup, legendItem) { // each '' legend group is treated as a separate group if(legendGroup === '' || !helpers.isGrouped(opts)) { - var uniqueGroup = '~~i' + lgroupi; // TODO: check this against fullData legendgroups? - + // TODO: check this against fullData legendgroups? + var uniqueGroup = '~~i' + lgroupi; lgroups.push(uniqueGroup); lgroupToTraces[uniqueGroup] = [[legendItem]]; lgroupi++; @@ -31,7 +32,9 @@ module.exports = function getLegendData(calcdata, opts) { lgroups.push(legendGroup); hasOneNonBlankGroup = true; lgroupToTraces[legendGroup] = [[legendItem]]; - } else lgroupToTraces[legendGroup].push([legendItem]); + } else { + lgroupToTraces[legendGroup].push([legendItem]); + } } // build an { legendgroup: [cd0, cd0], ... } object @@ -59,9 +62,13 @@ module.exports = function getLegendData(calcdata, opts) { }); slicesShown[lgroup][labelj] = true; + maxNameLength = Math.max(maxNameLength, (labelj || '').length); } } - } else addOneItem(lgroup, cd0); + } else { + addOneItem(lgroup, cd0); + maxNameLength = Math.max(maxNameLength, (trace.name || '').length); + } } // won't draw a legend in this case @@ -90,7 +97,10 @@ module.exports = function getLegendData(calcdata, opts) { lgroupsLength = 1; } - // needed in repositionLegend + // number of legend groups - needed in legend/draw.js opts._lgroupsLength = lgroupsLength; + // maximum name/label length - needed in legend/draw.js + opts._maxNameLength = maxNameLength; + return legendData; }; diff --git a/src/plots/layout_attributes.js b/src/plots/layout_attributes.js index e88c9df39df..e3fecbc40c8 100644 --- a/src/plots/layout_attributes.js +++ b/src/plots/layout_attributes.js @@ -225,7 +225,12 @@ module.exports = { valType: 'boolean', role: 'info', dflt: true, - editType: 'plot' + editType: 'plot', + description: [ + 'Turns on/off margin expansion computations.', + 'Legends, colorbars, updatemenus, sliders, axis rangeselector and rangeslider', + 'are allowed to push the margins by defaults.' + ].join(' ') }, editType: 'plot' }, @@ -417,7 +422,7 @@ module.exports = { description: [ 'Assigns extra meta information that can be used in various `text` attributes.', 'Attributes such as the graph, axis and colorbar `title.text`, annotation `text`', - '`trace.name` in legend items, `rangeselector`, `updatemenues` and `sliders` `label` text', + '`trace.name` in legend items, `rangeselector`, `updatemenus` and `sliders` `label` text', 'all support `meta`. One can access `meta` fields using template strings:', '`%{meta[i]}` where `i` is the index of the `meta`', 'item in question.', diff --git a/src/plots/plots.js b/src/plots/plots.js index c9aa68b131d..b34c025d72f 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1839,8 +1839,14 @@ plots.autoMargin = function(gd, id, o) { // if the item is too big, just give it enough automargin to // make sure you can still grab it and bring it back - if(o.l + o.r > fullLayout.width * 0.5) o.l = o.r = 0; - if(o.b + o.t > fullLayout.height * 0.5) o.b = o.t = 0; + if(o.l + o.r > fullLayout.width * 0.5) { + Lib.log('Margin push', id, 'is too big in x, dropping'); + o.l = o.r = 0; + } + if(o.b + o.t > fullLayout.height * 0.5) { + Lib.log('Margin push', id, 'is too big in y, dropping'); + o.b = o.t = 0; + } var xl = o.xl !== undefined ? o.xl : o.x; var xr = o.xr !== undefined ? o.xr : o.x; @@ -1857,7 +1863,7 @@ plots.autoMargin = function(gd, id, o) { } if(!fullLayout._replotting) { - plots.doAutoMargin(gd); + return plots.doAutoMargin(gd); } } }; diff --git a/test/image/baselines/legend_horizontal_bg_fit.png b/test/image/baselines/legend_horizontal_bg_fit.png new file mode 100644 index 00000000000..5e5023d4756 Binary files /dev/null and b/test/image/baselines/legend_horizontal_bg_fit.png differ diff --git a/test/image/baselines/legend_horizontal_wrap-alll-lines.png b/test/image/baselines/legend_horizontal_wrap-alll-lines.png index 116dea894e6..f4a894d170d 100644 Binary files a/test/image/baselines/legend_horizontal_wrap-alll-lines.png and b/test/image/baselines/legend_horizontal_wrap-alll-lines.png differ diff --git a/test/image/baselines/legend_margin-autoexpand-false.png b/test/image/baselines/legend_margin-autoexpand-false.png new file mode 100644 index 00000000000..48f3ca3bc6b Binary files /dev/null and b/test/image/baselines/legend_margin-autoexpand-false.png differ diff --git a/test/image/baselines/legend_negative_x.png b/test/image/baselines/legend_negative_x.png new file mode 100644 index 00000000000..e137976024d Binary files /dev/null and b/test/image/baselines/legend_negative_x.png differ diff --git a/test/image/baselines/legend_negative_x2.png b/test/image/baselines/legend_negative_x2.png new file mode 100644 index 00000000000..e5cb3665934 Binary files /dev/null and b/test/image/baselines/legend_negative_x2.png differ diff --git a/test/image/baselines/legend_scroll_beyond_plotarea.png b/test/image/baselines/legend_scroll_beyond_plotarea.png new file mode 100644 index 00000000000..c79c8c43c65 Binary files /dev/null and b/test/image/baselines/legend_scroll_beyond_plotarea.png differ diff --git a/test/image/baselines/legend_small_horizontal.png b/test/image/baselines/legend_small_horizontal.png new file mode 100644 index 00000000000..c79795d2ac2 Binary files /dev/null and b/test/image/baselines/legend_small_horizontal.png differ diff --git a/test/image/baselines/legend_small_vertical.png b/test/image/baselines/legend_small_vertical.png new file mode 100644 index 00000000000..ba4a4deec6a Binary files /dev/null and b/test/image/baselines/legend_small_vertical.png differ diff --git a/test/image/baselines/legend_x_push_margin_constrained.png b/test/image/baselines/legend_x_push_margin_constrained.png new file mode 100644 index 00000000000..56e23a7c5c0 Binary files /dev/null and b/test/image/baselines/legend_x_push_margin_constrained.png differ diff --git a/test/image/baselines/legendgroup_horizontal_bg_fit.png b/test/image/baselines/legendgroup_horizontal_bg_fit.png new file mode 100644 index 00000000000..cb77b06f3c7 Binary files /dev/null and b/test/image/baselines/legendgroup_horizontal_bg_fit.png differ diff --git a/test/image/baselines/legendgroup_horizontal_wrapping.png b/test/image/baselines/legendgroup_horizontal_wrapping.png index 19e410a63b4..e81c0358b5c 100644 Binary files a/test/image/baselines/legendgroup_horizontal_wrapping.png and b/test/image/baselines/legendgroup_horizontal_wrapping.png differ diff --git a/test/image/mocks/legend_horizontal_bg_fit.json b/test/image/mocks/legend_horizontal_bg_fit.json new file mode 100644 index 00000000000..d2995f4fd33 --- /dev/null +++ b/test/image/mocks/legend_horizontal_bg_fit.json @@ -0,0 +1,18 @@ +{ + "data": [ + { "y": [ 1 ], "name": "AAAAAAAAAAAAaaaaaaa" }, + { "y": [ 2 ], "name": "BB" }, + { "y": [ 3 ], "name": "C" }, + { "y": [ 4 ], "name": "D" }, + { "y": [ 5 ], "name": "E" }, + { "y": [ 6 ], "name": "F" }, + { "y": [ 7 ], "name": "G" } + ], + "layout": { + "legend": { + "bgcolor": "red", + "orientation": "h" + }, + "width": 600 + } +} diff --git a/test/image/mocks/legend_margin-autoexpand-false.json b/test/image/mocks/legend_margin-autoexpand-false.json new file mode 100644 index 00000000000..d7b92118260 --- /dev/null +++ b/test/image/mocks/legend_margin-autoexpand-false.json @@ -0,0 +1,19 @@ +{ + "data": [ + { + "labels": [ + "hello", + "this is a very long string and bad things are likely to happen", + "short string", + "moderately long string" + ], + "values": [4, 4, 4, 4], + "type": "pie" + } + ], + "layout": { + "width": 400, + "height": 400, + "margin": {"autoexpand": false} + } +} diff --git a/test/image/mocks/legend_negative_x.json b/test/image/mocks/legend_negative_x.json new file mode 100644 index 00000000000..7109c7ee462 --- /dev/null +++ b/test/image/mocks/legend_negative_x.json @@ -0,0 +1,51 @@ +{ + "data": [{ + "x": [0, 1, 2, 3, 4, 5, 6, 7, 8], + "y": [0, 3, 6, 4, 5, 2, 3, 5, 4], + "type": "scatter" + }, + { + "x": [0, 1, 2, 3, 4, 5, 6, 7, 8], + "y": [0, 4, 7, 8, 3, 6, 3, 3, 4], + "type": "scatter" + }, + { + "x": [0, 1, 2, 3, 4, 5, 6, 7, 8], + "y": [0, 4, 7, 8, 3, 6, 3, 3, 4], + "type": "scatter" + }, + { + "x": [0, 1, 2, 3, 4, 5, 6, 7, 8], + "y": [0, 4, 7, 8, 3, 6, 3, 3, 4], + "type": "scatter" + }, + { + "x": [0, 1, 2, 3, 4, 5, 6, 7, 8], + "y": [0, 4, 7, 8, 3, 6, 3, 3, 4], + "type": "scatter" + }, + { + "x": [0, 1, 2, 3, 4, 5, 6, 7, 8], + "y": [0, 4, 7, 8, 3, 6, 3, 3, 4], + "type": "scatter" + } + ], + "layout": { + "showlegend": true, + "legend": { + "orientation": "h", + "traceorder": "reversed", + "x": -0.5, + "y": 1.2 + }, + "margin": { + "l": 125, + "r": 5, + "b": 30, + "t": 20, + "pad": 0 + }, + "height": 350, + "width": 700 + } +} diff --git a/test/image/mocks/legend_negative_x2.json b/test/image/mocks/legend_negative_x2.json new file mode 100644 index 00000000000..732b6398f34 --- /dev/null +++ b/test/image/mocks/legend_negative_x2.json @@ -0,0 +1,28 @@ +{ + "data": [ + { + "x": [0, 1, 2, 3, 4, 5, 6, 7, 8], + "y": [0, 3, 6, 4, 5, 2, 3, 5, 4], + "name": "Plot" + }, + { + "x": [0, 1, 2, 3, 4, 5, 6, 7, 8], + "y": [0, 4, 7, 8, 3, 6, 3, 3, 4], + "name": "Plot Plot" + }, + { + "x": [0, 1, 2, 3, 4, 5, 6, 7, 8], + "y": [0, 5, 3, 10, 5.33, 2.24, 4.4, 5.1, 7.2], + "name": "Plot Plot Plot" + } + ], + "layout": { + "paper_bgcolor": "pink", + "legend": { + "orientation": "h", + "x": -0.4, + "y": -0.1 + }, + "width": 400 + } +} diff --git a/test/image/mocks/legend_scroll_beyond_plotarea.json b/test/image/mocks/legend_scroll_beyond_plotarea.json new file mode 100644 index 00000000000..d487abb4b09 --- /dev/null +++ b/test/image/mocks/legend_scroll_beyond_plotarea.json @@ -0,0 +1,126 @@ +{ + "data": [ + { + "x": [1, 2, 3], + "y": [44, 88, 132], + "name": "trace with wide trace name 0" + }, + { + "x": [1, 2, 3], + "y": [88, 176, 264], + "name": "trace with wide trace name 1", + "yaxis": "y2" + }, + { + "x": [1, 2, 3], + "y": [132, 264, 396.00000000000006], + "name": "trace with wide trace name 2" + }, + { + "x": [1, 2, 3], + "y": [176, 352, 528], + "name": "trace with wide trace name 3", + "yaxis": "y2", + "visible": "legendonly" + }, + { + "x": [1, 2, 3], + "y": [220, 440, 660], + "name": "trace with wide trace name 4", + "visible": "legendonly" + }, + { + "x": [1, 2, 3], + "y": [264, 528, 792], + "name": "trace with wide trace name 5", + "yaxis": "y2", + "visible": "legendonly" + }, + { + "x": [1, 2, 3], + "y": [308, 616, 923.9999999999999], + "name": "trace with wide trace name 6", + "visible": "legendonly" + }, + { + "x": [1, 2, 3], + "y": [351.99999999999994, 703.9999999999999, 1055.9999999999998], + "name": "trace with wide trace name 7", + "yaxis": "y2", + "visible": "legendonly" + }, + { + "x": [1, 2, 3], + "y": [395.99999999999994, 791.9999999999999, 1187.9999999999998], + "name": "trace with wide trace name 8", + "visible": "legendonly" + }, + { + "x": [1, 2, 3], + "y": [439.99999999999994, 879.9999999999999, 1319.9999999999998], + "name": "trace with wide trace name 9", + "yaxis": "y2", + "visible": "legendonly" + }, + { + "x": [1, 2, 3], + "y": [483.9999999999999, 967.9999999999998, 1451.9999999999998], + "name": "trace with wide trace name 10", + "visible": "legendonly" + }, + { + "x": [1, 2, 3], + "y": [527.9999999999999, 1055.9999999999998, 1583.9999999999998], + "name": "trace with wide trace name 11", + "yaxis": "y2", + "visible": "legendonly" + }, + { + "x": [1, 2, 3], + "y": [571.9999999999999, 1143.9999999999998, 1715.9999999999995], + "name": "trace with wide trace name 12", + "visible": "legendonly" + }, + { + "x": [1, 2, 3], + "y": [615.9999999999999, 1231.9999999999998, 1847.9999999999995], + "name": "trace with wide trace name 13", + "yaxis": "y2", + "visible": "legendonly" + }, + { + "x": [1, 2, 3], + "y": [659.9999999999999, 1319.9999999999998, 1979.9999999999995], + "name": "trace with wide trace name 14", + "visible": "legendonly" + }, + { + "x": [1, 2, 3], + "y": [703.9999999999999, 1407.9999999999998, 2111.9999999999995], + "name": "trace with wide trace name 15", + "yaxis": "y2", + "visible": "legendonly" + } + ], + "layout": { + "legend": { + "xanchor": "left", + "yanchor": "top", + "y": 0, + "x": 2, + "orientation": "h", + "bgcolor": "rgb(200, 200, 200)" + }, + "xaxis": { + "title": { "text": "X Axis" } + }, + "yaxis": { + "title": { "text": "Count#" } + }, + "yaxis2": { + "title": { "text": "Y-2Axis" }, + "overlaying": "y", + "side": "right" + } + } +} diff --git a/test/image/mocks/legend_small_horizontal.json b/test/image/mocks/legend_small_horizontal.json new file mode 100644 index 00000000000..2480d5b553d --- /dev/null +++ b/test/image/mocks/legend_small_horizontal.json @@ -0,0 +1,104 @@ +{ +"layout": { + "legend": { + "orientation": "h", + "bordercolor": "#000000", + "borderwidth": 1, + "bgcolor": "#ffffff00" + }, + "margin": { + "t": 25, + "b": 25, + "r": 25, + "l": 25 + }, + "width":500, + "height":200 +}, +"data": [ + { + "x": [1, 2, 3, 4], + "y": [63.69, 62.55, 61.64, 61.39] + }, { + "x": [1, 2, 3, 4], + "y": [58.24, 54.93, 42.11, 50.75] + }, { + "x": [1, 2, 3, 4], + "y": [51.49, 49.59, 37.12, 31.45] + }, { + "x": [1, 2, 3, 4], + "y": [49.09, 58.54, 53.91, 43.12] + }, { + "x": [1, 2, 3, 4], + "y": [70.53, 72.51, 72.28, 78.65] + }, { + "x": [1, 2, 3, 4], + "y": [62.69, 59.09, 63.82, 62] + }, { + "x": [1, 2, 3, 4], + "y": [76.27, 71.43, 59.83, 64.34] + }, { + "x": [1, 2, 3, 4], + "y": [71.15, 81.82, 88.46, 74.29] + }, { + "x": [1, 2, 3, 4], + "y": [57.89, 57.38, 52.08, 63.83] + }, { + "x": [1, 2, 3, 4], + "y": [65.4, 63.27, 65.78, 64.03] + }, { + "x": [1, 2, 3, 4], + "y": [58.24, 54.93, 42.11, 50.75] + }, { + "x": [1, 2, 3, 4], + "y": [51.49, 49.59, 37.12, 31.45] + }, { + "x": [1, 2, 3, 4], + "y": [49.09, 58.54, 53.91, 43.12] + }, { + "x": [1, 2, 3, 4], + "y": [70.53, 72.51, 72.28, 78.65] + }, { + "x": [1, 2, 3, 4], + "y": [62.69, 59.09, 63.82, 62] + }, { + "x": [1, 2, 3, 4], + "y": [76.27, 71.43, 59.83, 64.34] + }, { + "x": [1, 2, 3, 4], + "y": [71.15, 81.82, 88.46, 74.29] + }, { + "x": [1, 2, 3, 4], + "y": [57.89, 57.38, 52.08, 63.83] + }, { + "x": [1, 2, 3, 4], + "y": [65.4, 63.27, 65.78, 64.03] + }, { + "x": [1, 2, 3, 4], + "y": [58.24, 54.93, 42.11, 50.75] + }, { + "x": [1, 2, 3, 4], + "y": [51.49, 49.59, 37.12, 31.45] + }, { + "x": [1, 2, 3, 4], + "y": [49.09, 58.54, 53.91, 43.12] + }, { + "x": [1, 2, 3, 4], + "y": [70.53, 72.51, 72.28, 78.65] + }, { + "x": [1, 2, 3, 4], + "y": [62.69, 59.09, 63.82, 62] + }, { + "x": [1, 2, 3, 4], + "y": [76.27, 71.43, 59.83, 64.34] + }, { + "x": [1, 2, 3, 4], + "y": [71.15, 81.82, 88.46, 74.29] + }, { + "x": [1, 2, 3, 4], + "y": [57.89, 57.38, 52.08, 63.83] + }, { + "x": [1, 2, 3, 4], + "y": [65.4, 63.27, 65.78, 64.03] + }] +} diff --git a/test/image/mocks/legend_small_vertical.json b/test/image/mocks/legend_small_vertical.json new file mode 100644 index 00000000000..78b5b1d6ed0 --- /dev/null +++ b/test/image/mocks/legend_small_vertical.json @@ -0,0 +1,104 @@ +{ +"layout": { + "legend": { + "orientation": "v", + "bordercolor": "#000000", + "borderwidth": 1, + "bgcolor": "#ffffff00" + }, + "margin": { + "t": 25, + "b": 25, + "r": 25, + "l": 25 + }, + "width":200, + "height":500 +}, +"data": [ + { + "x": [1, 2, 3, 4], + "y": [63.69, 62.55, 61.64, 61.39] + }, { + "x": [1, 2, 3, 4], + "y": [58.24, 54.93, 42.11, 50.75] + }, { + "x": [1, 2, 3, 4], + "y": [51.49, 49.59, 37.12, 31.45] + }, { + "x": [1, 2, 3, 4], + "y": [49.09, 58.54, 53.91, 43.12] + }, { + "x": [1, 2, 3, 4], + "y": [70.53, 72.51, 72.28, 78.65] + }, { + "x": [1, 2, 3, 4], + "y": [62.69, 59.09, 63.82, 62] + }, { + "x": [1, 2, 3, 4], + "y": [76.27, 71.43, 59.83, 64.34] + }, { + "x": [1, 2, 3, 4], + "y": [71.15, 81.82, 88.46, 74.29] + }, { + "x": [1, 2, 3, 4], + "y": [57.89, 57.38, 52.08, 63.83] + }, { + "x": [1, 2, 3, 4], + "y": [65.4, 63.27, 65.78, 64.03] + }, { + "x": [1, 2, 3, 4], + "y": [58.24, 54.93, 42.11, 50.75] + }, { + "x": [1, 2, 3, 4], + "y": [51.49, 49.59, 37.12, 31.45] + }, { + "x": [1, 2, 3, 4], + "y": [49.09, 58.54, 53.91, 43.12] + }, { + "x": [1, 2, 3, 4], + "y": [70.53, 72.51, 72.28, 78.65] + }, { + "x": [1, 2, 3, 4], + "y": [62.69, 59.09, 63.82, 62] + }, { + "x": [1, 2, 3, 4], + "y": [76.27, 71.43, 59.83, 64.34] + }, { + "x": [1, 2, 3, 4], + "y": [71.15, 81.82, 88.46, 74.29] + }, { + "x": [1, 2, 3, 4], + "y": [57.89, 57.38, 52.08, 63.83] + }, { + "x": [1, 2, 3, 4], + "y": [65.4, 63.27, 65.78, 64.03] + }, { + "x": [1, 2, 3, 4], + "y": [58.24, 54.93, 42.11, 50.75] + }, { + "x": [1, 2, 3, 4], + "y": [51.49, 49.59, 37.12, 31.45] + }, { + "x": [1, 2, 3, 4], + "y": [49.09, 58.54, 53.91, 43.12] + }, { + "x": [1, 2, 3, 4], + "y": [70.53, 72.51, 72.28, 78.65] + }, { + "x": [1, 2, 3, 4], + "y": [62.69, 59.09, 63.82, 62] + }, { + "x": [1, 2, 3, 4], + "y": [76.27, 71.43, 59.83, 64.34] + }, { + "x": [1, 2, 3, 4], + "y": [71.15, 81.82, 88.46, 74.29] + }, { + "x": [1, 2, 3, 4], + "y": [57.89, 57.38, 52.08, 63.83] + }, { + "x": [1, 2, 3, 4], + "y": [65.4, 63.27, 65.78, 64.03] + }] +} diff --git a/test/image/mocks/legend_x_push_margin_constrained.json b/test/image/mocks/legend_x_push_margin_constrained.json new file mode 100644 index 00000000000..cd707635a58 --- /dev/null +++ b/test/image/mocks/legend_x_push_margin_constrained.json @@ -0,0 +1,20 @@ +{ + "data": [ + { + "x": [ 1, 2, 3, 4, 5 ], + "y": [ 1, 2, 4, 8, 16 ], + "name": "Kinda long data name" + } + ], + "layout": { + "margin": {"t": 0}, + "showlegend": true, + "legend": { + "x": 1, + "y": 0.5, + "xanchor": "left" + }, + "width": 381, + "height": 250 + } +} diff --git a/test/image/mocks/legendgroup_horizontal_bg_fit.json b/test/image/mocks/legendgroup_horizontal_bg_fit.json new file mode 100644 index 00000000000..70ae047e0dd --- /dev/null +++ b/test/image/mocks/legendgroup_horizontal_bg_fit.json @@ -0,0 +1,26 @@ +{ + "data": [ + { + "y": [ 1, 2, 1 ], + "name": "country=Canada", + "legendgroup": "country=Canada" + }, + { + "y": [ 2, 1, 2 ], + "name": "country=Mexico", + "legendgroup": "country=Mexico" + }, + { + "y": [ 3, 0, 4 ], + "name": "country=United States", + "legendgroup": "country=United States" + } + ], + "layout": { + "legend": { + "bgcolor": "red", + "orientation": "h" + }, + "width": 700 + } +} diff --git a/test/jasmine/tests/legend_test.js b/test/jasmine/tests/legend_test.js index 8e833191150..39c3ec695ba 100644 --- a/test/jasmine/tests/legend_test.js +++ b/test/jasmine/tests/legend_test.js @@ -591,9 +591,7 @@ describe('legend anchor utils:', function() { }); describe('legend relayout update', function() { - 'use strict'; var gd; - var mock = require('@mocks/0.json'); beforeEach(function() { gd = createGraphDiv(); @@ -601,7 +599,7 @@ describe('legend relayout update', function() { afterEach(destroyGraphDiv); it('should hide and show the legend', function(done) { - var mockCopy = Lib.extendDeep({}, mock, {layout: { + var mockCopy = Lib.extendDeep({}, require('@mocks/0.json'), {layout: { legend: {x: 1.1, xanchor: 'left'}, margin: {l: 50, r: 50, pad: 0}, width: 500 @@ -633,7 +631,7 @@ describe('legend relayout update', function() { }); it('should update border styling', function(done) { - var mockCopy = Lib.extendDeep({}, mock); + var mockCopy = Lib.extendDeep({}, require('@mocks/0.json')); function assertLegendStyle(bgColor, borderColor, borderWidth) { var node = d3.select('g.legend').select('rect').node(); @@ -670,21 +668,13 @@ describe('legend relayout update', function() { }); describe('should update legend valign', function() { - var mock = require('@mocks/legend_valign_top.json'); - var gd; - - beforeEach(function() { - gd = createGraphDiv(); - }); - afterEach(destroyGraphDiv); - function markerOffsetY() { var translate = Drawing.getTranslate(d3.select('.legend .traces .layers')); return translate.y; } it('it should translate markers', function(done) { - var mockCopy = Lib.extendDeep({}, mock); + var mockCopy = Lib.extendDeep({}, require('@mocks/legend_valign_top.json')); var top, middle, bottom; Plotly.plot(gd, mockCopy.data, mockCopy.layout) @@ -707,32 +697,52 @@ describe('legend relayout update', function() { }); describe('with legendgroup', function() { - var mock = require('@mocks/legendgroup_horizontal_wrapping.json'); - var gd; - - beforeEach(function() { - gd = createGraphDiv(); - }); - afterEach(destroyGraphDiv); - it('changes the margin size to fit tracegroupgap', function(done) { - var mockCopy = Lib.extendDeep({}, mock); + var mockCopy = Lib.extendDeep({}, require('@mocks/legendgroup_horizontal_wrapping.json')); Plotly.newPlot(gd, mockCopy) .then(function() { - expect(gd._fullLayout._size.b).toBe(130); + expect(gd._fullLayout._size.b).toBe(113); return Plotly.relayout(gd, 'legend.tracegroupgap', 70); }) .then(function() { - expect(gd._fullLayout._size.b).toBe(185); + expect(gd._fullLayout._size.b).toBe(167); return Plotly.relayout(gd, 'legend.tracegroupgap', 10); }) .then(function() { - expect(gd._fullLayout._size.b).toBe(130); + expect(gd._fullLayout._size.b).toBe(113); }) .catch(failTest) .then(done); }); }); + + it('should make legend fit in graph viewport', function(done) { + var fig = Lib.extendDeep({}, require('@mocks/legend_negative_x.json')); + + function _assert(msg, xy, wh) { + return function() { + var fullLayout = gd._fullLayout; + var legend3 = d3.select('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.plot(gd, fig) + .then(_assert('base', [5, 4.4], [512, 29])) + .then(function() { return Plotly.relayout(gd, 'legend.x', 0.8); }) + .then(_assert('after relayout almost to right edge', [188, 4.4], [512, 29])) + .catch(failTest) + .then(done); + }); }); describe('legend orientation change:', function() { @@ -788,7 +798,7 @@ describe('legend restyle update', function() { expect(node.attr('height')).toEqual('19'); var w = +node.attr('width'); - expect(Math.abs(w - 160)).toBeLessThan(10); + expect(w).toBeWithin(113, 10); }); } diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index af102e82662..8c4e18be088 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -945,6 +945,7 @@ describe('Test splom interactions:', function() { it('@gl should clear graph and replot when canvas and WebGL context dimensions do not match', function(done) { var fig = Lib.extendDeep({}, require('@mocks/splom_iris.json')); + fig.layout.showlegend = false; function assertDims(msg, w, h) { var canvas = gd._fullLayout._glcanvas;