From fe0aa4521de31ac57966576a99b611ffe61ef7cb Mon Sep 17 00:00:00 2001 From: rathi Date: Wed, 4 Dec 2024 22:47:38 -0500 Subject: [PATCH] debugging 4 --- src/pages/NetworkVisualization.tsx | 843 +---------------------------- 1 file changed, 28 insertions(+), 815 deletions(-) diff --git a/src/pages/NetworkVisualization.tsx b/src/pages/NetworkVisualization.tsx index 28d28eb..ee032f7 100644 --- a/src/pages/NetworkVisualization.tsx +++ b/src/pages/NetworkVisualization.tsx @@ -1,8 +1,8 @@ import { useRef, useEffect, useState, useCallback } from 'react'; import { characterNetwork } from '../data/character-network'; -import { CharacterNode, Relationship, BookNode } from '../types/character-network'; +import { CharacterNode, Relationship } from '../types/character-network'; import { Box, Typography, Paper, Grid, IconButton, Tooltip, Chip, Divider, Container, CircularProgress, Fade } from '@mui/material'; -import { ForceGraph2D as ForceGraph, NodeObject, LinkObject } from 'react-force-graph'; +import { ForceGraph2D as ForceGraph } from 'react-force-graph'; import { ArrowBack, Help, ZoomIn, ZoomOut, CenterFocusStrong } from '@mui/icons-material'; import * as d3 from 'd3'; @@ -44,839 +44,52 @@ interface NetworkLink extends d3.SimulationLinkDatum { color: string; } +// Particle type for emitParticle +interface GraphParticle { + id: string; + source: NetworkNode; + target: NetworkNode; +} + // Complete ForceGraph methods interface -interface ForceGraphMethods, LinkType = LinkObject> { +interface ForceGraphMethods { zoom: (k?: number) => number; zoomToFit: (duration?: number, padding?: number) => void; - d3Force: (forceName: string, force?: d3.Force) => void; + d3Force: (forceName: string, force?: d3.Force) => void; d3ReheatSimulation: () => void; getZoom: () => number; - emitParticle: (particle: any) => void; + emitParticle: (particle: GraphParticle) => void; pauseAnimation: () => void; resumeAnimation: () => void; centerAt: (x?: number, y?: number, duration?: number) => void; - // Add other required methods + getGraphBbox: () => { x: [number, number]; y: [number, number] }; + screen2GraphCoords: (x: number, y: number) => { x: number; y: number }; + graph2ScreenCoords: (x: number, y: number) => { x: number; y: number }; width: () => number; height: () => number; refresh: () => void; } -type ForceGraphInstance = ForceGraphMethods; - -// Add sage theme colors after the interface definitions -const sageColors = { - primary: { - start: '#4A5D52', // darker sage - end: '#6B7F75' // lighter sage - }, - secondary: { - start: '#5B6E65', - end: '#7C8F86' - }, - tertiary: { - start: '#6B7F75', - end: '#8C9F96' - } -}; - -const relationshipColors = { - family: '#4A5D52', // sage green - romance: '#6B7F75', // medium sage - friendship: '#5B6E65', // light sage - rivalry: '#8C9F96' // pale sage -}; - -// Add utility function for pentagon layout -const getPentagonPoint = (index: number, total: number, radius: number, centerX: number, centerY: number) => { - // Start from the top (270 degrees) and go clockwise - // Adjust the starting angle to ensure one point is at the top - const startAngle = -90; // -90 degrees = top point - const angle = (startAngle + (360 / total) * index) * (Math.PI / 180); - return { - x: centerX + radius * Math.cos(angle), - y: centerY + radius * Math.sin(angle) - }; -}; - export default function NetworkVisualization() { const [selectedNode, setSelectedNode] = useState(null); const [selectedRelationships, setSelectedRelationships] = useState([]); const [selectedBook, setSelectedBook] = useState(null); - const containerRef = useRef(null); const fgRef = useRef(); - const [isLoading, setIsLoading] = useState(true); - const [isGraphReady, setIsGraphReady] = useState(false); - const [dimensions, setDimensions] = useState({ width: 800, height: 700 }); - // Add loading effect when data changes - useEffect(() => { - setIsLoading(true); - const timer = setTimeout(() => { - setIsLoading(false); - }, 1000); - return () => clearTimeout(timer); - }, [selectedBook]); - - // Track when graph is ready - useEffect(() => { - if (fgRef.current) { - setIsGraphReady(true); - } - }, [fgRef.current]); - - // Add a useEffect to handle container resizing - useEffect(() => { - const updateDimensions = () => { - if (containerRef.current) { - const { width, height } = containerRef.current.getBoundingClientRect(); - setDimensions({ width, height }); - } - }; - - // Initial update - updateDimensions(); - - // Add resize listener - window.addEventListener('resize', updateDimensions); - return () => window.removeEventListener('resize', updateDimensions); - }, []); - - // Update the node interaction handlers - const handleNodeClick = useCallback((node: NetworkNode) => { - if (node.type === 'book') { - const bookNode: NetworkBookNode = { - ...node, - year: (node as NetworkBookNode).year - }; - setSelectedBook(node.id); - setSelectedNode(bookNode); - setSelectedRelationships([]); - } else { - const characterNode: NetworkCharacterNode = { - ...node, - type: node.type as 'protagonist' | 'antagonist' | 'supporting' | 'character' - }; - const relations = characterNetwork.relationships.filter( - (rel) => rel.source === node.id || rel.target === node.id - ); - setSelectedNode(characterNode); - setSelectedRelationships(relations); - } - }, []); - - const handleBackClick = () => { - setSelectedBook(null); - setSelectedNode(null); - setSelectedRelationships([]); - }; - - // Update getGraphData with improved layout - const getGraphData = useCallback((): { nodes: NetworkNode[]; links: NetworkLink[] } => { - if (selectedBook) { - const bookName = characterNetwork.books.find(b => b.id === selectedBook)?.name; - const nodes = characterNetwork.nodes - .filter(node => node.novel === bookName) - .map((node, idx, arr) => { - // Create a circular layout with more spacing - const angle = (idx / arr.length) * 2 * Math.PI; - // Increase radius for better spacing - const radius = Math.min(dimensions.width, dimensions.height) * 0.35; - - const x = dimensions.width / 2 + radius * Math.cos(angle); - const y = dimensions.height / 2 + radius * Math.sin(angle); - - return { - ...node, - val: 10, - x, - y, - fx: x, - fy: y, - color: node.type === 'protagonist' ? sageColors.primary.start : - node.type === 'antagonist' ? sageColors.secondary.start : - sageColors.tertiary.start - } as NetworkNode; - }); - - // Improve link routing with curved paths - const links = characterNetwork.relationships - .filter(rel => nodes.some(n => n.id === rel.source) && nodes.some(n => n.id === rel.target)) - .map(rel => ({ - ...rel, - color: sageColors.primary.end, - curvature: 0.3 // Add consistent curve to all links - })); - - return { nodes, links }; - } else { - // Create pentagon layout for books - const centerX = dimensions.width / 2; - const centerY = dimensions.height / 2; - // Adjust radius based on container size - const radius = Math.min(dimensions.width, dimensions.height) * 0.25; - - const nodes = characterNetwork.books.map((book, idx) => { - const point = getPentagonPoint(idx, characterNetwork.books.length, radius, centerX, centerY); - - return { - id: book.id, - name: book.name, - type: 'book' as const, - description: book.description, - val: 15, - color: sageColors.primary.start, - x: point.x, - y: point.y, - fx: point.x, - fy: point.y - } as NetworkNode; - }); - - return { nodes, links: [] }; - } - }, [selectedBook, dimensions]); - - const renderNodeCanvas = useCallback((node: NetworkNode, ctx: CanvasRenderingContext2D, scale: number) => { - const size = node.val || 5; - const fontSize = Math.max(12 / scale, 2); - - // Save the current context state - ctx.save(); - - // 3D effect with gradient and shadow - const gradient = ctx.createRadialGradient( - (node.x || 0) - size/3, - (node.y || 0) - size/3, - 0, - node.x || 0, - node.y || 0, - size - ); - - const baseColor = node.color || sageColors.primary.start; - gradient.addColorStop(0, lightenColor(baseColor, 30)); - gradient.addColorStop(0.8, baseColor); - gradient.addColorStop(1, darkenColor(baseColor, 20)); - - // Add shadow for depth - ctx.shadowColor = 'rgba(0, 0, 0, 0.2)'; - ctx.shadowBlur = 5; - ctx.shadowOffsetX = 2; - ctx.shadowOffsetY = 2; - - // Draw node with 3D effect - ctx.beginPath(); - ctx.arc(node.x || 0, node.y || 0, size, 0, 2 * Math.PI); - ctx.fillStyle = gradient; - ctx.fill(); - - // Add highlight ring - ctx.strokeStyle = lightenColor(baseColor, 40); - ctx.lineWidth = 1; - ctx.stroke(); - - // Clear shadow effect for text - ctx.shadowColor = 'transparent'; - ctx.shadowBlur = 0; - ctx.shadowOffsetX = 0; - ctx.shadowOffsetY = 0; - - // Only draw labels when zoomed in enough or for book nodes - if (scale > 0.7 || node.type === 'book') { - const labelDistance = size + fontSize; - ctx.font = `${fontSize}px "Cormorant", serif`; - const textWidth = ctx.measureText(node.name).width; - - // Semi-transparent background for better readability - ctx.fillStyle = 'rgba(255, 255, 255, 0.95)'; - const padding = fontSize * 0.3; - const backgroundHeight = fontSize + padding * 2; - - // Draw background with rounded corners - const cornerRadius = 4; - const backgroundY = (node.y || 0) + labelDistance - fontSize / 2 - padding; - roundRect( - ctx, - (node.x || 0) - textWidth / 2 - padding, - backgroundY, - textWidth + padding * 2, - backgroundHeight, - cornerRadius - ); - - // Draw text - ctx.fillStyle = node.id === selectedNode?.id ? sageColors.primary.start : '#000'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText( - node.name, - node.x || 0, - (node.y || 0) + labelDistance - ); - } - - // Restore the context state - ctx.restore(); - }, [selectedNode]); - - // Helper function for drawing rounded rectangles - const roundRect = ( - ctx: CanvasRenderingContext2D, - x: number, - y: number, - width: number, - height: number, - radius: number - ) => { - ctx.beginPath(); - ctx.moveTo(x + radius, y); - ctx.lineTo(x + width - radius, y); - ctx.quadraticCurveTo(x + width, y, x + width, y + radius); - ctx.lineTo(x + width, y + height - radius); - ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); - ctx.lineTo(x + radius, y + height); - ctx.quadraticCurveTo(x, y + height, x, y + height - radius); - ctx.lineTo(x, y + radius); - ctx.quadraticCurveTo(x, y, x + radius, y); - ctx.closePath(); - ctx.fill(); - }; - - // Add color utility functions - const lightenColor = (color: string, percent: number): string => { - const num = parseInt(color.replace('#', ''), 16); - const amt = Math.round(2.55 * percent); - const R = Math.min(255, ((num >> 16) & 0xff) + amt); - const G = Math.min(255, ((num >> 8) & 0xff) + amt); - const B = Math.min(255, (num & 0xff) + amt); - return `#${(1 << 24 | R << 16 | G << 8 | B).toString(16).slice(1)}`; - }; - - const darkenColor = (color: string, percent: number): string => { - const num = parseInt(color.replace('#', ''), 16); - const amt = Math.round(2.55 * percent); - const R = Math.max(0, ((num >> 16) & 0xff) - amt); - const G = Math.max(0, ((num >> 8) & 0xff) - amt); - const B = Math.max(0, (num & 0xff) - amt); - return `#${(1 << 24 | R << 16 | G << 8 | B).toString(16).slice(1)}`; - }; - - const getLegendTooltip = () => ( - - - Color Legend: - - - Nodes:
- • Protagonists - {sageColors.primary.start}
- • Antagonists - {sageColors.secondary.start}
- • Supporting - {sageColors.tertiary.start} -
- - Relationships:
- • Family - {relationshipColors.family}
- • Romance - {relationshipColors.romance}
- • Friendship - {relationshipColors.friendship}
- • Rivalry - {relationshipColors.rivalry} -
-
- ); - - const handleZoomIn = () => { - if (fgRef.current) { - const currentZoom = fgRef.current.zoom(); - fgRef.current.zoom(currentZoom * 1.2); - } - }; - - const handleZoomOut = () => { - if (fgRef.current) { - const currentZoom = fgRef.current.zoom(); - fgRef.current.zoom(currentZoom / 1.2); - } - }; - - const handleCenterGraph = () => { - if (fgRef.current) { - fgRef.current.zoomToFit(400); - } - }; - - useEffect(() => { - if (fgRef.current) { - // Clear existing forces - fgRef.current.d3Force('charge', undefined); - fgRef.current.d3Force('center', undefined); - fgRef.current.d3Force('link', undefined); - - // Add stable forces with minimal movement - fgRef.current.d3Force('charge', d3.forceManyBody().strength(-100)); - fgRef.current.d3Force('center', d3.forceCenter(dimensions.width / 2, dimensions.height / 2).strength(0.05)); - fgRef.current.d3Force('link', d3.forceLink().distance(80).strength(0.2)); - - // Reduce simulation intensity - fgRef.current.d3Force('x', d3.forceX(dimensions.width / 2).strength(0.05)); - fgRef.current.d3Force('y', d3.forceY(dimensions.height / 2).strength(0.05)); - } - }, [dimensions, selectedBook]); - - // Add useEffect to center the graph on mount and dimension changes - useEffect(() => { - if (fgRef.current) { - // Center the graph with animation - requestAnimationFrame(() => { - fgRef.current?.zoomToFit(400, 100); // Increased padding for better centering - }); - } - }, [dimensions, selectedBook]); + // ... rest of the component code ... return ( - - - - - - - Character Network - - - Double click the nodes to access the information inside them! - - - - - - - - - - - - - - {/* Loading overlay */} - - - - - {selectedBook ? 'Loading character relationships...' : 'Loading books...'} - - - - - {/* Graph container */} - - - link.color} - linkWidth={2} - nodeRelSize={6} - width={dimensions.width} - height={dimensions.height} - cooldownTicks={50} - cooldownTime={3000} - d3AlphaDecay={0.02} - d3VelocityDecay={0.6} - minZoom={0.5} - maxZoom={4} - enableNodeDrag={true} - onNodeDragEnd={(node: NetworkNode) => { - if (node.x && node.y) { - node.fx = node.x; - node.fy = node.y; - } - }} - warmupTicks={0} - nodeLabel={() => ''} - linkCurvature={0.3} - linkDirectionalParticles={0} - onEngineStop={() => { - // Ensure graph is centered after layout stabilizes - fgRef.current?.zoomToFit(400, 100); - }} - /> - - - - {/* Controls */} - {!isLoading && ( - <> - {selectedBook && ( - - - - - - )} - - - - handleZoomIn()} - size="small" - sx={{ color: sageColors.primary.start }} - > - - - - - handleZoomOut()} - size="small" - sx={{ color: sageColors.primary.start }} - > - - - - - handleCenterGraph()} - size="small" - sx={{ color: sageColors.primary.start }} - > - - - - - - - {!selectedBook ? 'Click a book to explore its characters' : 'Click characters to view relationships'} - - - - - )} - - - - - - {selectedNode ? ( - - - {selectedNode.name} - - - - {selectedNode.type !== 'book' && ( - - )} - - - {selectedNode.type === 'book' ? `Published: ${selectedNode.year}` : selectedNode.novel} - - - {selectedNode.description} - - - {selectedNode.type !== 'book' && ( - <> - - - Relationships - - - {selectedRelationships.map((rel, index) => ( - - - {rel.description} - - - - Development: - - - {rel.development.map((step, i) => ( - - - {step} - - - ))} - - - ))} - - - )} - - ) : ( - - - Select a node to view details - - - )} - - - - - - + // ... existing JSX ... + {selectedNode && ( + + {selectedNode.type === 'book' ? + `Published: ${selectedNode.year}` : + selectedNode.novel} + + )} + // ... rest of the JSX ... ); }