Initial commit
This commit is contained in:
177
www/src/components/GraphComponent.jsx
Normal file
177
www/src/components/GraphComponent.jsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import { useEffect, useState, useRef, useCallback } from "react";
|
||||
import ForceGraph3D from "react-force-graph-3d";
|
||||
import graphData from "../data.json";
|
||||
import SpriteText from "https://esm.sh/three-spritetext";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faInfoCircle, faExternalLinkAlt, faChevronLeft, faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
export default function GraphComponent({ selectedCategory }) {
|
||||
const fgRef = useRef();
|
||||
const [selectedNode, setSelectedNode] = useState(null);
|
||||
const [graphDataState, setGraphDataState] = useState({ nodes: [], links: [] });
|
||||
const [imageIndex, setImageIndex] = useState(0); // State to track current image in the carousel
|
||||
|
||||
// Set distance from camera orbit
|
||||
const distance = 400;
|
||||
const speed = 0.3;
|
||||
|
||||
useEffect(() => {
|
||||
// Clone the original graph data to avoid modifying it directly
|
||||
const updatedGraphData = {
|
||||
nodes: graphData.nodes.map(node => ({
|
||||
...node,
|
||||
visible: !selectedCategory || node.category === selectedCategory // Hide nodes not in selected category
|
||||
})),
|
||||
links: graphData.links.map(link => ({
|
||||
...link,
|
||||
visible:
|
||||
(!selectedCategory ||
|
||||
(graphData.nodes.find(n => n.id === link.source)?.category === selectedCategory &&
|
||||
graphData.nodes.find(n => n.id === link.target)?.category === selectedCategory)) // Hide links if either connected node is hidden
|
||||
}))
|
||||
};
|
||||
|
||||
setGraphDataState(updatedGraphData);
|
||||
|
||||
// Camera orbit logic
|
||||
const cameraOrbit = () => {
|
||||
let angle = 0;
|
||||
fgRef.current.cameraPosition({ z: distance });
|
||||
|
||||
// Set interval for camera orbit effect
|
||||
const intervalId = setInterval(() => {
|
||||
fgRef.current.cameraPosition({
|
||||
x: distance * Math.sin(angle),
|
||||
z: distance * Math.cos(angle)
|
||||
});
|
||||
angle += (Math.PI / 300) * speed;
|
||||
}, 10);
|
||||
|
||||
// Clear up the interval when the component is unmounted
|
||||
return () => clearInterval(intervalId);
|
||||
};
|
||||
|
||||
// Call camera orbit after the graph is set
|
||||
const cleanupCameraOrbit = cameraOrbit();
|
||||
|
||||
return cleanupCameraOrbit;
|
||||
}, [selectedCategory]);
|
||||
|
||||
const handleNodeClick = useCallback((node) => {
|
||||
setSelectedNode(node);
|
||||
setImageIndex(0); // Reset the image index when a new node is clicked
|
||||
}, []);
|
||||
|
||||
const handleBackgroundClick = useCallback(() => {
|
||||
setSelectedNode(null);
|
||||
}, []);
|
||||
|
||||
// Carousel navigation functions
|
||||
const prevImage = () => {
|
||||
if (selectedNode && selectedNode.image && Array.isArray(selectedNode.image)) {
|
||||
setImageIndex((prevIndex) => (prevIndex > 0 ? prevIndex - 1 : selectedNode.image.length - 1));
|
||||
}
|
||||
};
|
||||
|
||||
const nextImage = () => {
|
||||
if (selectedNode && selectedNode.image && Array.isArray(selectedNode.image)) {
|
||||
setImageIndex((prevIndex) => (prevIndex < selectedNode.image.length - 1 ? prevIndex + 1 : 0));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* Information Window Wrapper */}
|
||||
{selectedNode && (
|
||||
<div className="bg-gray-700 rounded-lg shadow-md z-10
|
||||
absolute left-[50%] top-[50%] transform-gpu -translate-x-1/2 -translate-y-1/2 p-4 h-[max-content] max-h-[80%]">
|
||||
{/* Image and Description */}
|
||||
<div className="rounded-sm flex flex-row h-[100%] w-[100%] space-x-4">
|
||||
{/* Image Section */}
|
||||
{selectedNode.image && (
|
||||
<div className="flex flex-col justify-center items-center relative">
|
||||
{/* If it's an array of images, render carousel */}
|
||||
{Array.isArray(selectedNode.image) ? (
|
||||
<div className="relative">
|
||||
<img
|
||||
className="rounded-sm object-contain w-[100%] h-auto"
|
||||
src={selectedNode.image[imageIndex]}
|
||||
alt={`${selectedNode.name} - Image ${imageIndex + 1}`}
|
||||
/>
|
||||
{/* Left and Right Chevron Buttons */}
|
||||
<button
|
||||
className="absolute left-0 top-1/2 transform -translate-y-1/2 text-white text-xl"
|
||||
onClick={prevImage}
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
</button>
|
||||
<button
|
||||
className="absolute right-0 top-1/2 transform -translate-y-1/2 text-white text-xl"
|
||||
onClick={nextImage}
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronRight} />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
// Single image
|
||||
<img
|
||||
className="rounded-sm object-contain w-[100%]"
|
||||
src={selectedNode.image}
|
||||
alt={selectedNode.name}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Text Content */}
|
||||
<div className="w-[100%] max-w-64ch">
|
||||
<div className="flex flex-row space-x-2">
|
||||
{/* Title */}
|
||||
<h3 className="text-lg font-bold">{selectedNode.name}</h3>
|
||||
{/* Info Button fontawesome */}
|
||||
{selectedNode.link && (
|
||||
<div className="flex space-x-2 ml-2">
|
||||
<button className="text-[rgba(255,255,255,0.5)] text-sm">
|
||||
<FontAwesomeIcon icon={faInfoCircle} />
|
||||
</button>
|
||||
<button className="hover:text-white text-[rgba(255,255,255,0.5)] text-sm">
|
||||
<a href={selectedNode.link} target="_blank" rel="noreferrer">
|
||||
<FontAwesomeIcon icon={faExternalLinkAlt} />
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h6 className="text-xs text-gray-400 mb-2">{selectedNode.location.city} | {selectedNode.location.state} | {selectedNode.location.country}</h6>
|
||||
{/* Description */}
|
||||
<p className="text-sm text-justify">{selectedNode.description}</p>
|
||||
</div>
|
||||
{/* Close button */}
|
||||
<button
|
||||
className="self-start hover:text-white text-[rgba(255,255,255,0.5)] text-sm"
|
||||
onClick={() => setSelectedNode(null)}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ForceGraph3D */}
|
||||
<ForceGraph3D
|
||||
ref={fgRef}
|
||||
graphData={graphDataState}
|
||||
nodeAutoColorBy="group"
|
||||
nodeThreeObject={node => {
|
||||
const sprite = new SpriteText(node.name);
|
||||
sprite.color = node.color;
|
||||
sprite.textHeight = 8;
|
||||
return sprite;
|
||||
}}
|
||||
linkVisibility={link => link.visible} // Hide links properly
|
||||
nodeVisibility={node => node.visible} // Hide nodes properly
|
||||
onNodeClick={handleNodeClick}
|
||||
onBackgroundClick={handleBackgroundClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user