mirror of
https://github.com/harivansh-afk/Austens-Wedding-Guide.git
synced 2026-04-15 03:00:43 +00:00
improved literary analysis pgae and added compare works sub page
This commit is contained in:
parent
0134c81526
commit
5ca37d03f2
13 changed files with 3953 additions and 59 deletions
2488
package-lock.json
generated
2488
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -10,7 +10,11 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.13.5",
|
||||
"@emotion/styled": "^11.13.5",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@mui/icons-material": "^6.1.9",
|
||||
"@mui/material": "^6.1.9",
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.1",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.0",
|
||||
|
|
@ -42,9 +46,11 @@
|
|||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@tailwindcss/line-clamp": "^0.4.4",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/d3": "^7.4.3",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"d3": "^7.9.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"embla-carousel-react": "^8.3.0",
|
||||
"input-otp": "^1.2.4",
|
||||
|
|
@ -53,6 +59,7 @@
|
|||
"react": "^18.3.1",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-force-graph": "^1.44.7",
|
||||
"react-hook-form": "^7.53.0",
|
||||
"react-resizable-panels": "^2.1.3",
|
||||
"react-router-dom": "^6.28.0",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import BlogPost from './pages/BlogPost/BlogPost';
|
|||
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||
import SuccessStories from './pages/SuccessStories';
|
||||
import Analysis from './pages/Analysis';
|
||||
import ComparativeAnalysis from './pages/ComparativeAnalysis';
|
||||
import NetworkVisualization from './pages/NetworkVisualization';
|
||||
|
||||
// Lazy load other pages
|
||||
const Quiz = React.lazy(() => import('./pages/Quiz'));
|
||||
|
|
@ -16,7 +18,7 @@ const MarketCalculator = React.lazy(() => import('./pages/MarketCalculator'));
|
|||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<Router basename="/">
|
||||
<MainLayout>
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
|
|
@ -30,6 +32,9 @@ function App() {
|
|||
<Route path="/success-stories" element={<SuccessStories />} />
|
||||
<Route path="/market-calculator" element={<MarketCalculator />} />
|
||||
<Route path="/analysis" element={<Analysis />} />
|
||||
<Route path="/comparative" element={<ComparativeAnalysis />} />
|
||||
<Route path="/network" element={<NetworkVisualization />} />
|
||||
<Route path="*" element={<div>Page not found</div>} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
|
|
|
|||
19
src/components/CharacterDetails.jsx
Normal file
19
src/components/CharacterDetails.jsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// For smaller network visualizations in subpages
|
||||
const subGraphConfig = {
|
||||
nodeRelSize: 4,
|
||||
linkDistance: 80,
|
||||
d3: {
|
||||
alphaDecay: 0.05, // Faster decay for smaller graphs
|
||||
alphaMin: 0.001,
|
||||
velocityDecay: 1
|
||||
},
|
||||
cooldownTicks: 30,
|
||||
cooldownTime: 1500,
|
||||
};
|
||||
|
||||
// Use static positioning for very small networks
|
||||
const staticLayout = {
|
||||
enableNodeDrag: false,
|
||||
staticGraph: true,
|
||||
nodePosition: { x: node => node.initialX, y: node => node.initialY }
|
||||
};
|
||||
132
src/components/InteractiveGraph.jsx
Normal file
132
src/components/InteractiveGraph.jsx
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import { useRef, useCallback, useState, useEffect } from 'react';
|
||||
|
||||
const InteractiveGraph = ({ graphData, onNodeSelect }) => {
|
||||
const graphRef = useRef();
|
||||
const [selectedNode, setSelectedNode] = useState(null);
|
||||
const clickTimeoutRef = useRef(null);
|
||||
const [isProcessingClick, setIsProcessingClick] = useState(false);
|
||||
|
||||
// Cleanup function for the timeout
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (clickTimeoutRef.current) {
|
||||
clearTimeout(clickTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleNodeClick = useCallback((node) => {
|
||||
if (!node || isProcessingClick) return;
|
||||
|
||||
// Prevent multiple clicks
|
||||
setIsProcessingClick(true);
|
||||
|
||||
// Clear any existing timeout
|
||||
if (clickTimeoutRef.current) {
|
||||
clearTimeout(clickTimeoutRef.current);
|
||||
}
|
||||
|
||||
// Set the selected node immediately
|
||||
setSelectedNode(node);
|
||||
onNodeSelect(node);
|
||||
|
||||
// Reset the processing flag after a short delay
|
||||
clickTimeoutRef.current = setTimeout(() => {
|
||||
setIsProcessingClick(false);
|
||||
}, 300); // 300ms debounce
|
||||
}, [onNodeSelect, isProcessingClick]);
|
||||
|
||||
// Calculate static positions based on container size
|
||||
const containerWidth = 600;
|
||||
const containerHeight = 600;
|
||||
|
||||
const graphConfig = {
|
||||
nodeRelSize: 8,
|
||||
nodeVal: 30,
|
||||
width: containerWidth,
|
||||
height: containerHeight,
|
||||
backgroundColor: "#ffffff",
|
||||
nodeColor: node => node.type === 'protagonist' ? "#4CAF50" :
|
||||
node.type === 'antagonist' ? "#f44336" : "#2196F3",
|
||||
|
||||
// Static layout configuration
|
||||
staticGraph: true,
|
||||
nodePosition: node => ({
|
||||
x: containerWidth/2 + Math.cos(node.index * (2 * Math.PI / graphData.nodes.length)) * (containerWidth/3),
|
||||
y: containerHeight/2 + Math.sin(node.index * (2 * Math.PI / graphData.nodes.length)) * (containerHeight/3)
|
||||
}),
|
||||
|
||||
// Interaction settings
|
||||
enableNodeDrag: false,
|
||||
enableZoom: true,
|
||||
minZoom: 0.5,
|
||||
maxZoom: 2.5,
|
||||
cooldownTime: 0, // Disable force simulation cooldown
|
||||
warmupTicks: 0, // Disable initial ticks
|
||||
|
||||
// Node appearance
|
||||
nodeResolution: 16,
|
||||
|
||||
// Completely disable all tooltips/labels
|
||||
nodeLabel: null,
|
||||
nodeCanvasObject: null,
|
||||
nodeCanvasObjectMode: null,
|
||||
enableNodeHover: false,
|
||||
enableLinkHover: false,
|
||||
|
||||
// Link appearance
|
||||
linkWidth: 2,
|
||||
linkColor: () => "#999999",
|
||||
linkOpacity: 0.6,
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '20px',
|
||||
width: '100%',
|
||||
maxWidth: '1000px',
|
||||
margin: '0 auto'
|
||||
}}>
|
||||
<div style={{
|
||||
width: containerWidth,
|
||||
height: containerHeight,
|
||||
border: '1px solid #eee',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<ForceGraph
|
||||
{...graphConfig}
|
||||
ref={graphRef}
|
||||
graphData={graphData}
|
||||
onNodeClick={handleNodeClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right side popup */}
|
||||
<div style={{
|
||||
width: '300px',
|
||||
padding: '20px',
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #eee',
|
||||
borderRadius: '4px',
|
||||
height: 'fit-content',
|
||||
visibility: selectedNode ? 'visible' : 'hidden',
|
||||
opacity: selectedNode ? 1 : 0,
|
||||
transition: 'opacity 0.2s ease-in-out'
|
||||
}}>
|
||||
{selectedNode && (
|
||||
<>
|
||||
<h3 style={{ margin: '0 0 10px 0' }}>{selectedNode.name}</h3>
|
||||
<p style={{ margin: '0 0 10px 0' }}>Type: {selectedNode.type}</p>
|
||||
<p style={{ margin: '0 0 10px 0' }}>Novel: {selectedNode.novel}</p>
|
||||
{selectedNode.class && (
|
||||
<p style={{ margin: '0 0 10px 0' }}>Class: {selectedNode.class}</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InteractiveGraph;
|
||||
|
|
@ -30,12 +30,20 @@ const Navbar = () => {
|
|||
<Link to="/market-calculator" className="text-sage-700 hover:text-sage-900 transition-colors">
|
||||
Market Value
|
||||
</Link>
|
||||
<Link
|
||||
to="/analysis"
|
||||
className="bg-sage-100 text-sage-700 hover:bg-sage-200 px-4 py-2 rounded-md transition-colors"
|
||||
>
|
||||
Literary Analysis
|
||||
</Link>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Link
|
||||
to="/analysis"
|
||||
className="bg-sage-100 text-sage-700 hover:bg-sage-200 px-4 py-2 rounded-md transition-colors"
|
||||
>
|
||||
Literary Analysis
|
||||
</Link>
|
||||
<Link
|
||||
to="/network"
|
||||
className="bg-sage-100 text-sage-700 hover:bg-sage-200 px-4 py-2 rounded-md transition-colors"
|
||||
>
|
||||
Character Network
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,15 +3,19 @@ import Navbar from '../Navbar';
|
|||
|
||||
const MainLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<div className="min-h-screen bg-cream-50">
|
||||
<div className="min-h-screen bg-cream-50 flex flex-col">
|
||||
<Navbar />
|
||||
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{children}
|
||||
<main className="flex-1 flex flex-col">
|
||||
<div className="flex-1 container mx-auto px-4 sm:px-6 lg:px-8 py-8 flex flex-col">
|
||||
<div className="flex-1 flex flex-col">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer className="bg-sage-100 border-t border-sage-200 mt-12">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<footer className="bg-sage-100 border-t border-sage-200">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="text-center font-cormorant text-sage-900">
|
||||
<p className="text-lg">"It is a truth universally acknowledged..."</p>
|
||||
<p className="mt-2">© {new Date().getFullYear()} Austen's Wedding Guide</p>
|
||||
|
|
|
|||
461
src/data/character-network.ts
Normal file
461
src/data/character-network.ts
Normal file
|
|
@ -0,0 +1,461 @@
|
|||
import { CharacterNetwork } from '../types/character-network';
|
||||
|
||||
export const characterNetwork: CharacterNetwork = {
|
||||
books: [
|
||||
{
|
||||
id: "pride-and-prejudice",
|
||||
name: "Pride and Prejudice",
|
||||
type: "book",
|
||||
color: "#e91e63",
|
||||
description: "A masterpiece of wit and social commentary exploring marriage, class, and prejudice in Regency England.",
|
||||
year: 1813
|
||||
},
|
||||
{
|
||||
id: "sense-and-sensibility",
|
||||
name: "Sense and Sensibility",
|
||||
type: "book",
|
||||
color: "#2196f3",
|
||||
description: "A story of two sisters with contrasting temperaments navigating love and heartbreak.",
|
||||
year: 1811
|
||||
},
|
||||
{
|
||||
id: "northanger-abbey",
|
||||
name: "Northanger Abbey",
|
||||
type: "book",
|
||||
color: "#4caf50",
|
||||
description: "A coming-of-age story that parodies Gothic novels while exploring the perils of confusing fiction with reality.",
|
||||
year: 1818
|
||||
},
|
||||
{
|
||||
id: "mansfield-park",
|
||||
name: "Mansfield Park",
|
||||
type: "book",
|
||||
color: "#9c27b0",
|
||||
description: "A complex exploration of morality, social position, and personal integrity through the story of Fanny Price.",
|
||||
year: 1814
|
||||
},
|
||||
{
|
||||
id: "longbourn",
|
||||
name: "Longbourn",
|
||||
type: "book",
|
||||
color: "#795548",
|
||||
description: "A retelling of Pride and Prejudice from the servants' perspective, exploring class and social inequality.",
|
||||
year: 2013
|
||||
}
|
||||
],
|
||||
nodes: [
|
||||
// Pride and Prejudice
|
||||
{
|
||||
id: "elizabeth-bennet",
|
||||
name: "Elizabeth Bennet",
|
||||
novel: "Pride and Prejudice",
|
||||
class: "middle",
|
||||
type: "protagonist",
|
||||
description: "The witty and independent second daughter of the Bennet family, known for her intelligence and prejudiced first impressions."
|
||||
},
|
||||
{
|
||||
id: "darcy",
|
||||
name: "Fitzwilliam Darcy",
|
||||
novel: "Pride and Prejudice",
|
||||
class: "upper",
|
||||
type: "protagonist",
|
||||
description: "A wealthy and proud nobleman who learns to overcome his prejudices and rigid social expectations."
|
||||
},
|
||||
{
|
||||
id: "jane-bennet",
|
||||
name: "Jane Bennet",
|
||||
novel: "Pride and Prejudice",
|
||||
class: "middle",
|
||||
type: "supporting",
|
||||
description: "The beautiful and gentle eldest Bennet daughter, whose sweet nature leads her to see only the good in others."
|
||||
},
|
||||
{
|
||||
id: "bingley",
|
||||
name: "Charles Bingley",
|
||||
novel: "Pride and Prejudice",
|
||||
class: "upper",
|
||||
type: "supporting",
|
||||
description: "Darcy's wealthy and amiable friend who falls in love with Jane Bennet, easily influenced by others."
|
||||
},
|
||||
{
|
||||
id: "lydia-bennet",
|
||||
name: "Lydia Bennet",
|
||||
novel: "Pride and Prejudice",
|
||||
class: "middle",
|
||||
type: "supporting",
|
||||
description: "The youngest Bennet sister, whose reckless behavior threatens her family's reputation."
|
||||
},
|
||||
{
|
||||
id: "wickham",
|
||||
name: "George Wickham",
|
||||
novel: "Pride and Prejudice",
|
||||
class: "middle",
|
||||
type: "antagonist",
|
||||
description: "A charming but deceitful officer who causes trouble for both the Bennets and Darcy."
|
||||
},
|
||||
// Sense and Sensibility
|
||||
{
|
||||
id: "elinor-dashwood",
|
||||
name: "Elinor Dashwood",
|
||||
novel: "Sense and Sensibility",
|
||||
class: "middle",
|
||||
type: "protagonist",
|
||||
description: "The sensible and reserved elder Dashwood sister, who represents sense and emotional restraint."
|
||||
},
|
||||
{
|
||||
id: "marianne-dashwood",
|
||||
name: "Marianne Dashwood",
|
||||
novel: "Sense and Sensibility",
|
||||
class: "middle",
|
||||
type: "protagonist",
|
||||
description: "The emotional and romantic younger Dashwood sister, who learns to balance sensibility with sense."
|
||||
},
|
||||
{
|
||||
id: "edward-ferrars",
|
||||
name: "Edward Ferrars",
|
||||
novel: "Sense and Sensibility",
|
||||
class: "upper",
|
||||
type: "supporting",
|
||||
description: "A gentle, reserved man who forms an attachment to Elinor despite his secret engagement."
|
||||
},
|
||||
{
|
||||
id: "willoughby",
|
||||
name: "John Willoughby",
|
||||
novel: "Sense and Sensibility",
|
||||
class: "upper",
|
||||
type: "antagonist",
|
||||
description: "A dashing young man who captures Marianne's heart but proves morally bankrupt."
|
||||
},
|
||||
{
|
||||
id: "colonel-brandon",
|
||||
name: "Colonel Brandon",
|
||||
novel: "Sense and Sensibility",
|
||||
class: "upper",
|
||||
type: "supporting",
|
||||
description: "A mature and honorable man who quietly loves Marianne and proves his worth through actions."
|
||||
},
|
||||
// Northanger Abbey
|
||||
{
|
||||
id: "catherine-morland",
|
||||
name: "Catherine Morland",
|
||||
novel: "Northanger Abbey",
|
||||
class: "middle",
|
||||
type: "protagonist",
|
||||
description: "A naive but charming young woman whose love of Gothic novels colors her view of reality."
|
||||
},
|
||||
{
|
||||
id: "henry-tilney",
|
||||
name: "Henry Tilney",
|
||||
novel: "Northanger Abbey",
|
||||
class: "upper",
|
||||
type: "protagonist",
|
||||
description: "An intelligent and witty clergyman who guides Catherine's maturation while falling in love with her."
|
||||
},
|
||||
{
|
||||
id: "isabella-thorpe",
|
||||
name: "Isabella Thorpe",
|
||||
novel: "Northanger Abbey",
|
||||
class: "middle",
|
||||
type: "antagonist",
|
||||
description: "A manipulative friend who pretends affection for Catherine's brother while pursuing others."
|
||||
},
|
||||
{
|
||||
id: "john-thorpe",
|
||||
name: "John Thorpe",
|
||||
novel: "Northanger Abbey",
|
||||
class: "middle",
|
||||
type: "antagonist",
|
||||
description: "Isabella's boastful and dishonest brother who pursues Catherine for her perceived wealth."
|
||||
},
|
||||
// Mansfield Park
|
||||
{
|
||||
id: "fanny-price",
|
||||
name: "Fanny Price",
|
||||
novel: "Mansfield Park",
|
||||
class: "lower",
|
||||
type: "protagonist",
|
||||
description: "A modest and moral young woman taken in by her wealthy relatives, steadfast in her principles."
|
||||
},
|
||||
{
|
||||
id: "edmund-bertram",
|
||||
name: "Edmund Bertram",
|
||||
novel: "Mansfield Park",
|
||||
class: "upper",
|
||||
type: "protagonist",
|
||||
description: "Fanny's cousin and friend, a would-be clergyman who is temporarily led astray by Mary Crawford."
|
||||
},
|
||||
{
|
||||
id: "mary-crawford",
|
||||
name: "Mary Crawford",
|
||||
novel: "Mansfield Park",
|
||||
class: "upper",
|
||||
type: "antagonist",
|
||||
description: "A charming but worldly woman who attracts Edmund while representing modern urban values."
|
||||
},
|
||||
{
|
||||
id: "henry-crawford",
|
||||
name: "Henry Crawford",
|
||||
novel: "Mansfield Park",
|
||||
class: "upper",
|
||||
type: "antagonist",
|
||||
description: "Mary's brother, a charismatic man who pursues Fanny after toying with her cousins' affections."
|
||||
},
|
||||
// Longbourn
|
||||
{
|
||||
id: "sarah",
|
||||
name: "Sarah",
|
||||
novel: "Longbourn",
|
||||
class: "lower",
|
||||
type: "protagonist",
|
||||
description: "A hardworking housemaid at Longbourn with dreams beyond her station."
|
||||
},
|
||||
{
|
||||
id: "james-smith",
|
||||
name: "James Smith",
|
||||
novel: "Longbourn",
|
||||
class: "lower",
|
||||
type: "protagonist",
|
||||
description: "The mysterious new footman with a hidden past, who catches Sarah's attention."
|
||||
},
|
||||
{
|
||||
id: "mrs-hill",
|
||||
name: "Mrs. Hill",
|
||||
novel: "Longbourn",
|
||||
class: "lower",
|
||||
type: "supporting",
|
||||
description: "The housekeeper of Longbourn, who holds many secrets about the household."
|
||||
},
|
||||
{
|
||||
id: "polly",
|
||||
name: "Polly",
|
||||
novel: "Longbourn",
|
||||
class: "lower",
|
||||
type: "supporting",
|
||||
description: "The young housemaid learning her duties alongside Sarah."
|
||||
}
|
||||
],
|
||||
relationships: [
|
||||
// Pride and Prejudice Relationships
|
||||
{
|
||||
source: "elizabeth-bennet",
|
||||
target: "jane-bennet",
|
||||
type: "family",
|
||||
description: "Sisters and closest confidantes",
|
||||
development: [
|
||||
"Share sisterly bond throughout the novel",
|
||||
"Support each other through romantic trials",
|
||||
"Maintain trust and confidence in each other"
|
||||
]
|
||||
},
|
||||
{
|
||||
source: "elizabeth-bennet",
|
||||
target: "darcy",
|
||||
type: "romance",
|
||||
description: "Central romance overcoming pride and prejudice",
|
||||
development: [
|
||||
"Initial mutual dislike",
|
||||
"Growing understanding through letters and visits",
|
||||
"Eventual recognition of true character",
|
||||
"Marriage"
|
||||
]
|
||||
},
|
||||
{
|
||||
source: "jane-bennet",
|
||||
target: "bingley",
|
||||
type: "romance",
|
||||
description: "Love at first sight couple",
|
||||
development: [
|
||||
"Immediate mutual attraction",
|
||||
"Separation due to social interference",
|
||||
"Reunion and marriage"
|
||||
]
|
||||
},
|
||||
{
|
||||
source: "lydia-bennet",
|
||||
target: "wickham",
|
||||
type: "romance",
|
||||
description: "Scandalous elopement",
|
||||
development: [
|
||||
"Flirtation and infatuation",
|
||||
"Reckless elopement",
|
||||
"Forced marriage through Darcy's intervention"
|
||||
]
|
||||
},
|
||||
// Sense and Sensibility Relationships
|
||||
{
|
||||
source: "elinor-dashwood",
|
||||
target: "marianne-dashwood",
|
||||
type: "family",
|
||||
description: "Sisters representing sense and sensibility",
|
||||
development: [
|
||||
"Contrasting approaches to emotion",
|
||||
"Support through family difficulties",
|
||||
"Mutual growth and understanding"
|
||||
]
|
||||
},
|
||||
{
|
||||
source: "elinor-dashwood",
|
||||
target: "edward-ferrars",
|
||||
type: "romance",
|
||||
description: "Reserved romance complicated by duty",
|
||||
development: [
|
||||
"Quiet mutual attraction",
|
||||
"Separation due to prior engagement",
|
||||
"Eventual reunion and marriage"
|
||||
]
|
||||
},
|
||||
{
|
||||
source: "marianne-dashwood",
|
||||
target: "willoughby",
|
||||
type: "romance",
|
||||
description: "Passionate but doomed romance",
|
||||
development: [
|
||||
"Intense initial attraction",
|
||||
"Shared romantic sensibilities",
|
||||
"Heartbreaking separation",
|
||||
"Eventual disillusionment"
|
||||
]
|
||||
},
|
||||
{
|
||||
source: "marianne-dashwood",
|
||||
target: "colonel-brandon",
|
||||
type: "romance",
|
||||
description: "Mature love developing from friendship",
|
||||
development: [
|
||||
"Initial indifference from Marianne",
|
||||
"Growing appreciation through trials",
|
||||
"Marriage based on deeper understanding"
|
||||
]
|
||||
},
|
||||
// Northanger Abbey Relationships
|
||||
{
|
||||
source: "catherine-morland",
|
||||
target: "henry-tilney",
|
||||
type: "romance",
|
||||
description: "Educational romance",
|
||||
development: [
|
||||
"Initial friendship and guidance",
|
||||
"Growing romantic attachment",
|
||||
"Overcoming misunderstandings",
|
||||
"Marriage despite family opposition"
|
||||
]
|
||||
},
|
||||
{
|
||||
source: "catherine-morland",
|
||||
target: "isabella-thorpe",
|
||||
type: "friendship",
|
||||
description: "False friendship",
|
||||
development: [
|
||||
"Initial close friendship",
|
||||
"Growing awareness of Isabella's true nature",
|
||||
"Betrayal and disillusionment"
|
||||
]
|
||||
},
|
||||
// Mansfield Park Relationships
|
||||
{
|
||||
source: "fanny-price",
|
||||
target: "edmund-bertram",
|
||||
type: "romance",
|
||||
description: "Constant love tested by rival",
|
||||
development: [
|
||||
"Childhood friendship",
|
||||
"Fanny's secret love",
|
||||
"Edmund's infatuation with Mary",
|
||||
"Final recognition of true feelings"
|
||||
]
|
||||
},
|
||||
{
|
||||
source: "edmund-bertram",
|
||||
target: "mary-crawford",
|
||||
type: "romance",
|
||||
description: "Tempting but unsuitable attachment",
|
||||
development: [
|
||||
"Initial attraction",
|
||||
"Growing awareness of moral differences",
|
||||
"Final disillusionment"
|
||||
]
|
||||
},
|
||||
{
|
||||
source: "fanny-price",
|
||||
target: "henry-crawford",
|
||||
type: "rivalry",
|
||||
description: "Unwanted pursuit",
|
||||
development: [
|
||||
"Henry's initial indifference",
|
||||
"Determined pursuit of Fanny",
|
||||
"Fanny's steadfast refusal",
|
||||
"Henry's moral failure"
|
||||
]
|
||||
},
|
||||
// Longbourn Relationships
|
||||
{
|
||||
source: "sarah",
|
||||
target: "james-smith",
|
||||
type: "romance",
|
||||
description: "A romance that crosses social boundaries",
|
||||
development: [
|
||||
"Initial curiosity about the mysterious new footman",
|
||||
"Growing understanding of each other's struggles",
|
||||
"Shared moments during daily tasks",
|
||||
"Overcoming past secrets and social barriers"
|
||||
]
|
||||
},
|
||||
{
|
||||
source: "sarah",
|
||||
target: "mrs-hill",
|
||||
type: "family",
|
||||
description: "Mentor-like relationship",
|
||||
development: [
|
||||
"Mrs. Hill's guidance in household duties",
|
||||
"Growing trust and confidence",
|
||||
"Sharing of household secrets",
|
||||
"Maternal care and protection"
|
||||
]
|
||||
},
|
||||
{
|
||||
source: "sarah",
|
||||
target: "polly",
|
||||
type: "friendship",
|
||||
description: "Fellow servants and friends",
|
||||
development: [
|
||||
"Teaching Polly household duties",
|
||||
"Supporting each other through daily challenges",
|
||||
"Sharing dreams and aspirations"
|
||||
]
|
||||
},
|
||||
// Add connections between Longbourn and Pride and Prejudice characters
|
||||
{
|
||||
source: "sarah",
|
||||
target: "elizabeth-bennet",
|
||||
type: "friendship",
|
||||
description: "Servant and mistress with mutual respect",
|
||||
development: [
|
||||
"Sarah's observations of Elizabeth's character",
|
||||
"Growing understanding across social boundaries",
|
||||
"Shared experiences from different perspectives"
|
||||
]
|
||||
},
|
||||
{
|
||||
source: "mrs-hill",
|
||||
target: "elizabeth-bennet",
|
||||
type: "family",
|
||||
description: "Housekeeper's care for the Bennet family",
|
||||
development: [
|
||||
"Years of service to the Bennet family",
|
||||
"Protection of family secrets",
|
||||
"Maternal guidance from below stairs"
|
||||
]
|
||||
},
|
||||
{
|
||||
source: "james-smith",
|
||||
target: "elizabeth-bennet",
|
||||
type: "friendship",
|
||||
description: "Distant but respectful relationship",
|
||||
development: [
|
||||
"Initial wariness",
|
||||
"Growing trust through service",
|
||||
"Understanding of social positions"
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
163
src/data/comparative-analysis.ts
Normal file
163
src/data/comparative-analysis.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
interface ThemeComparison {
|
||||
theme: string;
|
||||
description: string;
|
||||
appearances: {
|
||||
novel: string;
|
||||
manifestation: string;
|
||||
examples: {
|
||||
quote: string;
|
||||
context: string;
|
||||
analysis: string;
|
||||
}[];
|
||||
significance: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface CharacterType {
|
||||
archetype: string;
|
||||
description: string;
|
||||
examples: {
|
||||
novel: string;
|
||||
character: string;
|
||||
analysis: string;
|
||||
evolution: string;
|
||||
keyQuotes: {
|
||||
quote: string;
|
||||
context: string;
|
||||
}[];
|
||||
}[];
|
||||
}
|
||||
|
||||
interface WritingStyleElement {
|
||||
technique: string;
|
||||
description: string;
|
||||
evolution: {
|
||||
novel: string;
|
||||
usage: string;
|
||||
examples: string[];
|
||||
significance: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const themeComparisons: ThemeComparison[] = [
|
||||
{
|
||||
theme: "Marriage and Economic Reality",
|
||||
description: "The intersection of marriage, economic necessity, and social mobility across Austen's works",
|
||||
appearances: [
|
||||
{
|
||||
novel: "Pride and Prejudice",
|
||||
manifestation: "Marriage market and social climbing",
|
||||
examples: [
|
||||
{
|
||||
quote: "It is a truth universally acknowledged...",
|
||||
context: "Opening line",
|
||||
analysis: "Establishes the economic premise of marriage in Regency society"
|
||||
}
|
||||
],
|
||||
significance: "Direct examination of marriage as economic transaction"
|
||||
},
|
||||
{
|
||||
novel: "Sense and Sensibility",
|
||||
manifestation: "Financial vulnerability of women",
|
||||
examples: [
|
||||
{
|
||||
quote: "What have wealth or grandeur to do with happiness?",
|
||||
context: "Marianne's naive view",
|
||||
analysis: "Contrasts romantic ideals with economic reality"
|
||||
}
|
||||
],
|
||||
significance: "Explores the harsh realities of women's financial dependence"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
theme: "Social Class and Mobility",
|
||||
description: "The examination of class boundaries and social movement in Regency society",
|
||||
appearances: [
|
||||
{
|
||||
novel: "Mansfield Park",
|
||||
manifestation: "Class consciousness and moral worth",
|
||||
examples: [
|
||||
{
|
||||
quote: "We have all a better guide in ourselves...",
|
||||
context: "Fanny's moral stance",
|
||||
analysis: "Links class position with moral character"
|
||||
}
|
||||
],
|
||||
significance: "Explores the relationship between social position and moral integrity"
|
||||
},
|
||||
{
|
||||
novel: "Northanger Abbey",
|
||||
manifestation: "Social climbing and authenticity",
|
||||
examples: [
|
||||
{
|
||||
quote: "No one who had ever seen Catherine Morland in her infancy...",
|
||||
context: "Opening description",
|
||||
analysis: "Subverts expectations of class and heroism"
|
||||
}
|
||||
],
|
||||
significance: "Questions the relationship between social status and personal worth"
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export const characterTypes: CharacterType[] = [
|
||||
{
|
||||
archetype: "The Witty Heroine",
|
||||
description: "Intelligent, spirited female protagonists who challenge social norms",
|
||||
examples: [
|
||||
{
|
||||
novel: "Pride and Prejudice",
|
||||
character: "Elizabeth Bennet",
|
||||
analysis: "Combines wit with social observation",
|
||||
evolution: "Learns to balance judgment with understanding",
|
||||
keyQuotes: [
|
||||
{
|
||||
quote: "I could easily forgive his pride, if he had not mortified mine",
|
||||
context: "Early judgment of Darcy"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
novel: "Emma",
|
||||
character: "Emma Woodhouse",
|
||||
analysis: "Uses wit but must learn its proper application",
|
||||
evolution: "Develops from clever manipulation to genuine understanding",
|
||||
keyQuotes: [
|
||||
{
|
||||
quote: "I seem to have been doomed to blindness",
|
||||
context: "Moment of self-realization"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export const writingStyleEvolution: WritingStyleElement[] = [
|
||||
{
|
||||
technique: "Free Indirect Discourse",
|
||||
description: "Narrative technique blending character and narrator perspectives",
|
||||
evolution: [
|
||||
{
|
||||
novel: "Northanger Abbey",
|
||||
usage: "Early experimentation with narrative voice",
|
||||
examples: [
|
||||
"Commentary on Gothic conventions",
|
||||
"Catherine's naive perspectives"
|
||||
],
|
||||
significance: "Develops ironic distance while maintaining character sympathy"
|
||||
},
|
||||
{
|
||||
novel: "Emma",
|
||||
usage: "Sophisticated deployment for character insight",
|
||||
examples: [
|
||||
"Emma's self-deceptions",
|
||||
"Social observations"
|
||||
],
|
||||
significance: "Achieves complex character psychology and social commentary"
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import { useState } from 'react';
|
||||
import { novelAnalyses } from '../data/literary-analysis';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import type { NovelAnalysis, ThematicElement, CharacterAnalysis, SocialCommentary, LiteraryDevice } from '../data/literary-analysis';
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
|
|
@ -21,9 +23,13 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../components/ui/select";
|
||||
import { Button } from "../components/ui/button";
|
||||
|
||||
type NovelKey = keyof typeof novelAnalyses;
|
||||
|
||||
const Analysis = () => {
|
||||
const [selectedNovel, setSelectedNovel] = useState('prideAndPrejudice');
|
||||
const [selectedNovel, setSelectedNovel] = useState<NovelKey>('prideAndPrejudice');
|
||||
const navigate = useNavigate();
|
||||
const analysis = novelAnalyses[selectedNovel];
|
||||
|
||||
if (!analysis) {
|
||||
|
|
@ -40,10 +46,10 @@ const Analysis = () => {
|
|||
<h1 className="font-cormorant text-4xl text-sage-900">
|
||||
Literary Analysis
|
||||
</h1>
|
||||
<div className="max-w-xs mx-auto">
|
||||
<div className="max-w-xs mx-auto flex items-center gap-2">
|
||||
<Select
|
||||
value={selectedNovel}
|
||||
onValueChange={setSelectedNovel}
|
||||
onValueChange={(value: NovelKey) => setSelectedNovel(value)}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select a novel" />
|
||||
|
|
@ -55,6 +61,13 @@ const Analysis = () => {
|
|||
<SelectItem value="mansfieldPark">Mansfield Park (1814)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="whitespace-nowrap"
|
||||
onClick={() => navigate('/comparative')}
|
||||
>
|
||||
Compare Works
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<h2 className="font-cormorant text-2xl text-sage-900">{analysis.title}</h2>
|
||||
|
|
@ -73,7 +86,7 @@ const Analysis = () => {
|
|||
</TabsList>
|
||||
|
||||
<TabsContent value="themes" className="space-y-4">
|
||||
{analysis.mainThemes.map((theme, index) => (
|
||||
{analysis.mainThemes.map((theme: ThematicElement, index: number) => (
|
||||
<Card key={index}>
|
||||
<CardHeader>
|
||||
<CardTitle>{theme.theme}</CardTitle>
|
||||
|
|
@ -82,7 +95,7 @@ const Analysis = () => {
|
|||
<CardContent>
|
||||
<ScrollArea className="h-[300px] rounded-md border p-4">
|
||||
<div className="space-y-4">
|
||||
{theme.examples.map((example, i) => (
|
||||
{theme.examples.map((example, i: number) => (
|
||||
<div key={i} className="space-y-2">
|
||||
<blockquote className="border-l-2 border-sage-300 pl-4 italic">
|
||||
"{example.quote}"
|
||||
|
|
@ -105,7 +118,7 @@ const Analysis = () => {
|
|||
</TabsContent>
|
||||
|
||||
<TabsContent value="characters" className="space-y-4">
|
||||
{analysis.characterAnalysis.map((character, index) => (
|
||||
{analysis.characterAnalysis.map((character: CharacterAnalysis, index: number) => (
|
||||
<Card key={index}>
|
||||
<CardHeader>
|
||||
<CardTitle>{character.character}</CardTitle>
|
||||
|
|
@ -124,7 +137,7 @@ const Analysis = () => {
|
|||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-sage-900">Key Quotes</h4>
|
||||
{character.keyQuotes.map((quote, i) => (
|
||||
{character.keyQuotes.map((quote, i: number) => (
|
||||
<div key={i} className="mt-2 space-y-2">
|
||||
<blockquote className="border-l-2 border-sage-300 pl-4 italic">
|
||||
"{quote.quote}"
|
||||
|
|
@ -144,7 +157,7 @@ const Analysis = () => {
|
|||
</TabsContent>
|
||||
|
||||
<TabsContent value="social" className="space-y-4">
|
||||
{analysis.socialCommentary.map((topic, index) => (
|
||||
{analysis.socialCommentary.map((topic: SocialCommentary, index: number) => (
|
||||
<Card key={index}>
|
||||
<CardHeader>
|
||||
<CardTitle>{topic.topic}</CardTitle>
|
||||
|
|
@ -163,7 +176,7 @@ const Analysis = () => {
|
|||
<div>
|
||||
<h4 className="font-semibold text-sage-900">Examples</h4>
|
||||
<ul className="list-disc list-inside text-sage-700">
|
||||
{topic.examples.map((example, i) => (
|
||||
{topic.examples.map((example: string, i: number) => (
|
||||
<li key={i}>{example}</li>
|
||||
))}
|
||||
</ul>
|
||||
|
|
@ -176,7 +189,7 @@ const Analysis = () => {
|
|||
</TabsContent>
|
||||
|
||||
<TabsContent value="literary" className="space-y-4">
|
||||
{analysis.literaryDevices.map((device, index) => (
|
||||
{analysis.literaryDevices.map((device: LiteraryDevice, index: number) => (
|
||||
<Card key={index}>
|
||||
<CardHeader>
|
||||
<CardTitle>{device.device}</CardTitle>
|
||||
|
|
@ -191,7 +204,7 @@ const Analysis = () => {
|
|||
<div>
|
||||
<h4 className="font-semibold text-sage-900">Examples</h4>
|
||||
<ul className="list-disc list-inside text-sage-700">
|
||||
{device.examples.map((example, i) => (
|
||||
{device.examples.map((example: string, i: number) => (
|
||||
<li key={i}>{example}</li>
|
||||
))}
|
||||
</ul>
|
||||
|
|
|
|||
174
src/pages/ComparativeAnalysis.tsx
Normal file
174
src/pages/ComparativeAnalysis.tsx
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
import { themeComparisons, characterTypes, writingStyleEvolution } from '../data/comparative-analysis';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "../components/ui/tabs";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "../components/ui/card";
|
||||
import { ScrollArea } from "../components/ui/scroll-area";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
|
||||
const ComparativeAnalysis = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="absolute top-24 left-8">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-sage-700 hover:text-sage-900 hover:bg-sage-100"
|
||||
onClick={() => navigate('/analysis')}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back to Analysis
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto py-8 space-y-8">
|
||||
<div className="text-center space-y-4">
|
||||
<h1 className="font-cormorant text-4xl text-sage-900">
|
||||
Comparative Analysis
|
||||
</h1>
|
||||
<p className="text-sage-600 max-w-2xl mx-auto">
|
||||
Explore themes, character types, and writing techniques across Jane Austen's works
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="themes" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="themes">Themes</TabsTrigger>
|
||||
<TabsTrigger value="characters">Character Types</TabsTrigger>
|
||||
<TabsTrigger value="style">Writing Style</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="themes" className="space-y-6">
|
||||
{themeComparisons.map((theme, index) => (
|
||||
<Card key={index}>
|
||||
<CardHeader>
|
||||
<CardTitle>{theme.theme}</CardTitle>
|
||||
<CardDescription>{theme.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-[400px] rounded-md border p-4">
|
||||
<div className="space-y-6">
|
||||
{theme.appearances.map((appearance, i) => (
|
||||
<div key={i} className="border-l-2 border-sage-300 pl-4">
|
||||
<h3 className="font-cormorant text-xl text-sage-900 mb-2">
|
||||
{appearance.novel}
|
||||
</h3>
|
||||
<p className="text-sage-700 mb-3">
|
||||
{appearance.manifestation}
|
||||
</p>
|
||||
{appearance.examples.map((example, j) => (
|
||||
<div key={j} className="mb-4">
|
||||
<blockquote className="italic text-sage-800 mb-2">
|
||||
"{example.quote}"
|
||||
<footer className="text-sm text-sage-600">
|
||||
- {example.context}
|
||||
</footer>
|
||||
</blockquote>
|
||||
<p className="text-sage-700">{example.analysis}</p>
|
||||
</div>
|
||||
))}
|
||||
<p className="text-sage-800 mt-2">
|
||||
<span className="font-semibold">Significance: </span>
|
||||
{appearance.significance}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="characters" className="space-y-6">
|
||||
{characterTypes.map((type, index) => (
|
||||
<Card key={index}>
|
||||
<CardHeader>
|
||||
<CardTitle>{type.archetype}</CardTitle>
|
||||
<CardDescription>{type.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-[400px] rounded-md border p-4">
|
||||
<div className="space-y-6">
|
||||
{type.examples.map((example, i) => (
|
||||
<div key={i} className="border-l-2 border-sage-300 pl-4">
|
||||
<h3 className="font-cormorant text-xl text-sage-900 mb-2">
|
||||
{example.character} in {example.novel}
|
||||
</h3>
|
||||
<p className="text-sage-700 mb-3">{example.analysis}</p>
|
||||
<p className="text-sage-700 mb-3">
|
||||
<span className="font-semibold">Character Evolution: </span>
|
||||
{example.evolution}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{example.keyQuotes.map((quote, j) => (
|
||||
<blockquote key={j} className="italic text-sage-800">
|
||||
"{quote.quote}"
|
||||
<footer className="text-sm text-sage-600">
|
||||
- {quote.context}
|
||||
</footer>
|
||||
</blockquote>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="style" className="space-y-6">
|
||||
{writingStyleEvolution.map((technique, index) => (
|
||||
<Card key={index}>
|
||||
<CardHeader>
|
||||
<CardTitle>{technique.technique}</CardTitle>
|
||||
<CardDescription>{technique.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-[400px] rounded-md border p-4">
|
||||
<div className="space-y-6">
|
||||
{technique.evolution.map((stage, i) => (
|
||||
<div key={i} className="border-l-2 border-sage-300 pl-4">
|
||||
<h3 className="font-cormorant text-xl text-sage-900 mb-2">
|
||||
{stage.novel}
|
||||
</h3>
|
||||
<p className="text-sage-700 mb-3">{stage.usage}</p>
|
||||
<ul className="list-disc list-inside text-sage-700 mb-3">
|
||||
{stage.examples.map((example, j) => (
|
||||
<li key={j}>{example}</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="text-sage-800">
|
||||
<span className="font-semibold">Significance: </span>
|
||||
{stage.significance}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComparativeAnalysis;
|
||||
461
src/pages/NetworkVisualization.tsx
Normal file
461
src/pages/NetworkVisualization.tsx
Normal file
|
|
@ -0,0 +1,461 @@
|
|||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import { characterNetwork } from '../data/character-network';
|
||||
import { CharacterNode, Relationship, BookNode } from '../types/character-network';
|
||||
import { Box, Typography, Paper, Grid, Card, CardContent, IconButton, Tooltip, Popper } from '@mui/material';
|
||||
import { ForceGraph2D } from 'react-force-graph';
|
||||
import { ArrowBack, Help } from '@mui/icons-material';
|
||||
import * as d3 from 'd3';
|
||||
|
||||
interface NetworkNode {
|
||||
id: string;
|
||||
name: string;
|
||||
x?: number;
|
||||
y?: number;
|
||||
val: number;
|
||||
color: string;
|
||||
type: string;
|
||||
description?: string;
|
||||
novel?: string;
|
||||
class?: string;
|
||||
year?: number;
|
||||
fx?: number;
|
||||
fy?: number;
|
||||
}
|
||||
|
||||
interface NetworkLink {
|
||||
source: string;
|
||||
target: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface GraphData {
|
||||
nodes: NetworkNode[];
|
||||
links: NetworkLink[];
|
||||
}
|
||||
|
||||
interface TooltipState {
|
||||
open: boolean;
|
||||
content: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export default function NetworkVisualization() {
|
||||
const [selectedNode, setSelectedNode] = useState<CharacterNode | BookNode | null>(null);
|
||||
const [selectedRelationships, setSelectedRelationships] = useState<Relationship[]>([]);
|
||||
const [selectedBook, setSelectedBook] = useState<string | null>(null);
|
||||
const [tooltip, setTooltip] = useState<TooltipState>({ open: false, content: '', x: 0, y: 0 });
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleNodeHover = useCallback((node: NetworkNode | null) => {
|
||||
if (node) {
|
||||
const rect = containerRef.current?.getBoundingClientRect();
|
||||
if (rect) {
|
||||
setTooltip({
|
||||
open: true,
|
||||
content: `${node.name}\n${node.description || ''}`,
|
||||
x: rect.left + rect.width / 2, // Center of container
|
||||
y: rect.top + 100 // Fixed position from top
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setTooltip(prev => ({ ...prev, open: false }));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleNodeClick = useCallback((node: NetworkNode) => {
|
||||
if (!selectedBook && node.type === 'book') {
|
||||
setSelectedBook(node.id);
|
||||
setSelectedNode(characterNetwork.books.find(b => b.id === node.id) || null);
|
||||
} else if (selectedBook) {
|
||||
const characterNode = characterNetwork.nodes.find(n => n.id === node.id);
|
||||
if (characterNode) {
|
||||
setSelectedNode(characterNode);
|
||||
const relations = characterNetwork.relationships.filter(
|
||||
r => r.source === node.id || r.target === node.id
|
||||
);
|
||||
setSelectedRelationships(relations);
|
||||
}
|
||||
}
|
||||
setTooltip(prev => ({ ...prev, open: false }));
|
||||
}, [selectedBook]);
|
||||
|
||||
const handleBackClick = () => {
|
||||
setSelectedBook(null);
|
||||
setSelectedNode(null);
|
||||
setSelectedRelationships([]);
|
||||
};
|
||||
|
||||
const centerGraph = useCallback((graphData: GraphData) => {
|
||||
const containerWidth = 800;
|
||||
const containerHeight = 700;
|
||||
const padding = 50;
|
||||
|
||||
const xExtent = d3.extent(graphData.nodes, d => d.x || 0) as [number, number];
|
||||
const yExtent = d3.extent(graphData.nodes, d => d.y || 0) as [number, number];
|
||||
const xRange = xExtent[1] - xExtent[0] || 1;
|
||||
const yRange = yExtent[1] - yExtent[0] || 1;
|
||||
|
||||
const scale = Math.min(
|
||||
(containerWidth - 2 * padding) / xRange,
|
||||
(containerHeight - 2 * padding) / yRange
|
||||
);
|
||||
|
||||
graphData.nodes.forEach(node => {
|
||||
node.x = ((node.x || 0) - (xExtent[0] + xRange / 2)) * scale + containerWidth / 2;
|
||||
node.y = ((node.y || 0) - (yExtent[0] + yRange / 2)) * scale + containerHeight / 2;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const getGraphData = useCallback((): GraphData => {
|
||||
if (!selectedBook) {
|
||||
// Book-level view with fixed positions in a circle
|
||||
const data: GraphData = {
|
||||
nodes: characterNetwork.books.map((book, idx) => {
|
||||
const angle = (idx / characterNetwork.books.length) * 2 * Math.PI;
|
||||
const radius = 150; // Reduced radius for better visibility
|
||||
return {
|
||||
...book,
|
||||
val: 20,
|
||||
// Set initial positions in a circle around center
|
||||
x: Math.cos(angle) * radius + 400,
|
||||
y: Math.sin(angle) * radius + 350,
|
||||
// Don't fix positions to allow force layout to adjust
|
||||
fx: undefined,
|
||||
fy: undefined
|
||||
};
|
||||
}) as NetworkNode[],
|
||||
links: [] as NetworkLink[]
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
const bookName = characterNetwork.books.find(b => b.id === selectedBook)?.name;
|
||||
const nodes = characterNetwork.nodes
|
||||
.filter(node => node.novel === bookName)
|
||||
.map((node, idx) => {
|
||||
const angle = (idx / characterNetwork.nodes.length) * 2 * Math.PI;
|
||||
const radius = 100; // Initial radius for character layout
|
||||
return {
|
||||
...node,
|
||||
val: 10,
|
||||
// Set initial positions in a circle
|
||||
x: Math.cos(angle) * radius + 400,
|
||||
y: Math.sin(angle) * radius + 350,
|
||||
// Clear any fixed positions
|
||||
fx: undefined,
|
||||
fy: undefined,
|
||||
color: node.type === 'protagonist' ? '#e91e63' :
|
||||
node.type === 'antagonist' ? '#f44336' : '#2196f3'
|
||||
};
|
||||
}) as NetworkNode[];
|
||||
|
||||
const data: GraphData = {
|
||||
nodes,
|
||||
links: characterNetwork.relationships
|
||||
.filter(rel =>
|
||||
nodes.some((n: NetworkNode) => n.id === rel.source) &&
|
||||
nodes.some((n: NetworkNode) => n.id === rel.target)
|
||||
)
|
||||
.map(rel => ({
|
||||
source: rel.source,
|
||||
target: rel.target,
|
||||
color: rel.type === 'family' ? '#4caf50' :
|
||||
rel.type === 'romance' ? '#e91e63' :
|
||||
rel.type === 'friendship' ? '#2196f3' : '#ff9800'
|
||||
})) as NetworkLink[]
|
||||
};
|
||||
|
||||
return data;
|
||||
}, [selectedBook]);
|
||||
|
||||
const renderNodeCanvas = useCallback((node: NetworkNode, ctx: CanvasRenderingContext2D, scale: number) => {
|
||||
const size = node.val || 5;
|
||||
const fontSize = Math.max(12 / scale, 2);
|
||||
|
||||
// Draw node
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x || 0, node.y || 0, size, 0, 2 * Math.PI);
|
||||
ctx.fillStyle = node.color;
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
|
||||
// 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 Inter, Arial`;
|
||||
const textWidth = ctx.measureText(node.name).width;
|
||||
|
||||
// Semi-transparent background for better readability
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.85)';
|
||||
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 = '#000';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(
|
||||
node.name,
|
||||
node.x || 0,
|
||||
(node.y || 0) + labelDistance
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 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();
|
||||
};
|
||||
|
||||
const getLegendTooltip = () => (
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>Color Legend:</Typography>
|
||||
<Typography variant="body2">
|
||||
Books:<br />
|
||||
• Pride and Prejudice - Pink<br />
|
||||
• Sense and Sensibility - Blue<br />
|
||||
• Northanger Abbey - Green<br />
|
||||
• Mansfield Park - Purple<br />
|
||||
Longbourn - Brown
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||
Characters:<br />
|
||||
• Protagonists - Pink<br />
|
||||
• Antagonists - Red<br />
|
||||
• Supporting - Blue
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||
Relationships:<br />
|
||||
• Family - Green<br />
|
||||
• Romance - Pink<br />
|
||||
• Friendship - Blue<br />
|
||||
• Rivalry - Orange
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
mb: 2,
|
||||
px: 3,
|
||||
pt: 2
|
||||
}}>
|
||||
<Typography variant="h4">
|
||||
Character Network & Timeline
|
||||
</Typography>
|
||||
<Tooltip title={getLegendTooltip()} arrow placement="right">
|
||||
<IconButton size="small" sx={{ ml: 2 }}>
|
||||
<Help />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={0} sx={{ flex: 1, minHeight: 0 }}>
|
||||
<Grid item xs={12} md={8}>
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
height: '100%',
|
||||
minHeight: 700,
|
||||
position: 'relative',
|
||||
borderRadius: 0,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
ref={containerRef}
|
||||
>
|
||||
{selectedBook && (
|
||||
<Tooltip title="Return to book overview" arrow>
|
||||
<IconButton
|
||||
onClick={handleBackClick}
|
||||
sx={{ position: 'absolute', top: 8, left: 8, zIndex: 1 }}
|
||||
>
|
||||
<ArrowBack />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Box sx={{
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 8,
|
||||
zIndex: 1,
|
||||
bgcolor: 'rgba(255, 255, 255, 0.9)',
|
||||
p: 1,
|
||||
borderRadius: 1
|
||||
}}>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{!selectedBook ? 'Click a book to explore its characters' : 'Click characters to view relationships'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<ForceGraph2D
|
||||
graphData={getGraphData()}
|
||||
onNodeHover={handleNodeHover}
|
||||
onNodeClick={handleNodeClick}
|
||||
nodeCanvasObject={(node: NetworkNode, ctx: CanvasRenderingContext2D, scale: number) =>
|
||||
renderNodeCanvas(node, ctx, scale)}
|
||||
linkColor={(link: NetworkLink) => link.color}
|
||||
linkWidth={2}
|
||||
nodeRelSize={6}
|
||||
width={800}
|
||||
height={700}
|
||||
cooldownTicks={50}
|
||||
cooldownTime={3000}
|
||||
linkDirectionalParticles={2}
|
||||
linkDirectionalParticleSpeed={0.003}
|
||||
d3AlphaDecay={0.1}
|
||||
d3VelocityDecay={0.4}
|
||||
minZoom={0.5}
|
||||
maxZoom={4}
|
||||
dagMode={selectedBook ? undefined : 'radialin'}
|
||||
dagLevelDistance={100}
|
||||
enablePanInteraction={true}
|
||||
enableZoomInteraction={true}
|
||||
onEngineStop={() => {
|
||||
if (!selectedBook) {
|
||||
centerGraph(getGraphData());
|
||||
}
|
||||
}}
|
||||
onNodeDragEnd={(node: NetworkNode) => {
|
||||
if (node.x && node.y) {
|
||||
node.fx = node.x;
|
||||
node.fy = node.y;
|
||||
}
|
||||
}}
|
||||
warmupTicks={100}
|
||||
onZoom={() => centerGraph(getGraphData())}
|
||||
centerAt={[400, 350]}
|
||||
zoom={2}
|
||||
enableNodeDrag={true}
|
||||
enableZoomPanInteraction={true}
|
||||
/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<Box sx={{
|
||||
height: '100%',
|
||||
minHeight: 700,
|
||||
overflow: 'auto',
|
||||
borderLeft: '1px solid rgba(0, 0, 0, 0.12)'
|
||||
}}>
|
||||
{selectedNode && (
|
||||
<Card sx={{ boxShadow: 'none', borderRadius: 0 }}>
|
||||
<CardContent>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
{selectedNode.name}
|
||||
</Typography>
|
||||
<Typography color="textSecondary" gutterBottom>
|
||||
{selectedNode.type === 'book' ? `Published: ${(selectedNode as BookNode).year}` : selectedNode.novel}
|
||||
</Typography>
|
||||
<Typography variant="body1" paragraph>
|
||||
{selectedNode.description}
|
||||
</Typography>
|
||||
|
||||
{selectedNode.type !== 'book' && (
|
||||
<>
|
||||
<Typography variant="body2" sx={{ mt: 2 }} color="textSecondary">
|
||||
Social Class: {(selectedNode as CharacterNode).class}
|
||||
<br />
|
||||
Character Role: {selectedNode.type}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h6" sx={{ mt: 3, mb: 2 }}>
|
||||
Relationships
|
||||
</Typography>
|
||||
{selectedRelationships.map((rel, index) => (
|
||||
<Box key={index} sx={{ mt: 2, p: 2, bgcolor: 'rgba(0, 0, 0, 0.02)', borderRadius: 1 }}>
|
||||
<Typography variant="subtitle1" color="primary">
|
||||
{rel.description}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
Type: {rel.type}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||
Development:
|
||||
</Typography>
|
||||
<ul style={{ margin: '8px 0', paddingLeft: 20 }}>
|
||||
{rel.development.map((step, i) => (
|
||||
<li key={i}>
|
||||
<Typography variant="body2">{step}</Typography>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Popper
|
||||
open={tooltip.open}
|
||||
anchorEl={containerRef.current}
|
||||
placement="top"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${tooltip.x}px`,
|
||||
top: `${tooltip.y}px`,
|
||||
zIndex: 1500
|
||||
}}
|
||||
>
|
||||
<Paper sx={{
|
||||
p: 2,
|
||||
maxWidth: 300,
|
||||
bgcolor: 'rgba(255, 255, 255, 0.95)',
|
||||
boxShadow: 3,
|
||||
borderRadius: 1
|
||||
}}>
|
||||
<Typography variant="body2" whiteSpace="pre-line">
|
||||
{tooltip.content}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Popper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
31
src/types/character-network.ts
Normal file
31
src/types/character-network.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
export interface BookNode {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'book';
|
||||
color: string;
|
||||
description: string;
|
||||
year: number;
|
||||
}
|
||||
|
||||
export interface CharacterNode {
|
||||
id: string;
|
||||
name: string;
|
||||
novel: string;
|
||||
class: string;
|
||||
type: 'protagonist' | 'antagonist' | 'supporting';
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface Relationship {
|
||||
source: string;
|
||||
target: string;
|
||||
type: 'family' | 'romance' | 'friendship' | 'rivalry';
|
||||
description: string;
|
||||
development: string[];
|
||||
}
|
||||
|
||||
export interface CharacterNetwork {
|
||||
books: BookNode[];
|
||||
nodes: CharacterNode[];
|
||||
relationships: Relationship[];
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue