Skip to content

Commit

Permalink
Merge pull request #287 from reaviz/node-cluster-position
Browse files Browse the repository at this point in the history
Constrain Nodes to Clusters
  • Loading branch information
amcdnl authored Oct 28, 2024
2 parents 138dcb1 + 19259c6 commit ff19c2a
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 29 deletions.
103 changes: 87 additions & 16 deletions docs/demos/Cluster.story.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,107 @@
import React from 'react';
import React, { useCallback, useState } from 'react';
import { GraphCanvas, lightTheme } from '../../src';
import { clusterNodes, clusterEdges, random, singleNodeClusterNodes, imbalancedClusterNodes, manyClusterNodes } from '../assets/demo';
import {
clusterNodes,
clusterEdges,
random,
singleNodeClusterNodes,
imbalancedClusterNodes,
manyClusterNodes
} from '../assets/demo';

export default {
title: 'Demos/Cluster',
component: GraphCanvas
};

export const Simple = () => (
<GraphCanvas nodes={clusterNodes} draggable edges={[]} clusterAttribute="type" />
);
export const Simple = () => {
const [nodes, setNodes] = useState(clusterNodes);

const addNode = useCallback(() => {
const next = nodes.length + 2;
setNodes(prev => [
...prev,
{
id: `${next}`,
label: `Node ${next}`,
fill: '#3730a3',
data: {
type: 'IP',
segment: next % 2 === 0 ? 'A' : undefined
}
}
]);
}, [nodes]);

return (
<>
<GraphCanvas nodes={nodes} draggable edges={[]} clusterAttribute="type" constrainDragging />
<div style={{ zIndex: 9, position: 'absolute', top: 15, right: 15 }}>
<button type="button" onClick={addNode}>
Add node
</button>
</div>
</>
);
};

const clusterNodesWithSizes = clusterNodes.map(node => ({
...node,
size: random(0, 50)
}));

export const Sizes = () => (
<GraphCanvas nodes={clusterNodesWithSizes} draggable edges={[]} clusterAttribute="type" />
<GraphCanvas
nodes={clusterNodesWithSizes}
draggable
edges={[]}
clusterAttribute="type"
/>
);

export const SingleNodeClusters = () => (
<GraphCanvas nodes={singleNodeClusterNodes} draggable edges={[]} clusterAttribute="type" />
<GraphCanvas
nodes={singleNodeClusterNodes}
draggable
edges={[]}
clusterAttribute="type"
/>
);

export const ImbalancedClusters = () => (
<GraphCanvas nodes={imbalancedClusterNodes} draggable edges={[]} clusterAttribute="type" />
<GraphCanvas
nodes={imbalancedClusterNodes}
draggable
edges={[]}
clusterAttribute="type"
/>
);

export const LargeDataset = () => (
<GraphCanvas nodes={manyClusterNodes} draggable edges={[]} clusterAttribute="type" />
<GraphCanvas
nodes={manyClusterNodes}
draggable
edges={[]}
clusterAttribute="type"
/>
);

export const Edges = () => (
<GraphCanvas nodes={clusterNodes} draggable edges={clusterEdges} clusterAttribute="type" />
<GraphCanvas
nodes={clusterNodes}
draggable
edges={clusterEdges}
clusterAttribute="type"
/>
);

export const Selections = () => (
<GraphCanvas nodes={clusterNodes} selections={[clusterNodes[0].id]} edges={clusterEdges} clusterAttribute="type" />
<GraphCanvas
nodes={clusterNodes}
selections={[clusterNodes[0].id]}
edges={clusterEdges}
clusterAttribute="type"
/>
);

export const Events = () => (
Expand All @@ -47,7 +111,9 @@ export const Events = () => (
edges={clusterEdges}
clusterAttribute="type"
onClusterPointerOut={cluster => console.log('cluster pointer out', cluster)}
onClusterPointerOver={cluster => console.log('cluster pointer over', cluster)}
onClusterPointerOver={cluster =>
console.log('cluster pointer over', cluster)
}
onClusterClick={cluster => console.log('cluster click', cluster)}
/>
);
Expand Down Expand Up @@ -100,15 +166,20 @@ export const LabelsOnly = () => (
export const ThreeDimensions = () => (
<GraphCanvas
nodes={clusterNodesWithSizes}
draggable edges={[]}
draggable
edges={[]}
layoutType="forceDirected3d"
clusterAttribute="type"
>
<directionalLight position={[0, 5, -4]} intensity={1} />
</GraphCanvas>
);


export const Partial = () => (
<GraphCanvas nodes={clusterNodes} draggable edges={[]} clusterAttribute="segment" />
);
<GraphCanvas
nodes={clusterNodes}
draggable
edges={[]}
clusterAttribute="segment"
/>
);
11 changes: 10 additions & 1 deletion src/GraphScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,11 @@ export interface GraphSceneProps {
*/
draggable?: boolean;

/**
* Constrain dragging to the cluster bounds. Default is `false`.
*/
constrainDragging?: boolean;

/**
* Render a custom node
*/
Expand Down Expand Up @@ -322,6 +327,7 @@ export const GraphScene: FC<GraphSceneProps & { ref?: Ref<GraphSceneRef> }> =
animated,
disabled,
draggable,
constrainDragging,
edgeLabelPosition,
edgeArrowPosition,
edgeInterpolation,
Expand Down Expand Up @@ -384,6 +390,7 @@ export const GraphScene: FC<GraphSceneProps & { ref?: Ref<GraphSceneRef> }> =
id={n?.id}
labelFontUrl={labelFontUrl}
draggable={draggable}
constrainDragging={constrainDragging}
disabled={disabled}
animated={animated}
contextMenu={contextMenu}
Expand All @@ -397,6 +404,7 @@ export const GraphScene: FC<GraphSceneProps & { ref?: Ref<GraphSceneRef> }> =
/>
)),
[
constrainDragging,
animated,
contextMenu,
disabled,
Expand Down Expand Up @@ -503,5 +511,6 @@ export const GraphScene: FC<GraphSceneProps & { ref?: Ref<GraphSceneRef> }> =
);

GraphScene.defaultProps = {
edgeInterpolation: 'linear'
edgeInterpolation: 'linear',
constrainDragging: false
};
11 changes: 10 additions & 1 deletion src/symbols/Node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ export interface NodeProps {
*/
draggable?: boolean;

/**
* Constrain dragging to the cluster bounds.
*/
constrainDragging?: boolean;

/**
* The url for the label font.
*/
Expand Down Expand Up @@ -127,7 +132,8 @@ export const Node: FC<NodeProps> = ({
onDragged,
onPointerOut,
onContextMenu,
renderNode
renderNode,
constrainDragging
}) => {
const cameraControls = useCameraControls();
const theme = useStore(state => state.theme);
Expand All @@ -143,6 +149,7 @@ export const Node: FC<NodeProps> = ({
const isSelected = useStore(state => state.selections?.includes(id));
const hasSelections = useStore(state => state.selections?.length > 0);
const center = useStore(state => state.centerPosition);
const cluster = useStore(state => state.clusters.get(node.cluster));

const isDragging = draggingId === id;
const {
Expand Down Expand Up @@ -211,6 +218,8 @@ export const Node: FC<NodeProps> = ({
const bind = useDrag({
draggable,
position,
// If dragging is constrained to the cluster, use the cluster's position as the bounds
bounds: constrainDragging ? cluster?.position : undefined,
// @ts-ignore
set: pos => setNodePosition(id, pos),
onDragStart: () => {
Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ export interface GraphNode extends GraphElementBaseAttributes {
* Fill color for the node.
*/
fill?: string;

/**
* Cluster ID for the node.
*/
cluster?: string;
}

export interface GraphEdge extends GraphElementBaseAttributes {
Expand Down
15 changes: 8 additions & 7 deletions src/useGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export const useGraph = ({
const layoutMounted = useRef<boolean>(false);
const layout = useRef<LayoutStrategy | null>(null);
const camera = useThree(state => state.camera) as PerspectiveCamera;
const dragRef = useRef<DragReferences>(drags);

// Calculate the visible entities
const { visibleEdges, visibleNodes } = useMemo(
Expand All @@ -75,12 +76,6 @@ export const useGraph = ({
[stateCollapsedNodeIds, nodes, edges]
);

// Transient updates
const dragRef = useRef<DragReferences>(drags);
useEffect(() => {
dragRef.current = drags;
}, [drags]);

const updateLayout = useCallback(
async (curLayout?: any) => {
// Cache the layout provider
Expand All @@ -106,7 +101,8 @@ export const useGraph = ({
sizingAttribute,
maxNodeSize,
minNodeSize,
defaultNodeSize
defaultNodeSize,
clusterAttribute
});

// Calculate clusters
Expand Down Expand Up @@ -137,6 +133,11 @@ export const useGraph = ({
]
);

// Transient updates
useEffect(() => {
dragRef.current = drags;
}, [drags, clusterAttribute, updateLayout]);

useEffect(() => {
// When the camera position/zoom changes, update the label visibility
const nodes = stateNodes.map(node => ({
Expand Down
8 changes: 5 additions & 3 deletions src/utils/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,17 @@ export interface CalculateClustersInput {

export interface ClusterGroup {
/**
* The nodes in the cluster.
* Nodes in the cluster.
*/
nodes: InternalGraphNode[];

/**
* The position of the cluster.
* Center position of the cluster.
*/
position: CenterPositionVector;

/**
* The label of the cluster.
* Label of the cluster.
*/
label: string;
}
Expand Down
5 changes: 4 additions & 1 deletion src/utils/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ interface TransformGraphInput {
minNodeSize?: number;
maxNodeSize?: number;
defaultNodeSize?: number;
clusterAttribute?: string;
}

/**
Expand All @@ -62,7 +63,8 @@ export function transformGraph({
sizingAttribute,
defaultNodeSize,
minNodeSize,
maxNodeSize
maxNodeSize,
clusterAttribute
}: TransformGraphInput) {
const nodes: InternalGraphNode[] = [];
const edges: InternalGraphEdge[] = [];
Expand Down Expand Up @@ -96,6 +98,7 @@ export function transformGraph({
label,
icon,
fill,
cluster: clusterAttribute ? data[clusterAttribute] : undefined,
parents,
data: {
...rest,
Expand Down
23 changes: 23 additions & 0 deletions src/utils/useDrag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { useMemo } from 'react';
import { useGesture } from '@use-gesture/react';
import { Vector2, Vector3, Plane } from 'three';
import { InternalGraphPosition } from '../types';
import { CenterPositionVector } from './layout';

interface DragParams {
draggable: boolean;
position: InternalGraphPosition;
bounds?: CenterPositionVector;
set: (position: Vector3) => void;
onDragStart: () => void;
onDragEnd: () => void;
Expand All @@ -16,6 +18,7 @@ export const useDrag = ({
draggable,
set,
position,
bounds,
onDragStart,
onDragEnd
}: DragParams) => {
Expand Down Expand Up @@ -86,6 +89,26 @@ export const useDrag = ({
.copy(mouse3D)
.add(offset);

// If there's a cluster, clamp the position within its circular bounds
if (bounds) {
const center = new Vector3(
(bounds.minX + bounds.maxX) / 2,
(bounds.minY + bounds.maxY) / 2,
(bounds.minZ + bounds.maxZ) / 2
);
const radius = (bounds.maxX - bounds.minX) / 2;

// Calculate direction from center to updated position
const direction = updated.clone().sub(center);
const distance = direction.length();

// If outside the circle, clamp to the circle's edge
if (distance > radius) {
direction.normalize().multiplyScalar(radius);
updated.copy(center).add(direction);
}
}

return set(updated);
},
onDragEnd
Expand Down

0 comments on commit ff19c2a

Please sign in to comment.