Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions draftlogs/7925_add.md
Original file line number Diff line number Diff line change
@@ -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)]
11 changes: 11 additions & 0 deletions src/components/legend/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
1 change: 1 addition & 0 deletions src/components/legend/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ function groupDefaults(legendId, layoutIn, layoutOut, fullData, legendCount) {
coerce('indentation');
coerce('itemsizing');
coerce('itemwidth');
coerce('itemheight');

coerce('itemclick');
coerce('itemdoubleclick');
Expand Down
2 changes: 1 addition & 1 deletion src/components/legend/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/components/legend/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);
Expand Down
148 changes: 147 additions & 1 deletion test/jasmine/tests/legend_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' }]);

Expand Down Expand Up @@ -3498,4 +3513,135 @@ describe('legend title click', function() {
}
}).then(done, done.fail);
});
});
});
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);
});
});
7 changes: 7 additions & 0 deletions test/plot-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down