diff --git a/draftlogs/7925_add.md b/draftlogs/7925_add.md new file mode 100644 index 00000000000..aa89858fb6b --- /dev/null +++ b/draftlogs/7925_add.md @@ -0,0 +1 @@ +- Add `layout.legend.itemheight` to set the height of the legend fill swatch, so more of a trace `fillpattern` is visible in the legend [[#7925](https://github.com/plotly/plotly.js/pull/7925)] diff --git a/src/components/legend/attributes.js b/src/components/legend/attributes.js index 3fb8e5781e9..39935993797 100644 --- a/src/components/legend/attributes.js +++ b/src/components/legend/attributes.js @@ -141,6 +141,17 @@ module.exports = { editType: 'legend', description: 'Sets the width (in px) of the legend item symbols (the part other than the title.text).', }, + itemheight: { + valType: 'number', + min: 6, + dflt: 6, + editType: 'legend', + description: [ + 'Sets the height (in px) of the legend item fill swatch.', + 'Increasing it reveals more of a trace *fill* or *fillpattern* in the legend,', + 'and grows the legend item to fit.', + ].join(' '), + }, itemclick: { valType: 'enumerated', values: ['toggle', 'toggleothers', false], diff --git a/src/components/legend/defaults.js b/src/components/legend/defaults.js index eb8a6212a0f..b038c5b19f4 100644 --- a/src/components/legend/defaults.js +++ b/src/components/legend/defaults.js @@ -219,6 +219,7 @@ function groupDefaults(legendId, layoutIn, layoutOut, fullData, legendCount) { coerce('indentation'); coerce('itemsizing'); coerce('itemwidth'); + coerce('itemheight'); coerce('itemclick'); coerce('itemdoubleclick'); diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index cf6439f0cf8..00841d6bcc8 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -826,7 +826,7 @@ function computeTextDimensions(g, gd, legendObj, aTitle) { legendObj._titleHeight = height; } else { // legend item legendItem.lineHeight = lineHeight; - legendItem.height = Math.max(height, 16) + 3; + legendItem.height = Math.max(height, 16, legendObj.itemheight + 10) + 3; legendItem.width = width; } } diff --git a/src/components/legend/style.js b/src/components/legend/style.js index d2b3350c0c8..8c67ee7e17c 100644 --- a/src/components/legend/style.js +++ b/src/components/legend/style.js @@ -26,6 +26,7 @@ module.exports = function style(s, gd, legend) { if (!legend) legend = fullLayout.legend; var constantItemSizing = legend.itemsizing === 'constant'; var itemWidth = legend.itemwidth; + var itemHeight = legend.itemheight; var centerPos = (itemWidth + constants.itemGap * 2) / 2; var centerTransform = strTranslate(centerPos, 0); @@ -139,7 +140,7 @@ module.exports = function style(s, gd, legend) { .data(showFill || showGradientFill ? [d] : []); fill.enter().append('path').classed('js-fill', true); fill.exit().remove(); - fill.attr('d', pathStart + 'h' + itemWidth + 'v6h-' + itemWidth + 'z').call(fillStyle); + fill.attr('d', pathStart + 'h' + itemWidth + 'v' + itemHeight + 'h-' + itemWidth + 'z').call(fillStyle); if (showLine || showGradientLine) { var lw = boundLineWidth(undefined, trace.line, MAX_LINE_WIDTH, CST_LINE_WIDTH); diff --git a/test/jasmine/tests/legend_test.js b/test/jasmine/tests/legend_test.js index c5b252da9b1..0270f38af78 100644 --- a/test/jasmine/tests/legend_test.js +++ b/test/jasmine/tests/legend_test.js @@ -83,6 +83,21 @@ describe('legend defaults', function () { expect(layoutOut.showlegend).toBe(false); }); + it('defaults itemheight to 6 and clamps values below the minimum', function () { + fullData = allShown([{ type: 'scatter' }, { type: 'scatter' }]); + + supplyLayoutDefaults({}, layoutOut, fullData); + expect(layoutOut.legend.itemheight).toBe(6); + + layoutOut = { font: Plots.layoutAttributes.font, bg_color: Plots.layoutAttributes.bg_color }; + supplyLayoutDefaults({ showlegend: true, legend: { itemheight: 1 } }, layoutOut, fullData); + expect(layoutOut.legend.itemheight).toBe(6); + + layoutOut = { font: Plots.layoutAttributes.font, bg_color: Plots.layoutAttributes.bg_color }; + supplyLayoutDefaults({ showlegend: true, legend: { itemheight: 24 } }, layoutOut, fullData); + expect(layoutOut.legend.itemheight).toBe(24); + }); + it('shows with one visible pie', function () { fullData = allShown([{ type: 'pie' }]); @@ -3498,4 +3513,135 @@ describe('legend title click', function() { } }).then(done, done.fail); }); -}); \ No newline at end of file +}); +describe('legend itemheight:', function() { + 'use strict'; + + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + function fillPathD() { + return d3Select(gd).select('g.legendfill').select('path').attr('d'); + } + + function linePathD() { + return d3Select(gd).select('g.legendlines').select('path').attr('d'); + } + + // The toggle rect is sized to the computed row height, see setRect in draw.js + function rowHeights() { + var heights = []; + d3Select(gd).selectAll('rect.legendtoggle').each(function() { + heights.push(+this.getAttribute('height')); + }); + return heights; + } + + var filled = [ + { x: [1, 2], y: [1, 2], fill: 'tozeroy', name: 'a' }, + { x: [1, 2], y: [2, 3], fill: 'tozeroy', name: 'b' } + ]; + + it('reproduces the historical 6px swatch at the default', function(done) { + Plotly.newPlot(gd, filled, { showlegend: true }) + .then(function() { + expect(gd._fullLayout.legend.itemheight).toBe(6); + expect(fillPathD()).toBe('M5,0h30v6h-30z'); + }) + .then(done, done.fail); + }); + + it('grows the fill swatch to the requested height', function(done) { + Plotly.newPlot(gd, filled, { showlegend: true, legend: { itemheight: 24 } }) + .then(function() { + expect(fillPathD()).toBe('M5,0h30v24h-30z'); + }) + .then(done, done.fail); + }); + + it('grows the swatch downwards, leaving the line on top of the fill', function(done) { + var dfltLine; + + Plotly.newPlot(gd, filled, { showlegend: true }) + .then(function() { + dfltLine = linePathD(); + return Plotly.relayout(gd, 'legend.itemheight', 30); + }) + .then(function() { + // the line marks the top edge of the fill, exactly as in the plot itself + expect(linePathD()).toBe(dfltLine); + expect(fillPathD()).toBe('M5,0h30v30h-30z'); + }) + .then(done, done.fail); + }); + + it('does not move the swatch of a trace without fill', function(done) { + var unfilled = [ + { x: [1, 2], y: [1, 2], name: 'a' }, + { x: [1, 2], y: [2, 3], name: 'b' } + ]; + var dfltLine; + + Plotly.newPlot(gd, unfilled, { showlegend: true }) + .then(function() { + dfltLine = linePathD(); + return Plotly.relayout(gd, 'legend.itemheight', 40); + }) + .then(function() { + expect(linePathD()).toBe(dfltLine); + }) + .then(done, done.fail); + }); + + it('grows each legend row so taller swatches do not overlap', function(done) { + var dflt; + + Plotly.newPlot(gd, filled, { showlegend: true }) + .then(function() { + dflt = rowHeights(); + expect(dflt.length).toBe(2); + return Plotly.relayout(gd, 'legend.itemheight', 40); + }) + .then(function() { + var grown = rowHeights(); + expect(grown.length).toBe(dflt.length); + grown.forEach(function(h, i) { + expect(h).toBeGreaterThan(dflt[i]); + // Math.max(textHeight, 16, itemheight + 10) + 3 + expect(h).toBe(53); + }); + }) + .then(done, done.fail); + }); + + it('leaves row heights untouched at the default', function(done) { + var dflt; + + Plotly.newPlot(gd, filled, { showlegend: true }) + .then(function() { + dflt = rowHeights(); + return Plotly.relayout(gd, 'legend.itemheight', 6); + }) + .then(function() { + expect(rowHeights()).toEqual(dflt); + }) + .then(done, done.fail); + }); + + it('applies to unified hover labels without error', function(done) { + Plotly.newPlot(gd, filled, { + showlegend: true, + hovermode: 'x unified', + legend: { itemheight: 18 } + }) + .then(function() { + expect(gd._fullLayout.legend.itemheight).toBe(18); + }) + .then(done, done.fail); + }); +}); diff --git a/test/plot-schema.json b/test/plot-schema.json index 78614b86693..45529f6db85 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -3464,6 +3464,13 @@ false ] }, + "itemheight": { + "description": "Sets the height (in px) of the legend item fill swatch. Increasing it reveals more of a trace *fill* or *fillpattern* in the legend, and grows the legend item to fit.", + "dflt": 6, + "editType": "legend", + "min": 6, + "valType": "number" + }, "itemsizing": { "description": "Determines if the legend items symbols scale with their corresponding *trace* attributes or remain *constant* independent of the symbol size on the graph.", "dflt": "trace",