Skip to content

Commit

Permalink
Merge pull request #297 from reaviz/custom-label
Browse files Browse the repository at this point in the history
Add ability to define custom cluster
  • Loading branch information
SerhiiTsybulskyi authored Nov 22, 2024
2 parents be7cd09 + 227e42d commit 4d44b53
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 55 deletions.
53 changes: 52 additions & 1 deletion docs/demos/Cluster.story.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import React, { useCallback, useState } from 'react';
import { GraphCanvas, Icon, lightTheme, Sphere } from '../../src';
import { Billboard, Html, Svg, Text } from 'glodrei';
import { Color, DoubleSide } from 'three';
import { a } from '@react-spring/three';
import { GraphCanvas, Icon, Label, lightTheme, Sphere } from '../../src';
import {
clusterNodes,
clusterEdges,
Expand All @@ -10,6 +13,7 @@ import {
} from '../assets/demo';

import demonSvg from '../../docs/assets/twitter.svg';
import { Ring } from '../../src/symbols/clusters/Ring';

export default {
title: 'Demos/Cluster',
Expand Down Expand Up @@ -457,6 +461,53 @@ export const LabelsOnly = () => (
/>
);

export const Custom = () => (
<GraphCanvas
theme={lightTheme}
nodes={clusterNodes}
draggable
edges={clusterEdges}
clusterAttribute="type"
onRenderCluster={({ label, opacity, outerRadius, innerRadius, theme }) => (
<>
<Ring
outerRadius={outerRadius}
innerRadius={innerRadius}
padding={40}
normalizedFill={new Color('#075985')}
normalizedStroke={new Color('#075985')}
opacity={opacity}
theme={theme ?? lightTheme}
animated
/>
{label && (
<a.group position={label.position as any}>
<Html as="div" center distanceFactor={500}>
<div
style={{
display: 'flex',
alignItems: 'center'
}}
>
<svg
fill="#7CA0AB"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 30 30"
width="50px"
height="50px"
>
<path d="M28,6.937c-0.957,0.425-1.985,0.711-3.064,0.84c1.102-0.66,1.947-1.705,2.345-2.951c-1.03,0.611-2.172,1.055-3.388,1.295 c-0.973-1.037-2.359-1.685-3.893-1.685c-2.946,0-5.334,2.389-5.334,5.334c0,0.418,0.048,0.826,0.138,1.215 c-4.433-0.222-8.363-2.346-10.995-5.574C3.351,6.199,3.088,7.115,3.088,8.094c0,1.85,0.941,3.483,2.372,4.439 c-0.874-0.028-1.697-0.268-2.416-0.667c0,0.023,0,0.044,0,0.067c0,2.585,1.838,4.741,4.279,5.23 c-0.447,0.122-0.919,0.187-1.406,0.187c-0.343,0-0.678-0.034-1.003-0.095c0.679,2.119,2.649,3.662,4.983,3.705 c-1.825,1.431-4.125,2.284-6.625,2.284c-0.43,0-0.855-0.025-1.273-0.075c2.361,1.513,5.164,2.396,8.177,2.396 c9.812,0,15.176-8.128,15.176-15.177c0-0.231-0.005-0.461-0.015-0.69C26.38,8.945,27.285,8.006,28,6.937z" />
</svg>
<span style={{ fontSize: 24 }}>{label.text}</span>
</div>
</Html>
</a.group>
)}
</>
)}
/>
);

export const ThreeDimensions = () => (
<GraphCanvas
nodes={clusterNodesWithSizes}
Expand Down
13 changes: 11 additions & 2 deletions src/GraphScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import {
InternalGraphEdge,
InternalGraphNode,
NodeRenderer,
CollapseProps
CollapseProps,
ClusterRenderer
} from './types';
import { SizingType } from './sizing';
import {
Expand Down Expand Up @@ -159,6 +160,11 @@ export interface GraphSceneProps {
*/
renderNode?: NodeRenderer;

/**
* Render a custom cluster
*/
onRenderCluster?: ClusterRenderer;

/**
* Advanced overrides for the layout.
*/
Expand Down Expand Up @@ -340,6 +346,7 @@ export const GraphScene: FC<GraphSceneProps & { ref?: Ref<GraphSceneRef> }> =
edgeInterpolation = 'linear',
labelFontUrl,
renderNode,
onRenderCluster,
...rest
},
ref
Expand Down Expand Up @@ -505,6 +512,7 @@ export const GraphScene: FC<GraphSceneProps & { ref?: Ref<GraphSceneRef> }> =
onPointerOver={onClusterPointerOver}
onPointerOut={onClusterPointerOut}
onDragged={onClusterDragged}
onRender={onRenderCluster}
{...c}
/>
)),
Expand All @@ -517,7 +525,8 @@ export const GraphScene: FC<GraphSceneProps & { ref?: Ref<GraphSceneRef> }> =
onClusterClick,
onClusterPointerOut,
onClusterPointerOver,
onClusterDragged
onClusterDragged,
onRenderCluster
]
);

Expand Down
110 changes: 59 additions & 51 deletions src/symbols/Cluster.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import React, { FC, useMemo, useState } from 'react';
import { ClusterGroup, animationConfig, useHoverIntent } from '../utils';
import { useSpring, a } from '@react-spring/three';
import { Color, DoubleSide } from 'three';
import { Color } from 'three';
import { useStore } from '../store';
import { Label } from './Label';
import { useCursor } from 'glodrei';
import { ThreeEvent } from '@react-three/fiber';
import { useDrag } from '../utils/useDrag';
import { Vector3 } from 'three';
import { useCameraControls } from '../CameraControls';
import { ClusterRenderer } from '../types';
import { Ring } from './clusters/Ring';

export type ClusterEventArgs = Omit<ClusterGroup, 'position'>;

Expand Down Expand Up @@ -68,6 +70,11 @@ export interface ClusterProps extends ClusterGroup {
* Triggered after a cluster was dragged
*/
onDragged?: (cluster: ClusterEventArgs) => void;

/**
* Render a custom cluster label
*/
onRender?: ClusterRenderer;
}

export const Cluster: FC<ClusterProps> = ({
Expand All @@ -83,7 +90,8 @@ export const Cluster: FC<ClusterProps> = ({
onPointerOver,
onPointerOut,
draggable = false,
onDragged
onDragged,
onRender
}) => {
const theme = useStore(state => state.theme);
const rad = Math.max(position.width, position.height) / 2;
Expand Down Expand Up @@ -113,8 +121,8 @@ export const Cluster: FC<ClusterProps> = ({
: theme.cluster?.inactiveOpacity
: theme.cluster?.opacity;

const labelPositionOffset = useMemo(() => {
const defaultPosition = [0, -offset, 2];
const labelPosition: [number, number, number] = useMemo(() => {
const defaultPosition: [number, number, number] = [0, -offset, 2];
const themeOffset = theme.cluster?.label?.offset;
if (themeOffset) {
return [
Expand All @@ -127,16 +135,14 @@ export const Cluster: FC<ClusterProps> = ({
return defaultPosition;
}, [offset, theme.cluster?.label?.offset]);

const { circleOpacity, circlePosition, labelPosition } = useSpring({
const { circlePosition } = useSpring({
from: {
circlePosition: [center.x, center.y, -1],
circleOpacity: 0,
labelPosition: labelPositionOffset
circlePosition: [center.x, center.y, -1] as [number, number, number]
},
to: {
labelPosition: labelPositionOffset,
circlePosition: position ? [position.x, position.y, -1] : [0, 0, -1],
circleOpacity: opacity
circlePosition: position
? ([position.x, position.y, -1] as [number, number, number])
: ([0, 0, -1] as [number, number, number])
},
config: {
...animationConfig,
Expand Down Expand Up @@ -231,56 +237,56 @@ export const Cluster: FC<ClusterProps> = ({
}}
{...(bind() as any)}
>
<mesh>
<ringGeometry attach="geometry" args={[offset, 0, 128]} />
<a.meshBasicMaterial
attach="material"
color={normalizedFill}
transparent={true}
depthTest={false}
opacity={theme.cluster?.fill ? circleOpacity : 0}
side={DoubleSide}
fog={true}
/>
</mesh>
<mesh>
<ringGeometry
attach="geometry"
args={[offset, rad + padding, 128]}
/>
<a.meshBasicMaterial
attach="material"
color={normalizedStroke}
transparent={true}
depthTest={false}
opacity={circleOpacity}
side={DoubleSide}
fog={true}
/>
</mesh>
{theme.cluster?.label && (
<a.group position={labelPosition as any}>
<Label
text={label}
{onRender ? (
onRender({
label: {
position: labelPosition,
text: label,
opacity: opacity,
fontUrl: labelFontUrl
},
opacity,
outerRadius: offset,
innerRadius: rad,
padding,
theme
})
) : (
<>
<Ring
outerRadius={offset}
innerRadius={rad}
padding={padding}
normalizedFill={normalizedFill}
normalizedStroke={normalizedStroke}
opacity={opacity}
fontUrl={labelFontUrl}
stroke={theme.cluster.label.stroke}
active={false}
color={theme.cluster?.label.color}
fontSize={theme.cluster?.label.fontSize ?? 12}
animated={animated}
theme={theme}
/>
</a.group>
{theme.cluster?.label && (
<a.group position={labelPosition}>
<Label
text={label}
opacity={opacity}
fontUrl={labelFontUrl}
stroke={theme.cluster.label.stroke}
active={false}
color={theme.cluster?.label.color}
fontSize={theme.cluster?.label.fontSize ?? 12}
/>
</a.group>
)}
</>
)}
</a.group>
),
[
theme.cluster,
theme,
circlePosition,
pointerOver,
pointerOut,
offset,
normalizedFill,
circleOpacity,
rad,
padding,
normalizedStroke,
Expand All @@ -292,7 +298,9 @@ export const Cluster: FC<ClusterProps> = ({
onClick,
nodes,
bind,
isDraggingCurrent
isDraggingCurrent,
onRender,
animated
]
);

Expand Down
69 changes: 69 additions & 0 deletions src/symbols/clusters/Ring.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React, { FC } from 'react';
import { a, useSpring } from '@react-spring/three';
import { Color, DoubleSide } from 'three';

import { Theme } from '../../themes';
import { animationConfig } from '../../utils';

export interface RingProps {
outerRadius: number;
innerRadius: number;
padding: number;
normalizedFill: Color;
normalizedStroke: Color;
opacity: number;
animated: boolean;
theme: Theme;
}

export const Ring: FC<RingProps> = ({
outerRadius,
innerRadius,
padding,
normalizedFill,
normalizedStroke,
opacity,
animated,
theme
}) => {
const { opacity: springOpacity } = useSpring({
from: { opacity: 0 },
to: { opacity },
config: {
...animationConfig,
duration: animated ? undefined : 0
}
});

return (
<>
<mesh>
<ringGeometry attach="geometry" args={[outerRadius, 0, 128]} />
<a.meshBasicMaterial
attach="material"
color={normalizedFill}
transparent={true}
depthTest={false}
opacity={theme.cluster?.fill ? springOpacity : 0}
side={DoubleSide}
fog={true}
/>
</mesh>
<mesh>
<ringGeometry
attach="geometry"
args={[outerRadius, innerRadius + padding, 128]}
/>
<a.meshBasicMaterial
attach="material"
color={normalizedStroke}
transparent={true}
depthTest={false}
opacity={springOpacity}
side={DoubleSide}
fog={true}
/>
</mesh>
</>
);
};
1 change: 1 addition & 0 deletions src/symbols/clusters/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Ring';
Loading

0 comments on commit 4d44b53

Please sign in to comment.