From a47acb83d74245e5647d98e13deaef5bed4d7da8 Mon Sep 17 00:00:00 2001 From: Cinthia Davalos Date: Thu, 8 Jan 2026 13:10:27 -0600 Subject: [PATCH 1/7] Improve UX reducing timeseries tooltip to max 5 items --- .../scalar_card_component.ng.html | 6 + .../card_renderer/scalar_card_component.scss | 10 +- .../card_renderer/scalar_card_component.ts | 30 +++- .../scalar_card_line_chart_test.ts | 151 ++++++++++++++++-- .../views/card_renderer/scalar_card_test.ts | 101 ++++++++++++ .../line_chart_interactive_view.ng.html | 8 +- .../sub_view/line_chart_interactive_view.ts | 16 ++ .../line_chart_interactive_view_test.ts | 100 ++++++++++++ 8 files changed, 406 insertions(+), 16 deletions(-) diff --git a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ng.html b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ng.html index 79d4264b1e7..451daf40c3b 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ng.html +++ b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ng.html @@ -178,6 +178,12 @@ + + + {{ additionalItemsCount }} additional {{ additionalItemsCount === + 1 ? 'item' : 'items' }} + + diff --git a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.scss b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.scss index d27a4f00b6b..b25ac4ab576 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.scss +++ b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.scss @@ -115,13 +115,21 @@ $_data_table_initial_height: 100px; } .tooltip { - border-spacing: 4px; + border-spacing: 8px; font-size: 13px; th { text-align: left; } + td { + text-align: justify; + + .legend { + font-weight: 500; + } + } + $_circle-size: 12px; .tooltip-row { diff --git a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ts b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ts index 0d1292fe639..2b93999d27d 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ts @@ -149,10 +149,13 @@ export class ScalarCardComponent { @ViewChild('dataTableContainer') dataTableContainer?: ElementRef; + readonly MAX_TOOLTIP_ITEMS = 5; + constructor(private readonly ref: ElementRef, private dialog: MatDialog) {} yScaleType = ScaleType.LINEAR; isViewBoxOverridden: boolean = false; + tooltipTotalCount = 0; toggleYScaleType() { this.yScaleType = @@ -224,22 +227,31 @@ export class ScalarCardComponent { scalarTooltipData[minIndex].metadata.closest = true; } + let sortedData: ScalarTooltipDatum[]; switch (this.tooltipSort) { case TooltipSort.ASCENDING: - return scalarTooltipData.sort((a, b) => a.dataPoint.y - b.dataPoint.y); + sortedData = scalarTooltipData.sort( + (a, b) => a.dataPoint.y - b.dataPoint.y + ); + break; case TooltipSort.DESCENDING: - return scalarTooltipData.sort((a, b) => b.dataPoint.y - a.dataPoint.y); + sortedData = scalarTooltipData.sort( + (a, b) => b.dataPoint.y - a.dataPoint.y + ); + break; case TooltipSort.NEAREST: - return scalarTooltipData.sort((a, b) => { + sortedData = scalarTooltipData.sort((a, b) => { return a.metadata.distToCursorPixels - b.metadata.distToCursorPixels; }); + break; case TooltipSort.NEAREST_Y: - return scalarTooltipData.sort((a, b) => { + sortedData = scalarTooltipData.sort((a, b) => { return a.metadata.distToCursorY - b.metadata.distToCursorY; }); + break; case TooltipSort.DEFAULT: case TooltipSort.ALPHABETICAL: - return scalarTooltipData.sort((a, b) => { + sortedData = scalarTooltipData.sort((a, b) => { if (a.metadata.displayName < b.metadata.displayName) { return -1; } @@ -248,7 +260,15 @@ export class ScalarCardComponent { } return 0; }); + break; } + + this.tooltipTotalCount = sortedData.length; + return sortedData.slice(0, this.MAX_TOOLTIP_ITEMS); + } + + get additionalItemsCount(): number { + return Math.max(0, this.tooltipTotalCount - this.MAX_TOOLTIP_ITEMS); } openDataDownloadDialog(): void { diff --git a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_test.ts b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_test.ts index 5ac09278968..78644acab96 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_test.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_test.ts @@ -199,9 +199,9 @@ class TestableLineChart { @@ -233,6 +233,12 @@ class TestableLineChart { + + + {{ additionalItemsCount }} additional + {{ additionalItemsCount === 1 ? 'item' : 'items' }} + + @@ -257,10 +263,13 @@ class TestableScalarCardLineChart { readonly valueFormatter = numberFormatter; readonly stepFormatter = intlNumberFormatter; + readonly MAX_TOOLTIP_ITEMS = 5; + tooltipTotalCount = 0; + constructor(public readonly changeDetectorRef: ChangeDetectorRef) {} getCursorAwareTooltipData( - tooltipData: TooltipDatum[], + tooltipData: TooltipDatum[], cursorLocationInDataCoord: {x: number; y: number}, cursorLocation: {x: number; y: number} ) { @@ -293,22 +302,31 @@ class TestableScalarCardLineChart { scalarTooltipData[minIndex].metadata.closest = true; } + let sortedData; switch (this.tooltipSort) { case TooltipSort.ASCENDING: - return scalarTooltipData.sort((a, b) => a.dataPoint.y - b.dataPoint.y); + sortedData = scalarTooltipData.sort( + (a, b) => a.dataPoint.y - b.dataPoint.y + ); + break; case TooltipSort.DESCENDING: - return scalarTooltipData.sort((a, b) => b.dataPoint.y - a.dataPoint.y); + sortedData = scalarTooltipData.sort( + (a, b) => b.dataPoint.y - a.dataPoint.y + ); + break; case TooltipSort.NEAREST: - return scalarTooltipData.sort((a, b) => { + sortedData = scalarTooltipData.sort((a, b) => { return a.metadata.distToCursorPixels - b.metadata.distToCursorPixels; }); + break; case TooltipSort.NEAREST_Y: - return scalarTooltipData.sort((a, b) => { + sortedData = scalarTooltipData.sort((a, b) => { return a.metadata.distToCursorY - b.metadata.distToCursorY; }); + break; case TooltipSort.DEFAULT: case TooltipSort.ALPHABETICAL: - return scalarTooltipData.sort((a, b) => { + sortedData = scalarTooltipData.sort((a, b) => { if (a.metadata.displayName < b.metadata.displayName) { return -1; } @@ -317,7 +335,15 @@ class TestableScalarCardLineChart { } return 0; }); + break; } + + this.tooltipTotalCount = sortedData.length; + return sortedData.slice(0, this.MAX_TOOLTIP_ITEMS); + } + + get additionalItemsCount(): number { + return Math.max(0, this.tooltipTotalCount - this.MAX_TOOLTIP_ITEMS); } } @@ -784,6 +810,11 @@ describe('scalar card line chart', () => { tooltipData: TooltipDatum[] ) { fixture.componentInstance.tooltipDataForTesting = tooltipData; + const lineChart = fixture.debugElement.query(Selector.LINE_CHART); + if (lineChart) { + lineChart.componentInstance.tooltipDataForTesting = tooltipData; + lineChart.componentInstance.changeDetectorRef.markForCheck(); + } fixture.componentInstance.changeDetectorRef.markForCheck(); } @@ -792,11 +823,23 @@ describe('scalar card line chart', () => { dataPoint?: {x: number; y: number}, domPoint?: Point ) { + const lineChart = fixture.debugElement.query(Selector.LINE_CHART); if (dataPoint) { fixture.componentInstance.dataPointForTesting = dataPoint; + if (lineChart) { + lineChart.componentInstance.dataPointForTesting = dataPoint; + lineChart.componentInstance.cursorLocationInDataCoordForTesting = + dataPoint; + } } if (domPoint) { fixture.componentInstance.cursorLocationForTesting = domPoint; + if (lineChart) { + lineChart.componentInstance.cursorLocationForTesting = domPoint; + } + } + if (lineChart) { + lineChart.componentInstance.changeDetectorRef.markForCheck(); } fixture.componentInstance.changeDetectorRef.markForCheck(); } @@ -1369,6 +1412,96 @@ describe('scalar card line chart', () => { ['', 'world', '-500', '1,000', anyString, anyString], ]); })); + + describe('tooltip item limiting and legend', () => { + const colors = [ + '#00f', + '#0f0', + '#f00', + '#ff0', + '#0ff', + '#f0f', + '#fff', + '#000', + ]; + + function buildTooltipData(count: number) { + return Array.from({length: count}, (_, i) => + buildTooltipDatum({ + id: `row${i + 1}`, + type: SeriesType.ORIGINAL, + displayName: `Row ${i + 1}`, + alias: null, + visible: true, + color: colors[i % colors.length], + }) + ); + } + + function getLegendRow( + fixture: ComponentFixture + ) { + return fixture.debugElement.query(By.css('table.tooltip tr.legend')); + } + + it('displays all items when there are 5 or fewer', fakeAsync(() => { + store.overrideSelector(selectors.getMetricsScalarSmoothing, 0); + const fixture = createComponent(); + setTooltipData(fixture, buildTooltipData(5)); + fixture.detectChanges(); + + expect(fixture.debugElement.queryAll(Selector.TOOLTIP_ROW).length).toBe( + 5 + ); + expect(getLegendRow(fixture)).toBeNull(); + })); + + it('limits tooltip to 5 items when there are more than 5', fakeAsync(() => { + store.overrideSelector(selectors.getMetricsScalarSmoothing, 0); + const fixture = createComponent(); + setTooltipData(fixture, buildTooltipData(7)); + fixture.detectChanges(); + + expect(fixture.debugElement.queryAll(Selector.TOOLTIP_ROW).length).toBe( + 5 + ); + })); + + it('shows legend with singular text for 1 additional item', fakeAsync(() => { + store.overrideSelector(selectors.getMetricsScalarSmoothing, 0); + const fixture = createComponent(); + setTooltipData(fixture, buildTooltipData(6)); + fixture.detectChanges(); + + const legendRow = getLegendRow(fixture); + expect(legendRow).not.toBeNull(); + expect(legendRow.nativeElement.textContent.trim()).toBe( + '1 additional item' + ); + })); + + it('shows legend with plural text for multiple additional items', fakeAsync(() => { + store.overrideSelector(selectors.getMetricsScalarSmoothing, 0); + const fixture = createComponent(); + setTooltipData(fixture, buildTooltipData(8)); + fixture.detectChanges(); + + const legendRow = getLegendRow(fixture); + expect(legendRow).not.toBeNull(); + expect(legendRow.nativeElement.textContent.trim()).toBe( + '3 additional items' + ); + })); + + it('does not show legend when there are exactly 5 items', fakeAsync(() => { + store.overrideSelector(selectors.getMetricsScalarSmoothing, 0); + const fixture = createComponent(); + setTooltipData(fixture, buildTooltipData(5)); + fixture.detectChanges(); + + expect(getLegendRow(fixture)).toBeNull(); + })); + }); }); describe('linked time feature integration', () => { diff --git a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_test.ts b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_test.ts index d01555d5465..ce9e3f7f07f 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_test.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_test.ts @@ -1885,6 +1885,107 @@ describe('scalar card', () => { ['', 'world', '-500', '1,000', anyString, anyString], ]); })); + + describe('tooltip item limiting and legend', () => { + const colors = [ + '#00f', + '#0f0', + '#f00', + '#ff0', + '#0ff', + '#f0f', + '#fff', + '#000', + ]; + + function buildTooltipData(count: number) { + return Array.from({length: count}, (_, i) => + buildTooltipDatum({ + id: `row${i + 1}`, + type: SeriesType.ORIGINAL, + displayName: `Row ${i + 1}`, + alias: null, + visible: true, + color: colors[i % colors.length], + }) + ); + } + + function getLegendRow(fixture: ComponentFixture) { + return fixture.debugElement.query(By.css('table.tooltip tr.legend')); + } + + it('displays all items when there are 5 or fewer', fakeAsync(() => { + store.overrideSelector(selectors.getMetricsScalarSmoothing, 0); + const fixture = createComponent('card1'); + setTooltipData(fixture, buildTooltipData(5)); + fixture.detectChanges(); + + expect(fixture.debugElement.queryAll(Selector.TOOLTIP_ROW).length).toBe( + 5 + ); + expect(getLegendRow(fixture)).toBeNull(); + })); + + it('limits tooltip to 5 items when there are more than 5', fakeAsync(() => { + store.overrideSelector(selectors.getMetricsScalarSmoothing, 0); + const fixture = createComponent('card1'); + setTooltipData(fixture, buildTooltipData(7)); + fixture.detectChanges(); + + expect(fixture.debugElement.queryAll(Selector.TOOLTIP_ROW).length).toBe( + 5 + ); + })); + + it('shows legend with singular text for 1 additional item', fakeAsync(() => { + store.overrideSelector(selectors.getMetricsScalarSmoothing, 0); + const fixture = createComponent('card1'); + setTooltipData(fixture, buildTooltipData(6)); + fixture.detectChanges(); + + const legendRow = getLegendRow(fixture); + expect(legendRow).not.toBeNull(); + expect(legendRow.nativeElement.textContent.trim()).toBe( + '1 additional item' + ); + })); + + it('shows legend with plural text for multiple additional items', fakeAsync(() => { + store.overrideSelector(selectors.getMetricsScalarSmoothing, 0); + const fixture = createComponent('card1'); + setTooltipData(fixture, buildTooltipData(8)); + fixture.detectChanges(); + + const legendRow = getLegendRow(fixture); + expect(legendRow).not.toBeNull(); + expect(legendRow.nativeElement.textContent.trim()).toBe( + '3 additional items' + ); + })); + + it('does not show legend when there are exactly 5 items', fakeAsync(() => { + store.overrideSelector(selectors.getMetricsScalarSmoothing, 0); + const fixture = createComponent('card1'); + setTooltipData(fixture, buildTooltipData(5)); + fixture.detectChanges(); + + expect(getLegendRow(fixture)).toBeNull(); + })); + + it('shows legend with correct colspan when smoothing is enabled', fakeAsync(() => { + store.overrideSelector(selectors.getMetricsScalarSmoothing, 0.5); + const fixture = createComponent('card1'); + setTooltipData(fixture, buildTooltipData(6)); + fixture.detectChanges(); + + const legendRow = getLegendRow(fixture); + expect(legendRow).not.toBeNull(); + expect( + legendRow.query(By.css('td')).nativeElement.getAttribute('colspan') + ).toBe('100'); + })); + }); }); describe('non-monotonic increase in x-axis', () => { diff --git a/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view.ng.html b/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view.ng.html index 96ecc671428..1eeeae79c30 100644 --- a/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view.ng.html +++ b/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view.ng.html @@ -67,7 +67,7 @@ {{ datum.dataPoint.x }} + + + {{ additionalItemsCount }} additional {{ additionalItemsCount === 1 ? + 'item' : 'items' }} + + diff --git a/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view.ts b/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view.ts index c2b4ffd20cd..f61538a94b1 100644 --- a/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view.ts +++ b/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view.ts @@ -208,8 +208,12 @@ export class LineChartInteractiveViewComponent cursorLocationInDataCoord: {x: number; y: number} | null = null; cursorLocation: {x: number; y: number} | null = null; cursoredData: TooltipDatum[] = []; + limitedCursoredData: TooltipDatum[] = []; tooltipDisplayAttached: boolean = false; + readonly MAX_TOOLTIP_ITEMS = 5; + tooltipTotalCount = 0; + @HostBinding('class.show-zoom-instruction') showZoomInstruction: boolean = false; @@ -486,6 +490,10 @@ export class LineChartInteractiveViewComponent return datum.id; } + get additionalItemsCount(): number { + return Math.max(0, this.tooltipTotalCount - this.MAX_TOOLTIP_ITEMS); + } + getDomX(uiCoord: number): number { return this.xScale.forward( this.viewExtent.x, @@ -542,6 +550,8 @@ export class LineChartInteractiveViewComponent const cursorLoc = this.cursorLocationInDataCoord; if (cursorLoc === null) { this.cursoredData = []; + this.limitedCursoredData = []; + this.tooltipTotalCount = 0; this.tooltipDisplayAttached = false; return; } @@ -573,6 +583,12 @@ export class LineChartInteractiveViewComponent }) .filter((tooltipDatumOrNull) => tooltipDatumOrNull) as TooltipDatum[]) : []; + + this.tooltipTotalCount = this.cursoredData.length; + this.limitedCursoredData = this.cursoredData.slice( + 0, + this.MAX_TOOLTIP_ITEMS + ); this.tooltipDisplayAttached = Boolean(this.cursoredData.length); } } diff --git a/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view_test.ts b/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view_test.ts index 27fb2568aa5..ef674b27947 100644 --- a/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view_test.ts +++ b/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view_test.ts @@ -279,6 +279,106 @@ describe('line_chart_v2/sub_view/interactive_view test', () => { expect(overlayContainer.getContainerElement().childElementCount).toBe(0); }); + describe('tooltip item limiting and legend', () => { + const seriesNames = [ + 'foo', + 'bar', + 'baz', + 'qux', + 'quux', + 'corge', + 'grault', + 'garply', + ]; + const colors = [ + '#f00', + '#0f0', + '#00f', + '#ff0', + '#f0f', + '#0ff', + '#fff', + '#000', + ]; + + function setupSeriesData( + fixture: ComponentFixture, + count: number + ) { + fixture.componentInstance.seriesData = seriesNames + .slice(0, count) + .map((name, i) => + createSeries(name, (index: number) => index * (i + 1)) + ); + fixture.componentInstance.seriesMetadataMap = seriesNames + .slice(0, count) + .reduce((map, name, i) => { + map[name] = buildMetadata({ + id: name, + displayName: name.charAt(0).toUpperCase() + name.slice(1), + color: colors[i % colors.length], + }); + return map; + }, {} as any); + fixture.componentInstance.domDim = {width: 500, height: 200}; + fixture.detectChanges(); + emitEvent(fixture, 'mouseenter', {clientX: 250, clientY: 10}); + fixture.detectChanges(); + } + + function getLegendRow() { + return overlayContainer + .getContainerElement() + .querySelector('tbody tr.legend'); + } + + it('displays all items when there are 5 or fewer', () => { + const fixture = createComponent(); + setupSeriesData(fixture, 5); + + const rows = overlayContainer + .getContainerElement() + .querySelectorAll('tbody tr'); + expect(rows.length).toBe(5); + expect(getLegendRow()).toBeNull(); + }); + + it('limits tooltip to 5 items when there are more than 5', () => { + const fixture = createComponent(); + setupSeriesData(fixture, 7); + + const rows = overlayContainer + .getContainerElement() + .querySelectorAll('tbody tr:not(.legend)'); + expect(rows.length).toBe(5); + }); + + it('shows legend with singular text for 1 additional item', () => { + const fixture = createComponent(); + setupSeriesData(fixture, 6); + + const legendRow = getLegendRow(); + expect(legendRow).not.toBeNull(); + expect(legendRow!.textContent!.trim()).toBe('1 additional item'); + }); + + it('shows legend with plural text for multiple additional items', () => { + const fixture = createComponent(); + setupSeriesData(fixture, 8); + + const legendRow = getLegendRow(); + expect(legendRow).not.toBeNull(); + expect(legendRow!.textContent!.trim()).toBe('3 additional items'); + }); + + it('does not show legend when there are exactly 5 items', () => { + const fixture = createComponent(); + setupSeriesData(fixture, 5); + + expect(getLegendRow()).toBeNull(); + }); + }); + it('does not render tooltip when disableTooltip is true', () => { const fixture = createComponent(); fixture.componentInstance.disableTooltip = true; From 929fa17385a2d2b416a68312ef9d7f3c42415ffb Mon Sep 17 00:00:00 2001 From: Cinthia Davalos Date: Sun, 11 Jan 2026 14:12:06 -0600 Subject: [PATCH 2/7] Skip signature errors in unit tests for typescript compatibilty between multiple versions --- .../views/card_renderer/scalar_card_line_chart_test.ts | 4 ++-- .../webapp/metrics/views/card_renderer/scalar_card_test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_test.ts b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_test.ts index 78644acab96..b575d377fb8 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_test.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_test.ts @@ -487,9 +487,9 @@ describe('scalar card line chart', () => { store.overrideSelector(selectors.getMetricsCardUserViewBox, null); dispatchedActions = []; - spyOn(store, 'dispatch').and.callFake((action: Action) => { + spyOn(store, 'dispatch').and.callFake(((action: Action) => { dispatchedActions.push(action); - }); + }) as any); }); afterEach(() => { diff --git a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_test.ts b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_test.ts index ce9e3f7f07f..540a2b154d0 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_test.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_test.ts @@ -408,9 +408,9 @@ describe('scalar card', () => { store.overrideSelector(selectors.getMetricsCardUserViewBox, null); dispatchedActions = []; - spyOn(store, 'dispatch').and.callFake((action: Action) => { + spyOn(store, 'dispatch').and.callFake(((action: Action) => { dispatchedActions.push(action); - }); + }) as any); }); afterEach(() => { From b710d6efe935f2d36374a8b143e2a66859dbce59 Mon Sep 17 00:00:00 2001 From: Cinthia Davalos Date: Mon, 12 Jan 2026 11:18:48 -0600 Subject: [PATCH 3/7] Bypass signature check of card_renderer_tests compatible with current and future versions of typescript. --- .../views/card_renderer/card_lazy_loader_test.ts | 4 ++-- .../metrics/views/card_renderer/card_view_test.ts | 4 ++-- .../views/card_renderer/histogram_card_test.ts | 12 ++++++------ .../metrics/views/card_renderer/image_card_test.ts | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tensorboard/webapp/metrics/views/card_renderer/card_lazy_loader_test.ts b/tensorboard/webapp/metrics/views/card_renderer/card_lazy_loader_test.ts index f1268d7b65e..2550862aa0f 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/card_lazy_loader_test.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/card_lazy_loader_test.ts @@ -109,9 +109,9 @@ describe('card view test', () => { dispatchedActions = []; store = TestBed.inject>(Store) as MockStore; - spyOn(store, 'dispatch').and.callFake((action: Action) => { + spyOn(store, 'dispatch').and.callFake(((action: Action) => { dispatchedActions.push(action); - }); + }) as any); observeSpy = spyOn(IntersectionObserver.prototype, 'observe'); unobserveSpy = spyOn(IntersectionObserver.prototype, 'unobserve'); diff --git a/tensorboard/webapp/metrics/views/card_renderer/card_view_test.ts b/tensorboard/webapp/metrics/views/card_renderer/card_view_test.ts index 4d83e97af24..7377b8fe72f 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/card_view_test.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/card_view_test.ts @@ -65,9 +65,9 @@ describe('card view test', () => { dispatchedActions = []; store = TestBed.inject>(Store) as MockStore; - spyOn(store, 'dispatch').and.callFake((action: Action) => { + spyOn(store, 'dispatch').and.callFake(((action: Action) => { dispatchedActions.push(action); - }); + }) as any); store.overrideSelector(selectors.getRunColorMap, {}); intersectionObserver = TestBed.inject(IntersectionObserverTestingModule); }); diff --git a/tensorboard/webapp/metrics/views/card_renderer/histogram_card_test.ts b/tensorboard/webapp/metrics/views/card_renderer/histogram_card_test.ts index 9326f0e55bf..38f257abaca 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/histogram_card_test.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/histogram_card_test.ts @@ -267,9 +267,9 @@ describe('histogram card', () => { provideMockCardSeriesData(selectSpy, PluginType.HISTOGRAMS, 'card1'); dispatchedActions = []; - spyOn(store, 'dispatch').and.callFake((action: Action) => { + spyOn(store, 'dispatch').and.callFake(((action: Action) => { dispatchedActions.push(action); - }); + }) as any); }); it('dispatches metricsCardFullSizeToggled on full size toggle', () => { @@ -301,9 +301,9 @@ describe('histogram card', () => { const fixture = createHistogramCardContainer(); fixture.detectChanges(); const dispatchedActions: Action[] = []; - spyOn(store, 'dispatch').and.callFake((action: Action) => { + spyOn(store, 'dispatch').and.callFake(((action: Action) => { dispatchedActions.push(action); - }); + }) as any); const histogramWidget = fixture.debugElement.query( By.directive(TestableHistogramWidget) @@ -584,9 +584,9 @@ describe('histogram card', () => { const fixture = createHistogramCardContainer(); fixture.detectChanges(); const dispatchedActions: Action[] = []; - spyOn(store, 'dispatch').and.callFake((action: Action) => { + spyOn(store, 'dispatch').and.callFake(((action: Action) => { dispatchedActions.push(action); - }); + }) as any); const histogramWidget = fixture.debugElement.query( By.directive(TestableHistogramWidget) diff --git a/tensorboard/webapp/metrics/views/card_renderer/image_card_test.ts b/tensorboard/webapp/metrics/views/card_renderer/image_card_test.ts index 33ef483177d..e0a208654e5 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/image_card_test.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/image_card_test.ts @@ -107,9 +107,9 @@ describe('image card', () => { store = TestBed.inject>(Store) as MockStore; dataSource = TestBed.inject(MetricsDataSource); selectSpy = spyOn(store, 'select').and.callThrough(); - spyOn(store, 'dispatch').and.callFake((action: Action) => { + spyOn(store, 'dispatch').and.callFake(((action: Action) => { dispatchedActions.push(action); - }); + }) as any); store.overrideSelector(getExperimentIdForRunId, null); store.overrideSelector(getExperimentIdToExperimentAliasMap, {}); From 728b63c085a17d7986e046b371b641e660d81a5f Mon Sep 17 00:00:00 2001 From: Cinthia Davalos Date: Wed, 14 Jan 2026 15:19:49 -0600 Subject: [PATCH 4/7] Updated solution with code review suggestions --- .../card_renderer/card_lazy_loader_test.ts | 5 +-- .../card_renderer/scalar_card_component.ts | 32 ++++++++----------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/tensorboard/webapp/metrics/views/card_renderer/card_lazy_loader_test.ts b/tensorboard/webapp/metrics/views/card_renderer/card_lazy_loader_test.ts index 2550862aa0f..90b9f020907 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/card_lazy_loader_test.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/card_lazy_loader_test.ts @@ -109,9 +109,10 @@ describe('card view test', () => { dispatchedActions = []; store = TestBed.inject>(Store) as MockStore; - spyOn(store, 'dispatch').and.callFake(((action: Action) => { + // Cast to jasmine.Spy for compatibility between NgRx dispatch signature overloads. + (spyOn(store, 'dispatch') as jasmine.Spy).and.callFake((action: Action) => { dispatchedActions.push(action); - }) as any); + }); observeSpy = spyOn(IntersectionObserver.prototype, 'observe'); unobserveSpy = spyOn(IntersectionObserver.prototype, 'unobserve'); diff --git a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ts b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ts index 2b93999d27d..8a627599e4c 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ts @@ -75,6 +75,8 @@ type ScalarTooltipDatum = TooltipDatum< } >; +const MAX_TOOLTIP_ITEMS = 5; + @Component({ standalone: false, selector: 'scalar-card-component', @@ -149,13 +151,11 @@ export class ScalarCardComponent { @ViewChild('dataTableContainer') dataTableContainer?: ElementRef; - readonly MAX_TOOLTIP_ITEMS = 5; - constructor(private readonly ref: ElementRef, private dialog: MatDialog) {} yScaleType = ScaleType.LINEAR; isViewBoxOverridden: boolean = false; - tooltipTotalCount = 0; + additionalItemsCount = 0; toggleYScaleType() { this.yScaleType = @@ -227,31 +227,26 @@ export class ScalarCardComponent { scalarTooltipData[minIndex].metadata.closest = true; } - let sortedData: ScalarTooltipDatum[]; switch (this.tooltipSort) { case TooltipSort.ASCENDING: - sortedData = scalarTooltipData.sort( - (a, b) => a.dataPoint.y - b.dataPoint.y - ); + scalarTooltipData.sort((a, b) => a.dataPoint.y - b.dataPoint.y); break; case TooltipSort.DESCENDING: - sortedData = scalarTooltipData.sort( - (a, b) => b.dataPoint.y - a.dataPoint.y - ); + scalarTooltipData.sort((a, b) => b.dataPoint.y - a.dataPoint.y); break; case TooltipSort.NEAREST: - sortedData = scalarTooltipData.sort((a, b) => { + scalarTooltipData.sort((a, b) => { return a.metadata.distToCursorPixels - b.metadata.distToCursorPixels; }); break; case TooltipSort.NEAREST_Y: - sortedData = scalarTooltipData.sort((a, b) => { + scalarTooltipData.sort((a, b) => { return a.metadata.distToCursorY - b.metadata.distToCursorY; }); break; case TooltipSort.DEFAULT: case TooltipSort.ALPHABETICAL: - sortedData = scalarTooltipData.sort((a, b) => { + scalarTooltipData.sort((a, b) => { if (a.metadata.displayName < b.metadata.displayName) { return -1; } @@ -263,12 +258,11 @@ export class ScalarCardComponent { break; } - this.tooltipTotalCount = sortedData.length; - return sortedData.slice(0, this.MAX_TOOLTIP_ITEMS); - } - - get additionalItemsCount(): number { - return Math.max(0, this.tooltipTotalCount - this.MAX_TOOLTIP_ITEMS); + this.additionalItemsCount = Math.max( + 0, + scalarTooltipData.length - MAX_TOOLTIP_ITEMS + ); + return scalarTooltipData.slice(0, MAX_TOOLTIP_ITEMS); } openDataDownloadDialog(): void { From e5c5acaaa1e777db387f5a1c809867abaa24f3c3 Mon Sep 17 00:00:00 2001 From: Cinthia Davalos Date: Mon, 19 Jan 2026 14:40:35 -0600 Subject: [PATCH 5/7] Returned to previous state files related to linear chart with tooltip template default. Updated type assertion for unit tests --- .../views/card_renderer/card_view_test.ts | 5 +- .../card_renderer/histogram_card_test.ts | 15 +- .../views/card_renderer/image_card_test.ts | 5 +- .../scalar_card_line_chart_test.ts | 155 ++---------------- .../views/card_renderer/scalar_card_test.ts | 5 +- .../line_chart_interactive_view.ng.html | 8 +- .../sub_view/line_chart_interactive_view.ts | 16 -- .../line_chart_interactive_view_test.ts | 100 ----------- 8 files changed, 30 insertions(+), 279 deletions(-) diff --git a/tensorboard/webapp/metrics/views/card_renderer/card_view_test.ts b/tensorboard/webapp/metrics/views/card_renderer/card_view_test.ts index 7377b8fe72f..323dfe57626 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/card_view_test.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/card_view_test.ts @@ -65,9 +65,10 @@ describe('card view test', () => { dispatchedActions = []; store = TestBed.inject>(Store) as MockStore; - spyOn(store, 'dispatch').and.callFake(((action: Action) => { + // Cast to jasmine.Spy for compatibility between NgRx dispatch signature overloads. + (spyOn(store, 'dispatch') as jasmine.Spy).and.callFake((action: Action) => { dispatchedActions.push(action); - }) as any); + }); store.overrideSelector(selectors.getRunColorMap, {}); intersectionObserver = TestBed.inject(IntersectionObserverTestingModule); }); diff --git a/tensorboard/webapp/metrics/views/card_renderer/histogram_card_test.ts b/tensorboard/webapp/metrics/views/card_renderer/histogram_card_test.ts index 38f257abaca..f4c7d1219a4 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/histogram_card_test.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/histogram_card_test.ts @@ -267,9 +267,10 @@ describe('histogram card', () => { provideMockCardSeriesData(selectSpy, PluginType.HISTOGRAMS, 'card1'); dispatchedActions = []; - spyOn(store, 'dispatch').and.callFake(((action: Action) => { + // Cast to jasmine.Spy for compatibility between NgRx dispatch signature overloads. + (spyOn(store, 'dispatch') as jasmine.Spy).and.callFake((action: Action) => { dispatchedActions.push(action); - }) as any); + }); }); it('dispatches metricsCardFullSizeToggled on full size toggle', () => { @@ -301,9 +302,10 @@ describe('histogram card', () => { const fixture = createHistogramCardContainer(); fixture.detectChanges(); const dispatchedActions: Action[] = []; - spyOn(store, 'dispatch').and.callFake(((action: Action) => { + // Cast to jasmine.Spy for compatibility between NgRx dispatch signature overloads. + (spyOn(store, 'dispatch') as jasmine.Spy).and.callFake((action: Action) => { dispatchedActions.push(action); - }) as any); + }); const histogramWidget = fixture.debugElement.query( By.directive(TestableHistogramWidget) @@ -584,9 +586,10 @@ describe('histogram card', () => { const fixture = createHistogramCardContainer(); fixture.detectChanges(); const dispatchedActions: Action[] = []; - spyOn(store, 'dispatch').and.callFake(((action: Action) => { + // Cast to jasmine.Spy for compatibility between NgRx dispatch signature overloads. + (spyOn(store, 'dispatch') as jasmine.Spy).and.callFake((action: Action) => { dispatchedActions.push(action); - }) as any); + }); const histogramWidget = fixture.debugElement.query( By.directive(TestableHistogramWidget) diff --git a/tensorboard/webapp/metrics/views/card_renderer/image_card_test.ts b/tensorboard/webapp/metrics/views/card_renderer/image_card_test.ts index e0a208654e5..515330e2dfe 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/image_card_test.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/image_card_test.ts @@ -107,9 +107,10 @@ describe('image card', () => { store = TestBed.inject>(Store) as MockStore; dataSource = TestBed.inject(MetricsDataSource); selectSpy = spyOn(store, 'select').and.callThrough(); - spyOn(store, 'dispatch').and.callFake(((action: Action) => { + // Cast to jasmine.Spy for compatibility between NgRx dispatch signature overloads. + (spyOn(store, 'dispatch') as jasmine.Spy).and.callFake((action: Action) => { dispatchedActions.push(action); - }) as any); + }); store.overrideSelector(getExperimentIdForRunId, null); store.overrideSelector(getExperimentIdToExperimentAliasMap, {}); diff --git a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_test.ts b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_test.ts index b575d377fb8..5ac09278968 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_test.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_test.ts @@ -199,9 +199,9 @@ class TestableLineChart { @@ -233,12 +233,6 @@ class TestableLineChart { - - - {{ additionalItemsCount }} additional - {{ additionalItemsCount === 1 ? 'item' : 'items' }} - - @@ -263,13 +257,10 @@ class TestableScalarCardLineChart { readonly valueFormatter = numberFormatter; readonly stepFormatter = intlNumberFormatter; - readonly MAX_TOOLTIP_ITEMS = 5; - tooltipTotalCount = 0; - constructor(public readonly changeDetectorRef: ChangeDetectorRef) {} getCursorAwareTooltipData( - tooltipData: TooltipDatum[], + tooltipData: TooltipDatum[], cursorLocationInDataCoord: {x: number; y: number}, cursorLocation: {x: number; y: number} ) { @@ -302,31 +293,22 @@ class TestableScalarCardLineChart { scalarTooltipData[minIndex].metadata.closest = true; } - let sortedData; switch (this.tooltipSort) { case TooltipSort.ASCENDING: - sortedData = scalarTooltipData.sort( - (a, b) => a.dataPoint.y - b.dataPoint.y - ); - break; + return scalarTooltipData.sort((a, b) => a.dataPoint.y - b.dataPoint.y); case TooltipSort.DESCENDING: - sortedData = scalarTooltipData.sort( - (a, b) => b.dataPoint.y - a.dataPoint.y - ); - break; + return scalarTooltipData.sort((a, b) => b.dataPoint.y - a.dataPoint.y); case TooltipSort.NEAREST: - sortedData = scalarTooltipData.sort((a, b) => { + return scalarTooltipData.sort((a, b) => { return a.metadata.distToCursorPixels - b.metadata.distToCursorPixels; }); - break; case TooltipSort.NEAREST_Y: - sortedData = scalarTooltipData.sort((a, b) => { + return scalarTooltipData.sort((a, b) => { return a.metadata.distToCursorY - b.metadata.distToCursorY; }); - break; case TooltipSort.DEFAULT: case TooltipSort.ALPHABETICAL: - sortedData = scalarTooltipData.sort((a, b) => { + return scalarTooltipData.sort((a, b) => { if (a.metadata.displayName < b.metadata.displayName) { return -1; } @@ -335,15 +317,7 @@ class TestableScalarCardLineChart { } return 0; }); - break; } - - this.tooltipTotalCount = sortedData.length; - return sortedData.slice(0, this.MAX_TOOLTIP_ITEMS); - } - - get additionalItemsCount(): number { - return Math.max(0, this.tooltipTotalCount - this.MAX_TOOLTIP_ITEMS); } } @@ -487,9 +461,9 @@ describe('scalar card line chart', () => { store.overrideSelector(selectors.getMetricsCardUserViewBox, null); dispatchedActions = []; - spyOn(store, 'dispatch').and.callFake(((action: Action) => { + spyOn(store, 'dispatch').and.callFake((action: Action) => { dispatchedActions.push(action); - }) as any); + }); }); afterEach(() => { @@ -810,11 +784,6 @@ describe('scalar card line chart', () => { tooltipData: TooltipDatum[] ) { fixture.componentInstance.tooltipDataForTesting = tooltipData; - const lineChart = fixture.debugElement.query(Selector.LINE_CHART); - if (lineChart) { - lineChart.componentInstance.tooltipDataForTesting = tooltipData; - lineChart.componentInstance.changeDetectorRef.markForCheck(); - } fixture.componentInstance.changeDetectorRef.markForCheck(); } @@ -823,23 +792,11 @@ describe('scalar card line chart', () => { dataPoint?: {x: number; y: number}, domPoint?: Point ) { - const lineChart = fixture.debugElement.query(Selector.LINE_CHART); if (dataPoint) { fixture.componentInstance.dataPointForTesting = dataPoint; - if (lineChart) { - lineChart.componentInstance.dataPointForTesting = dataPoint; - lineChart.componentInstance.cursorLocationInDataCoordForTesting = - dataPoint; - } } if (domPoint) { fixture.componentInstance.cursorLocationForTesting = domPoint; - if (lineChart) { - lineChart.componentInstance.cursorLocationForTesting = domPoint; - } - } - if (lineChart) { - lineChart.componentInstance.changeDetectorRef.markForCheck(); } fixture.componentInstance.changeDetectorRef.markForCheck(); } @@ -1412,96 +1369,6 @@ describe('scalar card line chart', () => { ['', 'world', '-500', '1,000', anyString, anyString], ]); })); - - describe('tooltip item limiting and legend', () => { - const colors = [ - '#00f', - '#0f0', - '#f00', - '#ff0', - '#0ff', - '#f0f', - '#fff', - '#000', - ]; - - function buildTooltipData(count: number) { - return Array.from({length: count}, (_, i) => - buildTooltipDatum({ - id: `row${i + 1}`, - type: SeriesType.ORIGINAL, - displayName: `Row ${i + 1}`, - alias: null, - visible: true, - color: colors[i % colors.length], - }) - ); - } - - function getLegendRow( - fixture: ComponentFixture - ) { - return fixture.debugElement.query(By.css('table.tooltip tr.legend')); - } - - it('displays all items when there are 5 or fewer', fakeAsync(() => { - store.overrideSelector(selectors.getMetricsScalarSmoothing, 0); - const fixture = createComponent(); - setTooltipData(fixture, buildTooltipData(5)); - fixture.detectChanges(); - - expect(fixture.debugElement.queryAll(Selector.TOOLTIP_ROW).length).toBe( - 5 - ); - expect(getLegendRow(fixture)).toBeNull(); - })); - - it('limits tooltip to 5 items when there are more than 5', fakeAsync(() => { - store.overrideSelector(selectors.getMetricsScalarSmoothing, 0); - const fixture = createComponent(); - setTooltipData(fixture, buildTooltipData(7)); - fixture.detectChanges(); - - expect(fixture.debugElement.queryAll(Selector.TOOLTIP_ROW).length).toBe( - 5 - ); - })); - - it('shows legend with singular text for 1 additional item', fakeAsync(() => { - store.overrideSelector(selectors.getMetricsScalarSmoothing, 0); - const fixture = createComponent(); - setTooltipData(fixture, buildTooltipData(6)); - fixture.detectChanges(); - - const legendRow = getLegendRow(fixture); - expect(legendRow).not.toBeNull(); - expect(legendRow.nativeElement.textContent.trim()).toBe( - '1 additional item' - ); - })); - - it('shows legend with plural text for multiple additional items', fakeAsync(() => { - store.overrideSelector(selectors.getMetricsScalarSmoothing, 0); - const fixture = createComponent(); - setTooltipData(fixture, buildTooltipData(8)); - fixture.detectChanges(); - - const legendRow = getLegendRow(fixture); - expect(legendRow).not.toBeNull(); - expect(legendRow.nativeElement.textContent.trim()).toBe( - '3 additional items' - ); - })); - - it('does not show legend when there are exactly 5 items', fakeAsync(() => { - store.overrideSelector(selectors.getMetricsScalarSmoothing, 0); - const fixture = createComponent(); - setTooltipData(fixture, buildTooltipData(5)); - fixture.detectChanges(); - - expect(getLegendRow(fixture)).toBeNull(); - })); - }); }); describe('linked time feature integration', () => { diff --git a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_test.ts b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_test.ts index 540a2b154d0..b514bc64aff 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_test.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_test.ts @@ -408,9 +408,10 @@ describe('scalar card', () => { store.overrideSelector(selectors.getMetricsCardUserViewBox, null); dispatchedActions = []; - spyOn(store, 'dispatch').and.callFake(((action: Action) => { + // Cast to jasmine.Spy for compatibility between NgRx dispatch signature overloads. + (spyOn(store, 'dispatch') as jasmine.Spy).and.callFake((action: Action) => { dispatchedActions.push(action); - }) as any); + }); }); afterEach(() => { diff --git a/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view.ng.html b/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view.ng.html index 1eeeae79c30..96ecc671428 100644 --- a/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view.ng.html +++ b/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view.ng.html @@ -67,7 +67,7 @@ {{ datum.dataPoint.x }} - - - {{ additionalItemsCount }} additional {{ additionalItemsCount === 1 ? - 'item' : 'items' }} - - diff --git a/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view.ts b/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view.ts index f61538a94b1..c2b4ffd20cd 100644 --- a/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view.ts +++ b/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view.ts @@ -208,12 +208,8 @@ export class LineChartInteractiveViewComponent cursorLocationInDataCoord: {x: number; y: number} | null = null; cursorLocation: {x: number; y: number} | null = null; cursoredData: TooltipDatum[] = []; - limitedCursoredData: TooltipDatum[] = []; tooltipDisplayAttached: boolean = false; - readonly MAX_TOOLTIP_ITEMS = 5; - tooltipTotalCount = 0; - @HostBinding('class.show-zoom-instruction') showZoomInstruction: boolean = false; @@ -490,10 +486,6 @@ export class LineChartInteractiveViewComponent return datum.id; } - get additionalItemsCount(): number { - return Math.max(0, this.tooltipTotalCount - this.MAX_TOOLTIP_ITEMS); - } - getDomX(uiCoord: number): number { return this.xScale.forward( this.viewExtent.x, @@ -550,8 +542,6 @@ export class LineChartInteractiveViewComponent const cursorLoc = this.cursorLocationInDataCoord; if (cursorLoc === null) { this.cursoredData = []; - this.limitedCursoredData = []; - this.tooltipTotalCount = 0; this.tooltipDisplayAttached = false; return; } @@ -583,12 +573,6 @@ export class LineChartInteractiveViewComponent }) .filter((tooltipDatumOrNull) => tooltipDatumOrNull) as TooltipDatum[]) : []; - - this.tooltipTotalCount = this.cursoredData.length; - this.limitedCursoredData = this.cursoredData.slice( - 0, - this.MAX_TOOLTIP_ITEMS - ); this.tooltipDisplayAttached = Boolean(this.cursoredData.length); } } diff --git a/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view_test.ts b/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view_test.ts index ef674b27947..27fb2568aa5 100644 --- a/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view_test.ts +++ b/tensorboard/webapp/widgets/line_chart_v2/sub_view/line_chart_interactive_view_test.ts @@ -279,106 +279,6 @@ describe('line_chart_v2/sub_view/interactive_view test', () => { expect(overlayContainer.getContainerElement().childElementCount).toBe(0); }); - describe('tooltip item limiting and legend', () => { - const seriesNames = [ - 'foo', - 'bar', - 'baz', - 'qux', - 'quux', - 'corge', - 'grault', - 'garply', - ]; - const colors = [ - '#f00', - '#0f0', - '#00f', - '#ff0', - '#f0f', - '#0ff', - '#fff', - '#000', - ]; - - function setupSeriesData( - fixture: ComponentFixture, - count: number - ) { - fixture.componentInstance.seriesData = seriesNames - .slice(0, count) - .map((name, i) => - createSeries(name, (index: number) => index * (i + 1)) - ); - fixture.componentInstance.seriesMetadataMap = seriesNames - .slice(0, count) - .reduce((map, name, i) => { - map[name] = buildMetadata({ - id: name, - displayName: name.charAt(0).toUpperCase() + name.slice(1), - color: colors[i % colors.length], - }); - return map; - }, {} as any); - fixture.componentInstance.domDim = {width: 500, height: 200}; - fixture.detectChanges(); - emitEvent(fixture, 'mouseenter', {clientX: 250, clientY: 10}); - fixture.detectChanges(); - } - - function getLegendRow() { - return overlayContainer - .getContainerElement() - .querySelector('tbody tr.legend'); - } - - it('displays all items when there are 5 or fewer', () => { - const fixture = createComponent(); - setupSeriesData(fixture, 5); - - const rows = overlayContainer - .getContainerElement() - .querySelectorAll('tbody tr'); - expect(rows.length).toBe(5); - expect(getLegendRow()).toBeNull(); - }); - - it('limits tooltip to 5 items when there are more than 5', () => { - const fixture = createComponent(); - setupSeriesData(fixture, 7); - - const rows = overlayContainer - .getContainerElement() - .querySelectorAll('tbody tr:not(.legend)'); - expect(rows.length).toBe(5); - }); - - it('shows legend with singular text for 1 additional item', () => { - const fixture = createComponent(); - setupSeriesData(fixture, 6); - - const legendRow = getLegendRow(); - expect(legendRow).not.toBeNull(); - expect(legendRow!.textContent!.trim()).toBe('1 additional item'); - }); - - it('shows legend with plural text for multiple additional items', () => { - const fixture = createComponent(); - setupSeriesData(fixture, 8); - - const legendRow = getLegendRow(); - expect(legendRow).not.toBeNull(); - expect(legendRow!.textContent!.trim()).toBe('3 additional items'); - }); - - it('does not show legend when there are exactly 5 items', () => { - const fixture = createComponent(); - setupSeriesData(fixture, 5); - - expect(getLegendRow()).toBeNull(); - }); - }); - it('does not render tooltip when disableTooltip is true', () => { const fixture = createComponent(); fixture.componentInstance.disableTooltip = true; From c28516c443b09c7ed59f38ebcde272b15da6051c Mon Sep 17 00:00:00 2001 From: Cinthia Davalos Date: Mon, 19 Jan 2026 16:41:00 -0600 Subject: [PATCH 6/7] Fixed errors from prettier --- .../card_renderer/histogram_card_test.ts | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/tensorboard/webapp/metrics/views/card_renderer/histogram_card_test.ts b/tensorboard/webapp/metrics/views/card_renderer/histogram_card_test.ts index f4c7d1219a4..0141459630c 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/histogram_card_test.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/histogram_card_test.ts @@ -268,9 +268,11 @@ describe('histogram card', () => { dispatchedActions = []; // Cast to jasmine.Spy for compatibility between NgRx dispatch signature overloads. - (spyOn(store, 'dispatch') as jasmine.Spy).and.callFake((action: Action) => { - dispatchedActions.push(action); - }); + (spyOn(store, 'dispatch') as jasmine.Spy).and.callFake( + (action: Action) => { + dispatchedActions.push(action); + } + ); }); it('dispatches metricsCardFullSizeToggled on full size toggle', () => { @@ -303,9 +305,11 @@ describe('histogram card', () => { fixture.detectChanges(); const dispatchedActions: Action[] = []; // Cast to jasmine.Spy for compatibility between NgRx dispatch signature overloads. - (spyOn(store, 'dispatch') as jasmine.Spy).and.callFake((action: Action) => { - dispatchedActions.push(action); - }); + (spyOn(store, 'dispatch') as jasmine.Spy).and.callFake( + (action: Action) => { + dispatchedActions.push(action); + } + ); const histogramWidget = fixture.debugElement.query( By.directive(TestableHistogramWidget) @@ -587,9 +591,11 @@ describe('histogram card', () => { fixture.detectChanges(); const dispatchedActions: Action[] = []; // Cast to jasmine.Spy for compatibility between NgRx dispatch signature overloads. - (spyOn(store, 'dispatch') as jasmine.Spy).and.callFake((action: Action) => { - dispatchedActions.push(action); - }); + (spyOn(store, 'dispatch') as jasmine.Spy).and.callFake( + (action: Action) => { + dispatchedActions.push(action); + } + ); const histogramWidget = fixture.debugElement.query( By.directive(TestableHistogramWidget) From 0e3434f6e5866700e934f1f4e9dcf3f7d8945c28 Mon Sep 17 00:00:00 2001 From: Cinthia Davalos Date: Mon, 19 Jan 2026 18:57:15 -0600 Subject: [PATCH 7/7] Included type assertion for failing test in internal CL --- .../metrics/views/card_renderer/scalar_card_line_chart_test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_test.ts b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_test.ts index 5ac09278968..21334debeb1 100644 --- a/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_test.ts +++ b/tensorboard/webapp/metrics/views/card_renderer/scalar_card_line_chart_test.ts @@ -461,7 +461,8 @@ describe('scalar card line chart', () => { store.overrideSelector(selectors.getMetricsCardUserViewBox, null); dispatchedActions = []; - spyOn(store, 'dispatch').and.callFake((action: Action) => { + // Cast to jasmine.Spy for compatibility between NgRx dispatch signature overloads. + (spyOn(store, 'dispatch') as jasmine.Spy).and.callFake((action: Action) => { dispatchedActions.push(action); }); });