From e426c4ff920a4637f0fa45bf10f3cdc221cd398e Mon Sep 17 00:00:00 2001 From: rathi Date: Wed, 4 Dec 2024 21:42:08 -0500 Subject: [PATCH 1/6] deployment updates --- index.html | 2 +- netlify.toml | 8 ++++++++ public/feather-icon.svg | 5 +++++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 netlify.toml create mode 100644 public/feather-icon.svg diff --git a/index.html b/index.html index 76c3570..7139095 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + Austen's Wedding Guide diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000..df9f053 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,8 @@ +[build] + command = "npm run build" + publish = "dist" + +[[redirects]] + from = "/*" + to = "/index.html" + status = 200 diff --git a/public/feather-icon.svg b/public/feather-icon.svg new file mode 100644 index 0000000..6e72f49 --- /dev/null +++ b/public/feather-icon.svg @@ -0,0 +1,5 @@ + + + + + From 72193f99678df90429b7da5329a31c74cbab9180 Mon Sep 17 00:00:00 2001 From: rathi Date: Wed, 4 Dec 2024 22:18:13 -0500 Subject: [PATCH 2/6] debugging for deployment --- package-lock.json | 135 +++++++++++++++++++++++++++ package.json | 2 + src/components/ShareButtons.tsx | 9 +- src/components/ui/calendar.tsx | 4 +- src/components/ui/chart.tsx | 160 +++++++------------------------- src/components/ui/toast.tsx | 5 + src/pages/SuccessStories.tsx | 1 - src/types/vendors.ts | 2 + 8 files changed, 185 insertions(+), 133 deletions(-) diff --git a/package-lock.json b/package-lock.json index 517c8b1..408c51a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "@tailwindcss/line-clamp": "^0.4.4", "@tailwindcss/typography": "^0.5.15", "@types/d3": "^7.4.3", + "@types/recharts": "^1.8.29", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.0", @@ -61,6 +62,7 @@ "react-hook-form": "^7.53.0", "react-resizable-panels": "^2.1.3", "react-router-dom": "^6.28.0", + "recharts": "^2.14.1", "sonner": "^1.5.0", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", @@ -3491,6 +3493,31 @@ "@types/react": "*" } }, + "node_modules/@types/recharts": { + "version": "1.8.29", + "resolved": "https://registry.npmjs.org/@types/recharts/-/recharts-1.8.29.tgz", + "integrity": "sha512-ulKklaVsnFIIhTQsQw226TnOibrddW1qUQNFVhoQEyY1Z7FRQrNecFCGt7msRuJseudzE9czVawZb17dK/aPXw==", + "license": "MIT", + "dependencies": { + "@types/d3-shape": "^1", + "@types/react": "*" + } + }, + "node_modules/@types/recharts/node_modules/@types/d3-path": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.11.tgz", + "integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==", + "license": "MIT" + }, + "node_modules/@types/recharts/node_modules/@types/d3-shape": { + "version": "1.3.12", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.12.tgz", + "integrity": "sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "^1" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.8.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz", @@ -5286,6 +5313,12 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/decompress-response": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", @@ -5749,12 +5782,27 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-equals": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", + "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -6526,6 +6574,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", @@ -7501,6 +7555,21 @@ "react-dom": ">=16.8" } }, + "node_modules/react-smooth": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.3.tgz", + "integrity": "sha512-PyxIrra8WZWrMRFcCiJsZ+JqFaxEINAt+v/w++wQKQlmO99Eh3+JTLeKApdTsLX2roBdWYXqPsaS8sO4UmdzIg==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", @@ -7558,6 +7627,44 @@ "node": ">=8.10.0" } }, + "node_modules/recharts": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.14.1.tgz", + "integrity": "sha512-xtWulflkA+/xu4/QClBdtZYN30dbvTHjxjkh5XTMrH/CQ3WGDDPHHa/LLKCbgoqz0z3UaSH2/blV1i6VNMeh1g==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.0", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -8062,6 +8169,12 @@ "three": ">=0.168" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinycolor2": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", @@ -8281,6 +8394,28 @@ "react-dom": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/vite": { "version": "5.4.8", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", diff --git a/package.json b/package.json index 99964f1..1065529 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@tailwindcss/line-clamp": "^0.4.4", "@tailwindcss/typography": "^0.5.15", "@types/d3": "^7.4.3", + "@types/recharts": "^1.8.29", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.0", @@ -63,6 +64,7 @@ "react-hook-form": "^7.53.0", "react-resizable-panels": "^2.1.3", "react-router-dom": "^6.28.0", + "recharts": "^2.14.1", "sonner": "^1.5.0", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", diff --git a/src/components/ShareButtons.tsx b/src/components/ShareButtons.tsx index 371383c..3835d04 100644 --- a/src/components/ShareButtons.tsx +++ b/src/components/ShareButtons.tsx @@ -1,14 +1,11 @@ import React from 'react'; -import { BlogPost } from '../data/blogPosts'; interface ShareButtonsProps { - post: BlogPost; + title: string; + url?: string; } -export const ShareButtons: React.FC = ({ post }) => { - const url = window.location.href; - const title = `Check out ${post.character}'s blog post: ${post.title}`; - +export const ShareButtons: React.FC = ({ title, url = window.location.href }) => { const shareLinks = [ { name: 'Twitter', diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx index 1b7bc45..1c59433 100644 --- a/src/components/ui/calendar.tsx +++ b/src/components/ui/calendar.tsx @@ -58,8 +58,8 @@ function Calendar({ ...classNames, }} components={{ - IconLeft: ({ ...props }) => , - IconRight: ({ ...props }) => , + IconLeft: () => , + IconRight: () => , }} {...props} /> diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx index 13c1bd3..3d6fcd9 100644 --- a/src/components/ui/chart.tsx +++ b/src/components/ui/chart.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import * as RechartsPrimitive from 'recharts'; -import { +import type { TooltipProps } from 'recharts'; +import type { NameType, - Payload, ValueType, } from 'recharts/types/component/DefaultTooltipContent'; @@ -72,7 +72,7 @@ ChartContainer.displayName = 'Chart'; const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { const colorConfig = Object.entries(config).filter( - ([_, config]) => config.theme || config.color + ([, itemConfig]) => itemConfig.theme || itemConfig.color ); if (!colorConfig.length) { @@ -105,17 +105,34 @@ ${colorConfig const ChartTooltip = RechartsPrimitive.Tooltip; -const ChartTooltipContent = React.forwardRef< - HTMLDivElement, - React.ComponentProps & - React.ComponentProps<'div'> & { - hideLabel?: boolean; - hideIndicator?: boolean; - indicator?: 'line' | 'dot' | 'dashed'; - nameKey?: string; - labelKey?: string; - } ->( +interface PayloadItem { + dataKey?: string; + name?: string; + value?: number; + payload?: { + fill?: string; + color?: string; + }; + color?: string; +} + +interface CustomTooltipProps extends Omit, 'formatter'> { + className?: string; + hideLabel?: boolean; + hideIndicator?: boolean; + indicator?: 'line' | 'dot' | 'dashed'; + nameKey?: string; + labelKey?: string; + labelClassName?: string; + color?: string; + formatter?: (value: ValueType, name: string, props: PayloadItem, index: number, payload: PayloadItem) => React.ReactNode; +} + +const getPayloadConfigFromPayload = (config: ChartConfig, payload: PayloadItem, key: string) => { + return config[key] || config[payload.dataKey || ''] || config[payload.name || '']; +}; + +const ChartTooltipContent = React.forwardRef( ( { active, @@ -188,10 +205,10 @@ const ChartTooltipContent = React.forwardRef< > {!nestLabel ? tooltipLabel : null}
- {payload.map((item, index) => { + {payload.map((item: PayloadItem, index: number) => { const key = `${nameKey || item.name || item.dataKey || 'value'}`; const itemConfig = getPayloadConfigFromPayload(config, item, key); - const indicatorColor = color || item.payload.fill || item.color; + const indicatorColor = color || (item.payload?.fill || item.color); return (
{formatter && item?.value !== undefined && item.name ? ( - formatter(item.value, item.name, item, index, item.payload) + formatter(item.value, item.name, item, index, item) ) : ( <> {itemConfig?.icon ? ( @@ -257,112 +274,7 @@ const ChartTooltipContent = React.forwardRef< ); } ); -ChartTooltipContent.displayName = 'ChartTooltip'; -const ChartLegend = RechartsPrimitive.Legend; +ChartTooltipContent.displayName = 'ChartTooltipContent'; -const ChartLegendContent = React.forwardRef< - HTMLDivElement, - React.ComponentProps<'div'> & - Pick & { - hideIcon?: boolean; - nameKey?: string; - } ->( - ( - { className, hideIcon = false, payload, verticalAlign = 'bottom', nameKey }, - ref - ) => { - const { config } = useChart(); - - if (!payload?.length) { - return null; - } - - return ( -
- {payload.map((item) => { - const key = `${nameKey || item.dataKey || 'value'}`; - const itemConfig = getPayloadConfigFromPayload(config, item, key); - - return ( -
svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground' - )} - > - {itemConfig?.icon && !hideIcon ? ( - - ) : ( -
- )} - {itemConfig?.label} -
- ); - })} -
- ); - } -); -ChartLegendContent.displayName = 'ChartLegend'; - -// Helper to extract item config from a payload. -function getPayloadConfigFromPayload( - config: ChartConfig, - payload: unknown, - key: string -) { - if (typeof payload !== 'object' || payload === null) { - return undefined; - } - - const payloadPayload = - 'payload' in payload && - typeof payload.payload === 'object' && - payload.payload !== null - ? payload.payload - : undefined; - - let configLabelKey: string = key; - - if ( - key in payload && - typeof payload[key as keyof typeof payload] === 'string' - ) { - configLabelKey = payload[key as keyof typeof payload] as string; - } else if ( - payloadPayload && - key in payloadPayload && - typeof payloadPayload[key as keyof typeof payloadPayload] === 'string' - ) { - configLabelKey = payloadPayload[ - key as keyof typeof payloadPayload - ] as string; - } - - return configLabelKey in config - ? config[configLabelKey] - : config[key as keyof typeof config]; -} - -export { - ChartContainer, - ChartTooltip, - ChartTooltipContent, - ChartLegend, - ChartLegendContent, - ChartStyle, -}; +export { ChartContainer, ChartTooltip, ChartTooltipContent }; diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx index 48e2b11..505c274 100644 --- a/src/components/ui/toast.tsx +++ b/src/components/ui/toast.tsx @@ -83,7 +83,12 @@ const ToastDescription = React.forwardRef< )); ToastDescription.displayName = ToastPrimitives.Description.displayName; +type ToastProps = React.ComponentPropsWithoutRef; +type ToastActionElement = React.ReactElement; + export { + type ToastProps, + type ToastActionElement, ToastProvider, ToastViewport, Toast, diff --git a/src/pages/SuccessStories.tsx b/src/pages/SuccessStories.tsx index 1944fbd..75475b4 100644 --- a/src/pages/SuccessStories.tsx +++ b/src/pages/SuccessStories.tsx @@ -1,7 +1,6 @@ import { useState } from 'react'; import { Heart, MapPin, Calendar } from 'lucide-react'; import { successStories, SuccessStory } from '@/data/success-stories'; -import { cn } from '@/lib/utils'; const StoryModal = ({ story, onClose }: { story: SuccessStory; onClose: () => void }) => { return ( diff --git a/src/types/vendors.ts b/src/types/vendors.ts index c267f8f..c7f5523 100644 --- a/src/types/vendors.ts +++ b/src/types/vendors.ts @@ -5,6 +5,8 @@ export type VendorCategory = | 'catering' | 'music' | 'flowers' + | 'transport' + | 'stationery' | 'matchmaking' | 'modiste'; From 4870fb5e9221e2ebca8a1a327db5f56ccfb6b11b Mon Sep 17 00:00:00 2001 From: rathi Date: Wed, 4 Dec 2024 22:32:37 -0500 Subject: [PATCH 3/6] debugging 2 --- src/components/ui/chart.tsx | 9 +-- src/pages/NetworkVisualization.tsx | 89 +++++++++++++++--------------- 2 files changed, 50 insertions(+), 48 deletions(-) diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx index 3d6fcd9..bfb5e32 100644 --- a/src/components/ui/chart.tsx +++ b/src/components/ui/chart.tsx @@ -106,7 +106,7 @@ ${colorConfig const ChartTooltip = RechartsPrimitive.Tooltip; interface PayloadItem { - dataKey?: string; + dataKey?: string | number; name?: string; value?: number; payload?: { @@ -125,7 +125,8 @@ interface CustomTooltipProps extends Omit, 'fo labelKey?: string; labelClassName?: string; color?: string; - formatter?: (value: ValueType, name: string, props: PayloadItem, index: number, payload: PayloadItem) => React.ReactNode; + formatter?: (value: ValueType, name: string, props: PayloadItem, index: number, payload: PayloadItem[]) => React.ReactNode; + payload?: PayloadItem[]; } const getPayloadConfigFromPayload = (config: ChartConfig, payload: PayloadItem, key: string) => { @@ -205,7 +206,7 @@ const ChartTooltipContent = React.forwardRef > {!nestLabel ? tooltipLabel : null}
- {payload.map((item: PayloadItem, index: number) => { + {payload.map((item, index) => { const key = `${nameKey || item.name || item.dataKey || 'value'}`; const itemConfig = getPayloadConfigFromPayload(config, item, key); const indicatorColor = color || (item.payload?.fill || item.color); @@ -219,7 +220,7 @@ const ChartTooltipContent = React.forwardRef )} > {formatter && item?.value !== undefined && item.name ? ( - formatter(item.value, item.name, item, index, item) + formatter(item.value, item.name, item, index, payload) ) : ( <> {itemConfig?.icon ? ( diff --git a/src/pages/NetworkVisualization.tsx b/src/pages/NetworkVisualization.tsx index c071c07..419ac45 100644 --- a/src/pages/NetworkVisualization.tsx +++ b/src/pages/NetworkVisualization.tsx @@ -2,11 +2,12 @@ import { useRef, useEffect, useState, useCallback } from 'react'; import { characterNetwork } from '../data/character-network'; import { CharacterNode, Relationship, BookNode } from '../types/character-network'; import { Box, Typography, Paper, Grid, IconButton, Tooltip, Chip, Divider, Container, CircularProgress, Fade } from '@mui/material'; -import { ForceGraph2D, ForceGraphMethods } 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'; -interface NetworkNode extends d3.SimulationNodeDatum { +// Base node interface extending SimulationNodeDatum +interface BaseNetworkNode extends d3.SimulationNodeDatum { id: string; name: string; type: 'protagonist' | 'antagonist' | 'supporting' | 'book' | 'character'; @@ -21,23 +22,37 @@ interface NetworkNode extends d3.SimulationNodeDatum { fy?: number; } -interface NetworkLink { - source: string; - target: string; +// Book-specific node interface +interface NetworkBookNode extends BaseNetworkNode { + type: 'book'; + year: string; +} + +// Character-specific node interface +interface NetworkCharacterNode extends BaseNetworkNode { + type: 'protagonist' | 'antagonist' | 'supporting' | 'character'; +} + +// Union type for all possible node types +type NetworkNode = NetworkBookNode | NetworkCharacterNode; + +interface NetworkLink extends d3.SimulationLinkDatum { + source: string | NetworkNode; + target: string | NetworkNode; type: string; color: string; } // Proper typing for ForceGraph methods -interface ForceGraphMethods { +interface ForceGraphMethods = NetworkLink> { 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; } -type ForceGraphInstance = ForceGraphMethods; +type ForceGraphInstance = ForceGraphMethods; // Add sage theme colors after the interface definitions const sageColors = { @@ -75,11 +90,11 @@ const getPentagonPoint = (index: number, total: number, radius: number, centerX: }; export default function NetworkVisualization() { - const [selectedNode, setSelectedNode] = useState(null); + const [selectedNode, setSelectedNode] = useState(null); const [selectedRelationships, setSelectedRelationships] = useState([]); const [selectedBook, setSelectedBook] = useState(null); const containerRef = useRef(null); - const fgRef = useRef(null); + const fgRef = useRef>(); const [isLoading, setIsLoading] = useState(true); const [isGraphReady, setIsGraphReady] = useState(false); const [dimensions, setDimensions] = useState({ width: 800, height: 700 }); @@ -118,41 +133,27 @@ export default function NetworkVisualization() { }, []); // Update the node interaction handlers - const handleNodeClick = (node: NetworkNode) => { - // Book node click + const handleNodeClick = useCallback((node: NetworkNode) => { if (node.type === 'book') { - const bookNode = characterNetwork.books.find(b => b.id === node.id); - if (bookNode) { - // Update all states synchronously - setSelectedBook(node.id); - setSelectedNode(bookNode); - setSelectedRelationships([]); - - // Trigger force simulation update after state changes - requestAnimationFrame(() => { - if (fgRef.current) { - fgRef.current.d3ReheatSimulation(); - } - }); - } - return; + 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); } - - // Character node click - only process if we're in a book view - if (selectedBook) { - const characterNode = characterNetwork.nodes.find(n => n.id === node.id); - if (characterNode) { - // Get relationships first - const relations = characterNetwork.relationships.filter( - r => r.source === node.id || r.target === node.id - ); - - // Update states synchronously - setSelectedNode(characterNode); - setSelectedRelationships(relations); - } - } - }; + }, []); const handleBackClick = () => { setSelectedBook(null); @@ -533,7 +534,7 @@ export default function NetworkVisualization() { {/* Graph container */} - Date: Wed, 4 Dec 2024 22:43:06 -0500 Subject: [PATCH 4/6] debugging 3 --- src/pages/NetworkVisualization.tsx | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/pages/NetworkVisualization.tsx b/src/pages/NetworkVisualization.tsx index 419ac45..28d28eb 100644 --- a/src/pages/NetworkVisualization.tsx +++ b/src/pages/NetworkVisualization.tsx @@ -2,7 +2,7 @@ import { useRef, useEffect, useState, useCallback } from 'react'; import { characterNetwork } from '../data/character-network'; import { CharacterNode, Relationship, BookNode } from '../types/character-network'; import { Box, Typography, Paper, Grid, IconButton, Tooltip, Chip, Divider, Container, CircularProgress, Fade } from '@mui/material'; -import { ForceGraph2D as ForceGraph } from 'react-force-graph'; +import { ForceGraph2D as ForceGraph, NodeObject, LinkObject } from 'react-force-graph'; import { ArrowBack, Help, ZoomIn, ZoomOut, CenterFocusStrong } from '@mui/icons-material'; import * as d3 from 'd3'; @@ -25,7 +25,7 @@ interface BaseNetworkNode extends d3.SimulationNodeDatum { // Book-specific node interface interface NetworkBookNode extends BaseNetworkNode { type: 'book'; - year: string; + year: number; } // Character-specific node interface @@ -36,20 +36,29 @@ interface NetworkCharacterNode extends BaseNetworkNode { // Union type for all possible node types type NetworkNode = NetworkBookNode | NetworkCharacterNode; +// Updated NetworkLink interface to match D3's expectations interface NetworkLink extends d3.SimulationLinkDatum { - source: string | NetworkNode; - target: string | NetworkNode; + source: string | number | NetworkNode; + target: string | number | NetworkNode; type: string; color: string; } -// Proper typing for ForceGraph methods -interface ForceGraphMethods = NetworkLink> { +// Complete ForceGraph methods interface +interface ForceGraphMethods, LinkType = LinkObject> { zoom: (k?: number) => number; zoomToFit: (duration?: number, padding?: number) => void; d3Force: (forceName: string, force?: d3.Force) => void; d3ReheatSimulation: () => void; getZoom: () => number; + emitParticle: (particle: any) => void; + pauseAnimation: () => void; + resumeAnimation: () => void; + centerAt: (x?: number, y?: number, duration?: number) => void; + // Add other required methods + width: () => number; + height: () => number; + refresh: () => void; } type ForceGraphInstance = ForceGraphMethods; @@ -94,7 +103,7 @@ export default function NetworkVisualization() { const [selectedRelationships, setSelectedRelationships] = useState([]); const [selectedBook, setSelectedBook] = useState(null); const containerRef = useRef(null); - const fgRef = useRef>(); + const fgRef = useRef(); const [isLoading, setIsLoading] = useState(true); const [isGraphReady, setIsGraphReady] = useState(false); const [dimensions, setDimensions] = useState({ width: 800, height: 700 }); @@ -756,7 +765,7 @@ export default function NetworkVisualization() { - {selectedNode.type === 'book' ? `Published: ${(selectedNode as BookNode).year}` : selectedNode.novel} + {selectedNode.type === 'book' ? `Published: ${selectedNode.year}` : selectedNode.novel} Date: Wed, 4 Dec 2024 22:47:38 -0500 Subject: [PATCH 5/6] 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 ... ); } From eabdb7238765c82bfb25b152b29dd1895186754c Mon Sep 17 00:00:00 2001 From: rathi Date: Wed, 4 Dec 2024 22:54:46 -0500 Subject: [PATCH 6/6] Revert "debugging 3" This reverts commit 4d087fc3ce635c4bb26b5f7b0430c078942c29f2. --- src/pages/NetworkVisualization.tsx | 784 ++++++++++++++++++++++++++++- 1 file changed, 780 insertions(+), 4 deletions(-) diff --git a/src/pages/NetworkVisualization.tsx b/src/pages/NetworkVisualization.tsx index ee032f7..d8143a4 100644 --- a/src/pages/NetworkVisualization.tsx +++ b/src/pages/NetworkVisualization.tsx @@ -3,6 +3,7 @@ import { characterNetwork } from '../data/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 } 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'; @@ -25,7 +26,7 @@ interface BaseNetworkNode extends d3.SimulationNodeDatum { // Book-specific node interface interface NetworkBookNode extends BaseNetworkNode { type: 'book'; - year: number; + year: string; } // Character-specific node interface @@ -36,10 +37,9 @@ interface NetworkCharacterNode extends BaseNetworkNode { // Union type for all possible node types type NetworkNode = NetworkBookNode | NetworkCharacterNode; -// Updated NetworkLink interface to match D3's expectations interface NetworkLink extends d3.SimulationLinkDatum { - source: string | number | NetworkNode; - target: string | number | NetworkNode; + source: string | NetworkNode; + target: string | NetworkNode; type: string; color: string; } @@ -51,6 +51,8 @@ interface GraphParticle { target: NetworkNode; } +// Proper typing for ForceGraph methods +interface ForceGraphMethods = NetworkLink> { // Complete ForceGraph methods interface interface ForceGraphMethods { zoom: (k?: number) => number; @@ -74,11 +76,785 @@ 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]); const fgRef = useRef(); // ... 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 as BookNode).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 && (