Skip to content

Commit

Permalink
feat: Resize columns option in web UI (#2358)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ericlln authored Feb 18, 2025
1 parent 97c59b0 commit 35fc599
Show file tree
Hide file tree
Showing 7 changed files with 270 additions and 15 deletions.
58 changes: 54 additions & 4 deletions packages/grid/src/GridMetricCalculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,18 @@ export class GridMetricCalculator {
/** User set row heights */
protected userRowHeights: ModelSizeMap;

/** Calculated column widths based on cell contents */
/** Calculated column widths based on cell contents and caching largest value */
protected calculatedColumnWidths: ModelSizeMap;

/** Calculated row heights based on cell contents */
/** Calculated row heights based on cell contents and caching largest value */
protected calculatedRowHeights: ModelSizeMap;

/** Calculated column widths based on cell contents */
protected contentColumnWidths: ModelSizeMap;

/** Calculated row heights based on cell contents */
protected contentRowHeights: ModelSizeMap;

/** Cache of fonts to estimated width of the smallest char */
protected fontWidthsLower: Map<string, number>;

Expand All @@ -162,6 +168,8 @@ export class GridMetricCalculator {
userRowHeights = new Map(),
calculatedColumnWidths = new Map(),
calculatedRowHeights = new Map(),
contentColumnWidths = new Map(),
contentRowHeights = new Map(),
fontWidthsLower = new Map(),
fontWidthsUpper = new Map(),
allCharWidths = new Map(),
Expand All @@ -178,6 +186,8 @@ export class GridMetricCalculator {
this.calculatedColumnWidths = calculatedColumnWidths;
this.allCharWidths = allCharWidths;
this.fontWidthsLower = fontWidthsLower;
this.contentColumnWidths = contentColumnWidths;
this.contentRowHeights = contentRowHeights;
this.fontWidthsUpper = fontWidthsUpper;

// Need to track the last moved rows/columns array so we know if we need to reset our models cache
Expand Down Expand Up @@ -518,6 +528,8 @@ export class GridMetricCalculator {
userRowHeights,
calculatedRowHeights,
calculatedColumnWidths,
contentColumnWidths,
contentRowHeights,
} = this;

return {
Expand Down Expand Up @@ -608,6 +620,10 @@ export class GridMetricCalculator {
visibleRowHeights,
visibleColumnWidths,

// Map of the height/width of visible rows/columns without caching largest value
contentColumnWidths,
contentRowHeights,

// Array of floating rows/columns, by grid index
floatingRows,
floatingColumns,
Expand Down Expand Up @@ -1647,8 +1663,12 @@ export class GridMetricCalculator {
return rowHeight;
}

// Not sure how to accurately get the height of text. For now just return the theme height.
this.contentRowHeights.set(modelRow, Math.ceil(rowHeight));
trimMap(this.contentRowHeights);

const cachedValue = this.calculatedRowHeights.get(modelRow);
if (cachedValue != null) {
if (cachedValue != null && cachedValue > rowHeight) {
return cachedValue;
}

Expand Down Expand Up @@ -1695,6 +1715,9 @@ export class GridMetricCalculator {
let columnWidth = Math.ceil(Math.max(headerWidth, dataWidth));
columnWidth = Math.max(minColumnWidth, columnWidth);
columnWidth = Math.min(maxColumnWidth, columnWidth);
this.contentColumnWidths.set(modelColumn, columnWidth);
trimMap(this.contentColumnWidths);

if (cachedValue != null && cachedValue > columnWidth) {
columnWidth = cachedValue;
} else {
Expand Down Expand Up @@ -1768,7 +1791,8 @@ export class GridMetricCalculator {

let columnWidth = 0;

const rowsPerPage = height / rowHeight;
const gridY = this.getGridY(state);
const rowsPerPage = Math.floor((height - gridY) / rowHeight);
const bottom = Math.ceil(top + rowsPerPage);
const cellPadding = cellHorizontalPadding * 2;
GridUtils.iterateAllItems(
Expand Down Expand Up @@ -1941,6 +1965,19 @@ export class GridMetricCalculator {
this.userColumnWidths = userColumnWidths;
}

/**
* Sets the calculated width for the specified column
* @param column The column model index to set
* @param size The size to set it to
*/
setCalculatedColumnWidth(column: ModelIndex, size: number): void {
// Always use a new instance of the map so any consumer of the metrics knows there has been a change
const calculatedColumnWidths = new Map(this.calculatedColumnWidths);
calculatedColumnWidths.set(column, Math.ceil(size));
trimMap(calculatedColumnWidths);
this.calculatedColumnWidths = calculatedColumnWidths;
}

/**
* Resets all the calculated column widths
* Useful if the theme minimum column width changes
Expand Down Expand Up @@ -1974,6 +2011,19 @@ export class GridMetricCalculator {
this.calculatedRowHeights.delete(row);
}

/**
* Sets the calculated height for the specified row
* @param row The column model index to set
* @param size The size to set it to
*/
setCalculatedRowHeight(row: ModelIndex, size: number): void {
// Always use a new instance of the map so any consumer of the metrics knows there has been a change
const calculatedRowHeights = new Map(this.calculatedRowHeights);
calculatedRowHeights.set(row, Math.ceil(size));
trimMap(calculatedRowHeights);
this.calculatedColumnWidths = calculatedRowHeights;
}

/**
* Resets all the calculated row heights
* Useful if the theme row height changes
Expand Down
4 changes: 4 additions & 0 deletions packages/grid/src/GridMetrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ export type GridMetrics = {
calculatedRowHeights: ModelSizeMap;
calculatedColumnWidths: ModelSizeMap;

// Map of calculated row/column height/width without caching largest value
contentColumnWidths: ModelSizeMap;
contentRowHeights: ModelSizeMap;

// Max depth of column headers. Depth of 1 for a table without column groups
columnHeaderMaxDepth: number;
};
Expand Down
11 changes: 1 addition & 10 deletions packages/grid/src/mouse-handlers/GridSeparatorMouseHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,16 +277,7 @@ abstract class GridSeparatorMouseHandler extends GridMouseHandler {
const modelIndexes = metrics[this.modelIndexesProperty];
const modelIndex = getOrThrow(modelIndexes, separator.index);

const calculatedSize =
metrics[this.calculatedSizesProperty].get(modelIndex);
const defaultSize =
metricCalculator[this.initialSizesProperty].get(modelIndex);

if (calculatedSize === defaultSize || calculatedSize == null) {
this.resetSize(metricCalculator, modelIndex);
} else {
this.setSize(metricCalculator, modelIndex, calculatedSize);
}
this.resetSize(metricCalculator, modelIndex);

grid.forceUpdate();

Expand Down
137 changes: 137 additions & 0 deletions packages/iris-grid/src/IrisGrid.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,140 @@ it('should set gotoValueSelectedColumnName to empty string if no columns are giv

expect(component.state.gotoValueSelectedColumnName).toEqual('');
});

describe('handleResizeColumn', () => {
let irisGrid;
let metricCalculator;

beforeAll(() => {
irisGrid = makeComponent(
irisGridTestUtils.makeModel(
irisGridTestUtils.makeTable({
columns: irisGridTestUtils.makeColumns(1),
})
)
);
metricCalculator = irisGrid.state.metricCalculator;
});

it('should set column width to content width if undefined user width', async () => {
const modelIndex = 0;
const mockMetricCalculator = {
...metricCalculator,
userColumnWidths: new Map(),
setColumnWidth: jest.fn((column, size) => {
mockMetricCalculator.userColumnWidths.set(column, size);
}),
};
Object.assign(irisGrid.state.metricCalculator, mockMetricCalculator);
const contentWidth =
irisGrid.state.metrics.contentColumnWidths.get(modelIndex);
expect(contentWidth).toBeDefined();

irisGrid.handleResizeColumn(modelIndex);

expect(mockMetricCalculator.userColumnWidths.get(modelIndex)).toEqual(
contentWidth
);
});

it('should reset user width & set calculated width to content width if column has defined user width', () => {
const modelIndex = 0;
const mockMetricCalculator = {
...metricCalculator,
userColumnWidths: new Map([[modelIndex, 100]]),
setCalculatedColumnWidth: jest.fn((column, size) => {
mockMetricCalculator.calculatedColumnWidths.set(column, size);
}),
resetColumnWidth: jest.fn(() => {
mockMetricCalculator.userColumnWidths.delete(modelIndex);
}),
};
Object.assign(irisGrid.state.metricCalculator, mockMetricCalculator);
const contentWidth =
irisGrid.state.metrics.contentColumnWidths.get(modelIndex);
expect(contentWidth).toBeDefined();

irisGrid.handleResizeColumn(modelIndex);

expect(
mockMetricCalculator.userColumnWidths.get(modelIndex)
).toBeUndefined();
expect(mockMetricCalculator.calculatedColumnWidths.get(modelIndex)).toEqual(
contentWidth
);
});
});

// auto resize -> reset user width and set calculated width to content width
// manual resize -> set user width to content width
describe('handleResizeAllColumns', () => {
let irisGrid;
let metricCalculator;

beforeAll(() => {
irisGrid = makeComponent(
irisGridTestUtils.makeModel(
irisGridTestUtils.makeTable({
columns: irisGridTestUtils.makeColumns(3),
})
)
);
metricCalculator = irisGrid.state.metricCalculator;
});

it('should auto resize all columns if all were manually sized', () => {
const mockMetricCalculator = {
...metricCalculator,
userColumnWidths: new Map([
[0, 100],
[1, 100],
[2, 100],
]),
setCalculatedColumnWidth: jest.fn((column, size) => {
mockMetricCalculator.calculatedColumnWidths.set(column, size);
}),
resetColumnWidth: jest.fn(column => {
mockMetricCalculator.userColumnWidths.delete(column);
}),
};
Object.assign(irisGrid.state.metricCalculator, mockMetricCalculator);
const contentWidths = irisGrid.state.metrics.contentColumnWidths;

irisGrid.handleResizeAllColumns();

expect(mockMetricCalculator.userColumnWidths.size).toEqual(0);

contentWidths.forEach((contentWidth, modelIndex) => {
expect(
mockMetricCalculator.calculatedColumnWidths.get(modelIndex)
).toEqual(contentWidth);
});
});

it('should manual resize all columns if not all were manually sized', () => {
const mockMetricCalculator = {
...metricCalculator,
userColumnWidths: new Map([
[0, 100],
[1, 100],
]),
setColumnWidth: jest.fn((column, size) => {
mockMetricCalculator.userColumnWidths.set(column, size);
}),
resetColumnWidth: jest.fn(column => {
mockMetricCalculator.userColumnWidths.delete(column);
}),
};
Object.assign(irisGrid.state.metricCalculator, mockMetricCalculator);
const contentWidths = irisGrid.state.metrics.contentColumnWidths;

irisGrid.handleResizeAllColumns();

contentWidths.forEach((contentWidth, modelIndex) => {
expect(mockMetricCalculator.userColumnWidths.get(modelIndex)).toEqual(
contentWidth
);
});
});
});
53 changes: 53 additions & 0 deletions packages/iris-grid/src/IrisGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3443,6 +3443,59 @@ class IrisGrid extends Component<IrisGridProps, IrisGridState> {
this.grid?.forceUpdate();
}

handleResizeColumn(modelIndex: number): void {
const { metrics, metricCalculator } = this.state;
if (!metrics) throw new Error('Metrics not set');

const contentWidth = getOrThrow(metrics.contentColumnWidths, modelIndex);

const userWidths = metricCalculator.getUserColumnWidths();
if (userWidths.has(modelIndex)) {
metricCalculator.resetColumnWidth(modelIndex);
metricCalculator.setCalculatedColumnWidth(modelIndex, contentWidth);
} else {
metricCalculator.setColumnWidth(modelIndex, contentWidth);
}

this.grid?.forceUpdate();
}

handleResizeAllColumns(): void {
const { metrics, metricCalculator } = this.state;
if (!metrics) throw new Error('Metrics not set');

const allColumns = [...metrics.allColumnWidths.entries()];
const visibleColumns = allColumns
.filter(([_, width]) => width !== 0)
.map(([modelIndex]) => modelIndex);

const contentWidths = metrics.contentColumnWidths;
const userWidths = metricCalculator.getUserColumnWidths();

const manualColumns = visibleColumns.filter(modelIndex =>
userWidths.has(modelIndex)
);

if (visibleColumns.length === manualColumns.length) {
// All columns are manually sized, flip all to auto resize
for (let i = 0; i < visibleColumns.length; i += 1) {
const modelIndex = visibleColumns[i];
const contentWidth = getOrThrow(contentWidths, modelIndex);
metricCalculator.resetColumnWidth(modelIndex);
metricCalculator.setCalculatedColumnWidth(modelIndex, contentWidth);
}
} else {
// Flip all to manual sized
for (let i = 0; i < visibleColumns.length; i += 1) {
const modelIndex = visibleColumns[i];
const contentWidth = getOrThrow(contentWidths, modelIndex);
metricCalculator.setColumnWidth(modelIndex, contentWidth);
}
}

this.grid?.forceUpdate();
}

/**
* User added, removed, or changed the order of aggregations, or position
* @param aggregationSettings The new aggregation settings
Expand Down
4 changes: 4 additions & 0 deletions packages/iris-grid/src/IrisGridMetricCalculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ export class IrisGridMetricCalculator extends GridMetricCalculator {
getUserColumnWidths(): ModelSizeMap {
return this.userColumnWidths;
}

getCalculatedColumnWidths(): ModelSizeMap {
return this.calculatedColumnWidths;
}
}

export default IrisGridMetricCalculator;
Loading

0 comments on commit 35fc599

Please sign in to comment.