Skip to content

Commit e7fef65

Browse files
authored
Merge pull request #621 from adobe/clamoureux/legendLabelColumns
2 parents 91c1a89 + da9fa5e commit e7fef65

File tree

8 files changed

+83
-9
lines changed

8 files changed

+83
-9
lines changed

packages/constants/constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ export const DEFAULT_TRANSFORMED_TIME_DIMENSION = `${DEFAULT_TIME_DIMENSION}0`;
4747
export const DEFAULT_TITLE_FONT_WEIGHT = 'bold';
4848
export const DEFAULT_INTERACTION_MODE = 'nearest';
4949

50+
// legend constants
51+
export const DEFAULT_LEGEND_SYMBOL_SIZE = 250;
52+
export const DEFAULT_LEGEND_SYMBOL_WIDTH = 16; // approximate width for square symbols (√250 ≈ 15.8)
53+
export const DEFAULT_LEGEND_COLUMN_PADDING = 20;
54+
export const DEFAULT_LEGEND_LABEL_LIMIT = 184;
55+
5056
// vega data table name
5157
export const TABLE = 'table';
5258
export const FILTERED_TABLE = 'filteredTable';

packages/docs/docs/api/components/Legend.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ The `Legend` component is used to display a legend for the visualization.
6464
<td>labelLimit</td>
6565
<td>number</td>
6666
<td>–</td>
67-
<td>Max width in pixels before truncating a legend label. If not specified, labels will not be truncated.</td>
67+
<td>Max width in pixels before truncating a legend label. If not specified, labels will not be truncated. Influences legend column layout by calculating how many legend items can fit horizontally based on the label width (for top/bottom positioned legends).</td>
6868
</tr>
6969
<tr>
7070
<td>legendLabels</td>

packages/themes/src/spectrumTheme.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import {
1515
DEFAULT_BACKGROUND_COLOR,
1616
DEFAULT_FONT_COLOR,
1717
DEFAULT_FONT_SIZE,
18+
DEFAULT_LEGEND_COLUMN_PADDING,
19+
DEFAULT_LEGEND_LABEL_LIMIT,
20+
DEFAULT_LEGEND_SYMBOL_SIZE,
1821
DEFAULT_SYMBOL_SIZE,
1922
DEFAULT_SYMBOL_STROKE_WIDTH,
2023
ROUNDED_SQUARE_PATH,
@@ -91,20 +94,20 @@ export function getSpectrumVegaConfig(colorScheme: 'light' | 'dark'): Config {
9194
},
9295
background: DEFAULT_BACKGROUND_COLOR,
9396
legend: {
94-
columnPadding: 20,
97+
columnPadding: DEFAULT_LEGEND_COLUMN_PADDING,
9598
labelColor: FONT_COLOR,
9699
labelFont: ADOBE_CLEAN_FONT,
97100
labelFontSize: DEFAULT_FONT_SIZE,
98101
labelFontWeight: 'normal',
99-
labelLimit: 184,
102+
labelLimit: DEFAULT_LEGEND_LABEL_LIMIT,
100103
layout: {
101104
bottom: horizontalLegendLayout,
102105
top: horizontalLegendLayout,
103106
left: verticalLegendLayout,
104107
right: verticalLegendLayout,
105108
},
106109
rowPadding: 8,
107-
symbolSize: 250,
110+
symbolSize: DEFAULT_LEGEND_SYMBOL_SIZE,
108111
symbolType: ROUNDED_SQUARE_PATH,
109112
symbolStrokeColor: gray700,
110113
titleColor: FONT_COLOR,

packages/vega-spec-builder/src/legend/legendSpecBuilder.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ describe('addLegend()', () => {
270270
});
271271
const legend = legendSpec.legends?.[0];
272272
expect(legend?.labelLimit).toBe(300);
273-
expect(legendSpec.legends).toEqual([{ ...defaultLegend, labelLimit: 300, encode: defaultTooltipLegendEncoding }]);
273+
expect(legendSpec.legends).toEqual([{ ...defaultLegend, labelLimit: 300, encode: defaultTooltipLegendEncoding, columns: { signal: 'max(1, floor(width / 336))' } }]);
274274
expect(legendSpec.data).toEqual([defaultLegendAggregateData]);
275275
expect(legendSpec.scales).toEqual([...(defaultSpec.scales || []), defaultLegendEntriesScale]);
276276
});

packages/vega-spec-builder/src/legend/legendSpecBuilder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ const getCategoricalLegend = (facets: Facet[], options: LegendSpecOptions): Lege
209209
orient: position,
210210
title,
211211
encode: getEncodings(facets, options),
212-
columns: getColumns(position),
212+
columns: getColumns(position, labelLimit),
213213
labelLimit,
214214
};
215215
if (titleLimit !== undefined) legend.titleLimit = titleLimit;

packages/vega-spec-builder/src/legend/legendUtils.test.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@
99
* OF ANY KIND, either express or implied. See the License for the specific language
1010
* governing permissions and limitations under the License.
1111
*/
12-
import { FILTERED_TABLE } from '@spectrum-charts/constants';
12+
import { DEFAULT_LEGEND_COLUMN_PADDING, DEFAULT_LEGEND_SYMBOL_WIDTH, FILTERED_TABLE } from '@spectrum-charts/constants';
1313
import { spectrumColors } from '@spectrum-charts/themes';
1414

1515
import { defaultLegendOptions } from './legendTestUtils';
1616
import {
1717
getClickEncodings,
18+
getColumns,
1819
getHiddenSeriesColorRule,
1920
getSymbolEncodings,
2021
getSymbolType,
@@ -128,3 +129,55 @@ describe('getHiddenSeriesColorRule()', () => {
128129
expect(colorRules[0].test).toContain('hiddenSeries');
129130
});
130131
});
132+
133+
describe('getColumns()', () => {
134+
test('should return undefined for left position', () => {
135+
expect(getColumns('left')).toBeUndefined();
136+
});
137+
138+
test('should return undefined for right position', () => {
139+
expect(getColumns('right')).toBeUndefined();
140+
});
141+
142+
test('should return default signal for top position without labelLimit', () => {
143+
expect(getColumns('top')).toEqual({ signal: 'floor(width / 220)' });
144+
});
145+
146+
test('should return default signal for bottom position without labelLimit', () => {
147+
expect(getColumns('bottom')).toEqual({ signal: 'floor(width / 220)' });
148+
});
149+
150+
test('should return default signal when labelLimit is undefined', () => {
151+
expect(getColumns('top', undefined)).toEqual({ signal: 'floor(width / 220)' });
152+
});
153+
154+
test('should return default signal when labelLimit is 0', () => {
155+
expect(getColumns('bottom', 0)).toEqual({ signal: 'floor(width / 220)' });
156+
});
157+
158+
test('should return default signal when labelLimit is negative', () => {
159+
expect(getColumns('top', -10)).toEqual({ signal: 'floor(width / 220)' });
160+
});
161+
162+
test('should calculate columns based on labelLimit when provided', () => {
163+
const labelLimit = 100;
164+
const expectedItemWidth = labelLimit + DEFAULT_LEGEND_SYMBOL_WIDTH + DEFAULT_LEGEND_COLUMN_PADDING;
165+
const expectedSignal = `max(1, floor(width / ${expectedItemWidth}))`;
166+
167+
expect(getColumns('top', labelLimit)).toEqual({ signal: expectedSignal });
168+
});
169+
170+
test('should calculate columns with different labelLimit values', () => {
171+
const labelLimit50 = 50;
172+
const expectedItemWidth50 = 50 + DEFAULT_LEGEND_SYMBOL_WIDTH + DEFAULT_LEGEND_COLUMN_PADDING;
173+
expect(getColumns('bottom', labelLimit50)).toEqual({
174+
signal: `max(1, floor(width / ${expectedItemWidth50}))`
175+
});
176+
177+
const labelLimit200 = 200;
178+
const expectedItemWidth200 = 200 + DEFAULT_LEGEND_SYMBOL_WIDTH + DEFAULT_LEGEND_COLUMN_PADDING; // 200 + 16 + 20 = 236
179+
expect(getColumns('top', labelLimit200)).toEqual({
180+
signal: `max(1, floor(width / ${expectedItemWidth200}))`
181+
});
182+
});
183+
});

packages/vega-spec-builder/src/legend/legendUtils.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import {
2626
import {
2727
COLOR_SCALE,
2828
COMPONENT_NAME,
29+
DEFAULT_LEGEND_COLUMN_PADDING,
30+
DEFAULT_LEGEND_SYMBOL_WIDTH,
2931
DEFAULT_OPACITY_RULE,
3032
FILTERED_TABLE,
3133
HIGHLIGHTED_GROUP,
@@ -61,10 +63,20 @@ export interface Facet {
6163
/**
6264
* Get the number of columns for the legend
6365
* @param position
66+
* @param labelLimit
6467
* @returns
6568
*/
66-
export const getColumns = (position: Position): SignalRef | undefined => {
69+
export const getColumns = (position: Position, labelLimit?: number): SignalRef | undefined => {
6770
if (['left', 'right'].includes(position)) return;
71+
72+
if (labelLimit !== undefined && labelLimit > 0) {
73+
const symbolAndSpacingWidth = DEFAULT_LEGEND_SYMBOL_WIDTH + DEFAULT_LEGEND_COLUMN_PADDING;
74+
75+
const itemWidth = labelLimit + symbolAndSpacingWidth;
76+
return { signal: `max(1, floor(width / ${itemWidth}))` };
77+
}
78+
79+
// Keeping hardcoded 220 for so we don't break existing behavior.
6880
return { signal: 'floor(width / 220)' };
6981
};
7082

packages/vega-spec-builder/src/types/legendSpec.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export interface LegendOptions {
4646
keys?: string[];
4747
/** labels for each of the series */
4848
legendLabels?: LegendLabel[];
49-
/** max width in pixels before truncating a legend label */
49+
/** max width in pixels before truncating a legend label. Influences legend column layout by calculating how many legend items can fit horizontally based on the label width. */
5050
labelLimit?: number;
5151
/** line type or key in the data that is used as the line type facet for the symbols */
5252
lineType?: LineTypeFacet;

0 commit comments

Comments
 (0)