Skip to content

Commit fa91799

Browse files
authored
HParams: project table content components (#6427)
## Motivation for features / changes This is part of the effort to use the data table widget in the Runs table which will allow for lots of code reuse as we add HParam functionality to both tables. To allow more customization in each table this PR pulls all the table content out of the data_table_component and allows users to project it using the new content_row_component and content_cell_component. ## Technical description of changes Created new content_row_component and content_cell_component. The row is extremely basic and basically just wraps the content inside a table-row. The content_cell_component take all the logic for formatting each cell. To customize the content users of this component can simply pass in no datum or a ColumnHeaderType that does not have a specified way to format in the getFormattedDataForColumn function and then put the custom content inside the cell as a child it will be projected in. This is currently shown in the way the ScalarCardDataTable handles the Color column. ## Screenshots of UI changes (or N/A) This should result in no changes to the Scalar Data Table. However, the Runs Data Table is now empty as it has not implemented this new DataTable structure. It is behind a flag though and will be implemented in an upcoming PR. <img width="398" alt="Screenshot 2023-06-09 at 2 24 42 PM" src="https://github.com/tensorflow/tensorboard/assets/8672809/28002f09-bc93-45a1-99d5-752e4f71b2ed"> ## Detailed steps to verify changes work correctly (as executed by you) Ran it and clicked around a lot to try breaking. ## Alternate designs / implementations considered (or N/A) We considered some way to allow the data table to be highly configured instead of projecting the content. I also considered not creating the content_row_component class and requiring parents to add their own div with class=table-row.
1 parent 60b613f commit fa91799

18 files changed

+726
-589
lines changed

tensorboard/webapp/metrics/views/card_renderer/BUILD

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,16 @@ tf_sass_binary(
278278
],
279279
)
280280

281+
tf_sass_binary(
282+
name = "scalar_card_data_table_styles",
283+
src = "scalar_card_data_table.scss",
284+
strict_deps = False,
285+
deps = [
286+
"//tensorboard/webapp/metrics/views:metrics_common_styles",
287+
"//tensorboard/webapp/theme",
288+
],
289+
)
290+
281291
tf_ng_module(
282292
name = "scalar_card",
283293
srcs = [
@@ -288,9 +298,11 @@ tf_ng_module(
288298
"scalar_card_module.ts",
289299
],
290300
assets = [
301+
":scalar_card_data_table_styles",
291302
":scalar_card_styles",
292303
":scalar_card_fob_controller_styles",
293304
"scalar_card_component.ng.html",
305+
"scalar_card_data_table.ng.html",
294306
],
295307
deps = [
296308
":data_download_dialog",
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<!--
2+
@license
3+
Copyright 2023 The TensorFlow Authors. All Rights Reserved.
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
18+
<tb-data-table
19+
[headers]="columnHeaders"
20+
[sortingInfo]="sortingInfo"
21+
[columnCustomizationEnabled]="columnCustomizationEnabled"
22+
(sortDataBy)="sortDataBy.emit($event)"
23+
(orderColumns)="orderColumns($event)"
24+
(removeColumn)="removeColumn.emit($event)"
25+
>
26+
<ng-container header>
27+
<ng-container *ngFor="let header of columnHeaders">
28+
<tb-data-table-header-cell
29+
*ngIf="header.enabled && (header.type !== ColumnHeaderType.SMOOTHED || smoothingEnabled)"
30+
[header]="header"
31+
[sortingInfo]="sortingInfo"
32+
[hparamsEnabled]="hparamsEnabled"
33+
></tb-data-table-header-cell> </ng-container
34+
></ng-container>
35+
36+
<ng-container content>
37+
<ng-container *ngFor="let dataRow of getTimeSelectionTableData()">
38+
<tb-data-table-content-row>
39+
<ng-container *ngFor="let header of getHeaders()">
40+
<tb-data-table-content-cell
41+
*ngIf="header.enabled && (header.type !== ColumnHeaderType.SMOOTHED || smoothingEnabled)"
42+
[header]="header"
43+
[datum]="dataRow[header.name]"
44+
>
45+
<div
46+
*ngIf="header.type === ColumnHeaderType.COLOR"
47+
class="row-circle"
48+
>
49+
<span [style.backgroundColor]="dataRow['color']"></span>
50+
</div>
51+
</tb-data-table-content-cell>
52+
</ng-container>
53+
</tb-data-table-content-row>
54+
</ng-container>
55+
</ng-container>
56+
</tb-data-table>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/* Copyright 2023 The TensorFlow Authors. All Rights Reserved.
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
==============================================================================*/
15+
$_circle-size: 12px;
16+
17+
.row-circle {
18+
height: $_circle-size;
19+
width: $_circle-size;
20+
}
21+
.row-circle > span {
22+
border-radius: 50%;
23+
border: 1px solid rgba(255, 255, 255, 0.4);
24+
display: inline-block;
25+
height: $_circle-size - 2px; // size minus border
26+
width: $_circle-size - 2px; // size minus border
27+
vertical-align: middle;
28+
}

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

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -39,28 +39,8 @@ import {isDatumVisible} from './utils';
3939

4040
@Component({
4141
selector: 'scalar-card-data-table',
42-
template: `
43-
<tb-data-table
44-
[headers]="columnHeaders"
45-
[data]="getTimeSelectionTableData()"
46-
[sortingInfo]="sortingInfo"
47-
[columnCustomizationEnabled]="columnCustomizationEnabled"
48-
[smoothingEnabled]="smoothingEnabled"
49-
(sortDataBy)="sortDataBy.emit($event)"
50-
(orderColumns)="orderColumns($event)"
51-
(removeColumn)="removeColumn.emit($event)"
52-
>
53-
<ng-container header>
54-
<ng-container *ngFor="let header of columnHeaders">
55-
<tb-data-table-header-cell
56-
*ngIf="header.enabled"
57-
[header]="header"
58-
[sortingInfo]="sortingInfo"
59-
[hparamsEnabled]="hparamsEnabled"
60-
></tb-data-table-header-cell> </ng-container
61-
></ng-container>
62-
</tb-data-table>
63-
`,
42+
templateUrl: 'scalar_card_data_table.ng.html',
43+
styleUrls: ['scalar_card_data_table.css'],
6444
changeDetection: ChangeDetectionStrategy.OnPush,
6545
})
6646
export class ScalarCardDataTable {
@@ -79,6 +59,18 @@ export class ScalarCardDataTable {
7959
headerType: ColumnHeaderType;
8060
}>();
8161

62+
ColumnHeaderType = ColumnHeaderType;
63+
64+
getHeaders(): ColumnHeader[] {
65+
return [
66+
{
67+
name: 'color',
68+
displayName: '',
69+
type: ColumnHeaderType.COLOR,
70+
enabled: true,
71+
},
72+
].concat(this.columnHeaders);
73+
}
8274
getMinPointInRange(
8375
points: ScalarCardPoint[],
8476
startPointIndex: number,

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

Lines changed: 170 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ import {Extent} from '../../../widgets/line_chart_v2/lib/public_types';
124124
import {provideMockTbStore} from '../../../testing/utils';
125125
import * as commonSelectors from '../main_view/common_selectors';
126126
import {CardFeatureOverride} from '../../store/metrics_types';
127+
import {ContentCellComponent} from '../../../widgets/data_table/content_cell_component';
128+
import {ContentRowComponent} from '../../../widgets/data_table/content_row_component';
129+
import {HeaderCellComponent} from '../../../widgets/data_table/header_cell_component';
127130

128131
@Component({
129132
selector: 'line-chart',
@@ -2517,10 +2520,41 @@ describe('scalar card', () => {
25172520

25182521
describe('scalar card data table', () => {
25192522
beforeEach(() => {
2523+
store.overrideSelector(getMetricsLinkedTimeSelection, {
2524+
start: {step: 20},
2525+
end: null,
2526+
});
2527+
store.overrideSelector(getSingleSelectionHeaders, [
2528+
{
2529+
type: ColumnHeaderType.RUN,
2530+
name: 'run',
2531+
displayName: 'Run',
2532+
enabled: true,
2533+
},
2534+
{
2535+
type: ColumnHeaderType.VALUE,
2536+
name: 'value',
2537+
displayName: 'Value',
2538+
enabled: false,
2539+
},
2540+
{
2541+
type: ColumnHeaderType.STEP,
2542+
name: 'step',
2543+
displayName: 'Step',
2544+
enabled: true,
2545+
},
2546+
]);
25202547
const runToSeries = {
2521-
run1: [buildScalarStepData({step: 10})],
2522-
run2: [buildScalarStepData({step: 20})],
2523-
run3: [buildScalarStepData({step: 30})],
2548+
run1: [
2549+
{wallTime: 1, value: 1, step: 1},
2550+
{wallTime: 2, value: 10, step: 2},
2551+
{wallTime: 3, value: 20, step: 3},
2552+
],
2553+
run2: [
2554+
{wallTime: 1, value: 1, step: 1},
2555+
{wallTime: 2, value: 10, step: 2},
2556+
{wallTime: 3, value: 20, step: 3},
2557+
],
25242558
};
25252559
provideMockCardRunToSeriesData(
25262560
selectSpy,
@@ -2529,6 +2563,17 @@ describe('scalar card', () => {
25292563
null /* metadataOverride */,
25302564
runToSeries
25312565
);
2566+
store.overrideSelector(
2567+
selectors.getCurrentRouteRunSelection,
2568+
new Map([
2569+
['run1', true],
2570+
['run2', true],
2571+
])
2572+
);
2573+
store.overrideSelector(
2574+
commonSelectors.getFilteredRenderableRunsIdsFromRoute,
2575+
new Set(['run1', 'run2'])
2576+
);
25322577
store.overrideSelector(getCardStateMap, {
25332578
card1: {
25342579
dataMinMax: {
@@ -2603,6 +2648,128 @@ describe('scalar card', () => {
26032648

26042649
expect(dataTableComponent).toBeFalsy();
26052650
}));
2651+
2652+
it('projects tb-data-table-header-cell for enabled headers', fakeAsync(() => {
2653+
store.overrideSelector(getMetricsLinkedTimeSelection, {
2654+
start: {step: 20},
2655+
end: null,
2656+
});
2657+
store.overrideSelector(getSingleSelectionHeaders, [
2658+
{
2659+
type: ColumnHeaderType.RUN,
2660+
name: 'run',
2661+
displayName: 'Run',
2662+
enabled: true,
2663+
},
2664+
{
2665+
type: ColumnHeaderType.VALUE,
2666+
name: 'value',
2667+
displayName: 'Value',
2668+
enabled: false,
2669+
},
2670+
{
2671+
type: ColumnHeaderType.STEP,
2672+
name: 'step',
2673+
displayName: 'Step',
2674+
enabled: true,
2675+
},
2676+
]);
2677+
const fixture = createComponent('card1');
2678+
fixture.detectChanges();
2679+
2680+
const dataTableComponentInstance = fixture.debugElement.query(
2681+
By.directive(DataTableComponent)
2682+
).componentInstance;
2683+
2684+
expect(dataTableComponentInstance.headerCells.length).toEqual(2);
2685+
2686+
expect(
2687+
dataTableComponentInstance.headerCells.get(0).header.name
2688+
).toEqual('run');
2689+
expect(
2690+
dataTableComponentInstance.headerCells.get(1).header.name
2691+
).toEqual('step');
2692+
}));
2693+
2694+
it('projects tb-data-table-content-cell with data for enabled headers', fakeAsync(() => {
2695+
const fixture = createComponent('card1');
2696+
const scalarCardDataTable = fixture.debugElement.query(
2697+
By.directive(ScalarCardDataTable)
2698+
);
2699+
fixture.detectChanges();
2700+
2701+
const data =
2702+
scalarCardDataTable.componentInstance.getTimeSelectionTableData();
2703+
2704+
const contentRowComponents = fixture.debugElement.queryAll(
2705+
By.directive(ContentRowComponent)
2706+
);
2707+
2708+
expect(contentRowComponents.length).toEqual(2);
2709+
2710+
const firstRowContentCells = contentRowComponents[0].queryAll(
2711+
By.directive(ContentCellComponent)
2712+
);
2713+
2714+
expect(firstRowContentCells.length).toEqual(3);
2715+
2716+
expect(
2717+
firstRowContentCells.map((cell) => cell.componentInstance.datum)
2718+
).toEqual([data[0].color, data[0].run, data[0].step]);
2719+
2720+
const secondRowContentCells = contentRowComponents[1].queryAll(
2721+
By.directive(ContentCellComponent)
2722+
);
2723+
2724+
expect(secondRowContentCells.length).toEqual(3);
2725+
2726+
expect(
2727+
secondRowContentCells.map((cell) => cell.componentInstance.datum)
2728+
).toEqual([data[1].color, data[1].run, data[1].step]);
2729+
}));
2730+
2731+
it('does not project smoothed column when smoothing is disabled', fakeAsync(() => {
2732+
store.overrideSelector(getSingleSelectionHeaders, [
2733+
{
2734+
type: ColumnHeaderType.RUN,
2735+
name: 'run',
2736+
displayName: 'Run',
2737+
enabled: true,
2738+
},
2739+
{
2740+
type: ColumnHeaderType.SMOOTHED,
2741+
name: 'smoothed',
2742+
displayName: 'Smoothed',
2743+
enabled: true,
2744+
},
2745+
]);
2746+
2747+
store.overrideSelector(selectors.getMetricsScalarSmoothing, 0);
2748+
2749+
const fixture = createComponent('card1');
2750+
const scalarCardDataTable = fixture.debugElement.query(
2751+
By.directive(ScalarCardDataTable)
2752+
);
2753+
fixture.detectChanges();
2754+
2755+
let dataTableComponentInstance = fixture.debugElement.query(
2756+
By.directive(DataTableComponent)
2757+
).componentInstance;
2758+
2759+
let contentCellTypes = scalarCardDataTable
2760+
.queryAll(By.directive(ContentCellComponent))
2761+
.map((cell) => cell.componentInstance.header.type);
2762+
2763+
expect(
2764+
dataTableComponentInstance.headerCells.find(
2765+
(cell: HeaderCellComponent) =>
2766+
cell.header.type === ColumnHeaderType.SMOOTHED
2767+
)
2768+
).toBeFalsy();
2769+
expect(
2770+
contentCellTypes.find((type) => type === ColumnHeaderType.SMOOTHED)
2771+
).toBeFalsy();
2772+
}));
26062773
});
26072774

26082775
describe('line chart integration', () => {

0 commit comments

Comments
 (0)