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)], with thanks to @MannXo for the contribution!
13 changes: 13 additions & 0 deletions src/components/legend/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

var fontAttrs = require('../../plots/font_attributes');
var colorAttrs = require('../color/attributes');
const { dfltFillHeight } = require('./constants');


module.exports = {
Expand Down Expand Up @@ -141,6 +142,18 @@ 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: dfltFillHeight,
dflt: dfltFillHeight,
editType: 'legend',
description: [
'Sets the height (in px) of the legend item fill swatch.',
'A taller swatch shows more of a trace *fill* or *fillpattern*.',
'Only traces with a *fill* use this value.',
'Every legend item grows to fit, so a larger value makes every row taller.'
].join(' ')
},
itemclick: {
valType: 'enumerated',
values: ['toggle', 'toggleothers', false],
Expand Down
13 changes: 11 additions & 2 deletions src/components/legend/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,16 @@ export const scrollBarColor = '#808BA4';
export const scrollBarMargin = 4;
export const scrollBarEnterAttrs = { rx: 20, ry: 3, width: 0, height: 0 } as const;

// Number of px between legend title and (left) side of legend (always in x direction and from inner border)
/** Number of px between legend title and (left) side of legend (always in x direction and from inner border) */
export const titlePad = 2;
// Number of px between each legend item (x and/or y direction)
/** Number of px between each legend item (x and/or y direction) */
export const itemGap = 5;
/** Height (in px) of the legend fill swatch at the default `itemheight` */
export const dfltFillHeight = 6;
/** Minimum height (in px) of a legend item, so that the largest legend marker (16px across) fits */
export const itemMinHeight = 16;
/**
* Number of px added to the height of each legend item.
* Items stack with no other gap, so this value is the vertical space between them.
*/
export const itemHeightPad = 3;
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
4 changes: 3 additions & 1 deletion src/components/legend/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -824,7 +824,9 @@ function computeTextDimensions(g, gd, legendObj, aTitle) {
legendObj._titleHeight = height;
} else { // legend item
legendItem.lineHeight = lineHeight;
legendItem.height = Math.max(height, 16) + 3;
// A taller fill swatch keeps the space that the default row gives the default swatch
const fillItemHeight = constants.itemMinHeight + legendObj.itemheight - constants.dfltFillHeight;
legendItem.height = Math.max(height, constants.itemMinHeight, fillItemHeight) + constants.itemHeightPad;
legendItem.width = width;
}
}
Expand Down
10 changes: 9 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,14 @@ 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);

// Grow a tall fill swatch up and down by the same amount, so the space above and below the symbol
// stays as in the default row. The line and the markers move with the fill to stay on its top edge.
const fillShiftY = showFill || showGradientFill ? (constants.dfltFillHeight - itemHeight) / 2 : 0;
this3
.selectAll('g.legendfill, g.legendlines, g.legendsymbols')
.attr('transform', fillShiftY ? strTranslate(0, fillShiftY) : null);

if (showLine || showGradientLine) {
var lw = boundLineWidth(undefined, trace.line, MAX_LINE_WIDTH, CST_LINE_WIDTH);
Expand Down
6 changes: 6 additions & 0 deletions src/types/generated/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12758,6 +12758,12 @@ export interface Legend {
* @default 'toggleothers'
*/
itemdoubleclick?: 'toggle' | 'toggleothers' | false;
/**
* Sets the height (in px) of the legend item fill swatch. A taller swatch shows more of a trace *fill* or *fillpattern*. Only traces with a *fill* use this value. Every legend item grows to fit, so a larger value makes every row taller.
* @default 6
* Minimum: 6
*/
itemheight?: number;
/**
* Determines if the legend items symbols scale with their corresponding *trace* attributes or remain *constant* independent of the symbol size on the graph.
* @default 'trace'
Expand Down
194 changes: 194 additions & 0 deletions 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 @@ -3590,3 +3605,182 @@ 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('keeps the line on the top edge of a taller 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]);
// itemMinHeight + itemheight - dfltFillHeight + itemHeightPad = 16 + 40 - 6 + 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('keeps the default space above and below a taller symbol', (done) => {
// Space between the row edges and the symbol (fill, line, and marker) of each legend item
const symbolGaps = () =>
[...gd.querySelectorAll('g.traces')].map((item) => {
const row = item.querySelector('rect.legendtoggle').getBoundingClientRect();
const parts = [
...item.querySelectorAll('g.legendfill path, g.legendlines path, g.legendpoints path')
].map((el) => el.getBoundingClientRect());
return {
above: Math.min(...parts.map((r) => r.top)) - row.top,
below: row.bottom - Math.max(...parts.map((r) => r.bottom))
};
});
let dfltGaps;

Plotly.newPlot(gd, filled, { showlegend: true })
.then(() => {
dfltGaps = symbolGaps();
return Plotly.relayout(gd, 'legend.itemheight', 40);
})
.then(() => {
const gaps = symbolGaps();
expect(gaps.length).toBe(dfltGaps.length);
gaps.forEach((gap, i) => {
expect(gap.above).toBeCloseTo(dfltGaps[i].above, 1);
expect(gap.below).toBeCloseTo(dfltGaps[i].below, 1);
});
})
.then(done, done.fail);
});

it('keeps the default swatch in unified hover labels', (done) => {
Plotly.newPlot(gd, filled, {
showlegend: true,
hovermode: 'x unified',
legend: { itemheight: 18 }
})
.then(() => {
Plotly.Fx.hover(gd, { xval: 1 });
Lib.clearThrottle();

// The hover legend does not inherit layout.legend, the same as for itemwidth
const hoverFills = [...gd.querySelectorAll('g.hoverlayer g.legendfill path')];
expect(hoverFills.length).toBe(2);
hoverFills.forEach((fillPath) => {
expect(fillPath.getAttribute('d')).toBe('M5,0h30v6h-30z');
});

const legendFills = [...gd.querySelectorAll('g.infolayer g.legendfill path')];
expect(legendFills.length).toBe(2);
legendFills.forEach((fillPath) => {
expect(fillPath.getAttribute('d')).toBe('M5,0h30v18h-30z');
});
})
.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 @@ -3470,6 +3470,13 @@
false
]
},
"itemheight": {
"description": "Sets the height (in px) of the legend item fill swatch. A taller swatch shows more of a trace *fill* or *fillpattern*. Only traces with a *fill* use this value. Every legend item grows to fit, so a larger value makes every row taller.",
"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