Skip to content

Commit 2ebbe9c

Browse files
JamesHollyerdna2github
authored andcommitted
Column Customization: Add Data Table Column Dragging (tensorflow#6086)
* Motivation for features / changes This adds the main logic for dragging columns directly in the data table. * Technical description of changes This both adds the action/reducer which makes the actual change of column order and the UI logic while dragging. * Screenshots of UI changes ![columnDragging](https://user-images.githubusercontent.com/8672809/205188544-6db9e191-de3e-44be-9401-2684f8a55689.gif)
1 parent 418e7db commit 2ebbe9c

11 files changed

+249
-19
lines changed

tensorboard/webapp/metrics/actions/index.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ import {
3030
TooltipSort,
3131
XAxisType,
3232
} from '../internal_types';
33-
import {SortingInfo} from '../views/card_renderer/scalar_card_types';
33+
import {
34+
ColumnHeaders,
35+
SortingInfo,
36+
} from '../views/card_renderer/scalar_card_types';
3437

3538
export const metricsSettingsPaneClosed = createAction(
3639
'[Metrics] Metrics Settings Pane Closed'
@@ -197,6 +200,13 @@ export const sortingDataTable = createAction(
197200
props<SortingInfo>()
198201
);
199202

203+
export const dataTableColumnDrag = createAction(
204+
'[Metrics] Data table column dragged',
205+
props<{
206+
newOrder: ColumnHeaders[];
207+
}>()
208+
);
209+
200210
export const stepSelectorToggled = createAction(
201211
'[Metrics] Time Selector Enable Toggle',
202212
props<{

tensorboard/webapp/metrics/store/metrics_reducers.ts

+28
Original file line numberDiff line numberDiff line change
@@ -1092,6 +1092,34 @@ const reducer = createReducer(
10921092
linkedTimeSelection: null,
10931093
};
10941094
}),
1095+
on(actions.dataTableColumnDrag, (state, {newOrder}) => {
1096+
if (state.rangeSelectionEnabled) {
1097+
return {
1098+
...state,
1099+
rangeSelectionHeaders: newOrder,
1100+
};
1101+
}
1102+
1103+
// TODO(@jameshollyer): remove this logic with smoothing refactor *******
1104+
let orderAdjustedForSmoothed = newOrder;
1105+
const newSmoothedColumnIndex = newOrder.indexOf(ColumnHeaders.SMOOTHED);
1106+
const oldSmoothedColumnIndex = state.singleSelectionHeaders.indexOf(
1107+
ColumnHeaders.SMOOTHED
1108+
);
1109+
1110+
if (newSmoothedColumnIndex < 0 && oldSmoothedColumnIndex > 0) {
1111+
orderAdjustedForSmoothed = newOrder
1112+
.slice(0, oldSmoothedColumnIndex)
1113+
.concat([ColumnHeaders.SMOOTHED])
1114+
.concat(newOrder.slice(oldSmoothedColumnIndex, newOrder.length));
1115+
}
1116+
// *********************************************************************
1117+
1118+
return {
1119+
...state,
1120+
singleSelectionHeaders: orderAdjustedForSmoothed,
1121+
};
1122+
}),
10951123
on(actions.metricsToggleVisiblePlugin, (state, {plugin}) => {
10961124
let nextFilteredPluginTypes = new Set(state.filteredPluginTypes);
10971125
if (nextFilteredPluginTypes.has(plugin)) {

tensorboard/webapp/metrics/views/card_renderer/scalar_card_component.ng.html

+2
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,9 @@
177177
[stepOrLinkedTimeSelection]="stepOrLinkedTimeSelection"
178178
[dataHeaders]="dataHeaders"
179179
[sortingInfo]="sortingInfo"
180+
[columnCustomizationEnabled]="columnCustomizationEnabled"
180181
(sortDataBy)="sortDataBy($event)"
182+
(orderColumns)="reorderColumnHeaders.emit($event)"
181183
>
182184
</scalar-card-data-table>
183185
</div>

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

+2
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export class ScalarCardComponent<Downloader> {
9090
@Input() xScaleType!: ScaleType;
9191
@Input() useDarkMode!: boolean;
9292
@Input() forceSvg!: boolean;
93+
@Input() columnCustomizationEnabled!: boolean;
9394
@Input() linkedTimeSelection!: TimeSelectionView | null;
9495
@Input() stepOrLinkedTimeSelection!: TimeSelection | null;
9596
@Input() rangeSelectionEnabled: boolean = false;
@@ -106,6 +107,7 @@ export class ScalarCardComponent<Downloader> {
106107
@Output() onStepSelectorToggled =
107108
new EventEmitter<TimeSelectionToggleAffordance>();
108109
@Output() onDataTableSorting = new EventEmitter<SortingInfo>();
110+
@Output() reorderColumnHeaders = new EventEmitter<ColumnHeaders[]>();
109111

110112
@Output() onLineChartZoom = new EventEmitter<Extent>();
111113

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

+14-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ import {
4444
} from 'rxjs/operators';
4545
import {State} from '../../../app_state';
4646
import {ExperimentAlias} from '../../../experiments/types';
47-
import {getForceSvgFeatureFlag} from '../../../feature_flag/store/feature_flag_selectors';
47+
import {
48+
getForceSvgFeatureFlag,
49+
getIsScalarColumnCustomizationEnabled,
50+
} from '../../../feature_flag/store/feature_flag_selectors';
4851
import {
4952
getCardPinnedState,
5053
getCurrentRouteRunSelection,
@@ -68,6 +71,7 @@ import {classicSmoothing} from '../../../widgets/line_chart_v2/data_transformer'
6871
import {Extent} from '../../../widgets/line_chart_v2/lib/public_types';
6972
import {ScaleType} from '../../../widgets/line_chart_v2/types';
7073
import {
74+
dataTableColumnDrag,
7175
sortingDataTable,
7276
stepSelectorToggled,
7377
timeSelectionChanged,
@@ -162,6 +166,7 @@ function areSeriesEqual(
162166
[rangeSelectionEnabled]="rangeSelectionEnabled$ | async"
163167
[isProspectiveFobFeatureEnabled]="isProspectiveFobFeatureEnabled$ | async"
164168
[forceSvg]="forceSvg$ | async"
169+
[columnCustomizationEnabled]="columnCustomizationEnabled$ | async"
165170
[minMaxStep]="minMaxSteps$ | async"
166171
[dataHeaders]="columnHeaders$ | async"
167172
(onFullSizeToggle)="onFullSizeToggle()"
@@ -172,6 +177,7 @@ function areSeriesEqual(
172177
(onStepSelectorToggled)="onStepSelectorToggled($event)"
173178
(onDataTableSorting)="onDataTableSorting($event)"
174179
(onLineChartZoom)="onLineChartZoom($event)"
180+
(reorderColumnHeaders)="reorderColumnHeaders($event)"
175181
></scalar-card-component>
176182
`,
177183
styles: [
@@ -234,6 +240,9 @@ export class ScalarCardContainer implements CardRenderer, OnInit, OnDestroy {
234240
readonly tooltipSort$ = this.store.select(getMetricsTooltipSort);
235241
readonly xAxisType$ = this.store.select(getMetricsXAxisType);
236242
readonly forceSvg$ = this.store.select(getForceSvgFeatureFlag);
243+
readonly columnCustomizationEnabled$ = this.store.select(
244+
getIsScalarColumnCustomizationEnabled
245+
);
237246
readonly xScaleType$ = this.store.select(getMetricsXAxisType).pipe(
238247
map((xAxisType) => {
239248
switch (xAxisType) {
@@ -495,7 +504,6 @@ export class ScalarCardContainer implements CardRenderer, OnInit, OnDestroy {
495504
singleSelectionHeaders,
496505
rangeSelectionHeaders,
497506
]) => {
498-
const headers: ColumnHeaders[] = [];
499507
if (timeSelection === null || timeSelection.end === null) {
500508
if (!smoothingEnabled) {
501509
// Return single selection headers without smoothed header.
@@ -727,4 +735,8 @@ export class ScalarCardContainer implements CardRenderer, OnInit, OnDestroy {
727735
};
728736
this.lineChartZoom$.next(minMaxStepInViewPort);
729737
}
738+
739+
reorderColumnHeaders(headers: ColumnHeaders[]) {
740+
this.store.dispatch(dataTableColumnDrag({newOrder: headers}));
741+
}
730742
}

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

+4
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ import {
3838
[headers]="dataHeaders"
3939
[data]="getTimeSelectionTableData()"
4040
[sortingInfo]="sortingInfo"
41+
[columnCustomizationEnabled]="columnCustomizationEnabled"
4142
(sortDataBy)="sortDataBy.emit($event)"
43+
(orderColumns)="orderColumns.emit($event)"
4244
></tb-data-table>
4345
`,
4446
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -49,8 +51,10 @@ export class ScalarCardDataTable {
4951
@Input() stepOrLinkedTimeSelection!: TimeSelection;
5052
@Input() dataHeaders!: ColumnHeaders[];
5153
@Input() sortingInfo!: SortingInfo;
54+
@Input() columnCustomizationEnabled!: boolean;
5255

5356
@Output() sortDataBy = new EventEmitter<SortingInfo>();
57+
@Output() orderColumns = new EventEmitter<ColumnHeaders[]>();
5458

5559
getMinValueInRange(
5660
points: ScalarCardPoint[],

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

+4
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,10 @@ describe('scalar card', () => {
336336
store.overrideSelector(selectors.getRunColorMap, {});
337337
store.overrideSelector(selectors.getDarkModeEnabled, false);
338338
store.overrideSelector(selectors.getForceSvgFeatureFlag, false);
339+
store.overrideSelector(
340+
selectors.getIsScalarColumnCustomizationEnabled,
341+
false
342+
);
339343
store.overrideSelector(selectors.getMetricsStepSelectorEnabled, false);
340344
store.overrideSelector(
341345
selectors.getIsLinkedTimeProspectiveFobEnabled,

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

+12-5
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,15 @@
2121
</th>
2222
<ng-container *ngFor="let header of headers;">
2323
<th (click)="headerClicked(header)">
24-
<div [ngSwitch]="header" class="cell">
24+
<div
25+
[draggable]="columnCustomizationEnabled"
26+
(dragstart)="dragStart(header)"
27+
(dragend)="dragEnd()"
28+
(dragenter)="dragEnter(header)"
29+
[ngSwitch]="header"
30+
class="cell"
31+
[ngClass]="getHeaderHighlightStyle(header)"
32+
>
2533
<!-- order icon -->
2634
<mat-icon
2735
*ngSwitchCase="ColumnHeaders.VALUE_CHANGE"
@@ -35,16 +43,15 @@
3543

3644
<span>{{ getHeaderTextColumn(header) }}</span>
3745

38-
<div
39-
class="sorting-icon-container"
40-
[ngClass]="header === sortingInfo.header ? 'show' : 'show-on-hover'"
41-
>
46+
<div class="sorting-icon-container">
4247
<mat-icon
4348
*ngIf="sortingInfo.order === SortingOrder.ASCENDING || header !== sortingInfo.header"
49+
[ngClass]="header === sortingInfo.header ? 'show' : 'show-on-hover'"
4450
svgIcon="arrow_upward_24px"
4551
></mat-icon>
4652
<mat-icon
4753
*ngIf="sortingInfo.order === SortingOrder.DESCENDING && header === sortingInfo.header"
54+
[ngClass]="header === sortingInfo.header ? 'show' : 'show-on-hover'"
4855
svgIcon="arrow_downward_24px"
4956
></mat-icon>
5057
</div>

tensorboard/webapp/widgets/data_table/data_table_component.scss

+15
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ limitations under the License.
1515
@use '@angular/material' as mat;
1616
@import 'tensorboard/webapp/theme/tb_theme';
1717

18+
$_accent: map-get(mat.get-color-config($tb-theme), accent);
19+
1820
.data-table {
1921
border-spacing: 4px;
2022
font-size: 13px;
@@ -75,6 +77,7 @@ limitations under the License.
7577
.sorting-icon-container {
7678
width: 12px;
7779
height: 12px;
80+
border-radius: 5px;
7881
}
7982

8083
.show {
@@ -88,4 +91,16 @@ limitations under the License.
8891
th:hover .show-on-hover {
8992
opacity: 0.3;
9093
}
94+
95+
.highlight {
96+
background-color: mat.get-color-from-palette(mat.$gray-palette, 200);
97+
}
98+
99+
.highlight-border-right {
100+
border-right: 2px solid mat.get-color-from-palette($_accent);
101+
}
102+
103+
.highlight-border-left {
104+
border-left: 2px solid mat.get-color-from-palette($_accent);
105+
}
91106
}

tensorboard/webapp/widgets/data_table/data_table_component.ts

+81-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
Component,
1919
EventEmitter,
2020
Input,
21+
OnDestroy,
2122
Output,
2223
} from '@angular/core';
2324
import {
@@ -32,23 +33,43 @@ import {
3233
relativeTimeFormatter,
3334
} from '../line_chart_v2/lib/formatter';
3435

36+
enum Side {
37+
RIGHT,
38+
LEFT,
39+
}
40+
41+
const preventDefault = function (e: MouseEvent) {
42+
e.preventDefault();
43+
};
44+
3545
@Component({
3646
selector: 'tb-data-table',
3747
templateUrl: 'data_table_component.ng.html',
3848
styleUrls: ['data_table_component.css'],
3949
changeDetection: ChangeDetectionStrategy.OnPush,
4050
})
41-
export class DataTableComponent {
51+
export class DataTableComponent implements OnDestroy {
4252
// The order of this array of headers determines the order which they are
4353
// displayed in the table.
4454
@Input() headers!: ColumnHeaders[];
4555
@Input() data!: SelectedStepRunData[];
4656
@Input() sortingInfo!: SortingInfo;
57+
@Input() columnCustomizationEnabled!: boolean;
4758

4859
@Output() sortDataBy = new EventEmitter<SortingInfo>();
60+
@Output() orderColumns = new EventEmitter<ColumnHeaders[]>();
4961

5062
readonly ColumnHeaders = ColumnHeaders;
5163
readonly SortingOrder = SortingOrder;
64+
readonly Side = Side;
65+
66+
draggingHeader: ColumnHeaders | undefined;
67+
highlightedColumn: ColumnHeaders | undefined;
68+
highlightSide: Side = Side.RIGHT;
69+
70+
ngOnDestroy() {
71+
document.removeEventListener('dragover', preventDefault);
72+
}
5273

5374
getHeaderTextColumn(columnHeader: ColumnHeaders): string {
5475
switch (columnHeader) {
@@ -200,4 +221,63 @@ export class DataTableComponent {
200221
}
201222
this.sortDataBy.emit({header, order: SortingOrder.ASCENDING});
202223
}
224+
225+
dragStart(header: ColumnHeaders) {
226+
this.draggingHeader = header;
227+
228+
// This stop the end drag animation
229+
document.addEventListener('dragover', preventDefault);
230+
}
231+
232+
dragEnd() {
233+
if (!this.draggingHeader || !this.highlightedColumn) {
234+
return;
235+
}
236+
237+
this.orderColumns.emit(
238+
this.moveHeader(
239+
this.headers.indexOf(this.draggingHeader),
240+
this.headers.indexOf(this.highlightedColumn)
241+
)
242+
);
243+
this.draggingHeader = undefined;
244+
this.highlightedColumn = undefined;
245+
document.removeEventListener('dragover', preventDefault);
246+
}
247+
248+
dragEnter(header: ColumnHeaders) {
249+
if (!this.draggingHeader) {
250+
return;
251+
}
252+
if (
253+
this.headers.indexOf(header) < this.headers.indexOf(this.draggingHeader)
254+
) {
255+
this.highlightSide = Side.LEFT;
256+
} else {
257+
this.highlightSide = Side.RIGHT;
258+
}
259+
this.highlightedColumn = header;
260+
}
261+
262+
// Move the item at sourceIndex to destinationIndex
263+
moveHeader(sourceIndex: number, destinationIndex: number) {
264+
const newHeaders = [...this.headers];
265+
// Delete from original location
266+
newHeaders.splice(sourceIndex, 1);
267+
// Insert at destinationIndex.
268+
newHeaders.splice(destinationIndex, 0, this.headers[sourceIndex]);
269+
return newHeaders;
270+
}
271+
272+
getHeaderHighlightStyle(header: ColumnHeaders) {
273+
if (header !== this.highlightedColumn) {
274+
return {};
275+
}
276+
277+
return {
278+
highlight: true,
279+
'highlight-border-right': this.highlightSide === Side.RIGHT,
280+
'highlight-border-left': this.highlightSide === Side.LEFT,
281+
};
282+
}
203283
}

0 commit comments

Comments
 (0)