Skip to content

Commit 3a70adc

Browse files
authored
HParams: Allow runs in the time series dashboard to be filtered by hparam (#6488)
## Motivation for features / changes The previous runs table allowed the rows to be filtered by hparam. We would like to maintain this behavior. ## Technical description of changes The existing runs table had an UI for adding hparam filters but the components are all contained within the runs table component. Therefore I am breaking the UI affordances out into discrete components. ## Screenshots of UI changes (or N/A) The UI (Light Mode): ![image](https://github.com/tensorflow/tensorboard/assets/78179109/9fac5f87-70eb-4f8c-914c-c119fb4ed4e9) The UI (Dark Mode): ![image](https://github.com/tensorflow/tensorboard/assets/78179109/42922b06-e8ee-45f5-88fa-e5bbb99b063c) Interval Filters: ![image](https://github.com/tensorflow/tensorboard/assets/78179109/73a32a85-dd1e-42d2-8de3-f266f15cf578) Filtered: ![image](https://github.com/tensorflow/tensorboard/assets/78179109/dfea0642-498e-4004-bf87-e92d1aafb47d) No Matches: ![image](https://github.com/tensorflow/tensorboard/assets/78179109/c6b28829-3ed8-4109-8138-492d7264eaff) The option does not appear for non filterable columns: ![image](https://github.com/tensorflow/tensorboard/assets/78179109/0df13b73-75d1-46b4-ab82-58d2a1fc509f) Using The Feature ![536ff53b-ec85-4276-a1b8-5e887051faae](https://github.com/tensorflow/tensorboard/assets/78179109/fdbd66dc-d236-4721-98c1-08c2038f6b90)
1 parent 26f5e33 commit 3a70adc

12 files changed

+533
-11
lines changed

tensorboard/webapp/metrics/views/main_view/common_selectors.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
getDashboardMetricsFilterMap,
3333
getDashboardHparamsAndMetricsSpecs,
3434
getDashboardHparamFilterMap,
35+
getDashboardDefaultHparamFilters,
3536
} from '../../../hparams/_redux/hparams_selectors';
3637
import {
3738
DiscreteFilter,
@@ -185,6 +186,19 @@ const utils = {
185186
},
186187
};
187188

189+
export const getCurrentColumnFilters = createSelector(
190+
getDashboardDefaultHparamFilters,
191+
getDashboardHparamFilterMap,
192+
getDashboardMetricsFilterMap,
193+
(defaultHparamsFilters, hparamFilters, metricFilters) => {
194+
return new Map([
195+
...defaultHparamsFilters,
196+
...hparamFilters,
197+
...metricFilters,
198+
]);
199+
}
200+
);
201+
188202
const getRenderableRuns = createSelector(
189203
getDashboardRuns,
190204
getDashboardExperimentNames,
@@ -272,6 +286,7 @@ export const getPotentialHparamColumns = createSelector(
272286
removable: true,
273287
sortable: true,
274288
movable: true,
289+
filterable: true,
275290
}));
276291
}
277292
);
@@ -290,5 +305,6 @@ export const TEST_ONLY = {
290305
getRenderableRuns,
291306
getRenderableCardIdsWithMetadata,
292307
getScalarTagsForRunSelection,
308+
getCurrentColumnFilters,
293309
utils,
294310
};

tensorboard/webapp/metrics/views/main_view/common_selectors_test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -978,6 +978,7 @@ describe('common selectors', () => {
978978
removable: true,
979979
sortable: true,
980980
movable: true,
981+
filterable: true,
981982
},
982983
]);
983984
});
@@ -995,6 +996,7 @@ describe('common selectors', () => {
995996
removable: true,
996997
sortable: true,
997998
movable: true,
999+
filterable: true,
9981000
},
9991001
{
10001002
type: ColumnHeaderType.HPARAM,
@@ -1004,6 +1006,7 @@ describe('common selectors', () => {
10041006
removable: true,
10051007
sortable: true,
10061008
movable: true,
1009+
filterable: true,
10071010
},
10081011
]);
10091012
});

tensorboard/webapp/runs/store/runs_reducers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ const {initialState: uiInitialState, reducers: uiNamespaceContextedReducers} =
330330
enabled: true,
331331
sortable: true,
332332
movable: true,
333+
filterable: false,
333334
},
334335
],
335336
sortingInfo: {

tensorboard/webapp/runs/views/runs_table/runs_data_table.ng.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,12 @@
3232
[sortingInfo]="sortingInfo"
3333
[columnCustomizationEnabled]="true"
3434
[selectableColumns]="selectableColumns"
35+
[columnFilters]="columnFilters"
3536
(sortDataBy)="sortDataBy.emit($event)"
3637
(orderColumns)="orderColumns.emit($event)"
3738
(addColumn)="addColumn.emit($event)"
3839
(removeColumn)="removeColumn.emit($event)"
40+
(addFilter)="addFilter.emit($event)"
3941
>
4042
<ng-container header>
4143
<ng-container *ngFor="let header of getHeaders()">

tensorboard/webapp/runs/views/runs_table/runs_data_table.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ import {
2424
TableData,
2525
SortingInfo,
2626
ColumnHeaderType,
27+
FilterAddedEvent,
28+
DiscreteFilter,
29+
IntervalFilter,
2730
} from '../../../widgets/data_table/types';
28-
2931
@Component({
3032
selector: 'runs-data-table',
3133
templateUrl: 'runs_data_table.ng.html',
@@ -41,6 +43,7 @@ export class RunsDataTable {
4143
@Input() isFullScreen!: boolean;
4244
@Input() selectableColumns!: ColumnHeader[];
4345
@Input() loading!: boolean;
46+
@Input() columnFilters!: Map<string, DiscreteFilter | IntervalFilter>;
4447

4548
ColumnHeaderType = ColumnHeaderType;
4649

@@ -60,6 +63,7 @@ export class RunsDataTable {
6063
}>();
6164
@Output() removeColumn = new EventEmitter<ColumnHeader>();
6265
@Output() onSelectionDblClick = new EventEmitter<string>();
66+
@Output() addFilter = new EventEmitter<FilterAddedEvent>();
6367

6468
// These columns must be stored and reused to stop needless re-rendering of
6569
// the content and headers in these columns. This has been known to cause

tensorboard/webapp/runs/views/runs_table/runs_table_container.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ import {matchRunToRegex} from '../../../util/matcher';
6868
import {getEnableHparamsInTimeSeries} from '../../../feature_flag/store/feature_flag_selectors';
6969
import {
7070
ColumnHeader,
71-
ColumnHeaderType,
71+
FilterAddedEvent,
7272
SortingInfo,
7373
SortingOrder,
7474
TableData,
@@ -96,6 +96,7 @@ import {
9696
} from './runs_table_component';
9797
import {RunsTableColumn, RunTableItem} from './types';
9898
import {
99+
getCurrentColumnFilters,
99100
getFilteredRenderableRuns,
100101
getPotentialHparamColumns,
101102
} from '../../../metrics/views/main_view/common_selectors';
@@ -275,6 +276,7 @@ function matchFilter(
275276
[headers]="runsColumns$ | async"
276277
[data]="sortedRunsTableData$ | async"
277278
[selectableColumns]="selectableColumns$ | async"
279+
[columnFilters]="columnFilters$ | async"
278280
[sortingInfo]="sortingInfo$ | async"
279281
[experimentIds]="experimentIds"
280282
[regexFilter]="regexFilter$ | async"
@@ -290,6 +292,7 @@ function matchFilter(
290292
(toggleFullScreen)="toggleFullScreen()"
291293
(addColumn)="addColumn($event)"
292294
(removeColumn)="removeColumn($event)"
295+
(addFilter)="addHparamFilter($event)"
293296
></runs-data-table>
294297
`,
295298
host: {
@@ -372,6 +375,8 @@ export class RunsTableContainer implements OnInit, OnDestroy {
372375
})
373376
);
374377

378+
columnFilters$ = this.store.select(getCurrentColumnFilters);
379+
375380
allRunsTableData$ = this.store.select(getFilteredRenderableRuns).pipe(
376381
map((filteredRenderableRuns) => {
377382
return filteredRenderableRuns.map((runTableItem) => {
@@ -826,6 +831,15 @@ export class RunsTableContainer implements OnInit, OnDestroy {
826831
useDataTable() {
827832
return this.hparamsEnabled.value && !this.forceLegacyTable;
828833
}
834+
835+
addHparamFilter(event: FilterAddedEvent) {
836+
this.store.dispatch(
837+
hparamsActions.dashboardHparamFilterAdded({
838+
name: event.header.name,
839+
filter: event.value,
840+
})
841+
);
842+
}
829843
}
830844

831845
export const TEST_ONLY = {

tensorboard/webapp/runs/views/runs_table/runs_table_test.ts

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,6 @@ describe('runs_table', () => {
257257
RunsGroupMenuButtonContainer,
258258
RunsTableComponent,
259259
RunsTableContainer,
260-
RunsTableContainer,
261260
TestableColorPicker,
262261
],
263262
providers: [provideMockTbStore(), ColorPickerTestHelper],
@@ -2974,6 +2973,129 @@ describe('runs_table', () => {
29742973
expect(action.type).not.toBe(runSelectorSortChanged.type);
29752974
}
29762975
});
2976+
2977+
describe('runs data table integration', () => {
2978+
beforeEach(() => {
2979+
store.overrideSelector(getEnableHparamsInTimeSeries, true);
2980+
store.overrideSelector(getFilteredRenderableRuns, [
2981+
{
2982+
run: buildRun({
2983+
id: 'id1',
2984+
name: 'Book 1',
2985+
hparams: [{name: 'qaz', value: 0.5}],
2986+
}),
2987+
experimentAlias: {aliasNumber: 0, aliasText: 'hp'},
2988+
experimentName: 'HP',
2989+
selected: true,
2990+
runColor: 'fff',
2991+
hparams: new Map([['qaz', 0.5]]),
2992+
metrics: new Map<string, any>(),
2993+
},
2994+
{
2995+
run: buildRun({
2996+
id: 'id2',
2997+
name: 'Book 2',
2998+
hparams: [{name: 'qaz', value: 0.5}],
2999+
}),
3000+
experimentAlias: {aliasNumber: 0, aliasText: 'hp'},
3001+
experimentName: 'HP',
3002+
selected: true,
3003+
runColor: 'fff',
3004+
hparams: new Map([['qaz', 0.5]]),
3005+
metrics: new Map<string, any>(),
3006+
},
3007+
]);
3008+
3009+
store.overrideSelector(getRunsTableHeaders, [
3010+
{
3011+
type: ColumnHeaderType.HPARAM,
3012+
name: 'foo',
3013+
displayName: 'Foo',
3014+
enabled: true,
3015+
removable: true,
3016+
filterable: true,
3017+
sortable: true,
3018+
movable: true,
3019+
},
3020+
{
3021+
type: ColumnHeaderType.HPARAM,
3022+
name: 'qaz',
3023+
displayName: 'Qaz',
3024+
enabled: true,
3025+
removable: true,
3026+
filterable: true,
3027+
sortable: true,
3028+
movable: true,
3029+
},
3030+
]);
3031+
});
3032+
3033+
it('adds interval filters', () => {
3034+
const fixture = createComponent(TEST_HPARAM_SPECS, TEST_METRIC_SPECS);
3035+
fixture.detectChanges();
3036+
const dataTable = fixture.debugElement.query(
3037+
By.directive(RunsDataTable)
3038+
);
3039+
3040+
dataTable.componentInstance.addFilter.emit({
3041+
header: {
3042+
name: 'qaz',
3043+
},
3044+
value: {
3045+
type: DomainType.INTERVAL,
3046+
includeUndefined: true,
3047+
filterLowerValue: 10,
3048+
filterUpperValue: 20,
3049+
minValue: 10,
3050+
maxValue: 20,
3051+
},
3052+
});
3053+
expect(dispatchSpy).toHaveBeenCalledWith(
3054+
hparamsActions.dashboardHparamFilterAdded({
3055+
name: 'qaz',
3056+
filter: {
3057+
type: DomainType.INTERVAL,
3058+
includeUndefined: true,
3059+
filterLowerValue: 10,
3060+
filterUpperValue: 20,
3061+
minValue: 10,
3062+
maxValue: 20,
3063+
},
3064+
})
3065+
);
3066+
});
3067+
3068+
it('adds discrete filters', () => {
3069+
const fixture = createComponent(TEST_HPARAM_SPECS, TEST_METRIC_SPECS);
3070+
fixture.detectChanges();
3071+
const dataTable = fixture.debugElement.query(
3072+
By.directive(RunsDataTable)
3073+
);
3074+
3075+
dataTable.componentInstance.addFilter.emit({
3076+
header: {
3077+
name: 'foo',
3078+
},
3079+
value: {
3080+
type: DomainType.DISCRETE,
3081+
includeUndefined: true,
3082+
filterValues: [2, 4, 6, 8],
3083+
possibleValues: [2, 4, 6, 8, 10],
3084+
},
3085+
});
3086+
expect(dispatchSpy).toHaveBeenCalledWith(
3087+
hparamsActions.dashboardHparamFilterAdded({
3088+
name: 'foo',
3089+
filter: {
3090+
type: DomainType.DISCRETE,
3091+
includeUndefined: true,
3092+
filterValues: [2, 4, 6, 8],
3093+
possibleValues: [2, 4, 6, 8, 10],
3094+
},
3095+
})
3096+
);
3097+
});
3098+
});
29773099
});
29783100

29793101
function setNoFilterHparamsAndMetrics(

tensorboard/webapp/widgets/data_table/data_table_component.ng.html

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,7 @@
1414

1515
<custom-modal #contextMenu (onClose)="onContextMenuClosed()">
1616
<div class="context-menu">
17-
<div
18-
*ngIf="!contextMenuHeader?.removable && !contextMenuHeader?.sortable && !canContextMenuInsert()"
19-
class="no-actions-message"
20-
>
17+
<div *ngIf="isContextMenuEmpty()" class="no-actions-message">
2118
No Actions Available
2219
</div>
2320
<button
@@ -46,6 +43,14 @@
4643
<mat-icon svgIcon="arrow_upward_24px"></mat-icon>Sort Ascending
4744
</ng-template>
4845
</button>
46+
<button
47+
*ngIf="contextMenuHeader?.filterable"
48+
class="context-menu-button"
49+
mat-button
50+
(click)="openFilterMenu($event, contextMenuHeader)"
51+
>
52+
<mat-icon svgIcon="filter_alt_24px"></mat-icon> Filter
53+
</button>
4954
<button
5055
mat-button
5156
*ngIf="canContextMenuInsert()"
@@ -69,14 +74,24 @@
6974
#columnSelectorModal
7075
*ngIf="selectableColumns && selectableColumns.length"
7176
(onOpen)="focusColumnSelector()"
72-
onClose="onColumnSelectorClosed()"
77+
(onClose)="onColumnSelectorClosed()"
7378
>
7479
<tb-data-table-column-selector-component
7580
[selectableColumns]="selectableColumns"
7681
(columnSelected)="onColumnAdded($event)"
7782
></tb-data-table-column-selector-component>
7883
</custom-modal>
7984

85+
<custom-modal #filterModal (onClose)="onFilterClosed()">
86+
<tb-data-table-filter
87+
*ngIf="getCurrentColumnFilter()"
88+
[filter]="getCurrentColumnFilter()"
89+
(intervalFilterChanged)="intervalFilterChanged($event)"
90+
(discreteFilterChanged)="discreteFilterChanged($event)"
91+
(includeUndefinedToggled)="includeUndefinedToggled()"
92+
></tb-data-table-filter>
93+
</custom-modal>
94+
8095
<div class="data-table">
8196
<div class="header">
8297
<ng-content select="[header]"></ng-content>

0 commit comments

Comments
 (0)