feat: enhance frontend graph visualization with cluster boundaries and improved rendering
- Add cluster boundary visualization with color-coded type grouping - Implement new MemoryGraphVisualization component for Cognee integration - Add TypeScript types for Cognee API integration (CogneeAPI, NodeSet) - Enhance node swarm materials with better color hierarchy - Improve edge materials with opacity controls - Add metaball density rendering for visual clustering - Update demo and dataset visualization pages with new features 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
8ae805284e
commit
73b293ed71
13 changed files with 1148 additions and 354 deletions
|
|
@ -2,37 +2,94 @@
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { fetch } from "@/utils";
|
import { fetch } from "@/utils";
|
||||||
|
import { adaptCogneeGraphData, validateCogneeGraphResponse } from "@/lib/adaptCogneeGraphData";
|
||||||
|
import { CogneeGraphResponse } from "@/types/CogneeAPI";
|
||||||
|
import MemoryGraphVisualization from "@/ui/elements/MemoryGraphVisualization";
|
||||||
import { Edge, Node } from "@/ui/rendering/graph/types";
|
import { Edge, Node } from "@/ui/rendering/graph/types";
|
||||||
import GraphVisualization from "@/ui/elements/GraphVisualization";
|
|
||||||
|
|
||||||
interface VisualizePageProps {
|
interface VisualizePageProps {
|
||||||
params: { datasetId: string };
|
params: { datasetId: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Page({ params }: VisualizePageProps) {
|
export default function Page({ params }: VisualizePageProps) {
|
||||||
const [graphData, setGraphData] = useState<{ nodes: Node[], edges: Edge[] }>();
|
const [graphData, setGraphData] = useState<{ nodes: Node[], edges: Edge[] } | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getData() {
|
async function getData() {
|
||||||
const datasetId = (await params).datasetId;
|
try {
|
||||||
const response = await fetch(`/v1/datasets/${datasetId}/graph`);
|
setLoading(true);
|
||||||
const newGraphData = await response.json();
|
setError(null);
|
||||||
setGraphData(newGraphData);
|
|
||||||
|
const datasetId = (await params).datasetId;
|
||||||
|
const response = await fetch(`/v1/datasets/${datasetId}/graph`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch graph data: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiData = await response.json();
|
||||||
|
|
||||||
|
// Validate API response
|
||||||
|
if (!validateCogneeGraphResponse(apiData)) {
|
||||||
|
throw new Error("Invalid graph data format from API");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adapt Cognee API format to visualization format
|
||||||
|
const adaptedData = adaptCogneeGraphData(apiData as CogneeGraphResponse);
|
||||||
|
setGraphData(adaptedData);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error loading graph data:", err);
|
||||||
|
setError(err instanceof Error ? err.message : "Unknown error");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
getData();
|
getData();
|
||||||
}, [params]);
|
}, [params]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-gray-900 via-black to-purple-900">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-6xl mb-4 animate-spin">⚛️</div>
|
||||||
|
<div className="text-2xl font-bold text-white">Loading graph data...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-gray-900 via-black to-purple-900">
|
||||||
|
<div className="text-center max-w-md p-6">
|
||||||
|
<div className="text-6xl mb-4">⚠️</div>
|
||||||
|
<div className="text-2xl font-bold text-white mb-2">Error Loading Graph</div>
|
||||||
|
<div className="text-gray-400">{error}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!graphData || graphData.nodes.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-gray-900 via-black to-purple-900">
|
||||||
|
<div className="text-center max-w-md p-6">
|
||||||
|
<div className="text-6xl mb-4">📊</div>
|
||||||
|
<div className="text-2xl font-bold text-white mb-2">No Graph Data</div>
|
||||||
|
<div className="text-gray-400">This dataset has no graph data to visualize.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen">
|
<MemoryGraphVisualization
|
||||||
{graphData && (
|
nodes={graphData.nodes}
|
||||||
<GraphVisualization
|
edges={graphData.edges}
|
||||||
nodes={graphData.nodes}
|
title="Cognee Memory Graph"
|
||||||
edges={graphData.edges}
|
showControls={true}
|
||||||
config={{
|
/>
|
||||||
fontSize: 10,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,279 +1,87 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState, useMemo } from "react";
|
||||||
import GraphVisualization from "@/ui/elements/GraphVisualization";
|
import { generateOntologyGraph } from "@/lib/generateOntologyGraph";
|
||||||
import { Edge, Node } from "@/ui/rendering/graph/types";
|
import MemoryGraphVisualization from "@/ui/elements/MemoryGraphVisualization";
|
||||||
|
|
||||||
// Rich mock dataset representing an AI/ML knowledge graph
|
type GraphMode = "small" | "medium" | "large";
|
||||||
const mockNodes: Node[] = [
|
|
||||||
// Core AI Concepts
|
|
||||||
{ id: "ai", label: "Artificial Intelligence", type: "Concept" },
|
|
||||||
{ id: "ml", label: "Machine Learning", type: "Concept" },
|
|
||||||
{ id: "dl", label: "Deep Learning", type: "Concept" },
|
|
||||||
{ id: "nlp", label: "Natural Language Processing", type: "Concept" },
|
|
||||||
{ id: "cv", label: "Computer Vision", type: "Concept" },
|
|
||||||
{ id: "rl", label: "Reinforcement Learning", type: "Concept" },
|
|
||||||
|
|
||||||
// ML Algorithms
|
|
||||||
{ id: "supervised", label: "Supervised Learning", type: "Algorithm" },
|
|
||||||
{ id: "unsupervised", label: "Unsupervised Learning", type: "Algorithm" },
|
|
||||||
{ id: "svm", label: "Support Vector Machine", type: "Algorithm" },
|
|
||||||
{ id: "decision-tree", label: "Decision Tree", type: "Algorithm" },
|
|
||||||
{ id: "random-forest", label: "Random Forest", type: "Algorithm" },
|
|
||||||
{ id: "kmeans", label: "K-Means Clustering", type: "Algorithm" },
|
|
||||||
{ id: "pca", label: "Principal Component Analysis", type: "Algorithm" },
|
|
||||||
|
|
||||||
// Deep Learning Architectures
|
|
||||||
{ id: "neural-net", label: "Neural Network", type: "Architecture" },
|
|
||||||
{ id: "cnn", label: "Convolutional Neural Network", type: "Architecture" },
|
|
||||||
{ id: "rnn", label: "Recurrent Neural Network", type: "Architecture" },
|
|
||||||
{ id: "lstm", label: "Long Short-Term Memory", type: "Architecture" },
|
|
||||||
{ id: "transformer", label: "Transformer", type: "Architecture" },
|
|
||||||
{ id: "gnn", label: "Graph Neural Network", type: "Architecture" },
|
|
||||||
{ id: "gan", label: "Generative Adversarial Network", type: "Architecture" },
|
|
||||||
{ id: "vae", label: "Variational Autoencoder", type: "Architecture" },
|
|
||||||
|
|
||||||
// NLP Technologies
|
|
||||||
{ id: "bert", label: "BERT", type: "Technology" },
|
|
||||||
{ id: "gpt", label: "GPT", type: "Technology" },
|
|
||||||
{ id: "word2vec", label: "Word2Vec", type: "Technology" },
|
|
||||||
{ id: "attention", label: "Attention Mechanism", type: "Technology" },
|
|
||||||
{ id: "tokenization", label: "Tokenization", type: "Technology" },
|
|
||||||
|
|
||||||
// CV Technologies
|
|
||||||
{ id: "resnet", label: "ResNet", type: "Technology" },
|
|
||||||
{ id: "yolo", label: "YOLO", type: "Technology" },
|
|
||||||
{ id: "segmentation", label: "Image Segmentation", type: "Technology" },
|
|
||||||
{ id: "detection", label: "Object Detection", type: "Technology" },
|
|
||||||
|
|
||||||
// RL Components
|
|
||||||
{ id: "q-learning", label: "Q-Learning", type: "Algorithm" },
|
|
||||||
{ id: "dqn", label: "Deep Q-Network", type: "Architecture" },
|
|
||||||
{ id: "policy-gradient", label: "Policy Gradient", type: "Algorithm" },
|
|
||||||
{ id: "actor-critic", label: "Actor-Critic", type: "Architecture" },
|
|
||||||
|
|
||||||
// Applications
|
|
||||||
{ id: "chatbot", label: "Chatbot", type: "Application" },
|
|
||||||
{ id: "recommendation", label: "Recommendation System", type: "Application" },
|
|
||||||
{ id: "autonomous", label: "Autonomous Vehicles", type: "Application" },
|
|
||||||
{ id: "medical-imaging", label: "Medical Imaging", type: "Application" },
|
|
||||||
{ id: "fraud-detection", label: "Fraud Detection", type: "Application" },
|
|
||||||
|
|
||||||
// Data & Training
|
|
||||||
{ id: "dataset", label: "Training Dataset", type: "Data" },
|
|
||||||
{ id: "feature", label: "Feature Engineering", type: "Data" },
|
|
||||||
{ id: "augmentation", label: "Data Augmentation", type: "Data" },
|
|
||||||
{ id: "normalization", label: "Normalization", type: "Data" },
|
|
||||||
|
|
||||||
// Optimization
|
|
||||||
{ id: "gradient-descent", label: "Gradient Descent", type: "Optimization" },
|
|
||||||
{ id: "adam", label: "Adam Optimizer", type: "Optimization" },
|
|
||||||
{ id: "backprop", label: "Backpropagation", type: "Optimization" },
|
|
||||||
{ id: "regularization", label: "Regularization", type: "Optimization" },
|
|
||||||
{ id: "dropout", label: "Dropout", type: "Optimization" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const mockEdges: Edge[] = [
|
|
||||||
// Core relationships
|
|
||||||
{ id: "e1", source: "ml", target: "ai", label: "is subfield of" },
|
|
||||||
{ id: "e2", source: "dl", target: "ml", label: "is subfield of" },
|
|
||||||
{ id: "e3", source: "nlp", target: "ai", label: "is subfield of" },
|
|
||||||
{ id: "e4", source: "cv", target: "ai", label: "is subfield of" },
|
|
||||||
{ id: "e5", source: "rl", target: "ml", label: "is subfield of" },
|
|
||||||
|
|
||||||
// ML paradigms
|
|
||||||
{ id: "e6", source: "supervised", target: "ml", label: "is paradigm of" },
|
|
||||||
{ id: "e7", source: "unsupervised", target: "ml", label: "is paradigm of" },
|
|
||||||
|
|
||||||
// ML algorithms
|
|
||||||
{ id: "e8", source: "svm", target: "supervised", label: "implements" },
|
|
||||||
{ id: "e9", source: "decision-tree", target: "supervised", label: "implements" },
|
|
||||||
{ id: "e10", source: "random-forest", target: "decision-tree", label: "ensemble of" },
|
|
||||||
{ id: "e11", source: "kmeans", target: "unsupervised", label: "implements" },
|
|
||||||
{ id: "e12", source: "pca", target: "unsupervised", label: "implements" },
|
|
||||||
|
|
||||||
// Deep Learning
|
|
||||||
{ id: "e13", source: "neural-net", target: "dl", label: "foundation of" },
|
|
||||||
{ id: "e14", source: "cnn", target: "neural-net", label: "type of" },
|
|
||||||
{ id: "e15", source: "rnn", target: "neural-net", label: "type of" },
|
|
||||||
{ id: "e16", source: "lstm", target: "rnn", label: "variant of" },
|
|
||||||
{ id: "e17", source: "transformer", target: "neural-net", label: "type of" },
|
|
||||||
{ id: "e18", source: "gnn", target: "neural-net", label: "type of" },
|
|
||||||
{ id: "e19", source: "gan", target: "neural-net", label: "type of" },
|
|
||||||
{ id: "e20", source: "vae", target: "neural-net", label: "type of" },
|
|
||||||
|
|
||||||
// CV architectures
|
|
||||||
{ id: "e21", source: "cnn", target: "cv", label: "used in" },
|
|
||||||
{ id: "e22", source: "resnet", target: "cnn", label: "implementation of" },
|
|
||||||
{ id: "e23", source: "yolo", target: "detection", label: "implements" },
|
|
||||||
{ id: "e24", source: "detection", target: "cv", label: "task in" },
|
|
||||||
{ id: "e25", source: "segmentation", target: "cv", label: "task in" },
|
|
||||||
|
|
||||||
// NLP connections
|
|
||||||
{ id: "e26", source: "transformer", target: "nlp", label: "used in" },
|
|
||||||
{ id: "e27", source: "bert", target: "transformer", label: "based on" },
|
|
||||||
{ id: "e28", source: "gpt", target: "transformer", label: "based on" },
|
|
||||||
{ id: "e29", source: "attention", target: "transformer", label: "key component of" },
|
|
||||||
{ id: "e30", source: "word2vec", target: "nlp", label: "technique in" },
|
|
||||||
{ id: "e31", source: "tokenization", target: "nlp", label: "preprocessing for" },
|
|
||||||
|
|
||||||
// RL connections
|
|
||||||
{ id: "e32", source: "q-learning", target: "rl", label: "algorithm in" },
|
|
||||||
{ id: "e33", source: "dqn", target: "q-learning", label: "deep version of" },
|
|
||||||
{ id: "e34", source: "policy-gradient", target: "rl", label: "algorithm in" },
|
|
||||||
{ id: "e35", source: "actor-critic", target: "policy-gradient", label: "combines" },
|
|
||||||
|
|
||||||
// Applications
|
|
||||||
{ id: "e36", source: "chatbot", target: "nlp", label: "application of" },
|
|
||||||
{ id: "e37", source: "chatbot", target: "gpt", label: "powered by" },
|
|
||||||
{ id: "e38", source: "recommendation", target: "ml", label: "application of" },
|
|
||||||
{ id: "e39", source: "autonomous", target: "rl", label: "application of" },
|
|
||||||
{ id: "e40", source: "autonomous", target: "cv", label: "application of" },
|
|
||||||
{ id: "e41", source: "medical-imaging", target: "cv", label: "application of" },
|
|
||||||
{ id: "e42", source: "medical-imaging", target: "cnn", label: "uses" },
|
|
||||||
{ id: "e43", source: "fraud-detection", target: "ml", label: "application of" },
|
|
||||||
|
|
||||||
// Data & Training
|
|
||||||
{ id: "e44", source: "dataset", target: "supervised", label: "required for" },
|
|
||||||
{ id: "e45", source: "feature", target: "ml", label: "preprocessing for" },
|
|
||||||
{ id: "e46", source: "augmentation", target: "dataset", label: "expands" },
|
|
||||||
{ id: "e47", source: "normalization", target: "feature", label: "step in" },
|
|
||||||
|
|
||||||
// Optimization
|
|
||||||
{ id: "e48", source: "backprop", target: "neural-net", label: "trains" },
|
|
||||||
{ id: "e49", source: "gradient-descent", target: "backprop", label: "uses" },
|
|
||||||
{ id: "e50", source: "adam", target: "gradient-descent", label: "variant of" },
|
|
||||||
{ id: "e51", source: "regularization", target: "neural-net", label: "improves" },
|
|
||||||
{ id: "e52", source: "dropout", target: "regularization", label: "technique for" },
|
|
||||||
|
|
||||||
// Cross-connections
|
|
||||||
{ id: "e53", source: "attention", target: "cv", label: "also used in" },
|
|
||||||
{ id: "e54", source: "gan", target: "augmentation", label: "generates" },
|
|
||||||
{ id: "e55", source: "transformer", target: "cv", label: "adapted to" },
|
|
||||||
{ id: "e56", source: "gnn", target: "recommendation", label: "powers" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function VisualizationDemoPage() {
|
export default function VisualizationDemoPage() {
|
||||||
const [showLegend, setShowLegend] = useState(true);
|
const [graphMode, setGraphMode] = useState<GraphMode>("medium");
|
||||||
const [showStats, setShowStats] = useState(true);
|
const [isGenerating, setIsGenerating] = useState(false);
|
||||||
|
|
||||||
const nodeTypes = Array.from(new Set(mockNodes.map(n => n.type)));
|
// Generate graph based on mode
|
||||||
const typeColors: Record<string, string> = {
|
const { nodes, edges } = useMemo(() => {
|
||||||
"Concept": "#5C10F4",
|
console.log(`Generating ${graphMode} ontology graph...`);
|
||||||
"Algorithm": "#A550FF",
|
setIsGenerating(true);
|
||||||
"Architecture": "#0DFF00",
|
|
||||||
"Technology": "#00D9FF",
|
let result;
|
||||||
"Application": "#FF6B35",
|
switch (graphMode) {
|
||||||
"Data": "#F7B801",
|
case "small":
|
||||||
"Optimization": "#FF1E56",
|
result = { ...generateOntologyGraph("simple"), clusters: new Map() };
|
||||||
};
|
break;
|
||||||
|
case "medium":
|
||||||
|
result = generateOntologyGraph("medium");
|
||||||
|
break;
|
||||||
|
case "large":
|
||||||
|
result = generateOntologyGraph("complex");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => setIsGenerating(false), 500);
|
||||||
|
return result;
|
||||||
|
}, [graphMode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen bg-black text-white">
|
<div className="relative min-h-screen">
|
||||||
{/* Main Visualization */}
|
{isGenerating ? (
|
||||||
<div className="flex-1 relative">
|
<div className="absolute inset-0 flex items-center justify-center bg-black/90 z-50 backdrop-blur-sm">
|
||||||
<GraphVisualization
|
<div className="text-center">
|
||||||
nodes={mockNodes}
|
<div className="relative">
|
||||||
edges={mockEdges}
|
<div className="text-6xl mb-4 animate-spin">⚛️</div>
|
||||||
config={{
|
<div className="absolute inset-0 text-6xl mb-4 animate-ping opacity-20">⚛️</div>
|
||||||
fontSize: 12,
|
</div>
|
||||||
}}
|
<div className="text-2xl font-bold mb-2 bg-gradient-to-r from-purple-400 to-cyan-400 bg-clip-text text-transparent">
|
||||||
/>
|
Building Knowledge Graph...
|
||||||
|
</div>
|
||||||
{/* Header Overlay */}
|
<div className="text-gray-400">
|
||||||
<div className="absolute top-0 left-0 right-0 p-6 bg-gradient-to-b from-black/80 to-transparent pointer-events-none">
|
Creating {
|
||||||
<h1 className="text-3xl font-bold mb-2">AI/ML Knowledge Graph</h1>
|
graphMode === "small" ? "~500" :
|
||||||
<p className="text-gray-400">
|
graphMode === "medium" ? "~1,000" :
|
||||||
Interactive visualization of artificial intelligence concepts and relationships
|
"~1,500"
|
||||||
</p>
|
} interconnected nodes
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{/* Controls */}
|
{/* Mode Selector Overlay */}
|
||||||
<div className="absolute top-6 right-6 flex gap-2">
|
<div className="absolute top-6 left-6 z-10 pointer-events-auto">
|
||||||
<button
|
<div className="flex gap-1 bg-black/70 backdrop-blur-md rounded-lg p-1 border border-purple-500/30">
|
||||||
onClick={() => setShowLegend(!showLegend)}
|
{(["small", "medium", "large"] as GraphMode[]).map((mode) => (
|
||||||
className="px-4 py-2 bg-white/10 hover:bg-white/20 rounded-lg backdrop-blur-sm transition-colors"
|
<button
|
||||||
>
|
key={mode}
|
||||||
{showLegend ? "Hide" : "Show"} Legend
|
onClick={() => setGraphMode(mode)}
|
||||||
</button>
|
className={`flex-1 px-3 py-2 rounded transition-all text-sm font-medium ${
|
||||||
<button
|
graphMode === mode
|
||||||
onClick={() => setShowStats(!showStats)}
|
? "bg-gradient-to-r from-purple-600 to-pink-600 text-white shadow-lg shadow-purple-500/50"
|
||||||
className="px-4 py-2 bg-white/10 hover:bg-white/20 rounded-lg backdrop-blur-sm transition-colors"
|
: "hover:bg-white/10 text-gray-300"
|
||||||
>
|
}`}
|
||||||
{showStats ? "Hide" : "Show"} Stats
|
>
|
||||||
</button>
|
{mode === "small" && "500"}
|
||||||
</div>
|
{mode === "medium" && "1K"}
|
||||||
|
{mode === "large" && "1.5K"}
|
||||||
{/* Instructions */}
|
</button>
|
||||||
<div className="absolute bottom-6 left-6 bg-black/70 backdrop-blur-md p-4 rounded-lg max-w-md pointer-events-none">
|
))}
|
||||||
<h3 className="font-semibold mb-2">💡 How to Explore</h3>
|
|
||||||
<ul className="text-sm text-gray-300 space-y-1">
|
|
||||||
<li>• <strong>Hover</strong> over nodes to see labels</li>
|
|
||||||
<li>• <strong>Zoom in</strong> (scroll) to see connections</li>
|
|
||||||
<li>• <strong>Click & drag</strong> to pan around</li>
|
|
||||||
<li>• <strong>Click</strong> on nodes to select them</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Legend Panel */}
|
<MemoryGraphVisualization
|
||||||
{showLegend && (
|
nodes={nodes}
|
||||||
<div className="w-80 bg-gray-900/95 backdrop-blur-md p-6 border-l border-gray-800 overflow-y-auto">
|
edges={edges}
|
||||||
<h2 className="text-xl font-bold mb-4">Node Types</h2>
|
title="Memory Retrieval Debugger (Demo)"
|
||||||
<div className="space-y-3">
|
showControls={true}
|
||||||
{nodeTypes.map((type) => (
|
/>
|
||||||
<div key={type} className="flex items-center gap-3">
|
|
||||||
<div
|
|
||||||
className="w-4 h-4 rounded-full"
|
|
||||||
style={{ backgroundColor: typeColors[type] || "#F4F4F4" }}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<div className="font-medium">{type}</div>
|
|
||||||
<div className="text-xs text-gray-400">
|
|
||||||
{mockNodes.filter(n => n.type === type).length} nodes
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showStats && (
|
|
||||||
<div className="mt-8 pt-6 border-t border-gray-800">
|
|
||||||
<h2 className="text-xl font-bold mb-4">Statistics</h2>
|
|
||||||
<div className="space-y-2 text-sm">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-400">Total Nodes:</span>
|
|
||||||
<span className="font-semibold">{mockNodes.length}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-400">Total Edges:</span>
|
|
||||||
<span className="font-semibold">{mockEdges.length}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-400">Avg. Connections:</span>
|
|
||||||
<span className="font-semibold">
|
|
||||||
{(mockEdges.length * 2 / mockNodes.length).toFixed(1)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-400">Node Types:</span>
|
|
||||||
<span className="font-semibold">{nodeTypes.length}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-8 pt-6 border-t border-gray-800">
|
|
||||||
<h3 className="font-semibold mb-2">About This Graph</h3>
|
|
||||||
<p className="text-sm text-gray-400 leading-relaxed">
|
|
||||||
This knowledge graph represents the interconnected landscape of
|
|
||||||
artificial intelligence, machine learning, and deep learning. Nodes
|
|
||||||
represent concepts, algorithms, architectures, and applications,
|
|
||||||
while edges show their relationships.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
45
cognee-frontend/src/types/CogneeAPI.ts
Normal file
45
cognee-frontend/src/types/CogneeAPI.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
/**
|
||||||
|
* Type definitions for Cognee API responses
|
||||||
|
* Based on Cognee SDK data point model and graph API
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cognee DataPoint representation from API
|
||||||
|
* Corresponds to: cognee/infrastructure/engine/models/DataPoint.py
|
||||||
|
*/
|
||||||
|
export interface CogneeDataPoint {
|
||||||
|
id: string; // UUID
|
||||||
|
label: string; // Display name
|
||||||
|
type: string; // Node type (Entity, EntityType, DocumentChunk, etc.)
|
||||||
|
properties?: Record<string, any>; // Additional metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cognee Edge representation from API
|
||||||
|
* Corresponds to: cognee/infrastructure/engine/models/Edge.py
|
||||||
|
*/
|
||||||
|
export interface CogneeEdge {
|
||||||
|
source: string; // Source node UUID
|
||||||
|
target: string; // Target node UUID
|
||||||
|
label: string; // Relationship type
|
||||||
|
weight?: number; // Optional weight
|
||||||
|
weights?: Record<string, number>; // Optional multiple weights
|
||||||
|
properties?: Record<string, any>; // Additional properties
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cognee Graph API response format
|
||||||
|
* From: /api/v1/datasets/{dataset_id}/graph
|
||||||
|
*/
|
||||||
|
export interface CogneeGraphResponse {
|
||||||
|
nodes: CogneeDataPoint[];
|
||||||
|
edges: CogneeEdge[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cognee API error response
|
||||||
|
*/
|
||||||
|
export interface CogneeAPIError {
|
||||||
|
detail: string;
|
||||||
|
status_code: number;
|
||||||
|
}
|
||||||
165
cognee-frontend/src/types/NodeSet.ts
Normal file
165
cognee-frontend/src/types/NodeSet.ts
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
/**
|
||||||
|
* Node Sets: The primary abstraction for grouping nodes
|
||||||
|
* Replaces fixed types with dynamic, inferred, overlapping groups
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type NodeSetSource =
|
||||||
|
| "model-inferred" // Created by AI/ML model
|
||||||
|
| "user-defined" // Manually created by user
|
||||||
|
| "query-result" // Result of a search query
|
||||||
|
| "imported" // From external source
|
||||||
|
| "set-algebra"; // Created by combining other sets
|
||||||
|
|
||||||
|
export type NodeSetStability =
|
||||||
|
| "stable" // Won't change often
|
||||||
|
| "evolving" // Changes gradually
|
||||||
|
| "ephemeral"; // Temporary, will be removed
|
||||||
|
|
||||||
|
export interface NodeSet {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
// Required properties
|
||||||
|
nodeIds: string[]; // Member node IDs
|
||||||
|
size: number; // Number of nodes
|
||||||
|
definition: string; // How it was created (e.g., "semantic cluster around 'AI'")
|
||||||
|
stability: NodeSetStability;
|
||||||
|
source: NodeSetSource;
|
||||||
|
lastUpdated: Date;
|
||||||
|
|
||||||
|
// Confidence metrics
|
||||||
|
confidence?: number; // 0-1, how confident we are in this grouping
|
||||||
|
cohesion?: number; // 0-1, how tightly connected members are
|
||||||
|
|
||||||
|
// Set algebra metadata
|
||||||
|
parentSets?: string[]; // If created from other sets
|
||||||
|
operation?: "union" | "intersect" | "diff";
|
||||||
|
|
||||||
|
// Retrieval metadata
|
||||||
|
retrievalScore?: number; // If this set was retrieved
|
||||||
|
retrievalSignals?: string[]; // Why it was retrieved
|
||||||
|
|
||||||
|
// Visual properties (for rendering)
|
||||||
|
color?: string;
|
||||||
|
visible?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set operations
|
||||||
|
*/
|
||||||
|
export function unionSets(sets: NodeSet[], name: string): NodeSet {
|
||||||
|
const allNodeIds = new Set<string>();
|
||||||
|
sets.forEach(set => set.nodeIds.forEach(id => allNodeIds.add(id)));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `union_${Date.now()}`,
|
||||||
|
name,
|
||||||
|
nodeIds: Array.from(allNodeIds),
|
||||||
|
size: allNodeIds.size,
|
||||||
|
definition: `Union of: ${sets.map(s => s.name).join(", ")}`,
|
||||||
|
stability: "ephemeral",
|
||||||
|
source: "set-algebra",
|
||||||
|
lastUpdated: new Date(),
|
||||||
|
parentSets: sets.map(s => s.id),
|
||||||
|
operation: "union",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function intersectSets(sets: NodeSet[], name: string): NodeSet {
|
||||||
|
if (sets.length === 0) {
|
||||||
|
return {
|
||||||
|
id: `intersect_${Date.now()}`,
|
||||||
|
name,
|
||||||
|
nodeIds: [],
|
||||||
|
size: 0,
|
||||||
|
definition: "Empty intersection",
|
||||||
|
stability: "ephemeral",
|
||||||
|
source: "set-algebra",
|
||||||
|
lastUpdated: new Date(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const intersection = new Set(sets[0].nodeIds);
|
||||||
|
sets.slice(1).forEach(set => {
|
||||||
|
const setIds = new Set(set.nodeIds);
|
||||||
|
intersection.forEach(id => {
|
||||||
|
if (!setIds.has(id)) intersection.delete(id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `intersect_${Date.now()}`,
|
||||||
|
name,
|
||||||
|
nodeIds: Array.from(intersection),
|
||||||
|
size: intersection.size,
|
||||||
|
definition: `Intersection of: ${sets.map(s => s.name).join(", ")}`,
|
||||||
|
stability: "ephemeral",
|
||||||
|
source: "set-algebra",
|
||||||
|
lastUpdated: new Date(),
|
||||||
|
parentSets: sets.map(s => s.id),
|
||||||
|
operation: "intersect",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function diffSets(setA: NodeSet, setB: NodeSet, name: string): NodeSet {
|
||||||
|
const diff = new Set(setA.nodeIds);
|
||||||
|
setB.nodeIds.forEach(id => diff.delete(id));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `diff_${Date.now()}`,
|
||||||
|
name,
|
||||||
|
nodeIds: Array.from(diff),
|
||||||
|
size: diff.size,
|
||||||
|
definition: `${setA.name} minus ${setB.name}`,
|
||||||
|
stability: "ephemeral",
|
||||||
|
source: "set-algebra",
|
||||||
|
lastUpdated: new Date(),
|
||||||
|
parentSets: [setA.id, setB.id],
|
||||||
|
operation: "diff",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieval result with explanation
|
||||||
|
*/
|
||||||
|
export interface RetrievalResult {
|
||||||
|
type: "node" | "nodeSet" | "suggestedSet";
|
||||||
|
|
||||||
|
// For nodes
|
||||||
|
nodeId?: string;
|
||||||
|
nodeLabel?: string;
|
||||||
|
|
||||||
|
// For node sets
|
||||||
|
nodeSet?: NodeSet;
|
||||||
|
|
||||||
|
// For suggested sets
|
||||||
|
suggestedSetDefinition?: string;
|
||||||
|
suggestedNodeIds?: string[];
|
||||||
|
|
||||||
|
// Explanation (critical for trust)
|
||||||
|
why: string; // Human-readable explanation
|
||||||
|
similarityScore: number; // 0-1
|
||||||
|
signals: {
|
||||||
|
name: string; // e.g., "semantic", "recency", "provenance"
|
||||||
|
weight: number; // Contribution to final score
|
||||||
|
value: string | number; // The actual value
|
||||||
|
}[];
|
||||||
|
|
||||||
|
// Confidence
|
||||||
|
confidence: number; // 0-1, how confident we are in this retrieval
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recall simulation: "If the agent were asked X, what would be retrieved?"
|
||||||
|
*/
|
||||||
|
export interface RecallSimulation {
|
||||||
|
query: string;
|
||||||
|
rankedMemories: RetrievalResult[];
|
||||||
|
activatedSets: NodeSet[];
|
||||||
|
conflicts?: {
|
||||||
|
nodeId: string;
|
||||||
|
reason: string;
|
||||||
|
conflictingSets: string[];
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
@ -6,11 +6,18 @@ import { useEffect, useRef } from "react";
|
||||||
import { Edge, Node } from "@/ui/rendering/graph/types";
|
import { Edge, Node } from "@/ui/rendering/graph/types";
|
||||||
import animate from "@/ui/rendering/animate";
|
import animate from "@/ui/rendering/animate";
|
||||||
|
|
||||||
|
// IMPROVEMENT #8: Extended config for layered view controls
|
||||||
interface GraphVisualizationProps {
|
interface GraphVisualizationProps {
|
||||||
nodes: Node[];
|
nodes: Node[];
|
||||||
edges: Edge[];
|
edges: Edge[];
|
||||||
className?: string;
|
className?: string;
|
||||||
config?: { fontSize: number };
|
config?: {
|
||||||
|
fontSize?: number;
|
||||||
|
showNodes?: boolean; // Toggle node visibility
|
||||||
|
showEdges?: boolean; // Toggle edge/path visibility
|
||||||
|
showMetaballs?: boolean; // Toggle density cloud visibility
|
||||||
|
highlightedNodeIds?: Set<string>; // Nodes to highlight (neutral-by-default)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function GraphVisualization({
|
export default function GraphVisualization({
|
||||||
|
|
@ -20,13 +27,32 @@ export default function GraphVisualization({
|
||||||
config,
|
config,
|
||||||
}: GraphVisualizationProps) {
|
}: GraphVisualizationProps) {
|
||||||
const visualizationRef = useRef<HTMLDivElement>(null);
|
const visualizationRef = useRef<HTMLDivElement>(null);
|
||||||
|
const cleanupRef = useRef<(() => void) | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const visualizationContainer = visualizationRef.current;
|
const visualizationContainer = visualizationRef.current;
|
||||||
|
|
||||||
if (visualizationContainer) {
|
if (visualizationContainer) {
|
||||||
animate(nodes, edges, visualizationContainer, config);
|
// Clean up previous visualization
|
||||||
|
if (cleanupRef.current) {
|
||||||
|
cleanupRef.current();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the container
|
||||||
|
while (visualizationContainer.firstChild) {
|
||||||
|
visualizationContainer.removeChild(visualizationContainer.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new visualization
|
||||||
|
const cleanup = animate(nodes, edges, visualizationContainer, config);
|
||||||
|
cleanupRef.current = cleanup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (cleanupRef.current) {
|
||||||
|
cleanupRef.current();
|
||||||
|
}
|
||||||
|
};
|
||||||
}, [config, edges, nodes]);
|
}, [config, edges, nodes]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
374
cognee-frontend/src/ui/elements/MemoryGraphVisualization.tsx
Normal file
374
cognee-frontend/src/ui/elements/MemoryGraphVisualization.tsx
Normal file
|
|
@ -0,0 +1,374 @@
|
||||||
|
/**
|
||||||
|
* Memory Graph Visualization
|
||||||
|
*
|
||||||
|
* Reusable visualization component with retrieval-first features:
|
||||||
|
* - Node set inference and display
|
||||||
|
* - Retrieval search with explanations
|
||||||
|
* - Neutral-by-default highlighting
|
||||||
|
* - Type attributes vs. inferred sets separation
|
||||||
|
*
|
||||||
|
* Works with any graph data (mock or real Cognee API data)
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useMemo } from "react";
|
||||||
|
import GraphVisualization from "@/ui/elements/GraphVisualization";
|
||||||
|
import { Edge, Node } from "@/ui/rendering/graph/types";
|
||||||
|
import { NodeSet } from "@/types/NodeSet";
|
||||||
|
import { inferNodeSets, mockRetrievalSearch } from "@/lib/inferNodeSets";
|
||||||
|
import type { RetrievalResult } from "@/types/NodeSet";
|
||||||
|
|
||||||
|
interface MemoryGraphVisualizationProps {
|
||||||
|
nodes: Node[];
|
||||||
|
edges: Edge[];
|
||||||
|
title?: string;
|
||||||
|
showControls?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MemoryGraphVisualization({
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
title = "Memory Retrieval Debugger",
|
||||||
|
showControls = true,
|
||||||
|
}: MemoryGraphVisualizationProps) {
|
||||||
|
const [showLegend, setShowLegend] = useState(true);
|
||||||
|
|
||||||
|
// Retrieval-first: search replaces static filtering
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [retrievalResults, setRetrievalResults] = useState<RetrievalResult[]>([]);
|
||||||
|
|
||||||
|
// Node sets: primary abstraction
|
||||||
|
const [selectedNodeSet, setSelectedNodeSet] = useState<NodeSet | null>(null);
|
||||||
|
|
||||||
|
// Layer visibility controls
|
||||||
|
const [showNodes, setShowNodes] = useState(true);
|
||||||
|
const [showEdges, setShowEdges] = useState(true);
|
||||||
|
const [showMetaballs, setShowMetaballs] = useState(false);
|
||||||
|
|
||||||
|
// Node Attributes section collapsed by default (secondary concern)
|
||||||
|
const [showNodeAttributes, setShowNodeAttributes] = useState(false);
|
||||||
|
|
||||||
|
// Infer node sets from graph structure (CRITICAL: separate attributes from sets)
|
||||||
|
const { typeAttributes, inferredSets } = useMemo(() => {
|
||||||
|
return inferNodeSets(nodes, edges, {
|
||||||
|
minSetSize: 5,
|
||||||
|
maxSets: 15,
|
||||||
|
});
|
||||||
|
}, [nodes, edges]);
|
||||||
|
|
||||||
|
// Neutral-by-default: only highlight nodes that are selected or retrieved
|
||||||
|
const highlightedNodeIds = useMemo(() => {
|
||||||
|
const ids = new Set<string>();
|
||||||
|
|
||||||
|
// Nodes from retrieval results
|
||||||
|
retrievalResults.forEach(result => {
|
||||||
|
if (result.type === "node" && result.nodeId) {
|
||||||
|
ids.add(result.nodeId);
|
||||||
|
} else if (result.type === "nodeSet" && result.nodeSet) {
|
||||||
|
result.nodeSet.nodeIds.forEach(id => ids.add(id));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Nodes from selected set
|
||||||
|
if (selectedNodeSet) {
|
||||||
|
selectedNodeSet.nodeIds.forEach(id => ids.add(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}, [retrievalResults, selectedNodeSet]);
|
||||||
|
|
||||||
|
// Handle retrieval search
|
||||||
|
const handleSearch = (query: string) => {
|
||||||
|
setSearchQuery(query);
|
||||||
|
if (query.trim()) {
|
||||||
|
const results = mockRetrievalSearch(query, nodes, inferredSets);
|
||||||
|
setRetrievalResults(results);
|
||||||
|
} else {
|
||||||
|
setRetrievalResults([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setSearchQuery("");
|
||||||
|
setRetrievalResults([]);
|
||||||
|
setSelectedNodeSet(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen bg-gradient-to-br from-gray-900 via-black to-purple-900 text-white">
|
||||||
|
{/* Main Visualization */}
|
||||||
|
<div className="flex-1 relative">
|
||||||
|
<GraphVisualization
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
config={{
|
||||||
|
fontSize: 11,
|
||||||
|
showNodes,
|
||||||
|
showEdges,
|
||||||
|
showMetaballs,
|
||||||
|
highlightedNodeIds,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="absolute top-0 left-0 right-0 p-6 bg-gradient-to-b from-black/90 via-black/50 to-transparent pointer-events-none">
|
||||||
|
<h1 className="text-4xl font-bold mb-2 bg-gradient-to-r from-purple-400 via-pink-400 to-cyan-400 bg-clip-text text-transparent">
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-300">
|
||||||
|
{highlightedNodeIds.size > 0 ? (
|
||||||
|
<>
|
||||||
|
<span className="text-purple-400 font-semibold">{highlightedNodeIds.size}</span> retrieved
|
||||||
|
{" / "}
|
||||||
|
<span className="text-gray-500">{nodes.length.toLocaleString()} total</span>
|
||||||
|
{" • "}
|
||||||
|
<span className="text-sm">{inferredSets.length} inferred sets</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span>{nodes.length.toLocaleString()} nodes</span>
|
||||||
|
{" • "}
|
||||||
|
<span>{inferredSets.length} inferred sets</span>
|
||||||
|
{" • "}
|
||||||
|
<span className="text-gray-500">Search to retrieve</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showControls && (
|
||||||
|
<div className="absolute top-6 right-6 flex flex-col gap-3 pointer-events-auto max-w-md">
|
||||||
|
{/* Retrieval Search */}
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="🔍 Retrieve memories... (e.g., 'AI', 'Physics')"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
|
className="w-full px-4 py-2 bg-black/70 backdrop-blur-md border border-purple-500/30 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20"
|
||||||
|
/>
|
||||||
|
{searchQuery && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleSearch("")}
|
||||||
|
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-white"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* View Controls */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowLegend(!showLegend)}
|
||||||
|
className="flex-1 px-4 py-2 bg-black/70 hover:bg-black/90 backdrop-blur-md rounded-lg border border-purple-500/30 transition-all"
|
||||||
|
>
|
||||||
|
{showLegend ? "Hide" : "Show"} Panel
|
||||||
|
</button>
|
||||||
|
{(searchQuery || selectedNodeSet || retrievalResults.length > 0) && (
|
||||||
|
<button
|
||||||
|
onClick={handleReset}
|
||||||
|
className="px-4 py-2 bg-gradient-to-r from-red-600 to-orange-600 hover:from-red-500 hover:to-orange-500 rounded-lg transition-all shadow-lg"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Layer Visibility Controls */}
|
||||||
|
<div className="bg-black/70 backdrop-blur-md rounded-lg p-3 border border-purple-500/30">
|
||||||
|
<div className="text-xs font-semibold text-gray-400 mb-2">Layers</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowNodes(!showNodes)}
|
||||||
|
className={`flex items-center justify-between px-3 py-2 rounded-lg transition-all text-sm ${
|
||||||
|
showNodes
|
||||||
|
? "bg-purple-600/30 border border-purple-500/50"
|
||||||
|
: "bg-white/5 border border-gray-600/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>● Nodes</span>
|
||||||
|
<span className="text-xs">{showNodes ? "ON" : "OFF"}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowEdges(!showEdges)}
|
||||||
|
className={`flex items-center justify-between px-3 py-2 rounded-lg transition-all text-sm ${
|
||||||
|
showEdges
|
||||||
|
? "bg-amber-600/30 border border-amber-500/50"
|
||||||
|
: "bg-white/5 border border-gray-600/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>─ Paths</span>
|
||||||
|
<span className="text-xs">{showEdges ? "ON" : "OFF"}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowMetaballs(!showMetaballs)}
|
||||||
|
className={`flex items-center justify-between px-3 py-2 rounded-lg transition-all text-sm ${
|
||||||
|
showMetaballs
|
||||||
|
? "bg-purple-600/20 border border-purple-500/30"
|
||||||
|
: "bg-white/5 border border-gray-600/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>◉ Clouds</span>
|
||||||
|
<span className="text-xs">{showMetaballs ? "ON" : "OFF"}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Side Panel */}
|
||||||
|
{showLegend && (
|
||||||
|
<div className="w-96 bg-black/95 backdrop-blur-xl border-l border-purple-500/20 overflow-y-auto">
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* Retrieval Results */}
|
||||||
|
{retrievalResults.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold mb-4 bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent">
|
||||||
|
Retrieved Memories
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{retrievalResults.slice(0, 10).map((result, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="bg-white/5 hover:bg-white/10 p-3 rounded-lg border border-purple-500/20 transition-all"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-2 mb-2">
|
||||||
|
<div className="font-medium text-sm">
|
||||||
|
{result.type === "node" ? result.nodeLabel : result.nodeSet?.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs px-2 py-0.5 bg-purple-600/30 rounded">
|
||||||
|
{(result.similarityScore * 100).toFixed(0)}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400 mb-2">
|
||||||
|
{result.why}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
{result.signals.map((signal, sidx) => (
|
||||||
|
<span
|
||||||
|
key={sidx}
|
||||||
|
className="text-xs px-2 py-0.5 bg-black/30 rounded"
|
||||||
|
>
|
||||||
|
{signal.name}: {(signal.weight * 100).toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Node Attributes - Secondary (NOT sets, just metadata) */}
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowNodeAttributes(!showNodeAttributes)}
|
||||||
|
className="w-full flex items-center justify-between mb-3 text-left"
|
||||||
|
>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-400">
|
||||||
|
Node Attributes {showNodeAttributes ? "▼" : "▶"}
|
||||||
|
</h3>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
{typeAttributes.size} types
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{showNodeAttributes && (
|
||||||
|
<div className="space-y-1 mb-6 pl-2">
|
||||||
|
{Array.from(typeAttributes.entries())
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.map(([type, count]) => (
|
||||||
|
<div
|
||||||
|
key={type}
|
||||||
|
className="flex items-center justify-between text-xs px-3 py-1.5 bg-white/5 rounded"
|
||||||
|
>
|
||||||
|
<span className="text-gray-400">type: {type}</span>
|
||||||
|
<span className="text-gray-500">({count})</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Inferred Node Sets - PRIMARY ABSTRACTION */}
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold mb-3 flex items-center justify-between">
|
||||||
|
<span className="bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent">
|
||||||
|
Node Sets
|
||||||
|
</span>
|
||||||
|
{selectedNodeSet && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedNodeSet(null)}
|
||||||
|
className="text-xs px-2 py-1 bg-red-600 hover:bg-red-500 rounded"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{inferredSets.map((nodeSet) => {
|
||||||
|
const isSelected = selectedNodeSet?.id === nodeSet.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={nodeSet.id}
|
||||||
|
onClick={() => setSelectedNodeSet(isSelected ? null : nodeSet)}
|
||||||
|
className={`w-full text-left p-3 rounded-lg transition-all ${
|
||||||
|
isSelected
|
||||||
|
? "bg-gradient-to-r from-purple-600 to-pink-600 shadow-lg"
|
||||||
|
: "bg-white/5 hover:bg-white/10"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<div className="font-medium text-sm">{nodeSet.name}</div>
|
||||||
|
<div className="text-xs px-2 py-0.5 bg-black/30 rounded">
|
||||||
|
{nodeSet.size}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Enhanced explanatory section with explicit semantics */}
|
||||||
|
<div className="pt-6 border-t border-purple-500/20">
|
||||||
|
<h3 className="font-semibold mb-3 text-purple-400">Visual Elements</h3>
|
||||||
|
<div className="space-y-2 text-sm text-gray-400">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<span className="text-purple-400 mt-0.5 font-bold text-lg">●</span>
|
||||||
|
<div>
|
||||||
|
<strong className="text-gray-300">Node Size = Importance:</strong>
|
||||||
|
<div className="text-xs mt-0.5">Larger = Domain/Field (structural), Smaller = Application (leaf)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<span className="text-amber-400 mt-0.5">─</span>
|
||||||
|
<div>
|
||||||
|
<strong className="text-gray-300">Paths (zoom to see):</strong>
|
||||||
|
<div className="text-xs mt-0.5">Relationships • Hover node to highlight connections</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<span className="text-purple-400/40 mt-0.5">◉</span>
|
||||||
|
<div>
|
||||||
|
<strong className="text-gray-300">Background Clouds:</strong>
|
||||||
|
<div className="text-xs mt-0.5">Conceptual Density • Visible at far zoom for cluster overview</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<span className="text-cyan-400/60 mt-0.5">○</span>
|
||||||
|
<div>
|
||||||
|
<strong className="text-gray-300">Boundary Rings:</strong>
|
||||||
|
<div className="text-xs mt-0.5">Type Clusters • Spatial grouping by semantic category</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Graph, Node as GraphNode, Link as GraphLink } from "ngraph.graph";
|
import { Graph, Node as GraphNode, Link as GraphLink } from "ngraph.graph";
|
||||||
|
import * as three from "three";
|
||||||
import {
|
import {
|
||||||
Color,
|
Color,
|
||||||
DataTexture,
|
DataTexture,
|
||||||
|
|
@ -25,11 +26,19 @@ import createNodePositionsTexture from "./textures/createNodePositionsTexture";
|
||||||
import createDensityRenderTarget from "./render-targets/createDensityRenderTarget";
|
import createDensityRenderTarget from "./render-targets/createDensityRenderTarget";
|
||||||
import createDensityAccumulatorMesh from "./meshes/createDensityAccumulatorMesh";
|
import createDensityAccumulatorMesh from "./meshes/createDensityAccumulatorMesh";
|
||||||
import createMetaballMesh from "./meshes/createMetaballMesh";
|
import createMetaballMesh from "./meshes/createMetaballMesh";
|
||||||
|
import createClusterBoundaryMesh, { ClusterInfo } from "./meshes/createClusterBoundaryMesh";
|
||||||
|
|
||||||
const INITIAL_CAMERA_DISTANCE = 2000;
|
const INITIAL_CAMERA_DISTANCE = 2000;
|
||||||
|
|
||||||
|
// Extended config for layered view controls + zoom semantics
|
||||||
interface Config {
|
interface Config {
|
||||||
fontSize: number;
|
fontSize?: number;
|
||||||
|
showNodes?: boolean;
|
||||||
|
showEdges?: boolean;
|
||||||
|
showMetaballs?: boolean;
|
||||||
|
pathFilterMode?: "all" | "hoverOnly" | "strongOnly"; // Path filtering
|
||||||
|
zoomLevel?: "far" | "mid" | "near"; // Zoom-based semantics
|
||||||
|
highlightedNodeIds?: Set<string>; // Nodes to highlight (neutral-by-default)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function animate(
|
export default function animate(
|
||||||
|
|
@ -37,45 +46,57 @@ export default function animate(
|
||||||
edges: Edge[],
|
edges: Edge[],
|
||||||
parentElement: HTMLElement,
|
parentElement: HTMLElement,
|
||||||
config?: Config
|
config?: Config
|
||||||
): void {
|
): () => void {
|
||||||
const nodeLabelMap = new Map();
|
const nodeLabelMap = new Map();
|
||||||
const edgeLabelMap = new Map();
|
const edgeLabelMap = new Map();
|
||||||
// Enhanced color palette with vibrant, distinguishable colors
|
|
||||||
const colorPalette = [
|
// Semantic color encoding: hierarchy drives saturation + brightness
|
||||||
new Color("#5C10F4"), // Deep Purple - Primary concepts
|
const typeColorMap: Record<string, string> = {
|
||||||
new Color("#A550FF"), // Light Purple - Algorithms
|
"Domain": "#C4B5FD", // Bright Purple - Highest importance
|
||||||
new Color("#0DFF00"), // Neon Green - Architectures
|
"Field": "#67E8F9", // Bright Cyan - High importance
|
||||||
new Color("#00D9FF"), // Cyan - Technologies
|
"Subfield": "#A78BFA", // Medium Purple - Medium-high importance
|
||||||
new Color("#FF6B35"), // Coral - Applications
|
"Concept": "#5EEAD4", // Teal - Medium importance
|
||||||
new Color("#F7B801"), // Golden Yellow - Data
|
"Method": "#6EE7B7", // Green - Medium importance
|
||||||
new Color("#FF1E56"), // Hot Pink - Optimization
|
"Theory": "#F9A8D4", // Pink - Medium importance
|
||||||
new Color("#00E5FF"), // Bright Cyan - Additional
|
"Technology": "#FCA5A5", // Soft Red - Lower importance
|
||||||
new Color("#7DFF8C"), // Mint Green - Additional
|
"Application": "#71717A", // Desaturated Gray - Background/lowest importance
|
||||||
new Color("#FFB347"), // Peach - Additional
|
};
|
||||||
];
|
|
||||||
let lastColorIndex = 0;
|
// Size hierarchy: more important = larger
|
||||||
const colorPerType = new Map();
|
const typeSizeMap: Record<string, number> = {
|
||||||
|
"Domain": 2.5, // Largest
|
||||||
|
"Field": 2.0,
|
||||||
|
"Subfield": 1.6,
|
||||||
|
"Concept": 1.2,
|
||||||
|
"Method": 1.1,
|
||||||
|
"Theory": 1.0,
|
||||||
|
"Technology": 0.9,
|
||||||
|
"Application": 0.6, // Smallest
|
||||||
|
};
|
||||||
|
|
||||||
function getColorForType(nodeType: string): Color {
|
function getColorForType(nodeType: string): Color {
|
||||||
if (colorPerType.has(nodeType)) {
|
const colorHex = typeColorMap[nodeType];
|
||||||
return colorPerType.get(nodeType);
|
if (colorHex) {
|
||||||
|
return new Color(colorHex);
|
||||||
}
|
}
|
||||||
|
// Fallback for unknown types
|
||||||
const color = colorPalette[lastColorIndex % colorPalette.length];
|
return new Color("#9CA3AF"); // Gray for unknown types
|
||||||
colorPerType.set(nodeType, color);
|
|
||||||
lastColorIndex += 1;
|
|
||||||
|
|
||||||
return color;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mousePosition = new Vector2();
|
const mousePosition = new Vector2();
|
||||||
|
|
||||||
// Node related data
|
// Node related data
|
||||||
const nodeColors = new Float32Array(nodes.length * 3);
|
const nodeColors = new Float32Array(nodes.length * 3);
|
||||||
|
const nodeSizes = new Float32Array(nodes.length); // Size per node for hierarchy
|
||||||
|
const nodeHighlights = new Float32Array(nodes.length); // 1.0 = highlighted, 0.3 = dimmed
|
||||||
const nodeIndices = new Map();
|
const nodeIndices = new Map();
|
||||||
const textureSize = Math.ceil(Math.sqrt(nodes.length));
|
const textureSize = Math.ceil(Math.sqrt(nodes.length));
|
||||||
const nodePositionsData = new Float32Array(textureSize * textureSize * 4);
|
const nodePositionsData = new Float32Array(textureSize * textureSize * 4);
|
||||||
|
|
||||||
|
// Determine which nodes are highlighted
|
||||||
|
const highlightedIds = config?.highlightedNodeIds;
|
||||||
|
const hasHighlights = highlightedIds && highlightedIds.size > 0;
|
||||||
|
|
||||||
let nodeIndex = 0;
|
let nodeIndex = 0;
|
||||||
function forNode(node: Node) {
|
function forNode(node: Node) {
|
||||||
const color = getColorForType(node.type);
|
const color = getColorForType(node.type);
|
||||||
|
|
@ -83,6 +104,16 @@ export default function animate(
|
||||||
nodeColors[nodeIndex * 3 + 1] = color.g;
|
nodeColors[nodeIndex * 3 + 1] = color.g;
|
||||||
nodeColors[nodeIndex * 3 + 2] = color.b;
|
nodeColors[nodeIndex * 3 + 2] = color.b;
|
||||||
|
|
||||||
|
// Set highlight state: if no highlights, all at 1.0; if highlights exist, dim non-highlighted
|
||||||
|
if (hasHighlights) {
|
||||||
|
nodeHighlights[nodeIndex] = highlightedIds!.has(node.id) ? 1.0 : 0.3;
|
||||||
|
} else {
|
||||||
|
nodeHighlights[nodeIndex] = 1.0; // All visible when no highlights
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store size multiplier based on type
|
||||||
|
nodeSizes[nodeIndex] = typeSizeMap[node.type] || 1.0;
|
||||||
|
|
||||||
nodePositionsData[nodeIndex * 4 + 0] = 0.0;
|
nodePositionsData[nodeIndex * 4 + 0] = 0.0;
|
||||||
nodePositionsData[nodeIndex * 4 + 1] = 0.0;
|
nodePositionsData[nodeIndex * 4 + 1] = 0.0;
|
||||||
nodePositionsData[nodeIndex * 4 + 2] = 0.0;
|
nodePositionsData[nodeIndex * 4 + 2] = 0.0;
|
||||||
|
|
@ -109,12 +140,17 @@ export default function animate(
|
||||||
// Graph creation and layout
|
// Graph creation and layout
|
||||||
const graph = createGraph(nodes, edges, forNode, forEdge);
|
const graph = createGraph(nodes, edges, forNode, forEdge);
|
||||||
|
|
||||||
// Improved layout parameters for better visualization
|
// Adaptive layout parameters based on graph size
|
||||||
|
const nodeCount = nodes.length;
|
||||||
|
const isLargeGraph = nodeCount > 5000;
|
||||||
|
const isMassiveGraph = nodeCount > 15000;
|
||||||
|
|
||||||
|
// Apple embedding atlas style: stronger repulsion for clear cluster separation
|
||||||
const graphLayout = createForceLayout(graph, {
|
const graphLayout = createForceLayout(graph, {
|
||||||
dragCoefficient: 0.8, // Reduced for smoother movement
|
dragCoefficient: isMassiveGraph ? 0.95 : 0.85,
|
||||||
springLength: 180, // Slightly tighter clustering
|
springLength: isMassiveGraph ? 120 : isLargeGraph ? 180 : 220, // Longer springs for spacing
|
||||||
springCoefficient: 0.25, // Stronger connections
|
springCoefficient: isMassiveGraph ? 0.12 : isLargeGraph ? 0.15 : 0.18, // Weaker springs
|
||||||
gravity: -1200, // Stronger repulsion for better spread
|
gravity: isMassiveGraph ? -1200 : isLargeGraph ? -1500 : -1800, // Stronger repulsion
|
||||||
});
|
});
|
||||||
|
|
||||||
// Node Mesh
|
// Node Mesh
|
||||||
|
|
@ -127,6 +163,8 @@ export default function animate(
|
||||||
nodes,
|
nodes,
|
||||||
nodePositionsTexture,
|
nodePositionsTexture,
|
||||||
nodeColors,
|
nodeColors,
|
||||||
|
nodeSizes,
|
||||||
|
nodeHighlights,
|
||||||
INITIAL_CAMERA_DISTANCE
|
INITIAL_CAMERA_DISTANCE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -137,9 +175,10 @@ export default function animate(
|
||||||
INITIAL_CAMERA_DISTANCE
|
INITIAL_CAMERA_DISTANCE
|
||||||
);
|
);
|
||||||
|
|
||||||
// Density cloud setup
|
// Density cloud setup - adaptive resolution for performance
|
||||||
const densityCloudScene = new Scene();
|
const densityCloudScene = new Scene();
|
||||||
const densityCloudTarget = createDensityRenderTarget(512);
|
const densityResolution = isMassiveGraph ? 256 : isLargeGraph ? 384 : 512;
|
||||||
|
const densityCloudTarget = createDensityRenderTarget(densityResolution);
|
||||||
|
|
||||||
const densityAccumulatorMesh = createDensityAccumulatorMesh(
|
const densityAccumulatorMesh = createDensityAccumulatorMesh(
|
||||||
nodes,
|
nodes,
|
||||||
|
|
@ -167,9 +206,18 @@ export default function animate(
|
||||||
pickNodeFromScene(event);
|
pickNodeFromScene(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Group nodes by type for cluster boundaries
|
||||||
|
const nodesByType = new Map<string, Node[]>();
|
||||||
|
nodes.forEach(node => {
|
||||||
|
if (!nodesByType.has(node.type)) {
|
||||||
|
nodesByType.set(node.type, []);
|
||||||
|
}
|
||||||
|
nodesByType.get(node.type)!.push(node);
|
||||||
|
});
|
||||||
|
|
||||||
const scene = new Scene();
|
const scene = new Scene();
|
||||||
// Darker background for better contrast with vibrant colors
|
// Apple embedding atlas style: pure black background
|
||||||
scene.background = new Color("#0a0a0f");
|
scene.background = new Color("#000000");
|
||||||
const renderer = new WebGLRenderer({ antialias: true });
|
const renderer = new WebGLRenderer({ antialias: true });
|
||||||
|
|
||||||
renderer.setPixelRatio(window.devicePixelRatio);
|
renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
|
|
@ -259,29 +307,119 @@ export default function animate(
|
||||||
});
|
});
|
||||||
// Node picking setup end
|
// Node picking setup end
|
||||||
|
|
||||||
// Setup scene - More layout iterations for better initial positioning
|
// Adaptive layout iterations based on graph size
|
||||||
for (let i = 0; i < 800; i++) {
|
const layoutIterations = isMassiveGraph ? 300 : isLargeGraph ? 500 : 800;
|
||||||
|
console.log(`Running ${layoutIterations} layout iterations for ${nodeCount} nodes...`);
|
||||||
|
|
||||||
|
for (let i = 0; i < layoutIterations; i++) {
|
||||||
graphLayout.step();
|
graphLayout.step();
|
||||||
|
|
||||||
|
// Progress logging for large graphs
|
||||||
|
if (isMassiveGraph && i % 50 === 0) {
|
||||||
|
console.log(`Layout progress: ${((i / layoutIterations) * 100).toFixed(0)}%`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
console.log("Layout complete!");
|
||||||
|
|
||||||
let visibleLabels: unknown[] = [];
|
let visibleLabels: unknown[] = [];
|
||||||
|
|
||||||
|
// Only create entity type labels for smaller graphs (performance optimization)
|
||||||
const entityTypeLabels: [string, unknown][] = [];
|
const entityTypeLabels: [string, unknown][] = [];
|
||||||
for (const node of nodes) {
|
if (!isMassiveGraph) {
|
||||||
if (node.type === "EntityType") {
|
for (const node of nodes) {
|
||||||
const label = createLabel(node.label, config?.fontSize);
|
if (node.type === "EntityType") {
|
||||||
entityTypeLabels.push([node.id, label]);
|
const label = createLabel(node.label, config?.fontSize);
|
||||||
|
entityTypeLabels.push([node.id, label]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// const processingStep = 0;
|
// const processingStep = 0;
|
||||||
|
|
||||||
|
// Performance monitoring
|
||||||
|
let frameCount = 0;
|
||||||
|
let lastFpsUpdate = performance.now();
|
||||||
|
let currentFps = 60;
|
||||||
|
|
||||||
|
// Cluster boundaries
|
||||||
|
let clusterBoundariesCreated = false;
|
||||||
|
const clusterBoundaryMeshes: three.Mesh[] = [];
|
||||||
|
|
||||||
|
function calculateClusterBoundaries(): ClusterInfo[] {
|
||||||
|
const clusters: ClusterInfo[] = [];
|
||||||
|
|
||||||
|
nodesByType.forEach((typeNodes, nodeType) => {
|
||||||
|
if (typeNodes.length < 3) return; // Skip small clusters
|
||||||
|
|
||||||
|
// Calculate center and radius from actual node positions
|
||||||
|
let sumX = 0;
|
||||||
|
let sumY = 0;
|
||||||
|
typeNodes.forEach(node => {
|
||||||
|
const pos = graphLayout.getNodePosition(node.id);
|
||||||
|
sumX += pos.x;
|
||||||
|
sumY += pos.y;
|
||||||
|
});
|
||||||
|
|
||||||
|
const center = {
|
||||||
|
x: sumX / typeNodes.length,
|
||||||
|
y: sumY / typeNodes.length
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate radius as max distance from center + padding
|
||||||
|
let maxDist = 0;
|
||||||
|
typeNodes.forEach(node => {
|
||||||
|
const pos = graphLayout.getNodePosition(node.id);
|
||||||
|
const dist = Math.sqrt(
|
||||||
|
Math.pow(pos.x - center.x, 2) + Math.pow(pos.y - center.y, 2)
|
||||||
|
);
|
||||||
|
maxDist = Math.max(maxDist, dist);
|
||||||
|
});
|
||||||
|
|
||||||
|
clusters.push({
|
||||||
|
center,
|
||||||
|
radius: maxDist + 350, // Apple style: more spacing between clusters
|
||||||
|
color: getColorForType(nodeType)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return clusters;
|
||||||
|
}
|
||||||
|
|
||||||
// Render loop
|
// Render loop
|
||||||
function render() {
|
function render() {
|
||||||
graphLayout.step();
|
// Adaptive physics updates - skip for large graphs after stabilization
|
||||||
|
if (!isMassiveGraph || frameCount < 100) {
|
||||||
|
graphLayout.step();
|
||||||
|
} else if (frameCount % 2 === 0) {
|
||||||
|
// Update physics every other frame for massive graphs
|
||||||
|
graphLayout.step();
|
||||||
|
}
|
||||||
|
|
||||||
|
// FPS monitoring
|
||||||
|
frameCount++;
|
||||||
|
const now = performance.now();
|
||||||
|
if (now - lastFpsUpdate > 1000) {
|
||||||
|
currentFps = Math.round((frameCount * 1000) / (now - lastFpsUpdate));
|
||||||
|
frameCount = 0;
|
||||||
|
lastFpsUpdate = now;
|
||||||
|
if (isMassiveGraph && currentFps < 30) {
|
||||||
|
console.warn(`Low FPS detected: ${currentFps} fps`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
controls.update();
|
controls.update();
|
||||||
|
|
||||||
|
// Create cluster boundaries after layout stabilizes
|
||||||
|
if (!clusterBoundariesCreated && frameCount === 60) {
|
||||||
|
const clusters = calculateClusterBoundaries();
|
||||||
|
clusters.forEach(cluster => {
|
||||||
|
const mesh = createClusterBoundaryMesh(cluster);
|
||||||
|
clusterBoundaryMeshes.push(mesh);
|
||||||
|
scene.add(mesh);
|
||||||
|
});
|
||||||
|
clusterBoundariesCreated = true;
|
||||||
|
}
|
||||||
|
|
||||||
updateNodePositions(
|
updateNodePositions(
|
||||||
nodes,
|
nodes,
|
||||||
graphLayout,
|
graphLayout,
|
||||||
|
|
@ -308,24 +446,53 @@ export default function animate(
|
||||||
// @ts-expect-error uniforms does exist on material
|
// @ts-expect-error uniforms does exist on material
|
||||||
pickingMesh.material.uniforms.camDist.value = Math.floor(camera.zoom * 500);
|
pickingMesh.material.uniforms.camDist.value = Math.floor(camera.zoom * 500);
|
||||||
|
|
||||||
|
// Zoom-level semantics: determine what to show based on zoom
|
||||||
|
let zoomLevel: "far" | "mid" | "near" = "mid";
|
||||||
|
if (camera.zoom < 1.0) {
|
||||||
|
zoomLevel = "far"; // Show clusters, domains, density
|
||||||
|
} else if (camera.zoom > 3.0) {
|
||||||
|
zoomLevel = "near"; // Show applications, paths, labels
|
||||||
|
}
|
||||||
|
|
||||||
edgeMesh.renderOrder = 1;
|
edgeMesh.renderOrder = 1;
|
||||||
nodeSwarmMesh.renderOrder = 2;
|
nodeSwarmMesh.renderOrder = 2;
|
||||||
|
|
||||||
scene.add(edgeMesh);
|
// IMPROVEMENT #8: Conditional layer rendering based on config and zoom
|
||||||
scene.add(nodeSwarmMesh);
|
const showEdges = config?.showEdges !== false && zoomLevel !== "far"; // Hide edges when far
|
||||||
|
const showNodes = config?.showNodes !== false; // Always show nodes
|
||||||
|
const showMetaballs = config?.showMetaballs !== false && zoomLevel === "far"; // Only show at far zoom
|
||||||
|
|
||||||
// Pass 1: draw points into density texture
|
// Path filtering based on hover
|
||||||
renderer.setRenderTarget(densityCloudTarget);
|
const pathFilterMode = config?.pathFilterMode || "all";
|
||||||
renderer.clear();
|
const shouldShowPath = pathFilterMode === "all" || (pathFilterMode === "hoverOnly" && pickedNodeIndex >= 0);
|
||||||
densityCloudScene.clear();
|
|
||||||
densityCloudScene.add(densityAccumulatorMesh);
|
|
||||||
renderer.render(densityCloudScene, camera);
|
|
||||||
|
|
||||||
// Pass 2: render density map to screen
|
if (showEdges) {
|
||||||
renderer.setRenderTarget(null);
|
scene.add(edgeMesh);
|
||||||
renderer.clear();
|
}
|
||||||
metaballMesh.renderOrder = 0;
|
if (showNodes) {
|
||||||
scene.add(metaballMesh);
|
scene.add(nodeSwarmMesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metaball rendering - reduce frequency for massive graphs
|
||||||
|
const shouldRenderMetaballs = showMetaballs && (!isMassiveGraph || frameCount % 2 === 0);
|
||||||
|
|
||||||
|
if (shouldRenderMetaballs) {
|
||||||
|
// Pass 1: draw points into density texture
|
||||||
|
renderer.setRenderTarget(densityCloudTarget);
|
||||||
|
renderer.clear();
|
||||||
|
densityCloudScene.clear();
|
||||||
|
densityCloudScene.add(densityAccumulatorMesh);
|
||||||
|
renderer.render(densityCloudScene, camera);
|
||||||
|
|
||||||
|
// Pass 2: render density map to screen
|
||||||
|
renderer.setRenderTarget(null);
|
||||||
|
renderer.clear();
|
||||||
|
metaballMesh.renderOrder = 0;
|
||||||
|
scene.add(metaballMesh);
|
||||||
|
} else {
|
||||||
|
renderer.setRenderTarget(null);
|
||||||
|
renderer.clear();
|
||||||
|
}
|
||||||
|
|
||||||
for (const [nodeId, label] of entityTypeLabels) {
|
for (const [nodeId, label] of entityTypeLabels) {
|
||||||
const nodePosition = graphLayout.getNodePosition(nodeId);
|
const nodePosition = graphLayout.getNodePosition(nodeId);
|
||||||
|
|
@ -364,12 +531,15 @@ export default function animate(
|
||||||
);
|
);
|
||||||
pickedNodeLabel.scale.setScalar(textScale);
|
pickedNodeLabel.scale.setScalar(textScale);
|
||||||
|
|
||||||
if (camera.zoom > 2) {
|
// Adaptive label display based on graph size and zoom
|
||||||
|
const minZoomForLabels = isMassiveGraph ? 4 : isLargeGraph ? 3 : 2;
|
||||||
|
const maxLabels = isMassiveGraph ? 5 : isLargeGraph ? 10 : 15;
|
||||||
|
|
||||||
|
if (camera.zoom > minZoomForLabels) {
|
||||||
graph.forEachLinkedNode(
|
graph.forEachLinkedNode(
|
||||||
pickedNode.id,
|
pickedNode.id,
|
||||||
(otherNode: GraphNode, edge: GraphLink) => {
|
(otherNode: GraphNode, edge: GraphLink) => {
|
||||||
// Show more labels when zoomed in further
|
if (visibleLabels.length > maxLabels) {
|
||||||
if (visibleLabels.length > 15) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -431,10 +601,29 @@ export default function animate(
|
||||||
|
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
|
|
||||||
requestAnimationFrame(render);
|
animationFrameId = requestAnimationFrame(render);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let animationFrameId: number;
|
||||||
render();
|
render();
|
||||||
|
|
||||||
|
// Return cleanup function
|
||||||
|
return () => {
|
||||||
|
if (animationFrameId) {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
}
|
||||||
|
// Clean up cluster boundaries
|
||||||
|
clusterBoundaryMeshes.forEach(mesh => {
|
||||||
|
scene.remove(mesh);
|
||||||
|
mesh.geometry.dispose();
|
||||||
|
if (mesh.material instanceof three.Material) {
|
||||||
|
mesh.material.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
graphLayout.dispose();
|
||||||
|
renderer.dispose();
|
||||||
|
controls.dispose();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateNodePositions(
|
function updateNodePositions(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import * as three from "three";
|
||||||
|
|
||||||
|
export default function createClusterBoundaryMaterial(
|
||||||
|
clusterColor: three.Color
|
||||||
|
): three.ShaderMaterial {
|
||||||
|
const material = new three.ShaderMaterial({
|
||||||
|
transparent: true,
|
||||||
|
depthWrite: false,
|
||||||
|
side: three.DoubleSide,
|
||||||
|
uniforms: {
|
||||||
|
clusterColor: { value: clusterColor },
|
||||||
|
},
|
||||||
|
vertexShader: `
|
||||||
|
varying vec2 vUv;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vUv = uv;
|
||||||
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
fragmentShader: `
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
uniform vec3 clusterColor;
|
||||||
|
varying vec2 vUv;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Apple embedding atlas style: soft circular regions
|
||||||
|
vec2 center = vec2(0.5, 0.5);
|
||||||
|
float dist = length(vUv - center);
|
||||||
|
|
||||||
|
// Soft radial gradient background
|
||||||
|
float alpha = smoothstep(0.5, 0.25, dist) * 0.12; // More visible background
|
||||||
|
|
||||||
|
// Prominent boundary ring (Apple style)
|
||||||
|
float ring = smoothstep(0.49, 0.47, dist) - smoothstep(0.51, 0.49, dist);
|
||||||
|
alpha += ring * 0.25; // More prominent border
|
||||||
|
|
||||||
|
// Lighter, more vibrant colors for Apple aesthetic
|
||||||
|
vec3 bgColor = clusterColor * 1.1;
|
||||||
|
|
||||||
|
gl_FragColor = vec4(bgColor, alpha);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return material;
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,8 @@ export default function createEdgeMaterial(
|
||||||
textureSize: { value: texture.image.width },
|
textureSize: { value: texture.image.width },
|
||||||
camDist: { value: initialCameraDistance },
|
camDist: { value: initialCameraDistance },
|
||||||
mousePos: { value: new three.Vector2(9999, 9999) }, // start offscreen
|
mousePos: { value: new three.Vector2(9999, 9999) }, // start offscreen
|
||||||
color: { value: new three.Color(0xffffff) },
|
// Apple embedding atlas style: soft pastel edges
|
||||||
|
color: { value: new three.Color("#FCD34D") }, // Soft amber for minimalist aesthetic
|
||||||
},
|
},
|
||||||
vertexShader: `
|
vertexShader: `
|
||||||
attribute vec2 edgeIndices;
|
attribute vec2 edgeIndices;
|
||||||
|
|
@ -24,6 +25,7 @@ export default function createEdgeMaterial(
|
||||||
|
|
||||||
varying float vFade;
|
varying float vFade;
|
||||||
varying float vHighlight;
|
varying float vHighlight;
|
||||||
|
varying float vEdgePosition; // IMPROVEMENT #2: For directional gradient
|
||||||
|
|
||||||
vec3 getNodePos(float idx) {
|
vec3 getNodePos(float idx) {
|
||||||
float x = mod(idx, textureSize);
|
float x = mod(idx, textureSize);
|
||||||
|
|
@ -37,6 +39,9 @@ export default function createEdgeMaterial(
|
||||||
vec3 end = getNodePos(edgeIndices.y);
|
vec3 end = getNodePos(edgeIndices.y);
|
||||||
vec3 nodePos = mix(start, end, position.x);
|
vec3 nodePos = mix(start, end, position.x);
|
||||||
|
|
||||||
|
// IMPROVEMENT #2: Pass edge position for gradient
|
||||||
|
vEdgePosition = position.x;
|
||||||
|
|
||||||
// Project world-space position to clip-space
|
// Project world-space position to clip-space
|
||||||
vec4 clipPos = projectionMatrix * modelViewMatrix * vec4(nodePos, 1.0);
|
vec4 clipPos = projectionMatrix * modelViewMatrix * vec4(nodePos, 1.0);
|
||||||
vec3 ndc = clipPos.xyz / clipPos.w; // normalized device coordinates [-1,1]
|
vec3 ndc = clipPos.xyz / clipPos.w; // normalized device coordinates [-1,1]
|
||||||
|
|
@ -44,8 +49,9 @@ export default function createEdgeMaterial(
|
||||||
float distanceFromMouse = length(ndc.xy - mousePos);
|
float distanceFromMouse = length(ndc.xy - mousePos);
|
||||||
vHighlight = smoothstep(0.2, 0.0, distanceFromMouse);
|
vHighlight = smoothstep(0.2, 0.0, distanceFromMouse);
|
||||||
|
|
||||||
|
// Apple embedding atlas style: subtle edge opacity
|
||||||
vFade = smoothstep(500.0, 1500.0, camDist);
|
vFade = smoothstep(500.0, 1500.0, camDist);
|
||||||
vFade = 0.2 * clamp(vFade, 0.0, 1.0);
|
vFade = 0.25 * clamp(vFade, 0.0, 1.0); // Subtle for clean aesthetic
|
||||||
|
|
||||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(nodePos, 1.0);
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(nodePos, 1.0);
|
||||||
}
|
}
|
||||||
|
|
@ -54,13 +60,24 @@ export default function createEdgeMaterial(
|
||||||
precision highp float;
|
precision highp float;
|
||||||
|
|
||||||
uniform vec3 color;
|
uniform vec3 color;
|
||||||
varying vec3 vColor;
|
|
||||||
varying float vFade;
|
varying float vFade;
|
||||||
varying float vHighlight;
|
varying float vHighlight;
|
||||||
|
varying float vEdgePosition; // IMPROVEMENT #2: For directional gradient
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vec3 finalColor = mix(color, vec3(1.0), vHighlight * 0.8);
|
// IMPROVEMENT #2: Directional gradient from start to end
|
||||||
float alpha = mix(vFade, 0.8, vHighlight);
|
// Brighter at start, slightly darker at end for flow direction
|
||||||
|
float gradientFactor = 1.0 - (vEdgePosition * 0.3); // 30% dimming from start to end
|
||||||
|
|
||||||
|
// IMPROVEMENT #2: Add subtle glow effect
|
||||||
|
vec3 glowColor = vec3(1.0, 0.9, 0.7); // Warm white glow
|
||||||
|
vec3 baseColor = color * gradientFactor;
|
||||||
|
vec3 finalColor = mix(baseColor, glowColor, vHighlight * 0.9);
|
||||||
|
|
||||||
|
// IMPROVEMENT #2: Increased visibility and glow
|
||||||
|
float baseAlpha = vFade * 1.5; // Increased visibility
|
||||||
|
float alpha = mix(baseAlpha, 0.95, vHighlight); // Stronger highlight
|
||||||
|
|
||||||
gl_FragColor = vec4(finalColor, alpha);
|
gl_FragColor = vec4(finalColor, alpha);
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,9 @@ export function createMetaballMaterial(fieldTexture: Texture) {
|
||||||
finalColor = accumulatedColor / totalInfluence;
|
finalColor = accumulatedColor / totalInfluence;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Smooth transition around threshold
|
// Apple embedding atlas style: very subtle density clouds
|
||||||
float alphaEdge = smoothstep(threshold - smoothing, threshold + smoothing, totalInfluence);
|
float alphaEdge = smoothstep(threshold - smoothing, threshold + smoothing, totalInfluence);
|
||||||
float alpha = alphaEdge * 0.3;
|
float alpha = alphaEdge * 0.08; // Very subtle for clean Apple aesthetic
|
||||||
|
|
||||||
if (alpha < 0.01) {
|
if (alpha < 0.01) {
|
||||||
discard;
|
discard;
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,12 @@ export default function createNodeSwarmMaterial(
|
||||||
uniform float camDist;
|
uniform float camDist;
|
||||||
uniform vec2 mousePos;
|
uniform vec2 mousePos;
|
||||||
attribute vec3 nodeColor;
|
attribute vec3 nodeColor;
|
||||||
|
attribute float nodeSize; // Hierarchy-based size multiplier
|
||||||
|
attribute float nodeHighlight; // Selection-based highlight (1.0 = selected, 0.3 = dimmed)
|
||||||
varying vec3 vColor;
|
varying vec3 vColor;
|
||||||
varying float vHighlight;
|
varying float vHighlight;
|
||||||
|
varying float vSelectionHighlight;
|
||||||
|
varying vec2 vUv; // IMPROVEMENT #4: For radial halo effect
|
||||||
|
|
||||||
vec3 getNodePos(float idx) {
|
vec3 getNodePos(float idx) {
|
||||||
float size = textureSize;
|
float size = textureSize;
|
||||||
|
|
@ -33,8 +37,12 @@ export default function createNodeSwarmMaterial(
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vColor = nodeColor;
|
vColor = nodeColor;
|
||||||
|
vSelectionHighlight = nodeHighlight;
|
||||||
vec3 nodePos = getNodePos(float(gl_InstanceID));
|
vec3 nodePos = getNodePos(float(gl_InstanceID));
|
||||||
|
|
||||||
|
// IMPROVEMENT #4: Pass UV coordinates for halo effect
|
||||||
|
vUv = position.xy * 0.5 + 0.5; // Convert from [-1,1] to [0,1]
|
||||||
|
|
||||||
// Project world-space position to clip-space
|
// Project world-space position to clip-space
|
||||||
vec4 clipPos = projectionMatrix * modelViewMatrix * vec4(nodePos, 1.0);
|
vec4 clipPos = projectionMatrix * modelViewMatrix * vec4(nodePos, 1.0);
|
||||||
vec3 ndc = clipPos.xyz / clipPos.w; // normalized device coordinates [-1,1]
|
vec3 ndc = clipPos.xyz / clipPos.w; // normalized device coordinates [-1,1]
|
||||||
|
|
@ -42,13 +50,14 @@ export default function createNodeSwarmMaterial(
|
||||||
float distanceFromMouse = length(ndc.xy - mousePos);
|
float distanceFromMouse = length(ndc.xy - mousePos);
|
||||||
vHighlight = smoothstep(0.2, 0.0, distanceFromMouse);
|
vHighlight = smoothstep(0.2, 0.0, distanceFromMouse);
|
||||||
|
|
||||||
float baseNodeSize = 8.0;
|
// Hierarchy-based sizing: base size * type size multiplier
|
||||||
|
float baseNodeSize = 7.0;
|
||||||
|
|
||||||
// Normalize camera distance into [0,1]
|
// Normalize camera distance into [0,1]
|
||||||
float t = clamp((camDist - 500.0) / (2000.0 - 500.0), 0.0, 1.0);
|
float t = clamp((camDist - 500.0) / (2000.0 - 500.0), 0.0, 1.0);
|
||||||
float nodeSize = baseNodeSize * mix(1.1, 1.3, t);
|
float finalSize = baseNodeSize * nodeSize * mix(1.0, 1.2, t); // Apply hierarchy multiplier
|
||||||
|
|
||||||
vec3 transformed = nodePos + position * nodeSize;
|
vec3 transformed = nodePos + position * finalSize;
|
||||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|
@ -57,11 +66,36 @@ export default function createNodeSwarmMaterial(
|
||||||
|
|
||||||
varying vec3 vColor;
|
varying vec3 vColor;
|
||||||
varying float vHighlight;
|
varying float vHighlight;
|
||||||
|
varying float vSelectionHighlight;
|
||||||
|
varying vec2 vUv; // IMPROVEMENT #4: For radial halo effect
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vec3 finalColor = mix(vColor, vec3(1.0), vHighlight * 0.3);
|
// Apple embedding atlas style: subtle radial glow
|
||||||
gl_FragColor = vec4(finalColor, 1.0);
|
vec2 center = vec2(0.5, 0.5);
|
||||||
// gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
|
float distFromCenter = length(vUv - center) * 2.0;
|
||||||
|
|
||||||
|
// Create sharp node with very subtle glow
|
||||||
|
float coreRadius = 0.75; // Slightly larger core
|
||||||
|
float haloRadius = 1.0;
|
||||||
|
|
||||||
|
// Core node (solid)
|
||||||
|
float core = 1.0 - smoothstep(0.0, coreRadius, distFromCenter);
|
||||||
|
|
||||||
|
// Very subtle outer glow (Apple aesthetic)
|
||||||
|
float halo = smoothstep(haloRadius, coreRadius, distFromCenter);
|
||||||
|
|
||||||
|
// Subtle color mixing
|
||||||
|
vec3 haloColor = vColor * 1.15; // Subtle brightness increase
|
||||||
|
vec3 baseColor = mix(vColor, vec3(1.0), vHighlight * 0.4);
|
||||||
|
vec3 finalColor = mix(haloColor, baseColor, core);
|
||||||
|
|
||||||
|
// Alpha with subtle glow
|
||||||
|
float alpha = mix(halo * 0.4, 1.0, core); // Reduced halo opacity
|
||||||
|
|
||||||
|
// Apply selection-based dimming (neutral-by-default)
|
||||||
|
alpha *= vSelectionHighlight;
|
||||||
|
|
||||||
|
gl_FragColor = vec4(finalColor, alpha);
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import * as three from "three";
|
||||||
|
import createClusterBoundaryMaterial from "../materials/createClusterBoundaryMaterial";
|
||||||
|
|
||||||
|
export interface ClusterInfo {
|
||||||
|
center: { x: number; y: number };
|
||||||
|
radius: number;
|
||||||
|
color: three.Color;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function createClusterBoundaryMesh(
|
||||||
|
cluster: ClusterInfo
|
||||||
|
): three.Mesh {
|
||||||
|
// Create a circle geometry for the cluster boundary
|
||||||
|
const geometry = new three.PlaneGeometry(
|
||||||
|
cluster.radius * 2.5, // Make it larger to encompass the cluster
|
||||||
|
cluster.radius * 2.5
|
||||||
|
);
|
||||||
|
|
||||||
|
const material = createClusterBoundaryMaterial(cluster.color);
|
||||||
|
const mesh = new three.Mesh(geometry, material);
|
||||||
|
|
||||||
|
// Position the mesh at the cluster center
|
||||||
|
mesh.position.set(cluster.center.x, cluster.center.y, -100); // Behind everything else
|
||||||
|
mesh.renderOrder = -1;
|
||||||
|
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,8 @@ export default function createNodeSwarmMesh(
|
||||||
nodes: Node[],
|
nodes: Node[],
|
||||||
nodePositionsTexture: DataTexture,
|
nodePositionsTexture: DataTexture,
|
||||||
nodeColors: Float32Array,
|
nodeColors: Float32Array,
|
||||||
|
nodeSizes: Float32Array,
|
||||||
|
nodeHighlights: Float32Array,
|
||||||
initialCameraDistance: number
|
initialCameraDistance: number
|
||||||
) {
|
) {
|
||||||
const nodeGeom = new CircleGeometry(2, 16);
|
const nodeGeom = new CircleGeometry(2, 16);
|
||||||
|
|
@ -22,6 +24,8 @@ export default function createNodeSwarmMesh(
|
||||||
geom.setAttribute("position", nodeGeom.attributes.position);
|
geom.setAttribute("position", nodeGeom.attributes.position);
|
||||||
geom.setAttribute("uv", nodeGeom.attributes.uv);
|
geom.setAttribute("uv", nodeGeom.attributes.uv);
|
||||||
geom.setAttribute("nodeColor", new InstancedBufferAttribute(nodeColors, 3));
|
geom.setAttribute("nodeColor", new InstancedBufferAttribute(nodeColors, 3));
|
||||||
|
geom.setAttribute("nodeSize", new InstancedBufferAttribute(nodeSizes, 1));
|
||||||
|
geom.setAttribute("nodeHighlight", new InstancedBufferAttribute(nodeHighlights, 1));
|
||||||
|
|
||||||
const material = createNodeSwarmMaterial(
|
const material = createNodeSwarmMaterial(
|
||||||
nodePositionsTexture,
|
nodePositionsTexture,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue