Skip to content

Commit

Permalink
Merge pull request #244 from reaviz/Add-fitNodesInView-func
Browse files Browse the repository at this point in the history
Add fit nodes in view func
  • Loading branch information
ghsteff authored May 23, 2024
2 parents abac3c0 + 85173b1 commit e90e46d
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 43 deletions.
30 changes: 20 additions & 10 deletions docs/Centering.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { GraphCanvas } from '../src';
<Meta title="Docs/Advanced/Centering" />

# Centering
Reagraph supports the ability to dynamically center nodes using the `centerGraph` method from the `GraphCanvasRef`. This method allows you to programmatically center the camera view around specific nodes or the entire graph.
Reagraph supports the ability to dynamically center nodes using the `centerGraph` and `fitNodesInView` methods from the `GraphCanvasRef`. These methods allows you to programmatically center the camera view around specific nodes or the entire graph.

### Usage
First, you need to get a reference to the `GraphRef`:
Expand All @@ -17,42 +17,52 @@ return (
)
```

Then, you can use the `centerGraph` method to center all nodes within view of the camera:
Then, you can use the `fitNodesInView` method to center all nodes within view of the camera:

```js
graphRef.current?.centerGraph();
graphRef.current?.fitNodesInView();
```
If you want to center the view around specific nodes, you can pass an array of node ids to the `centerGraph` method:
If you want to fit the view around specific nodes, you can pass an array of node ids to the `fitNodesInView` method:
```jsx
graphRef.current?.fitNodesInView(['node1', 'node2']);
```
If you want to center the camera on a given set of nodes without adjusting the zoom, you can use the `centerGraph` method:
```jsx
// Center the camera position based on all nodes in the graph
graphRef.current?.centerGraph();

// Center the camera position based on specific nodes
graphRef.current?.centerGraph(['node1', 'node2']);
```
### Examples
In this example, clicking the "Center Graph" button will center the camera around all the nodes in the graph:
In this example, clicking the "Fit View" button will fit the view around all the nodes in the graph:
```jsx
import React, { useRef } from 'react';
import { GraphCanvas } from 'reagraph';

const MyComponent = () => {
const graphRef = useRef<GraphCanvasRef | null>(null);

const centerGraph = () => {
graphRef.current?.centerGraph();
const fitView = () => {
graphRef.current?.fitNodesInView();
};

return (
<div>
<GraphCanvas ref={graphRef} {...} />
<button onClick={centerGraph}>Center Graph</button>
<button onClick={fitView}>Fit View</button>
</div>
);
};
```
Here's a more advanced example that centers the camera on the whole graph whenever new nodes are added:
Here's a more advanced example that fits the view on the whole graph whenever new nodes are added:
```jsx
import React, { useRef, useEffect } from 'react';
import { GraphCanvas } from 'reagraph';
Expand All @@ -61,7 +71,7 @@ const MyComponent = ({ nodes }) => {
const graphRef = useRef<GraphCanvasRef | null>(null);

useEffect(() => {
graphRef.current?.centerGraph();
graphRef.current?.fitNodesInView();
}, [nodes]);

return <GraphCanvas ref={graphRef} {...} />;
Expand Down
2 changes: 1 addition & 1 deletion docs/demos/Basic.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export const LiveUpdates = () => {
const [edges, setEdges] = useState(simpleEdges);

useEffect(() => {
ref.current?.centerGraph();
ref.current?.fitNodesInView();
}, [nodes]);

return (
Expand Down
1 change: 1 addition & 0 deletions docs/demos/Controls.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const All = () => {
<div style={{ zIndex: 9, position: 'absolute', top: 15, right: 15, background: 'rgba(0, 0, 0, .5)', padding: 1, color: 'white' }}>
<button style={{ display: 'block', width: '100%' }} onClick={() => ref.current?.centerGraph()}>Center</button>
<button style={{ display: 'block', width: '100%' }} onClick={() => ref.current?.centerGraph([simpleNodes[2].id])}>Center Node 2</button>
<button style={{ display: 'block', width: '100%' }} onClick={() => ref.current?.fitNodesInView()}>Fit View</button>
<br />
<button style={{ display: 'block', width: '100%' }} onClick={() => ref.current?.zoomIn()}>Zoom In</button>
<button style={{ display: 'block', width: '100%' }} onClick={() => ref.current?.zoomOut()}>Zoom Out</button>
Expand Down
97 changes: 78 additions & 19 deletions src/CameraControls/useCenterGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ export interface CenterNodesParams {
centerOnlyIfNodesNotInView?: boolean;
}

export interface FitNodesParams {
animated?: boolean;
fitOnlyIfNodesNotInView?: boolean;
}

export interface CenterGraphInput {
/**
* Whether the animate the transition or not.
Expand Down Expand Up @@ -54,10 +59,10 @@ export interface CenterGraphOutput {
/**
* Centers the graph on a specific node or list of nodes.
*
* @param ids - An array of node IDs to center the graph on. If this parameter is omitted,
* @param nodeIds - An array of node IDs to center the graph on. If this parameter is omitted,
* the graph will be centered on all nodes.
*
* @param centerOnlyIfNodesNotInView - A boolean flag that determines whether the graph should
* @param opts.centerOnlyIfNodesNotInView - A boolean flag that determines whether the graph should
* only be centered if the nodes specified by `ids` are not currently in view. If this
* parameter is `true`, the graph will only be re-centered if one or more of the nodes
* specified by `ids` are not currently in view. If this parameter is
Expand All @@ -66,6 +71,21 @@ export interface CenterGraphOutput {
*/
centerNodesById: (nodeIds: string[], opts?: CenterNodesParams) => void;

/**
* Fit all the given nodes into view of the camera.
*
* @param nodeIds - An array of node IDs to fit the view on. If this parameter is omitted,
* the view will fit to all nodes.
*
* @param opts.fitOnlyIfNodesNotInView - A boolean flag that determines whether the view should
* only be fit if the nodes specified by `ids` are not currently in view. If this
* parameter is `true`, the view will only be fit if one or more of the nodes
* specified by `ids` are not currently visible in the viewport. If this parameter is
* `false` or omitted, the view will be fit regardless of whether the nodes
* are currently in view.
*/
fitNodesInViewById: (nodeIds: string[], opts?: FitNodesParams) => void;

/**
* Whether the graph is centered or not.
*/
Expand Down Expand Up @@ -99,8 +119,34 @@ export const useCenterGraph = ({
nodes?.some(node => !isNodeInView(camera, node.position)))
) {
// Centers the graph based on the central most node
const { minX, maxX, minY, maxY, minZ, maxZ, x, y, z } =
getLayoutCenter(nodes);
const { x, y, z } = getLayoutCenter(nodes);

await controls.setTarget(x, y, z, animated);

if (!isCentered) {
setIsCentered(true);
}

invalidate();
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[invalidate, controls, nodes]
);

const fitNodesInView = useCallback(
async (
nodes,
opts: FitNodesParams = { animated: true, fitOnlyIfNodesNotInView: false }
) => {
const { fitOnlyIfNodesNotInView } = opts;

if (
!fitOnlyIfNodesNotInView ||
(fitOnlyIfNodesNotInView &&
nodes?.some(node => !isNodeInView(camera, node.position)))
) {
const { minX, maxX, minY, maxY, minZ, maxZ } = getLayoutCenter(nodes);

if (!layoutType.includes('3d')) {
// fitToBox will auto rotate to the closest axis including the z axis,
Expand All @@ -115,12 +161,14 @@ export const useCenterGraph = ({
void controls?.rotate(horizontalRotation, verticalRotation, true);
}

void controls?.zoomTo(1, opts?.animated);

await controls?.fitToBox(
new Box3(
new Vector3(minX, minY, minZ),
new Vector3(maxX, maxY, maxZ)
),
animated,
opts?.animated,
{
cover: false,
paddingLeft: PADDING,
Expand All @@ -129,21 +177,13 @@ export const useCenterGraph = ({
paddingTop: PADDING
}
);
await controls.setTarget(x, y, z, animated);

if (!isCentered) {
setIsCentered(true);
}

invalidate();
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[invalidate, controls, nodes]
[camera, controls, layoutType]
);

const centerNodesById = useCallback(
(nodeIds: string[], opts: CenterNodesParams) => {
const getNodesById = useCallback(
(nodeIds: string[]) => {
let mappedNodes: InternalGraphNode[] | null = null;

if (nodeIds?.length) {
Expand All @@ -162,12 +202,30 @@ export const useCenterGraph = ({
}, []);
}

return mappedNodes;
},
[nodes]
);

const centerNodesById = useCallback(
(nodeIds: string[], opts: CenterNodesParams) => {
const mappedNodes = getNodesById(nodeIds);

centerNodes(mappedNodes || nodes, {
animated,
centerOnlyIfNodesNotInView: opts?.centerOnlyIfNodesNotInView
});
},
[animated, centerNodes, nodes]
[animated, centerNodes, getNodesById, nodes]
);

const fitNodesInViewById = useCallback(
async (nodeIds: string[], opts: FitNodesParams) => {
const mappedNodes = getNodesById(nodeIds);

await fitNodesInView(mappedNodes || nodes, { animated, ...opts });
},
[animated, fitNodesInView, getNodesById, nodes]
);

useLayoutEffect(() => {
Expand All @@ -177,13 +235,14 @@ export const useCenterGraph = ({
if (!mounted.current) {
// Center the graph once nodes are loaded on mount
await centerNodes(nodes, { animated: false });
await fitNodesInView(nodes, { animated: false });
mounted.current = true;
}
}
}

load();
}, [controls, centerNodes, nodes, animated, camera]);
}, [controls, centerNodes, nodes, animated, camera, fitNodesInView]);

useHotkeys([
{
Expand All @@ -195,5 +254,5 @@ export const useCenterGraph = ({
}
]);

return { centerNodes, centerNodesById, isCentered };
return { centerNodes, centerNodesById, fitNodesInViewById, isCentered };
};
2 changes: 2 additions & 0 deletions src/GraphCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ export const GraphCanvas: FC<GraphCanvasProps & { ref?: Ref<GraphCanvasRef> }> =
useImperativeHandle(ref, () => ({
centerGraph: (nodeIds, opts) =>
rendererRef.current?.centerGraph(nodeIds, opts),
fitNodesInView: (nodeIds, opts) =>
rendererRef.current?.fitNodesInView(nodeIds, opts),
zoomIn: () => controlsRef.current?.zoomIn(),
zoomOut: () => controlsRef.current?.zoomOut(),
panLeft: () => controlsRef.current?.panLeft(),
Expand Down
37 changes: 29 additions & 8 deletions src/GraphScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ import {
Edges,
Node
} from './symbols';
import { CenterNodesParams, useCenterGraph } from './CameraControls';
import {
CenterNodesParams,
FitNodesParams,
useCenterGraph
} from './CameraControls';
import { LabelVisibilityType } from './utils';
import { useStore } from './store';
import Graph from 'graphology';
Expand Down Expand Up @@ -266,7 +270,7 @@ export interface GraphSceneRef {
* @param nodeIds - An array of node IDs to center the graph on. If this parameter is omitted,
* the graph will be centered on all nodes.
*
* @param centerOnlyIfNodesNotInView - A boolean flag that determines whether the graph should
* @param opts.centerOnlyIfNodesNotInView - A boolean flag that determines whether the graph should
* only be centered if the nodes specified by `ids` are not currently in view. If this
* parameter is `true`, the graph will only be re-centered if one or more of the nodes
* specified by `ids` are not currently in view. If this parameter is
Expand All @@ -275,6 +279,21 @@ export interface GraphSceneRef {
*/
centerGraph: (nodeIds?: string[], opts?: CenterNodesParams) => void;

/**
* Fit all the given nodes into view of the camera.
*
* @param nodeIds - An array of node IDs to fit the view on. If this parameter is omitted,
* the view will fit to all nodes.
*
* @param opts.fitOnlyIfNodesNotInView - A boolean flag that determines whether the view should
* only be fit if the nodes specified by `ids` are not currently in view. If this
* parameter is `true`, the view will only be fit if one or more of the nodes
* specified by `ids` are not currently visible in the viewport. If this parameter is
* `false` or omitted, the view will be fit regardless of whether the nodes
* are currently in view.
*/
fitNodesInView: (nodeIds?: string[], opts?: FitNodesParams) => void;

/**
* Calls render scene on the graph. this is useful when you want to manually render the graph
* for things like screenshots.
Expand Down Expand Up @@ -338,21 +357,23 @@ export const GraphScene: FC<GraphSceneProps & { ref?: Ref<GraphSceneRef> }> =
const clusters = useStore(state => [...state.clusters.values()]);

// Center the graph on the nodes
const { centerNodesById, isCentered } = useCenterGraph({
animated,
disabled,
layoutType
});
const { centerNodesById, fitNodesInViewById, isCentered } =
useCenterGraph({
animated,
disabled,
layoutType
});

// Let's expose some helper methods
useImperativeHandle(
ref,
() => ({
centerGraph: centerNodesById,
fitNodesInView: fitNodesInViewById,
graph,
renderScene: () => gl.render(scene, camera)
}),
[centerNodesById, graph, gl, scene, camera]
[centerNodesById, fitNodesInViewById, graph, gl, scene, camera]
);

const nodeComponents = useMemo(
Expand Down
8 changes: 3 additions & 5 deletions src/selection/useSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,8 @@ export const useSelection = ({
pathSelectionType
);

ref.current?.centerGraph([data.id, ...adjacents], {
centerOnlyIfNodesNotInView: true
ref.current.fitNodesInView([data.id, ...adjacents], {
fitOnlyIfNodesNotInView: true
});
}
},
Expand Down Expand Up @@ -352,9 +352,7 @@ export const useSelection = ({
throw new Error('No ref found for the graph canvas.');
}

ref.current?.centerGraph([], {
centerOnlyIfNodesNotInView: true
});
ref.current.fitNodesInView([], { fitOnlyIfNodesNotInView: true });
}
}
},
Expand Down

0 comments on commit e90e46d

Please sign in to comment.