Skip to content

Commit dc8ca19

Browse files
committed
spt-62362: Resize graph on container resize
1 parent e18b75b commit dc8ca19

2 files changed

Lines changed: 189 additions & 0 deletions

File tree

projects/swimlane/ngx-graph/src/lib/graph/graph.component.spec.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1548,3 +1548,117 @@ describe('GraphComponent snapAddedNodeIds before resetToPrevious', () => {
15481548
expect(parsed.ty).toBeCloseTo(120, 5);
15491549
}));
15501550
});
1551+
1552+
@Component({
1553+
selector: 'test-graph-container-resize-host',
1554+
template: `
1555+
<div class="graph-container" [style.width.px]="containerWidth" [style.height.px]="containerHeight">
1556+
<ngx-graph [nodes]="nodes" [links]="links" [layout]="syncLayout" [animate]="false"></ngx-graph>
1557+
</div>
1558+
`,
1559+
imports: [GraphComponent]
1560+
})
1561+
class TestGraphContainerResizeHostComponent {
1562+
syncLayout = new TestSyncLayout();
1563+
containerWidth = 600;
1564+
containerHeight = 400;
1565+
nodes: Node[] = [
1566+
{ id: 'n1', label: 'A' },
1567+
{ id: 'n2', label: 'B' }
1568+
];
1569+
links: Edge[] = [{ id: 'e1', source: 'n1', target: 'n2' }];
1570+
}
1571+
1572+
@Component({
1573+
selector: 'test-graph-fixed-view-host',
1574+
template: `<ngx-graph [nodes]="nodes" [links]="links" [layout]="syncLayout" [view]="[800, 600]"></ngx-graph>`,
1575+
imports: [GraphComponent]
1576+
})
1577+
class TestGraphFixedViewHostComponent {
1578+
syncLayout = new TestSyncLayout();
1579+
nodes: Node[] = [{ id: 'n1', label: 'A' }];
1580+
links: Edge[] = [];
1581+
}
1582+
1583+
describe('GraphComponent container resize', () => {
1584+
let resizeObserverCallback: ResizeObserverCallback | undefined;
1585+
let OriginalResizeObserver: typeof ResizeObserver;
1586+
1587+
beforeEach(async () => {
1588+
OriginalResizeObserver = window.ResizeObserver;
1589+
resizeObserverCallback = undefined;
1590+
class ResizeObserverMock {
1591+
constructor(callback: ResizeObserverCallback) {
1592+
resizeObserverCallback = callback;
1593+
}
1594+
observe(): void {}
1595+
disconnect(): void {}
1596+
unobserve(): void {}
1597+
}
1598+
(window as any).ResizeObserver = ResizeObserverMock;
1599+
1600+
await TestBed.configureTestingModule({
1601+
imports: [TestGraphContainerResizeHostComponent, TestGraphFixedViewHostComponent],
1602+
providers: [LayoutService]
1603+
}).compileComponents();
1604+
});
1605+
1606+
afterEach(() => {
1607+
window.ResizeObserver = OriginalResizeObserver;
1608+
});
1609+
1610+
it('refreshViewportDimensions updates width and height without re-running layout', fakeAsync(() => {
1611+
const fixture = TestBed.createComponent(TestGraphContainerResizeHostComponent);
1612+
fixture.detectChanges();
1613+
flush();
1614+
tick(16);
1615+
fixture.detectChanges();
1616+
flush();
1617+
1618+
const graph = fixture.debugElement.query(By.directive(GraphComponent)).componentInstance as GraphComponent;
1619+
const createGraphSpy = spyOn(graph, 'createGraph').and.callThrough();
1620+
spyOn(graph, 'getContainerDims').and.returnValue({ width: 960, height: 500 });
1621+
1622+
graph.refreshViewportDimensions();
1623+
1624+
expect(graph.width).toBe(960);
1625+
expect(graph.height).toBe(500);
1626+
expect(createGraphSpy).not.toHaveBeenCalled();
1627+
}));
1628+
1629+
it('observes the parent container and refreshes viewport dimensions on resize', fakeAsync(() => {
1630+
const fixture = TestBed.createComponent(TestGraphContainerResizeHostComponent);
1631+
fixture.detectChanges();
1632+
flush();
1633+
tick(16);
1634+
fixture.detectChanges();
1635+
flush();
1636+
1637+
expect(resizeObserverCallback).toBeDefined();
1638+
1639+
const graph = fixture.debugElement.query(By.directive(GraphComponent)).componentInstance as GraphComponent;
1640+
const createGraphSpy = spyOn(graph, 'createGraph').and.callThrough();
1641+
spyOn(graph, 'getContainerDims').and.returnValue({ width: 840, height: 400 });
1642+
1643+
resizeObserverCallback!([], {} as ResizeObserver);
1644+
tick(16);
1645+
fixture.detectChanges();
1646+
1647+
expect(graph.width).toBe(840);
1648+
expect(createGraphSpy).not.toHaveBeenCalled();
1649+
}));
1650+
1651+
it('refreshViewportDimensions is a no-op when [view] supplies explicit dimensions', fakeAsync(() => {
1652+
const fixture = TestBed.createComponent(TestGraphFixedViewHostComponent);
1653+
fixture.detectChanges();
1654+
flush();
1655+
tick(16);
1656+
fixture.detectChanges();
1657+
flush();
1658+
1659+
const graph = fixture.debugElement.query(By.directive(GraphComponent)).componentInstance as GraphComponent;
1660+
graph.refreshViewportDimensions();
1661+
expect(graph.width).toBe(800);
1662+
expect(graph.height).toBe(600);
1663+
}));
1664+
});

projects/swimlane/ngx-graph/src/lib/graph/graph.component.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,8 @@ export class GraphComponent implements OnInit, OnChanges, OnDestroy, AfterViewIn
228228
width: number;
229229
height: number;
230230
resizeSubscription: any;
231+
private containerResizeObserver: ResizeObserver | null = null;
232+
private containerResizeRafId: number | null = null;
231233
visibilityObserver: VisibilityObserver;
232234
private waitForGraphDims: ReturnType<typeof setInterval>;
233235
private destroy$ = new Subject<void>();
@@ -562,6 +564,7 @@ export class GraphComponent implements OnInit, OnChanges, OnDestroy, AfterViewIn
562564
*/
563565
ngAfterViewInit(): void {
564566
this.bindWindowResizeEvent();
567+
this.bindContainerResizeEvent();
565568

566569
// listen for visibility of the element for hidden by default scenario
567570
this.visibilityObserver = new VisibilityObserver(this.el, this.zone);
@@ -3494,6 +3497,43 @@ export class GraphComponent implements OnInit, OnChanges, OnDestroy, AfterViewIn
34943497
};
34953498
}
34963499

3500+
/**
3501+
* Remeasures the parent container and updates SVG viewport size without re-running layout.
3502+
* Invoked automatically when the parent element resizes (e.g. side panels opening or closing).
3503+
*/
3504+
public refreshViewportDimensions(): void {
3505+
if (this.view()) {
3506+
return;
3507+
}
3508+
3509+
const previousWidth = this.width;
3510+
const previousHeight = this.height;
3511+
3512+
this.basicUpdate();
3513+
3514+
if (this.width === previousWidth && this.height === previousHeight) {
3515+
return;
3516+
}
3517+
3518+
this.dims = calculateViewDimensions({
3519+
width: this.width,
3520+
height: this.height
3521+
});
3522+
3523+
this.updateTransform();
3524+
3525+
if (this.showMiniMap()) {
3526+
this.updateMinimap();
3527+
}
3528+
3529+
this.lastFullUpdateWidth = this.width;
3530+
this.lastFullUpdateHeight = this.height;
3531+
3532+
if (this.cd) {
3533+
this.cd.markForCheck();
3534+
}
3535+
}
3536+
34973537
public basicUpdate(): void {
34983538
const view = this.view();
34993539
if (view) {
@@ -3587,6 +3627,41 @@ export class GraphComponent implements OnInit, OnChanges, OnDestroy, AfterViewIn
35873627
if (this.resizeSubscription) {
35883628
this.resizeSubscription.unsubscribe();
35893629
}
3630+
if (this.containerResizeObserver) {
3631+
this.containerResizeObserver.disconnect();
3632+
this.containerResizeObserver = null;
3633+
}
3634+
if (this.containerResizeRafId != null) {
3635+
cancelAnimationFrame(this.containerResizeRafId);
3636+
this.containerResizeRafId = null;
3637+
}
3638+
}
3639+
3640+
private scheduleContainerResizeRefresh(): void {
3641+
if (this.containerResizeRafId != null) {
3642+
return;
3643+
}
3644+
3645+
this.containerResizeRafId = requestAnimationFrame(() => {
3646+
this.containerResizeRafId = null;
3647+
this.zone.run(() => this.refreshViewportDimensions());
3648+
});
3649+
}
3650+
3651+
private bindContainerResizeEvent(): void {
3652+
if (typeof ResizeObserver === 'undefined' || this.view()) {
3653+
return;
3654+
}
3655+
3656+
const parent = this.el.nativeElement.parentNode as HTMLElement | null;
3657+
if (!parent) {
3658+
return;
3659+
}
3660+
3661+
this.containerResizeObserver = new ResizeObserver(() => {
3662+
this.scheduleContainerResizeRefresh();
3663+
});
3664+
this.containerResizeObserver.observe(parent);
35903665
}
35913666

35923667
private bindWindowResizeEvent(): void {

0 commit comments

Comments
 (0)