Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
3600a4c
Add middle text truncation for charts legend labels
awahab07 Jan 18, 2026
adde860
Add truncationPosition option to legend stories
awahab07 Jan 18, 2026
2b2650b
Add 4% safety margin for Firefox text rendering differences
awahab07 Jan 18, 2026
a6e8f6c
Revert changing action group in story.
awahab07 Jan 19, 2026
b12791b
Improve middle truncation with iterative refinement algorithm
awahab07 Jan 22, 2026
c00b88a
Add startTransition for low-priority legend truncation in React 18
awahab07 Jan 22, 2026
e041d76
Fix typings
awahab07 Jan 22, 2026
9dd1b8d
Fix Safari single-line end truncation with resetWrapText mixin
awahab07 Jan 22, 2026
666ee61
test(vrt): update screenshots [skip ci]
elastic-datavis[bot] Jan 22, 2026
0f0961e
Update API docs.
awahab07 Jan 28, 2026
884f1c8
Do not truncate in the middle if legend available width is too narrow…
awahab07 Jan 28, 2026
49da38b
test(vrt): update screenshots [skip ci]
elastic-datavis[bot] Jan 28, 2026
da843bd
Merge branch 'main' into 1925_middle-text-truncation-for-legend-labels
elasticmachine Feb 4, 2026
425aec4
test(vrt): update screenshots [skip ci]
elastic-datavis[bot] Feb 4, 2026
f9ebd91
chore: trigger CI after VRT update
awahab07 Feb 4, 2026
76efdd5
Merge remote-tracking branch 'upstream/main' into 1925_middle-text-tr…
awahab07 Feb 19, 2026
a4a4140
test(vrt): update screenshots [skip ci]
elastic-datavis[bot] Feb 19, 2026
b433cfe
test(vrt): revert all screenshots to upstream main baseline
awahab07 Feb 19, 2026
c5fecb8
test(vrt): update screenshots [skip ci]
elastic-datavis[bot] Feb 19, 2026
8f0bfc0
Merge remote-tracking branch 'upstream/main' into 1925_middle-text-tr…
awahab07 Feb 26, 2026
cd1f423
test(vrt): update screenshots [skip ci]
elastic-datavis[bot] Mar 3, 2026
89d78ea
Fix typo in comment.
awahab07 Mar 3, 2026
1621252
Merge remote-tracking branch 'upstream/main' into 1925_middle-text-tr…
awahab07 Mar 10, 2026
9ba7a8e
Remove default.
awahab07 Mar 10, 2026
2b398a5
Prevent inifite ResizeObserver loop.
awahab07 Mar 11, 2026
5f55058
Prefer `element.style.maxWidth` over `window.getComputedStyle(element…
awahab07 Mar 11, 2026
c60ab74
Add `truncationPosition` dropdown to Legend Layout story.
awahab07 Mar 11, 2026
55cd67c
Add `legendLayout` input to Label Truncation story.
awahab07 Mar 11, 2026
9b51d67
Update API documentation.
awahab07 Mar 11, 2026
7f0ea01
test(vrt): update screenshots [skip ci]
elastic-datavis[bot] Mar 11, 2026
1981921
Add "Hide large labels" knob to Legend -> Label Truncation story.
awahab07 Mar 11, 2026
85cbe84
Simplify legend size in the story by having only one "Legend size" in…
awahab07 Mar 14, 2026
1007f14
Avoid using ResizeObserver which could result in infinite loop when p…
awahab07 Mar 14, 2026
5d6a890
Restrict legend from going off the chart container bounds.
awahab07 Mar 14, 2026
aa42c29
When `legendSize` is provided, make sure it always takes affect, rega…
awahab07 Mar 14, 2026
4994565
Add e2e snapthost test.
awahab07 Mar 15, 2026
5667c59
test(vrt): update screenshots [skip ci]
elastic-datavis[bot] Mar 15, 2026
fdbb58b
Respect configured width and ensure legends always maintain a 5% of c…
awahab07 Mar 15, 2026
5a1c0f4
Use `minmax(0, auto)` for `gridTemplateColumns`.
awahab07 Mar 15, 2026
7e3cf54
test(vrt): update screenshots [skip ci]
elastic-datavis[bot] Mar 15, 2026
873a67d
Delete outdated snapshots.
awahab07 Mar 15, 2026
4be1d9a
test(vrt): update screenshots [skip ci]
elastic-datavis[bot] Mar 15, 2026
686324d
Synchronous middle text truncation computation.
awahab07 Mar 16, 2026
6ae77af
Merge remote-tracking branch 'upstream/main' into 1925_middle-text-tr…
awahab07 Apr 9, 2026
ad65ceb
Account for 'px' mode for middle truncation.
awahab07 Apr 10, 2026
c36bd88
Use `'end'` as the default label `truncationPosition`.
awahab07 Apr 10, 2026
4c445a1
test(vrt): update screenshots [skip ci]
elastic-datavis[bot] Apr 10, 2026
42f4200
Remove deleted/renamed test snapshots.
awahab07 Apr 10, 2026
c971199
Merge remote-tracking branch 'upstream/main' into 1925_middle-text-tr…
awahab07 Apr 10, 2026
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This snapshot test has been renamed, hence old snapshot is deleted (also see a similar "../position-right/..." below).

Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Copy link
Copy Markdown
Collaborator Author

@awahab07 awahab07 Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ These area chart snapshots have been updated because the PR fixes a behavior where legend labels overflow the chart in floating mode (legend inside chart layout). Notice the legends in the before snapshots extending out the chart borders. (see)

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 16 additions & 2 deletions e2e/tests/legend_stories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ test.describe('Legend stories', () => {
(position) => {
const isVertical = position === 'left' || position === 'right';
if (isVertical) {
test('should limit width to min of 30% of computed width', async ({ page }) => {
test('should respect very small legend size', async ({ page }) => {
await common.expectChartAtUrlToMatchScreenshot(page)(getUrl(position, 1));
});
}
Expand Down Expand Up @@ -348,7 +348,21 @@ test.describe('Legend stories', () => {
);
});

test.describe('Legend tabular data', () => {
test.describe('Label truncation', () => {
test('should correctly render middle truncation in layout story', async ({ page }) => {
await common.expectChartAtUrlToMatchScreenshot(page)(
'http://localhost:9001/?path=/story/legend--layout&knob-Legend position=top&knob-Legend Layout=list&knob-truncationPosition_Label options=middle',
);
});

test('should correctly render middle truncation with floating legend and multiline', async ({ page }) => {
await common.expectChartAtUrlToMatchScreenshot(page)(
'http://localhost:9001/?path=/story/legend--label-truncation&knob-Inside chart (floating)_Legend options=true&knob-Horizontal alignment_Legend options=left&knob-maxLines_Label options=2&knob-Hide short labels_Data=true&knob-enable legend size_Legend options=true&knob-legend size_Legend options=600',
);
});
});

http: test.describe('Legend tabular data', () => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting fist time seeing a label used in JS. I don't think this is used at all

const disableActionOnHover = '&knob-Show legend action on hover_Legend=false';

const datasetKnob = (p1: string, p2: string) => `&globals=&knob-Dataset_Legend=${p1}&knob-dataset=${p2}`;
Expand Down
4 changes: 4 additions & 0 deletions packages/charts/api/charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1851,9 +1851,13 @@ export type LegendItemValue = {
// @public (undocumented)
export interface LegendLabelOptions {
maxLines: number;
truncationPosition: LegendLabelTruncationPosition;
widthLimit: number;
}

// @public (undocumented)
export type LegendLabelTruncationPosition = 'end' | 'middle';

// @public (undocumented)
export const LegendLayout: Readonly<{
List: "list";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ describe('Computed chart dimensions', () => {
labelOptions: {
maxLines: 1,
widthLimit: 250,
truncationPosition: 'end',
},
};
const defaultTheme = LIGHT_THEME;
Expand Down
7 changes: 7 additions & 0 deletions packages/charts/src/components/_mixins.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
min-width: 1px;
}

@mixin resetWrapText {
overflow-wrap: initial;
word-wrap: initial;
word-break: initial;
hyphens: initial;
}

@mixin lineClamp($maxLines) {
text-overflow: ellipsis;
display: -webkit-box;
Expand Down
13 changes: 13 additions & 0 deletions packages/charts/src/components/legend/_legend_item.scss
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,13 @@ $legendItemHeight: #{$euiFontSizeXS * $euiLineHeight};
@include wrapText;

&--singleline {
@include resetWrapText;
@include euiTextTruncate;

// When using middle truncation, JS handles the ellipsis. `clip` will disable euiTextTruncate
&--middle {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This creates --singleline--middle which isn't proper BEM terminology. Could we do --middle as a separate class and apply both or would that breat things?

text-overflow: clip;
}
}

// div to prevent changing to button
Expand All @@ -120,6 +126,13 @@ $legendItemHeight: #{$euiFontSizeXS * $euiLineHeight};
-webkit-line-clamp: 2; // number of lines to show, overridden in element styles
}

// When using middle truncation, JS handles the ellipsis so disable CSS line-clamp truncation
&--multiline--middle:is(div) {
-webkit-line-clamp: none; // Disable CSS truncation, JS handles it
line-break: anywhere; // Allow breaking at any point for accurate text fitting
word-break: break-all; // Fallback for older browsers
}

&--clickable:hover {
cursor: pointer;
text-decoration: underline;
Expand Down
57 changes: 48 additions & 9 deletions packages/charts/src/components/legend/label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import classNames from 'classnames';
import type { KeyboardEventHandler, MouseEventHandler } from 'react';
import React, { useCallback } from 'react';

import type { TruncationMode } from './types';
import { useMiddleTruncatedLabel } from './use_truncated_label';
import { isRTLString } from '../../utils/common';
import type { LegendLabelOptions } from '../../utils/themes/theme';

type TruncationMode = 'line' | 'px';

interface LabelProps {
label: string;
isSeriesHidden?: boolean;
Expand Down Expand Up @@ -79,7 +79,17 @@ export function Label({
totalSeriesCount,
truncationMode,
}: LabelProps) {
const { className, dir, clampStyles } = getSharedProps(label, options, !!onToggle, truncationMode);
const shouldTruncateMiddle = options.truncationPosition === 'middle' && options.maxLines > 0;
const { labelRef, truncatedLabel, isComputed } = useMiddleTruncatedLabel({
label,
maxLines: options.maxLines,
shouldTruncateMiddle,
truncationMode,
});

// Only apply middle truncation CSS classes when JS computation is complete
const useMiddleClasses = shouldTruncateMiddle && isComputed;
const { className, dir, clampStyles } = getSharedProps(label, options, !!onToggle, truncationMode, useMiddleClasses);

const onClick: MouseEventHandler = useCallback(
({ metaKey, ctrlKey }) => onToggle?.(isAppleDevice ? metaKey : ctrlKey),
Expand All @@ -98,6 +108,7 @@ export function Label({
// This div is required to allow multiline text truncation, all ARIA requirements are still met
// https://stackoverflow.com/questions/68673034/webkit-line-clamp-does-not-apply-to-buttons
<div
ref={labelRef}
role="button"
tabIndex={0}
dir={dir}
Expand All @@ -110,21 +121,46 @@ export function Label({
aria-label={`${label}; ${getInteractivityAriaLabel(!isSeriesHidden, hiddenSeriesCount, totalSeriesCount)}`}
data-testid="echLegendItemLabel"
>
{label}
{truncatedLabel}
</div>
) : (
<div dir={dir} className={className} title={label} style={clampStyles} data-testid="echLegendItemLabel">
{label}
<div
ref={labelRef}
dir={dir}
className={className}
title={label}
style={clampStyles}
data-testid="echLegendItemLabel"
>
{truncatedLabel}
</div>
);
}

/** @internal */
export function NonInteractiveLabel({ label, options }: { label: string; options: LegendLabelOptions }) {
const { className, dir, clampStyles } = getSharedProps(label, options);
const shouldTruncateMiddle = options.truncationPosition === 'middle' && options.maxLines > 0;
const { labelRef, truncatedLabel, isComputed } = useMiddleTruncatedLabel({
label,
maxLines: options.maxLines,
shouldTruncateMiddle,
truncationMode: 'line',
});

// Only apply middle truncation CSS classes when JS computation is complete
const useMiddleClasses = shouldTruncateMiddle && isComputed;
const { className, dir, clampStyles } = getSharedProps(label, options, false, undefined, useMiddleClasses);

return (
<div dir={dir} className={className} title={label} style={clampStyles} data-testid="echLegendItemLabel">
{label}
<div
ref={labelRef}
dir={dir}
className={className}
title={label}
style={clampStyles}
data-testid="echLegendItemLabel"
>
{truncatedLabel}
</div>
);
}
Expand All @@ -134,13 +170,16 @@ function getSharedProps(
options: LegendLabelOptions,
isToggleable?: boolean,
truncationMode: TruncationMode = 'line',
useMiddleClasses?: boolean,
) {
const maxLines = Math.abs(options.maxLines);
const widthLimit = Math.abs(options.widthLimit);
const className = classNames('echLegendItem__label', {
'echLegendItem__label--clickable': Boolean(isToggleable),
'echLegendItem__label--singleline': truncationMode === 'px' || maxLines === 1,
'echLegendItem__label--singleline--middle': (truncationMode === 'px' || maxLines === 1) && useMiddleClasses,
'echLegendItem__label--multiline': maxLines > 1 && truncationMode === 'line',
'echLegendItem__label--multiline--middle': maxLines > 1 && truncationMode === 'line' && useMiddleClasses,
});

const dir = isRTLString(label) ? 'rtl' : 'ltr'; // forced for individual labels in case mixed charset
Expand Down
2 changes: 1 addition & 1 deletion packages/charts/src/components/legend/legend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ function LegendComponent(props: LegendStateProps & LegendDispatchProps) {
}

const positionConfig = getLegendPositionConfig(config.legendPosition);
const containerStyle = getLegendStyle(positionConfig, size, legend.margin);
const containerStyle = getLegendStyle(positionConfig, size, legend.margin, Number.isFinite(config.legendSize));
const listStyle = getLegendListStyle(positionConfig, chartMargins, legend, items.length);
const isMostlyRTL = hasMostlyRTLItems(items.map(({ label }) => label));

Expand Down
2 changes: 2 additions & 0 deletions packages/charts/src/components/legend/position_style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export function legendPositionStyle(
top: vAlign === Top ? chart.top : undefined,
bottom: vAlign === Bottom ? container.height - chart.top - chart.height : undefined,
height: legendSize.height >= chart.height ? chart.height : undefined,
maxWidth: chart.width - 2 * INSIDE_PADDING,
overflow: 'hidden',
};
}

Expand Down
13 changes: 9 additions & 4 deletions packages/charts/src/components/legend/style_utils.ts
Copy link
Copy Markdown
Collaborator Author

@awahab07 awahab07 Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reason for style adjustments in this file (and also in .../legend/position_style.ts and .../legend/legend.tsx above).

Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function getLegendListStyle(
paddingTop,
paddingBottom,
...(floating && {
gridTemplateColumns: `repeat(${clamp(floatingColumns ?? 1, 1, totalItems)}, auto)`,
gridTemplateColumns: `repeat(${clamp(floatingColumns ?? 1, 1, totalItems)}, minmax(0, auto))`,
}),
};
}
Expand All @@ -68,12 +68,17 @@ export function getLegendListStyle(
* Get the legend global style
* @internal
*/
export function getLegendStyle({ direction, floating }: LegendPositionConfig, size: Size, margin: number): LegendStyle {
export function getLegendStyle(
{ direction, floating }: LegendPositionConfig,
size: Size,
margin: number,
hasConfiguredWidth?: boolean,
): LegendStyle {
if (direction === LayoutDirection.Vertical) {
const width = `${size.width}px`;
return {
width: floating ? undefined : width,
maxWidth: floating ? undefined : width,
width: floating && !hasConfiguredWidth ? undefined : width,
maxWidth: floating ? '100%' : width,
marginLeft: margin,
marginRight: margin,
};
Expand Down
3 changes: 3 additions & 0 deletions packages/charts/src/components/legend/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import type {
import type { LegendPath, onToggleDeselectSeriesAction } from '../../state/actions/legend';
import type { LegendLabelOptions } from '../../utils/themes/theme';

/** @internal */
export type TruncationMode = 'line' | 'px';

/** @internal */
export interface SharedLegendItemProps {
flatLegend: boolean;
Expand Down
Loading