Skip to content

Commit 35311c8

Browse files
authored
feat: Add ResizeObserver to Grid and Chart (#1626)
- ResizeObserver is now widely available in all browsers, so use it to listen for resizing of our grid and plot elements - Pull `grid-wrapper` from `IrisGrid` and put it directly in `Grid` - Now `Grid` doesn't have to listen to the "parent" element, which was kind of strange in the first place. - Tested by opening up some tables and charts, resizing the panels and ensuring they updated correctly.
1 parent 80f29f5 commit 35311c8

11 files changed

Lines changed: 130 additions & 69 deletions

File tree

packages/chart/src/Chart.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ export class Chart extends Component<ChartProps, ChartState> {
132132
this.handleModelEvent = this.handleModelEvent.bind(this);
133133
this.handlePlotUpdate = this.handlePlotUpdate.bind(this);
134134
this.handleRelayout = this.handleRelayout.bind(this);
135+
this.handleResize = this.handleResize.bind(this);
135136
this.handleRestyle = this.handleRestyle.bind(this);
136137

137138
this.PlotComponent = createPlotlyComponent(props.Plotly);
@@ -144,6 +145,7 @@ export class Chart extends Component<ChartProps, ChartState> {
144145
this.isSubscribed = false;
145146
this.isLoadedFired = false;
146147
this.currentSeries = 0;
148+
this.resizeObserver = new window.ResizeObserver(this.handleResize);
147149

148150
this.state = {
149151
data: null,
@@ -170,6 +172,9 @@ export class Chart extends Component<ChartProps, ChartState> {
170172
if (isActive) {
171173
this.subscribe(model);
172174
}
175+
if (this.plotWrapper.current != null) {
176+
this.resizeObserver.observe(this.plotWrapper.current);
177+
}
173178
}
174179

175180
componentDidUpdate(prevProps: ChartProps): void {
@@ -183,6 +188,7 @@ export class Chart extends Component<ChartProps, ChartState> {
183188

184189
if (isActive !== prevProps.isActive) {
185190
if (isActive) {
191+
this.updateDimensions();
186192
this.subscribe(model);
187193
} else {
188194
this.unsubscribe(model);
@@ -193,6 +199,8 @@ export class Chart extends Component<ChartProps, ChartState> {
193199
componentWillUnmount(): void {
194200
const { model } = this.props;
195201
this.unsubscribe(model);
202+
203+
this.resizeObserver.disconnect();
196204
}
197205

198206
currentSeries: number;
@@ -219,6 +227,9 @@ export class Chart extends Component<ChartProps, ChartState> {
219227

220228
isLoadedFired: boolean;
221229

230+
// Listen for resizing of the element and update the canvas appropriately
231+
resizeObserver: ResizeObserver;
232+
222233
getCachedConfig = memoize(
223234
(
224235
downsamplingError: unknown,
@@ -468,6 +479,10 @@ export class Chart extends Component<ChartProps, ChartState> {
468479
this.updateModelDimensions();
469480
}
470481

482+
handleResize(): void {
483+
this.updateDimensions();
484+
}
485+
471486
handleRestyle([changes, seriesIndexes]: readonly [
472487
Record<string, unknown>,
473488
number[],

packages/dashboard-core-plugins/src/panels/ChartPanel.tsx

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,6 @@ export class ChartPanel extends Component<ChartPanelProps, ChartPanelState> {
218218
this.handleError = this.handleError.bind(this);
219219
this.handleLoadError = this.handleLoadError.bind(this);
220220
this.handleLoadSuccess = this.handleLoadSuccess.bind(this);
221-
this.handleResize = this.handleResize.bind(this);
222221
this.handleSettingsChanged = this.handleSettingsChanged.bind(this);
223222
this.handleOpenLinker = this.handleOpenLinker.bind(this);
224223
this.handleShow = this.handleShow.bind(this);
@@ -235,7 +234,6 @@ export class ChartPanel extends Component<ChartPanelProps, ChartPanelState> {
235234
this.handleClearAllFilters = this.handleClearAllFilters.bind(this);
236235

237236
this.panelContainer = props.containerRef ?? React.createRef();
238-
this.chart = React.createRef();
239237
this.pending = new Pending();
240238

241239
const { metadata, panelState } = props;
@@ -337,8 +335,6 @@ export class ChartPanel extends Component<ChartPanelProps, ChartPanelState> {
337335

338336
panelContainer: RefObject<HTMLDivElement>;
339337

340-
chart: RefObject<Chart>;
341-
342338
pending: Pending;
343339

344340
initModel(): void {
@@ -683,10 +679,6 @@ export class ChartPanel extends Component<ChartPanelProps, ChartPanelState> {
683679
this.setState({ isLoading: false });
684680
}
685681

686-
handleResize(): void {
687-
this.updateChart();
688-
}
689-
690682
handleSettingsChanged(update: Partial<Settings>): void {
691683
this.setState(({ settings: prevSettings }) => {
692684
const settings = {
@@ -752,7 +744,6 @@ export class ChartPanel extends Component<ChartPanelProps, ChartPanelState> {
752744
this.setState({ isActive }, () => {
753745
if (isActive) {
754746
this.loadModelIfNecessary();
755-
this.updateChart();
756747
}
757748
});
758749
}
@@ -1026,12 +1017,6 @@ export class ChartPanel extends Component<ChartPanelProps, ChartPanelState> {
10261017
});
10271018
}
10281019

1029-
updateChart(): void {
1030-
if (this.chart.current) {
1031-
this.chart.current.updateDimensions();
1032-
}
1033-
}
1034-
10351020
render(): ReactElement {
10361021
const {
10371022
columnSelectionValidator,
@@ -1097,7 +1082,6 @@ export class ChartPanel extends Component<ChartPanelProps, ChartPanelState> {
10971082
glEventHub={glEventHub}
10981083
onHide={this.handleHide}
10991084
onClearAllFilters={this.handleClearAllFilters}
1100-
onResize={this.handleResize}
11011085
onShow={this.handleShow}
11021086
onTabBlur={this.handleTabBlur}
11031087
onTabFocus={this.handleTabFocus}
@@ -1118,7 +1102,6 @@ export class ChartPanel extends Component<ChartPanelProps, ChartPanelState> {
11181102
isActive={isActive}
11191103
model={model}
11201104
settings={settings}
1121-
ref={this.chart}
11221105
onDisconnect={this.handleDisconnect}
11231106
onReconnect={this.handleReconnect}
11241107
onUpdate={this.handleUpdate}

packages/dashboard-core-plugins/src/panels/IrisGridPanel.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,6 @@ export class IrisGridPanel extends PureComponent<
251251
this.handleGridStateChange = this.handleGridStateChange.bind(this);
252252
this.handlePluginStateChange = this.handlePluginStateChange.bind(this);
253253
this.handleCreateChart = this.handleCreateChart.bind(this);
254-
this.handleResize = this.handleResize.bind(this);
255254
this.handleShow = this.handleShow.bind(this);
256255
this.handleTabClicked = this.handleTabClicked.bind(this);
257256
this.handleDisconnect = this.handleDisconnect.bind(this);
@@ -762,10 +761,6 @@ export class IrisGridPanel extends PureComponent<
762761
glEventHub.emit(IrisGridEvent.DATA_SELECTED, this, dataMap);
763762
}
764763

765-
handleResize(): void {
766-
this.updateGrid();
767-
}
768-
769764
handleShow(): void {
770765
this.updateGrid();
771766
}
@@ -1277,7 +1272,6 @@ export class IrisGridPanel extends PureComponent<
12771272
glContainer={glContainer}
12781273
glEventHub={glEventHub}
12791274
onClearAllFilters={this.handleClearAllFilters}
1280-
onResize={this.handleResize}
12811275
onShow={this.handleShow}
12821276
onTabFocus={this.handleShow}
12831277
onTabClicked={this.handleTabClicked}

packages/grid/src/Grid.scss

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
.grid-wrapper {
2+
flex: 1 1 0;
3+
max-width: 100%;
4+
max-height: 100%;
5+
// min-width/height used to make sure grid shrinks properly when notification bars are added/resized
6+
min-width: 0;
7+
min-height: 0;
8+
position: relative;
9+
font: sans-serif;
10+
font-feature-settings: 'tnum';
11+
}
12+
113
.grid-canvas {
214
display: block;
315
}

packages/grid/src/Grid.test.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,21 @@ function makeMockCanvas() {
7070
};
7171
}
7272

73+
function makeMockWrapper() {
74+
return {
75+
focus: jest.fn(),
76+
getBoundingClientRect: () => ({ width: VIEW_SIZE, height: VIEW_SIZE }),
77+
};
78+
}
79+
7380
function createNodeMock(element: ReactElement) {
7481
if (element.type === 'canvas') {
7582
return makeMockCanvas();
7683
}
84+
if (element?.props?.className?.includes('grid-wrapper') === true) {
85+
return makeMockWrapper();
86+
}
87+
7788
return null;
7889
}
7990

packages/grid/src/Grid.tsx

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
/* eslint react/no-did-update-set-state: "off" */
2-
import React, { CSSProperties, PureComponent, ReactNode } from 'react';
2+
import React, {
3+
CSSProperties,
4+
PureComponent,
5+
ReactNode,
6+
RefObject,
7+
} from 'react';
38
import classNames from 'classnames';
49
import memoize from 'memoize-one';
510
import clamp from 'lodash.clamp';
@@ -69,6 +74,9 @@ type LegacyCanvasRenderingContext2D = CanvasRenderingContext2D & {
6974
};
7075

7176
export type GridProps = typeof Grid.defaultProps & {
77+
// Children to render in the grid
78+
children?: ReactNode;
79+
7280
// Options to set on the canvas
7381
canvasOptions?: CanvasRenderingContext2DSettings;
7482

@@ -295,6 +303,12 @@ class Grid extends PureComponent<GridProps, GridState> {
295303

296304
canvasContext: CanvasRenderingContext2D | null;
297305

306+
// The wrapper element for the canvas, used for sizing
307+
canvasWrapper: RefObject<HTMLDivElement>;
308+
309+
// Listen for resizing of the element and update the canvas appropriately
310+
resizeObserver: ResizeObserver;
311+
298312
// We draw the canvas on the next animation frame, keep track of the next one
299313
animationFrame: number | null;
300314

@@ -351,6 +365,8 @@ class Grid extends PureComponent<GridProps, GridState> {
351365

352366
this.canvas = null;
353367
this.canvasContext = null;
368+
this.canvasWrapper = React.createRef();
369+
this.resizeObserver = new window.ResizeObserver(this.handleResize);
354370
this.animationFrame = null;
355371

356372
this.prevMetrics = null;
@@ -457,7 +473,9 @@ class Grid extends PureComponent<GridProps, GridState> {
457473
this.canvas?.addEventListener('wheel', this.handleWheel, {
458474
passive: false,
459475
});
460-
window.addEventListener('resize', this.handleResize);
476+
if (this.canvasWrapper.current != null) {
477+
this.resizeObserver.observe(this.canvasWrapper.current);
478+
}
461479

462480
this.updateCanvas();
463481

@@ -561,7 +579,7 @@ class Grid extends PureComponent<GridProps, GridState> {
561579
this.handleMouseUp as unknown as EventListenerOrEventListenerObject,
562580
true
563581
);
564-
window.removeEventListener('resize', this.handleResize);
582+
this.resizeObserver.disconnect();
565583

566584
this.stopDragTimer();
567585
}
@@ -801,17 +819,17 @@ class Grid extends PureComponent<GridProps, GridState> {
801819
}
802820

803821
private updateCanvasScale(): void {
804-
const { canvas, canvasContext } = this;
822+
const { canvas, canvasContext, canvasWrapper } = this;
805823
if (!canvas) throw new Error('canvas not set');
806824
if (!canvasContext) throw new Error('canvasContext not set');
807-
if (!canvas.parentElement) throw new Error('Canvas has no parent element');
825+
if (!canvasWrapper.current) throw new Error('canvasWrapper not set');
808826

809827
const scale = Grid.getScale(canvasContext);
810828
// the parent wrapper has 100% width/height, and is used for determining size
811829
// we don't want to stretch the canvas to 100%, to avoid fractional pixels.
812830
// A wrapper element must be used for sizing, and canvas size must be
813831
// set manually to a floored value in css and a scaled value in width/height
814-
const rect = canvas.parentElement.getBoundingClientRect();
832+
const rect = canvasWrapper.current.getBoundingClientRect();
815833
const width = Math.floor(rect.width);
816834
const height = Math.floor(rect.height);
817835
canvas.style.width = `${width}px`;
@@ -2177,10 +2195,11 @@ class Grid extends PureComponent<GridProps, GridState> {
21772195
}
21782196

21792197
render(): ReactNode {
2198+
const { children } = this.props;
21802199
const { cursor } = this.state;
21812200

21822201
return (
2183-
<>
2202+
<div className="grid-wrapper" ref={this.canvasWrapper}>
21842203
<canvas
21852204
className={classNames('grid-canvas', Grid.getCursorClassName(cursor))}
21862205
ref={canvas => {
@@ -2198,7 +2217,8 @@ class Grid extends PureComponent<GridProps, GridState> {
21982217
Your browser does not support HTML canvas. Update your browser?
21992218
</canvas>
22002219
{this.renderInputField()}
2201-
</>
2220+
{children}
2221+
</div>
22022222
);
22032223
}
22042224
}

packages/iris-grid/src/IrisGrid.scss

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,6 @@ $cell-invalid-box-shadow:
7979
}
8080

8181
.grid-wrapper {
82-
flex: 1 1 0;
83-
max-width: 100%;
84-
max-height: 100%;
85-
// min-width/height used to make sure grid shrinks properly when notification bars are added/resized
86-
min-width: 0;
87-
min-height: 0;
88-
position: relative;
8982
font: $iris-grid-font;
9083
font-feature-settings: $iris-grid-font-feature-settings;
9184
transition: all $transition-mid;

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { ReactElement } from 'react';
22
import TestRenderer from 'react-test-renderer';
33
import dh from '@deephaven/jsapi-shim';
44
import { DateUtils, Settings } from '@deephaven/jsapi-utils';
@@ -48,10 +48,19 @@ function makeMockCanvas() {
4848
};
4949
}
5050

51-
function createNodeMock(element) {
51+
function makeMockWrapper() {
52+
return {
53+
getBoundingClientRect: () => ({ width: VIEW_SIZE, height: VIEW_SIZE }),
54+
};
55+
}
56+
57+
function createNodeMock(element: ReactElement) {
5258
if (element.type === 'canvas') {
5359
return makeMockCanvas();
5460
}
61+
if (element?.props?.className?.includes('grid-wrapper') === true) {
62+
return makeMockWrapper();
63+
}
5564
return element;
5665
}
5766

0 commit comments

Comments
 (0)