Skip to content

Commit

Permalink
Fix: Rendering Issues (#544)
Browse files Browse the repository at this point in the history
* feat: new ZoomOptions gives users the ability to force update zoom to fit by omitting check and combining auto center transform to reduce chance of flicker

* feat: enablePreUpdateTransform @input allows users to disable extra call to updateTrasform, reducing chance for flicker

* feat: stateChange @output emits changes in state, allowing users to check status of the graph

* feat: hasGraphDims, hasDims, hasNodeDims, and hasCompoundNodeDims allow users to check elements have dimension

* chore: update docs

* fix: update graph dims before zoom because sometimes they were out of sync

* fix: README instructions
  • Loading branch information
steveblue authored Mar 15, 2024
1 parent bb35ab5 commit 02e1ca4
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 54 deletions.
26 changes: 13 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,19 +272,19 @@ Run `npm run test` to execute the linter

## Release

Checkout master (`git checkout master`)
Pull master (`git pull`)
Refresh node modules (`npm ci`)
Run tests (`npm test`)
Examine log to determine next version (X.Y.Z)
Run `git checkout -b release/X.Y.Z`
Update version in `projects/swimlane/ngx-graph/package.json`.
Update changelog in `projects/swimlane/ngx-graph/CHANGELOG.md`
Run `git commit -am "(release): X.Y.Z"`
Run `git tag X.Y.Z`
Run `git push origin HEAD --tags`
Run `npm run publish:lib`
Submit PR
- Checkout master (`git checkout master`)
- Pull master (`git pull`)
- Refresh node modules (`npm ci`)
- Run tests (`npm test`)
- Examine log to determine next version (X.Y.Z)
- Run `git checkout -b release/X.Y.Z`
- Update version in `projects/swimlane/ngx-graph/package.json`.
- Update changelog in `projects/swimlane/ngx-graph/CHANGELOG.md`
- Run `git commit -am "(release): X.Y.Z"`
- Run `git tag X.Y.Z`
- Run `git push origin HEAD --tags`
- Run `npm run publish:lib`
- Submit PR

## Credits

Expand Down
116 changes: 82 additions & 34 deletions projects/swimlane/ngx-graph/src/lib/graph/graph.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ import { select } from 'd3-selection';
import * as shape from 'd3-shape';
import * as ease from 'd3-ease';
import 'd3-transition';
import { Observable, Subscription, of, fromEvent as observableFromEvent } from 'rxjs';
import { first, debounceTime } from 'rxjs/operators';
import { Observable, Subscription, of, fromEvent as observableFromEvent, Subject } from 'rxjs';
import { first, debounceTime, takeUntil } from 'rxjs/operators';
import { identity, scale, smoothMatrix, toSVG, transform, translate } from 'transformation-matrix';
import { Layout } from '../models/layout.model';
import { LayoutService } from './layouts/layout.service';
Expand All @@ -53,6 +53,21 @@ export interface Matrix {
f: number;
}

export interface NgxGraphZoomOptions {
autoCenter?: boolean;
force?: boolean;
}

export enum NgxGraphStates {
Init = 'init',
Subscribe = 'subscribe',
Transform = 'transform'
}

export interface NgxGraphStateChangeEvent {
state: NgxGraphStates;
}

@Component({
selector: 'ngx-graph',
styleUrls: ['./graph.component.scss'],
Expand Down Expand Up @@ -91,7 +106,7 @@ export class GraphComponent implements OnInit, OnChanges, OnDestroy, AfterViewIn
@Input() autoCenter = false;
@Input() update$: Observable<any>;
@Input() center$: Observable<any>;
@Input() zoomToFit$: Observable<any>;
@Input() zoomToFit$: Observable<NgxGraphZoomOptions>;
@Input() panToNode$: Observable<any>;
@Input() layout: string | Layout;
@Input() layoutSettings: any;
Expand All @@ -106,12 +121,14 @@ export class GraphComponent implements OnInit, OnChanges, OnDestroy, AfterViewIn
@Input() animations: boolean = true;
@Input() deferDisplayUntilPosition: boolean = false;
@Input() centerNodesOnPositionChange = true;
@Input() enablePreUpdateTransform = true;

@Output() select = new EventEmitter();
@Output() activate: EventEmitter<any> = new EventEmitter();
@Output() deactivate: EventEmitter<any> = new EventEmitter();
@Output() zoomChange: EventEmitter<number> = new EventEmitter();
@Output() clickHandler: EventEmitter<MouseEvent> = new EventEmitter();
@Output() stateChange: EventEmitter<NgxGraphStateChangeEvent> = new EventEmitter();

@ContentChild('linkTemplate') linkTemplate: TemplateRef<any>;
@ContentChild('nodeTemplate') nodeTemplate: TemplateRef<any>;
Expand All @@ -127,7 +144,6 @@ export class GraphComponent implements OnInit, OnChanges, OnDestroy, AfterViewIn
private isMouseMoveCalled: boolean = false;

graphSubscription: Subscription = new Subscription();
subscriptions: Subscription[] = [];
colors: ColorHelper;
dims: ViewDimensions;
seriesDomain: any;
Expand Down Expand Up @@ -155,6 +171,7 @@ export class GraphComponent implements OnInit, OnChanges, OnDestroy, AfterViewIn
height: number;
resizeSubscription: any;
visibilityObserver: VisibilityObserver;
private destroy$ = new Subject<void>();

constructor(
private el: ElementRef,
Expand Down Expand Up @@ -219,38 +236,31 @@ export class GraphComponent implements OnInit, OnChanges, OnDestroy, AfterViewIn
*/
ngOnInit(): void {
if (this.update$) {
this.subscriptions.push(
this.update$.subscribe(() => {
this.update();
})
);
this.update$.pipe(takeUntil(this.destroy$)).subscribe(() => {
this.update();
});
}

if (this.center$) {
this.subscriptions.push(
this.center$.subscribe(() => {
this.center();
})
);
this.center$.pipe(takeUntil(this.destroy$)).subscribe(() => {
this.center();
});
}

if (this.zoomToFit$) {
this.subscriptions.push(
this.zoomToFit$.subscribe(() => {
this.zoomToFit();
})
);
this.zoomToFit$.pipe(takeUntil(this.destroy$)).subscribe(options => {
this.zoomToFit(options ? options : {});
});
}

if (this.panToNode$) {
this.subscriptions.push(
this.panToNode$.subscribe((nodeId: string) => {
this.panToNodeId(nodeId);
})
);
this.panToNode$.pipe(takeUntil(this.destroy$)).subscribe((nodeId: string) => {
this.panToNodeId(nodeId);
});
}

this.minimapClipPathId = `minimapClip${id()}`;
this.stateChange.emit({ state: NgxGraphStates.Subscribe });
}

ngOnChanges(changes: SimpleChanges): void {
Expand Down Expand Up @@ -293,11 +303,8 @@ export class GraphComponent implements OnInit, OnChanges, OnDestroy, AfterViewIn
this.visibilityObserver.visible.unsubscribe();
this.visibilityObserver.destroy();
}

for (const sub of this.subscriptions) {
sub.unsubscribe();
}
this.subscriptions = null;
this.destroy$.next();
this.destroy$.complete();
}

/**
Expand Down Expand Up @@ -338,6 +345,9 @@ export class GraphComponent implements OnInit, OnChanges, OnDestroy, AfterViewIn

this.createGraph();
this.updateTransform();
if (!this.initialized) {
this.stateChange.emit({ state: NgxGraphStates.Init });
}
this.initialized = true;
});
}
Expand Down Expand Up @@ -870,7 +880,9 @@ export class GraphComponent implements OnInit, OnChanges, OnDestroy, AfterViewIn
this.transformationMatrix.a = isNaN(level) ? this.transformationMatrix.a : Number(level);
this.transformationMatrix.d = isNaN(level) ? this.transformationMatrix.d : Number(level);
this.zoomChange.emit(this.zoomLevel);
this.updateTransform();
if (this.enablePreUpdateTransform) {
this.updateTransform();
}
this.update();
}

Expand Down Expand Up @@ -935,6 +947,7 @@ export class GraphComponent implements OnInit, OnChanges, OnDestroy, AfterViewIn
*/
updateTransform(): void {
this.transform = toSVG(smoothMatrix(this.transformationMatrix, 100));
this.stateChange.emit({ state: NgxGraphStates.Transform });
}

/**
Expand Down Expand Up @@ -1144,9 +1157,10 @@ export class GraphComponent implements OnInit, OnChanges, OnDestroy, AfterViewIn
}

/**
* Zooms to fit the entier graph
* Zooms to fit the entire graph
*/
zoomToFit(): void {
zoomToFit(zoomOptions?: NgxGraphZoomOptions): void {
this.updateGraphDims();
const heightZoom = this.dims.height / this.graphDims.height;
const widthZoom = this.dims.width / this.graphDims.width;
let zoomLevel = Math.min(heightZoom, widthZoom, 1);
Expand All @@ -1159,9 +1173,15 @@ export class GraphComponent implements OnInit, OnChanges, OnDestroy, AfterViewIn
zoomLevel = this.maxZoomLevel;
}

if (zoomLevel !== this.zoomLevel) {
if (zoomOptions?.force === true || zoomLevel !== this.zoomLevel) {
this.zoomLevel = zoomLevel;
this.updateTransform();

if (zoomOptions?.autoCenter !== true) {
this.updateTransform();
}
if (zoomOptions?.autoCenter === true) {
this.center();
}
this.zoomChange.emit(this.zoomLevel);
}
}
Expand Down Expand Up @@ -1309,6 +1329,34 @@ export class GraphComponent implements OnInit, OnChanges, OnDestroy, AfterViewIn
return null;
}

/**
* Checks if the graph has dimensions
*/
public hasGraphDims(): boolean {
return this.graphDims.width > 0 && this.graphDims.height > 0;
}

/**
* Checks if all nodes have dimension
*/
public hasNodeDims(): boolean {
return this.graph.nodes?.every(node => node.dimension.width > 0 && node.dimension.height > 0);
}

/**
* Checks if all compound nodes have dimension
*/
public hasCompoundNodeDims(): boolean {
return this.graph.compoundNodes?.every(node => node.dimension.width > 0 && node.dimension.height > 0);
}

/**
* Checks if the graph and all nodes have dimension.
*/
public hasDims(): boolean {
return this.hasGraphDims() && this.hasNodeDims() && this.hasCompoundNodeDims();
}

protected unbindEvents(): void {
if (this.resizeSubscription) {
this.resizeSubscription.unsubscribe();
Expand Down
52 changes: 50 additions & 2 deletions src/docs/demos/interactive-demo.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,43 @@ The graph component accepts a `view` input, which is an array with two numeric e
[[note | Note]]
| The parent container must have a non-zero height defined, and no padding in order for the graph to properly fit its size.

Methods exist on `GraphComponent` to check if the graph has dimension. This can be useful for waiting to display the entire graph after it has proper dimensions.

| Method | Description |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| hasGraphDims | Returns true when the graph container has dimension. |
| hasNodeDims | Returns true when every node in the graph has dimension. |
| hasCompoundNodeDims | Returns true when every compound node in the graph has dimension. |
| hasDims | Returns true when all of the above methods combined return true. This means entire graph is ready to be drawn and updated. |

These methods can be used with the `stateChange @Output` to check when the graph has dimension. Suppose you had `GraphComponent` has a `ViewChild` in a parent `Component`.

```typescript
@ViewChild(GraphComponent) graphRef: GraphComponent;
```

In your template, bind the `stateChange @Output` of `GraphComponent` to a handler on the parent `Component class`.

```javascript
<ngx-graph
(stateChange)="handleStateChange($event)"
[zoomToFit$]="zoomToFit$"
>
```

In this callback, you can check for the `NgxGraphStates.Transform` state. `GraphComponent` emits this particular state whenever the graph is transformed. `NgxGraphStates` is an enum exported from @swimlane/ngx-graph. Upon a transform of the graph, check the graph has dimension with `hasDims()`. When `hasDims()` is `true`, it should be safe to zoom and center the graph on load, since these operations depend on the graph having proper dimensions. Additionally, if the parent `Component` implements a loading indicator (`this.isLoading`), the loading indicator can be removed since the graph is ready to display.

```javascript
handleStateChange(event: NgxGraphStateChangeEvent) {
if (event.state === NgxGraphStates.Transform && this.graphRef?.hasDims()) {
this.zoomToFit$.next({ autoCenter: true, force: true });
this.isLoading = false;
}
}
```
If you are interested in what's happening in the above example with `zoomToFit$`, check out [Fit To View](#fit-to-view) below.
## Line Curve Interpolation
This input allows you to choose the curve interpolation function for the edge lines. It accepts a function as an input, which is compatibe with D3's line interpolation functions and accepts any of them as an input, or a custom one ([link](https://github.com/d3/d3-shape/blob/master/README.md#curves)).
Expand Down Expand Up @@ -57,7 +94,7 @@ this.nodes.push(newNode);
this.nodes = [...this.nodes];
```
To manually trigger the updade, the graph accepts an `update$` input as an observable:
To manually trigger the update, the graph accepts an `update$` input as an observable:
#### Component:
Expand Down Expand Up @@ -122,11 +159,22 @@ export class MyComponent{
...

fitGraph() {
this.zoomToFit$.next(true)
this.zoomToFit$.next()
}
}
```
`zoomToFit$` optionally takes a configuration typed `NgxGraphZoomOptions`, i.e.
```javascript
this.zoomToFit$.next({ force: true, autoCenter: true });
```
| Event | Description |
| ---------- | ---------------------------------------------------------- |
| force | skips an internal check for the zoom value and forces zoom |
| autoCenter | combines zoomToFit with center |
#### Template:
```html
Expand Down
12 changes: 7 additions & 5 deletions src/docs/main.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,15 @@ ngx-graph is a graph visualization library for Angular
| miniMapMaxWidth | number | 100 | the maximum width of the minimap (in pixels) |
| miniMapMaxHeight | number | | the maximum height of the minimap (in pixels) |
| miniMapPosition | MiniMapPosition | MiniMapPosition.UpperRight | the position of the minimap |
| enablePreUpdateTransform | boolean | true | When set to false, disables an extra transform cycle when the graph updates |

_Deprecated inputs will be removed in the next major version of the package._

## Outputs

| Event | Description |
| ---------- | ------------------------------------------- |
| activate | element activation event (mouse enter) |
| deactivate | element deactivation event (mouse leave) |
| zoomChange | zoom change event, emits the new zoom level |
| Event | Description |
| ----------- | ------------------------------------------------------------------------------------ |
| activate | element activation event (mouse enter) |
| deactivate | element deactivation event (mouse leave) |
| zoomChange | zoom change event, emits the new zoom level |
| stateChange | state change event, emits lifecycle events like `init`, `transform`, and `subscribe` |

0 comments on commit 02e1ca4

Please sign in to comment.