Browse Source
- Added PersonAnchor type to NetworkNode types - Created personNetworkBuilder utility for extracting and creating person nodes - Added person visualizer section to Legend component with collapsible UI - Implemented diamond shape rendering for person nodes in D3 visualization - Added state management for show/hide person nodes and individual toggles - Auto-disable all person nodes by default to prevent graph overload - Integrated person node filtering into the graph update logic - Added green color (#10B981) for person anchor nodes - Fixed duplicate handleTagToggle function declaration Person nodes now display as diamond shapes and can be individually toggled for visibility. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>master
4 changed files with 427 additions and 29 deletions
@ -0,0 +1,167 @@ |
|||||||
|
/** |
||||||
|
* Person Network Builder |
||||||
|
* |
||||||
|
* Creates person anchor nodes for event authors in the network visualization |
||||||
|
*/ |
||||||
|
|
||||||
|
import type { NDKEvent } from "@nostr-dev-kit/ndk"; |
||||||
|
import type { NetworkNode, NetworkLink } from "../types"; |
||||||
|
import { getDisplayNameSync } from "$lib/utils/profileCache"; |
||||||
|
|
||||||
|
const PERSON_ANCHOR_RADIUS = 15; |
||||||
|
const PERSON_ANCHOR_PLACEMENT_RADIUS = 1000; |
||||||
|
|
||||||
|
/** |
||||||
|
* Simple seeded random number generator |
||||||
|
*/ |
||||||
|
class SeededRandom { |
||||||
|
private seed: number; |
||||||
|
|
||||||
|
constructor(seed: number) { |
||||||
|
this.seed = seed; |
||||||
|
} |
||||||
|
|
||||||
|
next(): number { |
||||||
|
this.seed = (this.seed * 9301 + 49297) % 233280; |
||||||
|
return this.seed / 233280; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a deterministic seed from a string |
||||||
|
*/ |
||||||
|
function createSeed(str: string): number { |
||||||
|
let hash = 0; |
||||||
|
for (let i = 0; i < str.length; i++) { |
||||||
|
const char = str.charCodeAt(i); |
||||||
|
hash = (hash << 5) - hash + char; |
||||||
|
hash = hash & hash; |
||||||
|
} |
||||||
|
return Math.abs(hash); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Extracts unique persons (pubkeys) from events |
||||||
|
*/ |
||||||
|
export function extractUniquePersons( |
||||||
|
events: NDKEvent[] |
||||||
|
): Map<string, Set<string>> { |
||||||
|
// Map of pubkey -> Set of event IDs
|
||||||
|
const personMap = new Map<string, Set<string>>(); |
||||||
|
|
||||||
|
console.log(`[PersonBuilder] Extracting persons from ${events.length} events`); |
||||||
|
|
||||||
|
events.forEach((event) => { |
||||||
|
if (!event.pubkey || !event.id) return; |
||||||
|
|
||||||
|
if (!personMap.has(event.pubkey)) { |
||||||
|
personMap.set(event.pubkey, new Set()); |
||||||
|
} |
||||||
|
|
||||||
|
personMap.get(event.pubkey)!.add(event.id); |
||||||
|
}); |
||||||
|
|
||||||
|
console.log(`[PersonBuilder] Found ${personMap.size} unique persons`); |
||||||
|
|
||||||
|
return personMap; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates person anchor nodes |
||||||
|
*/ |
||||||
|
export function createPersonAnchorNodes( |
||||||
|
personMap: Map<string, Set<string>>, |
||||||
|
width: number, |
||||||
|
height: number |
||||||
|
): NetworkNode[] { |
||||||
|
const anchorNodes: NetworkNode[] = []; |
||||||
|
|
||||||
|
const centerX = width / 2; |
||||||
|
const centerY = height / 2; |
||||||
|
|
||||||
|
Array.from(personMap.entries()).forEach(([pubkey, eventIds]) => { |
||||||
|
// Create seeded random generator for consistent positioning
|
||||||
|
const rng = new SeededRandom(createSeed(pubkey)); |
||||||
|
|
||||||
|
// Generate deterministic position
|
||||||
|
const angle = rng.next() * 2 * Math.PI; |
||||||
|
const distance = rng.next() * PERSON_ANCHOR_PLACEMENT_RADIUS; |
||||||
|
const x = centerX + distance * Math.cos(angle); |
||||||
|
const y = centerY + distance * Math.sin(angle); |
||||||
|
|
||||||
|
// Get display name
|
||||||
|
const displayName = getDisplayNameSync(pubkey); |
||||||
|
|
||||||
|
const anchorNode: NetworkNode = { |
||||||
|
id: `person-anchor-${pubkey}`, |
||||||
|
title: displayName, |
||||||
|
content: `${eventIds.size} events`, |
||||||
|
author: "", |
||||||
|
kind: 0, // Special kind for anchors
|
||||||
|
type: "PersonAnchor", |
||||||
|
level: -1, |
||||||
|
isPersonAnchor: true, |
||||||
|
pubkey, |
||||||
|
displayName, |
||||||
|
connectedNodes: Array.from(eventIds), |
||||||
|
x, |
||||||
|
y, |
||||||
|
fx: x, // Fix position
|
||||||
|
fy: y, |
||||||
|
}; |
||||||
|
|
||||||
|
anchorNodes.push(anchorNode); |
||||||
|
}); |
||||||
|
|
||||||
|
return anchorNodes; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates links between person anchors and their events |
||||||
|
*/ |
||||||
|
export function createPersonLinks( |
||||||
|
personAnchors: NetworkNode[], |
||||||
|
nodes: NetworkNode[] |
||||||
|
): NetworkLink[] { |
||||||
|
const links: NetworkLink[] = []; |
||||||
|
const nodeMap = new Map(nodes.map((n) => [n.id, n])); |
||||||
|
|
||||||
|
personAnchors.forEach((anchor) => { |
||||||
|
if (!anchor.connectedNodes) return; |
||||||
|
|
||||||
|
anchor.connectedNodes.forEach((nodeId) => { |
||||||
|
const node = nodeMap.get(nodeId); |
||||||
|
if (node) { |
||||||
|
links.push({ |
||||||
|
source: anchor, |
||||||
|
target: node, |
||||||
|
isSequential: false, |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
return links; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Formats person anchor info for display in Legend |
||||||
|
*/ |
||||||
|
export interface PersonAnchorInfo { |
||||||
|
pubkey: string; |
||||||
|
displayName: string; |
||||||
|
eventCount: number; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Extracts person info for Legend display |
||||||
|
*/ |
||||||
|
export function extractPersonAnchorInfo( |
||||||
|
personAnchors: NetworkNode[] |
||||||
|
): PersonAnchorInfo[] { |
||||||
|
return personAnchors.map(anchor => ({ |
||||||
|
pubkey: anchor.pubkey || "", |
||||||
|
displayName: anchor.displayName || "", |
||||||
|
eventCount: anchor.connectedNodes?.length || 0, |
||||||
|
})); |
||||||
|
} |
||||||
Loading…
Reference in new issue