Skip to content
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
155 changes: 155 additions & 0 deletions src/symbols/nodes/Badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import React, { FC, useMemo } from 'react';
import { a, useSpring } from '@react-spring/three';
import { Billboard, RoundedBox, Text } from '@react-three/drei';
import { animationConfig } from '../../utils';
import { NodeRendererProps } from '../../types';
import { Color } from 'three';

export type BadgePosition =
| 'top-right'
| 'top-left'
| 'bottom-right'
| 'bottom-left'
| 'center'
| 'custom';

export interface BadgeProps extends NodeRendererProps {
/**
* The text to display in the badge.
*/
label: string;

/**
* Background color of the badge.
*/
backgroundColor?: string;

/**
* Text color of the badge.
*/
textColor?: string;

/**
* Size multiplier for the badge relative to the node size.
*/
badgeSize?: number;

/**
* Position offset from the node center or preset position.
*/
position?: [number, number, number] | BadgePosition;

/**
* Padding around the badge text.
*/
padding?: number;
}

export const Badge: FC<BadgeProps> = ({
label,
size,
opacity = 1,
animated,
backgroundColor = '#ffffff',
textColor = '#000000',
badgeSize = 1.5,
position = 'top-right',
padding = 0.3
}) => {
const normalizedBgColor = useMemo(
() => new Color(backgroundColor),
[backgroundColor]
);
const normalizedTextColor = useMemo(() => new Color(textColor), [textColor]);

// Calculate position based on preset or custom coordinates
const badgePosition = useMemo((): [number, number, number] => {
if (Array.isArray(position)) {
return position;
}

const offset = size * 0.65;
switch (position) {
case 'top-right':
return [offset, offset, 0.1];
case 'top-left':
return [-offset, offset, 0.1];
case 'bottom-right':
return [offset, -offset, 0.1];
case 'bottom-left':
return [-offset, -offset, 0.1];
case 'center':
return [0, 0, 0.1];
default:
return [offset, offset, 0.1];
}
}, [position, size]);

// Calculate dynamic badge dimensions based on text length
const badgeDimensions = useMemo(() => {
const baseWidth = 0.5;
const baseHeight = 0.5;
const minWidth = baseWidth;
const minHeight = baseHeight;

// Estimate text width based on character count
const charCount = label.length;
const estimatedWidth = Math.max(
minWidth,
Math.min(charCount * 0.15 + padding, 2.0 + padding)
); // Add padding to width
const estimatedHeight = Math.max(
minHeight,
Math.min(charCount * 0.05 + padding * 0.5, 0.8 + padding * 0.5)
); // Add padding to height

return {
width: estimatedWidth,
height: estimatedHeight
};
}, [label, padding]);

const { scale, badgeOpacity } = useSpring({
from: {
scale: [0.00001, 0.00001, 0.00001],
badgeOpacity: 0
},
to: {
scale: [size * badgeSize, size * badgeSize, size * badgeSize],
badgeOpacity: opacity
},
config: {
...animationConfig,
duration: animated ? undefined : 0
}
});

return (
<Billboard position={badgePosition}>
<a.group scale={scale as any} renderOrder={2}>
<a.mesh position={[0, 0, 1]}>
<RoundedBox
args={[badgeDimensions.width, badgeDimensions.height, 0.01]} // dynamic width, height, depth
radius={0.12} // corner radius
smoothness={8}
material-color={normalizedBgColor}
material-transparent={true}
/>
</a.mesh>
<Text
position={[0, 0, 1.1]}
fontSize={0.3}
color={normalizedTextColor}
anchorX="center"
anchorY="middle"
maxWidth={badgeDimensions.width - 0.2}
textAlign="center"
material-depthTest={false}
material-depthWrite={false}
>
{label}
</Text>
</a.group>
</Billboard>
);
};
1 change: 1 addition & 0 deletions src/symbols/nodes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './Sphere';
export * from './SphereWithIcon';
export * from './Svg';
export * from './SphereWithSvg';
export * from './Badge';
4 changes: 3 additions & 1 deletion stories/assets/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import computerSvg from './computer.svg';

export const random = (floor, ceil) => Math.floor(Math.random() * ceil) + floor;

const counts = [5, 100, 5843, 9992, 1000000];
export const simpleNodes: GraphNode[] =
range(5).map(i => ({
id: `n-${i}`,
label: `Node ${i}`,
data: {
priority: random(0, 10)
priority: random(0, 10),
count: counts[i]
}
}));

Expand Down
73 changes: 73 additions & 0 deletions stories/demos/Badge.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React from 'react';
import { GraphCanvas } from '../../src';
import { Badge, Sphere } from '../../src/symbols';
import { simpleNodes, simpleEdges } from '../assets/demo';

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

export const Default = () => (
<GraphCanvas
nodes={simpleNodes}
edges={simpleEdges}
cameraMode="rotate"
renderNode={({ node, ...rest }) => (
<group>
<Sphere {...rest} node={node} />
<Badge
{...rest}
node={node}
label={node.data.count.toLocaleString()}
backgroundColor="#000000"
textColor="#ffffff"
/>
</group>
)}
/>
);

export const CustomColors = () => (
<GraphCanvas
nodes={simpleNodes}
edges={simpleEdges}
cameraMode="rotate"
renderNode={({ node, ...rest }) => (
<group>
<Sphere {...rest} node={node} />
<Badge
{...rest}
node={node}
label="5"
backgroundColor="#ff6b6b"
textColor="#ffffff"
position="center"
/>
</group>
)}
/>
);

export const DifferentSizes = () => (
<GraphCanvas
nodes={simpleNodes}
edges={simpleEdges}
cameraMode="rotate"
renderNode={({ node, ...rest }) => (
<group>
<Sphere {...rest} node={node} />
<Badge
{...rest}
node={node}
label="99+"
backgroundColor="#4ecdc4"
textColor="#ffffff"
padding={0.8}
badgeSize={1}
position="bottom-left"
/>
</group>
)}
/>
);