Skip to content

Commit 0bafbe7

Browse files
authored
New checkbox setting to allow short list in tooltip (tensorflow#7133)
## Motivation for features / changes We wrapped a previous functionality to reduce redundancy of data in the tooltip for Scalar Chart into a checkbox so users can choose either to display the tooltip with the full list or short list for exploring data. ## Technical description of changes - Added a new limitTooltipRows setting. As redux setting like the other controls in panel settings: action → reducer → selector → container → presentational component input. - New checkbox added to the metrics settings panel; defaults to unchecked box (full tooltip shown). - When on, the scalar card tooltip caps at MAX_TOOLTIP_ITEMS rows and shows a "N additional items" legend for the overflow. - Setting persists to localStorage, so it sticks across reloads, same pattern as the other metrics settings. - Added unit tests covering the on/off behavior of the tooltip and persistance. ## Detailed steps to verify changes work correctly (as executed by you) - Ran the full frontend Karma suite `bazel test ... tensorboard/webapp:karma_test_chromium-local` - Verified the new functionality tests - Verified the new persistence tests ## Screenshots of UI changes (or N/A) <img width="845" height="380" alt="Screenshot 2026-06-23 at 2 59 19 p m" src="https://github.com/user-attachments/assets/18218c4d-b506-44f7-95d9-68843e645a08" /> <img width="847" height="750" alt="Screenshot 2026-06-23 at 2 59 52 p m" src="https://github.com/user-attachments/assets/d73e368e-b55f-44e1-b639-589319574d92" />
1 parent 37d609f commit 0bafbe7

18 files changed

Lines changed: 378 additions & 28 deletions

tensorboard/webapp/metrics/actions/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,17 @@ export const metricsScalarPartitionNonMonotonicXToggled = createAction(
125125
'[Metrics] Metrics Setting Partition Non Monotonic X Toggled'
126126
);
127127

128+
// Toggle the "Limit tooltip rows" checkbox on or off. No payload, just change the current boolean
129+
export const metricsToggleLimitTooltipRows = createAction(
130+
'[Metrics] Metrics Setting Toggle Limit Tooltip Rows'
131+
);
132+
133+
// Set the numeric limit for how many rows to show in the tooltip when limiting is enabled
134+
export const metricsChangeTooltipRowsLimit = createAction(
135+
'[Metrics] Metrics Setting Change Tooltip Rows Limit',
136+
props<{tooltipRowsLimit: number}>()
137+
);
138+
128139
export const metricsChangeImageBrightness = createAction(
129140
'[Metrics] Metrics Setting Change Image Brightness',
130141
props<{brightnessInMilli: number}>()

tensorboard/webapp/metrics/internal_types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,6 @@ export interface HeaderToggleInfo {
106106

107107
export const SCALARS_SMOOTHING_MIN = 0;
108108
export const SCALARS_SMOOTHING_MAX = 0.999;
109+
110+
/** Minimum value for the input: tooltip rows limit setting. */
111+
export const TOOLTIP_ROWS_LIMIT_MIN = 1;

tensorboard/webapp/metrics/metrics_module.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@ import {MetricsEffects} from './effects';
3030
import {
3131
getMetricsCardMinWidth,
3232
getMetricsIgnoreOutliers,
33+
getMetricsIsTooltipRowsLimitEnabled,
3334
getMetricsLinkedTimeEnabled,
3435
getMetricsRangeSelectionEnabled,
3536
getMetricsScalarSmoothing,
3637
getMetricsStepSelectorEnabled,
38+
getMetricsTooltipRowsLimit,
3739
getMetricsTooltipSort,
3840
getMetricsSavingPinsEnabled,
3941
getRangeSelectionHeaders,
@@ -132,6 +134,21 @@ export function getMetricsTimeSeriesSavingPinsEnabled() {
132134
});
133135
}
134136

137+
export function getMetricsIsTooltipRowsLimitEnabledSettingFactory() {
138+
return createSelector(
139+
getMetricsIsTooltipRowsLimitEnabled,
140+
(isTooltipRowsLimitEnabled) => {
141+
return {isTooltipRowsLimitEnabled};
142+
}
143+
);
144+
}
145+
146+
export function getMetricsTooltipRowsLimitSettingFactory() {
147+
return createSelector(getMetricsTooltipRowsLimit, (tooltipRowsLimit) => {
148+
return {tooltipRowsLimit};
149+
});
150+
}
151+
135152
export function getSingleSelectionHeadersFactory() {
136153
return createSelector(getSingleSelectionHeaders, (singleSelectionHeaders) => {
137154
return {singleSelectionHeaders};
@@ -198,6 +215,12 @@ export function getRangeSelectionHeadersFactory() {
198215
PersistentSettingsConfigModule.defineGlobalSetting(
199216
getMetricsTimeSeriesSavingPinsEnabled
200217
),
218+
PersistentSettingsConfigModule.defineGlobalSetting(
219+
getMetricsIsTooltipRowsLimitEnabledSettingFactory
220+
),
221+
PersistentSettingsConfigModule.defineGlobalSetting(
222+
getMetricsTooltipRowsLimitSettingFactory
223+
),
201224
],
202225
providers: [
203226
{

tensorboard/webapp/metrics/store/metrics_reducers.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
CardUniqueInfo,
4343
SCALARS_SMOOTHING_MAX,
4444
SCALARS_SMOOTHING_MIN,
45+
TOOLTIP_ROWS_LIMIT_MIN,
4546
TooltipSort,
4647
URLDeserializedState,
4748
} from '../types';
@@ -611,6 +612,13 @@ const reducer = createReducer(
611612
if (typeof partialSettings.savingPinsEnabled === 'boolean') {
612613
metricsSettings.savingPinsEnabled = partialSettings.savingPinsEnabled;
613614
}
615+
if (typeof partialSettings.isTooltipRowsLimitEnabled === 'boolean') {
616+
metricsSettings.isTooltipRowsLimitEnabled =
617+
partialSettings.isTooltipRowsLimitEnabled;
618+
}
619+
if (typeof partialSettings.tooltipRowsLimit === 'number') {
620+
metricsSettings.tooltipRowsLimit = partialSettings.tooltipRowsLimit;
621+
}
614622

615623
const isSettingsPaneOpen =
616624
partialSettings.timeSeriesSettingsPaneOpened ?? state.isSettingsPaneOpen;
@@ -842,6 +850,28 @@ const reducer = createReducer(
842850
},
843851
};
844852
}),
853+
on(actions.metricsToggleLimitTooltipRows, (state) => {
854+
const nextIsTooltipRowsLimitEnabled = !(
855+
state.settingOverrides.isTooltipRowsLimitEnabled ??
856+
state.settings.isTooltipRowsLimitEnabled
857+
);
858+
return {
859+
...state,
860+
settingOverrides: {
861+
...state.settingOverrides,
862+
isTooltipRowsLimitEnabled: nextIsTooltipRowsLimitEnabled,
863+
},
864+
};
865+
}),
866+
on(actions.metricsChangeTooltipRowsLimit, (state, {tooltipRowsLimit}) => {
867+
return {
868+
...state,
869+
settingOverrides: {
870+
...state.settingOverrides,
871+
tooltipRowsLimit: Math.max(tooltipRowsLimit, TOOLTIP_ROWS_LIMIT_MIN),
872+
},
873+
};
874+
}),
845875
on(actions.metricsScalarPartitionNonMonotonicXToggled, (state) => {
846876
const nextScalarPartitionNonMonotonicX = !(
847877
state.settingOverrides.scalarPartitionNonMonotonicX ??

tensorboard/webapp/metrics/store/metrics_selectors.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,16 @@ export const getMetricsScalarPartitionNonMonotonicX = createSelector(
367367
(settings): boolean => settings.scalarPartitionNonMonotonicX
368368
);
369369

370+
export const getMetricsIsTooltipRowsLimitEnabled = createSelector(
371+
selectSettings,
372+
(settings): boolean => settings.isTooltipRowsLimitEnabled
373+
);
374+
375+
export const getMetricsTooltipRowsLimit = createSelector(
376+
selectSettings,
377+
(settings): number => settings.tooltipRowsLimit
378+
);
379+
370380
export const getMetricsImageBrightnessInMilli = createSelector(
371381
selectSettings,
372382
(settings): number => settings.imageBrightnessInMilli

tensorboard/webapp/metrics/store/metrics_types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
NonPinnedCardId,
3636
PinnedCardId,
3737
TimeSelection,
38+
TOOLTIP_ROWS_LIMIT_MIN,
3839
TooltipSort,
3940
XAxisType,
4041
} from '../types';
@@ -234,6 +235,16 @@ export interface MetricsSettings {
234235
* future, we may fix this at the log writing, reading, or backend response time.
235236
*/
236237
scalarPartitionNonMonotonicX: boolean;
238+
/**
239+
* When enabled, the scalar card tooltip is truncated to
240+
* `tooltipRowsLimit` rows instead of showing all items.
241+
*/
242+
isTooltipRowsLimitEnabled: boolean;
243+
/**
244+
* Number of rows shown in the scalar card tooltip when
245+
* `isTooltipRowsLimitEnabled` is enabled.
246+
*/
247+
tooltipRowsLimit: number;
237248
/**
238249
* A non-negative, unitless number. A value of 5000 corresponds to 500%
239250
* increased brightness from normal.
@@ -284,6 +295,8 @@ export const METRICS_SETTINGS_DEFAULT: MetricsSettings = {
284295
hideEmptyCards: true,
285296
scalarSmoothing: 0.6,
286297
scalarPartitionNonMonotonicX: false,
298+
isTooltipRowsLimitEnabled: false,
299+
tooltipRowsLimit: TOOLTIP_ROWS_LIMIT_MIN,
287300
imageBrightnessInMilli: 1000,
288301
imageContrastInMilli: 1000,
289302
imageShowActualSize: false,

tensorboard/webapp/metrics/testing.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ export function buildMetricsSettingsState(
5555
scalarSmoothing: 0.3,
5656
hideEmptyCards: true,
5757
scalarPartitionNonMonotonicX: false,
58+
isTooltipRowsLimitEnabled: false,
59+
tooltipRowsLimit: 4,
5860
imageBrightnessInMilli: 123,
5961
imageContrastInMilli: 123,
6062
imageShowActualSize: true,

tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,6 @@ type ScalarTooltipDatum = TooltipDatum<
7575
}
7676
>;
7777

78-
const MAX_TOOLTIP_ITEMS = 5;
79-
8078
@Component({
8179
standalone: false,
8280
selector: 'scalar-card-component',
@@ -95,6 +93,8 @@ export class ScalarCardComponent<Downloader> {
9593
@Input() DataDownloadComponent!: ComponentType<Downloader>;
9694
@Input() dataSeries!: ScalarCardDataSeries[];
9795
@Input() ignoreOutliers!: boolean;
96+
@Input() isTooltipRowsLimitEnabled!: boolean;
97+
@Input() tooltipRowsLimit!: number;
9898
@Input() isCardVisible!: boolean;
9999
@Input() isPinned!: boolean;
100100
@Input() loadState!: DataLoadState;
@@ -258,11 +258,16 @@ export class ScalarCardComponent<Downloader> {
258258
break;
259259
}
260260

261+
if (!this.isTooltipRowsLimitEnabled) {
262+
this.additionalItemsCount = 0;
263+
return scalarTooltipData;
264+
}
265+
261266
this.additionalItemsCount = Math.max(
262267
0,
263-
scalarTooltipData.length - MAX_TOOLTIP_ITEMS
268+
scalarTooltipData.length - this.tooltipRowsLimit
264269
);
265-
return scalarTooltipData.slice(0, MAX_TOOLTIP_ITEMS);
270+
return scalarTooltipData.slice(0, this.tooltipRowsLimit);
266271
}
267272

268273
openDataDownloadDialog(): void {

tensorboard/webapp/metrics/views/card_renderer/scalar_card_container.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,10 @@ import {
9292
getCardTimeSeries,
9393
getMetricsCardMinMax,
9494
getMetricsIgnoreOutliers,
95+
getMetricsIsTooltipRowsLimitEnabled,
9596
getMetricsScalarPartitionNonMonotonicX,
9697
getMetricsScalarSmoothing,
98+
getMetricsTooltipRowsLimit,
9799
getMetricsTooltipSort,
98100
getMetricsXAxisType,
99101
RunToSeries,
@@ -172,6 +174,8 @@ function areSeriesEqual(
172174
[DataDownloadComponent]="DataDownloadComponent"
173175
[dataSeries]="dataSeries$ | async"
174176
[ignoreOutliers]="ignoreOutliers$ | async"
177+
[isTooltipRowsLimitEnabled]="isTooltipRowsLimitEnabled$ | async"
178+
[tooltipRowsLimit]="tooltipRowsLimit$ | async"
175179
[isCardVisible]="isVisible"
176180
[isPinned]="isPinned$ | async"
177181
[loadState]="loadState$ | async"
@@ -236,6 +240,10 @@ export class ScalarCardContainer implements CardRenderer, OnInit, OnDestroy {
236240
);
237241
this.useDarkMode$ = this.store.select(getDarkModeEnabled);
238242
this.ignoreOutliers$ = this.store.select(getMetricsIgnoreOutliers);
243+
this.isTooltipRowsLimitEnabled$ = this.store.select(
244+
getMetricsIsTooltipRowsLimitEnabled
245+
);
246+
this.tooltipRowsLimit$ = this.store.select(getMetricsTooltipRowsLimit);
239247
this.tooltipSort$ = this.store.select(getMetricsTooltipSort);
240248
this.xAxisType$ = this.store.select(getMetricsXAxisType);
241249
this.forceSvg$ = this.store.select(getForceSvgFeatureFlag);
@@ -304,6 +312,8 @@ export class ScalarCardContainer implements CardRenderer, OnInit, OnDestroy {
304312

305313
readonly useDarkMode$;
306314
readonly ignoreOutliers$;
315+
readonly isTooltipRowsLimitEnabled$;
316+
readonly tooltipRowsLimit$;
307317
readonly tooltipSort$;
308318
readonly xAxisType$;
309319
readonly forceSvg$;

tensorboard/webapp/metrics/views/card_renderer/scalar_card_test.ts

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1891,6 +1891,9 @@ describe('scalar card', () => {
18911891
}));
18921892

18931893
describe('tooltip item limiting and legend', () => {
1894+
const TOTAL_TOOLTIP_ITEMS_FOR_TEST = 12;
1895+
const TOOLTIP_ROWS_LIMIT_FOR_TEST = 5;
1896+
18941897
const colors = [
18951898
'#00f',
18961899
'#0f0',
@@ -1919,33 +1922,58 @@ describe('scalar card', () => {
19191922
return fixture.debugElement.query(By.css('table.tooltip tr.legend'));
19201923
}
19211924

1922-
it('displays all items when there are 5 or fewer', fakeAsync(() => {
1925+
beforeEach(() => {
1926+
store.overrideSelector(
1927+
selectors.getMetricsIsTooltipRowsLimitEnabled,
1928+
true
1929+
);
1930+
store.overrideSelector(
1931+
selectors.getMetricsTooltipRowsLimit,
1932+
TOOLTIP_ROWS_LIMIT_FOR_TEST
1933+
);
1934+
});
1935+
1936+
it('test: Shows all rows when the setting Limit Tooltip is unchecked', fakeAsync(() => {
1937+
store.overrideSelector(
1938+
selectors.getMetricsIsTooltipRowsLimitEnabled,
1939+
false
1940+
);
19231941
store.overrideSelector(selectors.getMetricsScalarSmoothing, 0);
19241942
const fixture = createComponent('card1');
1925-
setTooltipData(fixture, buildTooltipData(5));
1943+
setTooltipData(fixture, buildTooltipData(TOTAL_TOOLTIP_ITEMS_FOR_TEST));
19261944
fixture.detectChanges();
19271945

19281946
expect(fixture.debugElement.queryAll(Selector.TOOLTIP_ROW).length).toBe(
1929-
5
1947+
TOTAL_TOOLTIP_ITEMS_FOR_TEST
19301948
);
19311949
expect(getLegendRow(fixture)).toBeNull();
19321950
}));
19331951

1934-
it('limits tooltip to 5 items when there are more than 5', fakeAsync(() => {
1952+
it('test: Limits rows to the configured tooltip rows limit when the setting Limit Tooltip rows is checked', fakeAsync(() => {
19351953
store.overrideSelector(selectors.getMetricsScalarSmoothing, 0);
19361954
const fixture = createComponent('card1');
1937-
setTooltipData(fixture, buildTooltipData(7));
1955+
setTooltipData(fixture, buildTooltipData(TOTAL_TOOLTIP_ITEMS_FOR_TEST));
19381956
fixture.detectChanges();
19391957

19401958
expect(fixture.debugElement.queryAll(Selector.TOOLTIP_ROW).length).toBe(
1941-
5
1959+
TOOLTIP_ROWS_LIMIT_FOR_TEST
1960+
);
1961+
const additionalItemsCount =
1962+
TOTAL_TOOLTIP_ITEMS_FOR_TEST - TOOLTIP_ROWS_LIMIT_FOR_TEST;
1963+
const legendRow = getLegendRow(fixture);
1964+
expect(legendRow).not.toBeNull();
1965+
expect(legendRow.nativeElement.textContent.trim()).toBe(
1966+
`${additionalItemsCount} additional items`
19421967
);
19431968
}));
19441969

19451970
it('shows legend with singular text for 1 additional item', fakeAsync(() => {
19461971
store.overrideSelector(selectors.getMetricsScalarSmoothing, 0);
19471972
const fixture = createComponent('card1');
1948-
setTooltipData(fixture, buildTooltipData(6));
1973+
setTooltipData(
1974+
fixture,
1975+
buildTooltipData(TOOLTIP_ROWS_LIMIT_FOR_TEST + 1)
1976+
);
19491977
fixture.detectChanges();
19501978

19511979
const legendRow = getLegendRow(fixture);
@@ -1958,7 +1986,10 @@ describe('scalar card', () => {
19581986
it('shows legend with plural text for multiple additional items', fakeAsync(() => {
19591987
store.overrideSelector(selectors.getMetricsScalarSmoothing, 0);
19601988
const fixture = createComponent('card1');
1961-
setTooltipData(fixture, buildTooltipData(8));
1989+
setTooltipData(
1990+
fixture,
1991+
buildTooltipData(TOOLTIP_ROWS_LIMIT_FOR_TEST + 3)
1992+
);
19621993
fixture.detectChanges();
19631994

19641995
const legendRow = getLegendRow(fixture);
@@ -1968,10 +1999,10 @@ describe('scalar card', () => {
19681999
);
19692000
}));
19702001

1971-
it('does not show legend when there are exactly 5 items', fakeAsync(() => {
2002+
it('does not show legend when there are exactly the configured limit of items', fakeAsync(() => {
19722003
store.overrideSelector(selectors.getMetricsScalarSmoothing, 0);
19732004
const fixture = createComponent('card1');
1974-
setTooltipData(fixture, buildTooltipData(5));
2005+
setTooltipData(fixture, buildTooltipData(TOOLTIP_ROWS_LIMIT_FOR_TEST));
19752006
fixture.detectChanges();
19762007

19772008
expect(getLegendRow(fixture)).toBeNull();
@@ -1980,7 +2011,10 @@ describe('scalar card', () => {
19802011
it('shows legend with correct colspan when smoothing is enabled', fakeAsync(() => {
19812012
store.overrideSelector(selectors.getMetricsScalarSmoothing, 0.5);
19822013
const fixture = createComponent('card1');
1983-
setTooltipData(fixture, buildTooltipData(6));
2014+
setTooltipData(
2015+
fixture,
2016+
buildTooltipData(TOOLTIP_ROWS_LIMIT_FOR_TEST + 1)
2017+
);
19842018
fixture.detectChanges();
19852019

19862020
const legendRow = getLegendRow(fixture);

0 commit comments

Comments
 (0)