Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fit nodes in view func #244

Merged
merged 3 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading