Skip to content

Commit 35fc599

Browse files
authored
feat: Resize columns option in web UI (#2358)
Following below draft spec by @dsmmcken - **Resize Column**: If the column is currently manually sized, switch it to auto-resize, and reset the max column width seen to only what's in the current viewport (NOT the max ever seen, but the actual max visible in current viewport). If the column is currently auto sized, set the column to manual and size to max column width seen in the current viewport. - The assumption that can be made is that whatever the current behaviour is, it’s not what the users wants, so no need to require the user to make a conscious choice between auto and resize, just present one option to resize, and do the opposite of what we are doing currently. - **Resize All Columns**: Same behaviour as above but apply across all columns, and treat like an indeterminate checkbox. If any are manual, resize all as above, but flip all to manual. If all are manual, resize and flip all to auto. - We should try this and see if it feels natural. Fixed an issue where double clicking the column separator in the header bar resizes the column without changing the column to auto resize. - There is now a discrepancy between double clicking the column separator in the header bar and clicking "Resize Column", as the former auto-resizes to the largest cached width, whilst the latter resizes to the width of content in the current viewport. - I assume double clicking the column separator follows the behaviour of Excel so this discrepancy is intended. Below is a useful snippet for testing, that creates a table that has two columns that continuously gets bigger ``` from deephaven import time_table tt = time_table("PT0.5s").update(["x=`a`.repeat(i)", "y=`b`.repeat(i)"]) ``` Closes #1486
1 parent 97c59b0 commit 35fc599

7 files changed

Lines changed: 270 additions & 15 deletions

File tree

packages/grid/src/GridMetricCalculator.ts

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,18 @@ export class GridMetricCalculator {
130130
/** User set row heights */
131131
protected userRowHeights: ModelSizeMap;
132132

133-
/** Calculated column widths based on cell contents */
133+
/** Calculated column widths based on cell contents and caching largest value */
134134
protected calculatedColumnWidths: ModelSizeMap;
135135

136-
/** Calculated row heights based on cell contents */
136+
/** Calculated row heights based on cell contents and caching largest value */
137137
protected calculatedRowHeights: ModelSizeMap;
138138

139+
/** Calculated column widths based on cell contents */
140+
protected contentColumnWidths: ModelSizeMap;
141+
142+
/** Calculated row heights based on cell contents */
143+
protected contentRowHeights: ModelSizeMap;
144+
139145
/** Cache of fonts to estimated width of the smallest char */
140146
protected fontWidthsLower: Map<string, number>;
141147

@@ -162,6 +168,8 @@ export class GridMetricCalculator {
162168
userRowHeights = new Map(),
163169
calculatedColumnWidths = new Map(),
164170
calculatedRowHeights = new Map(),
171+
contentColumnWidths = new Map(),
172+
contentRowHeights = new Map(),
165173
fontWidthsLower = new Map(),
166174
fontWidthsUpper = new Map(),
167175
allCharWidths = new Map(),
@@ -178,6 +186,8 @@ export class GridMetricCalculator {
178186
this.calculatedColumnWidths = calculatedColumnWidths;
179187
this.allCharWidths = allCharWidths;
180188
this.fontWidthsLower = fontWidthsLower;
189+
this.contentColumnWidths = contentColumnWidths;
190+
this.contentRowHeights = contentRowHeights;
181191
this.fontWidthsUpper = fontWidthsUpper;
182192

183193
// Need to track the last moved rows/columns array so we know if we need to reset our models cache
@@ -518,6 +528,8 @@ export class GridMetricCalculator {
518528
userRowHeights,
519529
calculatedRowHeights,
520530
calculatedColumnWidths,
531+
contentColumnWidths,
532+
contentRowHeights,
521533
} = this;
522534

523535
return {
@@ -608,6 +620,10 @@ export class GridMetricCalculator {
608620
visibleRowHeights,
609621
visibleColumnWidths,
610622

623+
// Map of the height/width of visible rows/columns without caching largest value
624+
contentColumnWidths,
625+
contentRowHeights,
626+
611627
// Array of floating rows/columns, by grid index
612628
floatingRows,
613629
floatingColumns,
@@ -1647,8 +1663,12 @@ export class GridMetricCalculator {
16471663
return rowHeight;
16481664
}
16491665

1666+
// Not sure how to accurately get the height of text. For now just return the theme height.
1667+
this.contentRowHeights.set(modelRow, Math.ceil(rowHeight));
1668+
trimMap(this.contentRowHeights);
1669+
16501670
const cachedValue = this.calculatedRowHeights.get(modelRow);
1651-
if (cachedValue != null) {
1671+
if (cachedValue != null && cachedValue > rowHeight) {
16521672
return cachedValue;
16531673
}
16541674

@@ -1695,6 +1715,9 @@ export class GridMetricCalculator {
16951715
let columnWidth = Math.ceil(Math.max(headerWidth, dataWidth));
16961716
columnWidth = Math.max(minColumnWidth, columnWidth);
16971717
columnWidth = Math.min(maxColumnWidth, columnWidth);
1718+
this.contentColumnWidths.set(modelColumn, columnWidth);
1719+
trimMap(this.contentColumnWidths);
1720+
16981721
if (cachedValue != null && cachedValue > columnWidth) {
16991722
columnWidth = cachedValue;
17001723
} else {
@@ -1768,7 +1791,8 @@ export class GridMetricCalculator {
17681791

17691792
let columnWidth = 0;
17701793

1771-
const rowsPerPage = height / rowHeight;
1794+
const gridY = this.getGridY(state);
1795+
const rowsPerPage = Math.floor((height - gridY) / rowHeight);
17721796
const bottom = Math.ceil(top + rowsPerPage);
17731797
const cellPadding = cellHorizontalPadding * 2;
17741798
GridUtils.iterateAllItems(
@@ -1941,6 +1965,19 @@ export class GridMetricCalculator {
19411965
this.userColumnWidths = userColumnWidths;
19421966
}
19431967

1968+
/**
1969+
* Sets the calculated width for the specified column
1970+
* @param column The column model index to set
1971+
* @param size The size to set it to
1972+
*/
1973+
setCalculatedColumnWidth(column: ModelIndex, size: number): void {
1974+
// Always use a new instance of the map so any consumer of the metrics knows there has been a change
1975+
const calculatedColumnWidths = new Map(this.calculatedColumnWidths);
1976+
calculatedColumnWidths.set(column, Math.ceil(size));
1977+
trimMap(calculatedColumnWidths);
1978+
this.calculatedColumnWidths = calculatedColumnWidths;
1979+
}
1980+
19441981
/**
19451982
* Resets all the calculated column widths
19461983
* Useful if the theme minimum column width changes
@@ -1974,6 +2011,19 @@ export class GridMetricCalculator {
19742011
this.calculatedRowHeights.delete(row);
19752012
}
19762013

2014+
/**
2015+
* Sets the calculated height for the specified row
2016+
* @param row The column model index to set
2017+
* @param size The size to set it to
2018+
*/
2019+
setCalculatedRowHeight(row: ModelIndex, size: number): void {
2020+
// Always use a new instance of the map so any consumer of the metrics knows there has been a change
2021+
const calculatedRowHeights = new Map(this.calculatedRowHeights);
2022+
calculatedRowHeights.set(row, Math.ceil(size));
2023+
trimMap(calculatedRowHeights);
2024+
this.calculatedColumnWidths = calculatedRowHeights;
2025+
}
2026+
19772027
/**
19782028
* Resets all the calculated row heights
19792029
* Useful if the theme row height changes

packages/grid/src/GridMetrics.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,10 @@ export type GridMetrics = {
173173
calculatedRowHeights: ModelSizeMap;
174174
calculatedColumnWidths: ModelSizeMap;
175175

176+
// Map of calculated row/column height/width without caching largest value
177+
contentColumnWidths: ModelSizeMap;
178+
contentRowHeights: ModelSizeMap;
179+
176180
// Max depth of column headers. Depth of 1 for a table without column groups
177181
columnHeaderMaxDepth: number;
178182
};

packages/grid/src/mouse-handlers/GridSeparatorMouseHandler.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -277,16 +277,7 @@ abstract class GridSeparatorMouseHandler extends GridMouseHandler {
277277
const modelIndexes = metrics[this.modelIndexesProperty];
278278
const modelIndex = getOrThrow(modelIndexes, separator.index);
279279

280-
const calculatedSize =
281-
metrics[this.calculatedSizesProperty].get(modelIndex);
282-
const defaultSize =
283-
metricCalculator[this.initialSizesProperty].get(modelIndex);
284-
285-
if (calculatedSize === defaultSize || calculatedSize == null) {
286-
this.resetSize(metricCalculator, modelIndex);
287-
} else {
288-
this.setSize(metricCalculator, modelIndex, calculatedSize);
289-
}
280+
this.resetSize(metricCalculator, modelIndex);
290281

291282
grid.forceUpdate();
292283

packages/iris-grid/src/IrisGrid.test.tsx

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,140 @@ it('should set gotoValueSelectedColumnName to empty string if no columns are giv
222222

223223
expect(component.state.gotoValueSelectedColumnName).toEqual('');
224224
});
225+
226+
describe('handleResizeColumn', () => {
227+
let irisGrid;
228+
let metricCalculator;
229+
230+
beforeAll(() => {
231+
irisGrid = makeComponent(
232+
irisGridTestUtils.makeModel(
233+
irisGridTestUtils.makeTable({
234+
columns: irisGridTestUtils.makeColumns(1),
235+
})
236+
)
237+
);
238+
metricCalculator = irisGrid.state.metricCalculator;
239+
});
240+
241+
it('should set column width to content width if undefined user width', async () => {
242+
const modelIndex = 0;
243+
const mockMetricCalculator = {
244+
...metricCalculator,
245+
userColumnWidths: new Map(),
246+
setColumnWidth: jest.fn((column, size) => {
247+
mockMetricCalculator.userColumnWidths.set(column, size);
248+
}),
249+
};
250+
Object.assign(irisGrid.state.metricCalculator, mockMetricCalculator);
251+
const contentWidth =
252+
irisGrid.state.metrics.contentColumnWidths.get(modelIndex);
253+
expect(contentWidth).toBeDefined();
254+
255+
irisGrid.handleResizeColumn(modelIndex);
256+
257+
expect(mockMetricCalculator.userColumnWidths.get(modelIndex)).toEqual(
258+
contentWidth
259+
);
260+
});
261+
262+
it('should reset user width & set calculated width to content width if column has defined user width', () => {
263+
const modelIndex = 0;
264+
const mockMetricCalculator = {
265+
...metricCalculator,
266+
userColumnWidths: new Map([[modelIndex, 100]]),
267+
setCalculatedColumnWidth: jest.fn((column, size) => {
268+
mockMetricCalculator.calculatedColumnWidths.set(column, size);
269+
}),
270+
resetColumnWidth: jest.fn(() => {
271+
mockMetricCalculator.userColumnWidths.delete(modelIndex);
272+
}),
273+
};
274+
Object.assign(irisGrid.state.metricCalculator, mockMetricCalculator);
275+
const contentWidth =
276+
irisGrid.state.metrics.contentColumnWidths.get(modelIndex);
277+
expect(contentWidth).toBeDefined();
278+
279+
irisGrid.handleResizeColumn(modelIndex);
280+
281+
expect(
282+
mockMetricCalculator.userColumnWidths.get(modelIndex)
283+
).toBeUndefined();
284+
expect(mockMetricCalculator.calculatedColumnWidths.get(modelIndex)).toEqual(
285+
contentWidth
286+
);
287+
});
288+
});
289+
290+
// auto resize -> reset user width and set calculated width to content width
291+
// manual resize -> set user width to content width
292+
describe('handleResizeAllColumns', () => {
293+
let irisGrid;
294+
let metricCalculator;
295+
296+
beforeAll(() => {
297+
irisGrid = makeComponent(
298+
irisGridTestUtils.makeModel(
299+
irisGridTestUtils.makeTable({
300+
columns: irisGridTestUtils.makeColumns(3),
301+
})
302+
)
303+
);
304+
metricCalculator = irisGrid.state.metricCalculator;
305+
});
306+
307+
it('should auto resize all columns if all were manually sized', () => {
308+
const mockMetricCalculator = {
309+
...metricCalculator,
310+
userColumnWidths: new Map([
311+
[0, 100],
312+
[1, 100],
313+
[2, 100],
314+
]),
315+
setCalculatedColumnWidth: jest.fn((column, size) => {
316+
mockMetricCalculator.calculatedColumnWidths.set(column, size);
317+
}),
318+
resetColumnWidth: jest.fn(column => {
319+
mockMetricCalculator.userColumnWidths.delete(column);
320+
}),
321+
};
322+
Object.assign(irisGrid.state.metricCalculator, mockMetricCalculator);
323+
const contentWidths = irisGrid.state.metrics.contentColumnWidths;
324+
325+
irisGrid.handleResizeAllColumns();
326+
327+
expect(mockMetricCalculator.userColumnWidths.size).toEqual(0);
328+
329+
contentWidths.forEach((contentWidth, modelIndex) => {
330+
expect(
331+
mockMetricCalculator.calculatedColumnWidths.get(modelIndex)
332+
).toEqual(contentWidth);
333+
});
334+
});
335+
336+
it('should manual resize all columns if not all were manually sized', () => {
337+
const mockMetricCalculator = {
338+
...metricCalculator,
339+
userColumnWidths: new Map([
340+
[0, 100],
341+
[1, 100],
342+
]),
343+
setColumnWidth: jest.fn((column, size) => {
344+
mockMetricCalculator.userColumnWidths.set(column, size);
345+
}),
346+
resetColumnWidth: jest.fn(column => {
347+
mockMetricCalculator.userColumnWidths.delete(column);
348+
}),
349+
};
350+
Object.assign(irisGrid.state.metricCalculator, mockMetricCalculator);
351+
const contentWidths = irisGrid.state.metrics.contentColumnWidths;
352+
353+
irisGrid.handleResizeAllColumns();
354+
355+
contentWidths.forEach((contentWidth, modelIndex) => {
356+
expect(mockMetricCalculator.userColumnWidths.get(modelIndex)).toEqual(
357+
contentWidth
358+
);
359+
});
360+
});
361+
});

packages/iris-grid/src/IrisGrid.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3443,6 +3443,59 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
34433443
this.grid?.forceUpdate();
34443444
}
34453445

3446+
handleResizeColumn(modelIndex: number): void {
3447+
const { metrics, metricCalculator } = this.state;
3448+
if (!metrics) throw new Error('Metrics not set');
3449+
3450+
const contentWidth = getOrThrow(metrics.contentColumnWidths, modelIndex);
3451+
3452+
const userWidths = metricCalculator.getUserColumnWidths();
3453+
if (userWidths.has(modelIndex)) {
3454+
metricCalculator.resetColumnWidth(modelIndex);
3455+
metricCalculator.setCalculatedColumnWidth(modelIndex, contentWidth);
3456+
} else {
3457+
metricCalculator.setColumnWidth(modelIndex, contentWidth);
3458+
}
3459+
3460+
this.grid?.forceUpdate();
3461+
}
3462+
3463+
handleResizeAllColumns(): void {
3464+
const { metrics, metricCalculator } = this.state;
3465+
if (!metrics) throw new Error('Metrics not set');
3466+
3467+
const allColumns = [...metrics.allColumnWidths.entries()];
3468+
const visibleColumns = allColumns
3469+
.filter(([_, width]) => width !== 0)
3470+
.map(([modelIndex]) => modelIndex);
3471+
3472+
const contentWidths = metrics.contentColumnWidths;
3473+
const userWidths = metricCalculator.getUserColumnWidths();
3474+
3475+
const manualColumns = visibleColumns.filter(modelIndex =>
3476+
userWidths.has(modelIndex)
3477+
);
3478+
3479+
if (visibleColumns.length === manualColumns.length) {
3480+
// All columns are manually sized, flip all to auto resize
3481+
for (let i = 0; i < visibleColumns.length; i += 1) {
3482+
const modelIndex = visibleColumns[i];
3483+
const contentWidth = getOrThrow(contentWidths, modelIndex);
3484+
metricCalculator.resetColumnWidth(modelIndex);
3485+
metricCalculator.setCalculatedColumnWidth(modelIndex, contentWidth);
3486+
}
3487+
} else {
3488+
// Flip all to manual sized
3489+
for (let i = 0; i < visibleColumns.length; i += 1) {
3490+
const modelIndex = visibleColumns[i];
3491+
const contentWidth = getOrThrow(contentWidths, modelIndex);
3492+
metricCalculator.setColumnWidth(modelIndex, contentWidth);
3493+
}
3494+
}
3495+
3496+
this.grid?.forceUpdate();
3497+
}
3498+
34463499
/**
34473500
* User added, removed, or changed the order of aggregations, or position
34483501
* @param aggregationSettings The new aggregation settings

packages/iris-grid/src/IrisGridMetricCalculator.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ export class IrisGridMetricCalculator extends GridMetricCalculator {
5353
getUserColumnWidths(): ModelSizeMap {
5454
return this.userColumnWidths;
5555
}
56+
57+
getCalculatedColumnWidths(): ModelSizeMap {
58+
return this.calculatedColumnWidths;
59+
}
5660
}
5761

5862
export default IrisGridMetricCalculator;

0 commit comments

Comments
 (0)