From 61b44548f210f2908b9952538bab06f44edb852f Mon Sep 17 00:00:00 2001 From: limina1 Date: Sat, 24 May 2025 20:37:51 -0400 Subject: [PATCH] Enable publication -> visualize view, tag depth --- src/lib/components/cards/BlogHeader.svelte | 1 + src/lib/navigator/EventNetwork/Legend.svelte | 10 +- .../navigator/EventNetwork/Settings.svelte | 79 ++++++++--- src/lib/navigator/EventNetwork/index.svelte | 134 +++++++++++++++++- .../EventNetwork/utils/tagNetworkBuilder.ts | 8 +- src/routes/visualize/+page.svelte | 86 ++++++++++- 6 files changed, 285 insertions(+), 33 deletions(-) diff --git a/src/lib/components/cards/BlogHeader.svelte b/src/lib/components/cards/BlogHeader.svelte index a91d0a4..b80b3cf 100644 --- a/src/lib/components/cards/BlogHeader.svelte +++ b/src/lib/components/cards/BlogHeader.svelte @@ -6,6 +6,7 @@ import Interactions from "$components/util/Interactions.svelte"; import { quintOut } from "svelte/easing"; import CardActions from "$components/util/CardActions.svelte"; + import { getMatchingTags } from '$lib/utils/nostrUtils'; const { rootId, event, onBlogUpdate, active = true } = $props<{ rootId: string, event: NDKEvent, onBlogUpdate?: any, active: boolean }>(); diff --git a/src/lib/navigator/EventNetwork/Legend.svelte b/src/lib/navigator/EventNetwork/Legend.svelte index a7f253f..46c5acc 100644 --- a/src/lib/navigator/EventNetwork/Legend.svelte +++ b/src/lib/navigator/EventNetwork/Legend.svelte @@ -12,12 +12,14 @@ starMode = false, showTags = false, tagAnchors = [], + eventCounts = {}, } = $props<{ collapsedOnInteraction: boolean; className: string; starMode?: boolean; showTags?: boolean; tagAnchors?: any[]; + eventCounts?: { [kind: number]: number }; }>(); let expanded = $state(true); @@ -65,7 +67,7 @@ Index events (kind 30040) - Star centers with unique colors{eventCounts[30040] || 0} Index events (kind 30040) - Star centers with unique colors @@ -77,7 +79,7 @@ Content nodes (kind 30041) - Arranged around star centers{eventCounts[30041] || 0} Content nodes (kind 30041) - Arranged around star centers @@ -107,7 +109,7 @@ Index events (kind 30040) - Each with a unique pastel color{eventCounts[30040] || 0} Index events (kind 30040) - Each with a unique pastel color @@ -119,7 +121,7 @@ Content events (kinds 30041, 30818) - Publication sections{(eventCounts[30041] || 0) + (eventCounts[30818] || 0)} Content events (kinds 30041, 30818) - Publication sections diff --git a/src/lib/navigator/EventNetwork/Settings.svelte b/src/lib/navigator/EventNetwork/Settings.svelte index 00e294e..cc31dc3 100644 --- a/src/lib/navigator/EventNetwork/Settings.svelte +++ b/src/lib/navigator/EventNetwork/Settings.svelte @@ -17,12 +17,14 @@ starVisualization = $bindable(true), showTagAnchors = $bindable(false), selectedTagType = $bindable("t"), + tagExpansionDepth = $bindable(0), } = $props<{ count: number; onupdate: () => void; starVisualization?: boolean; showTagAnchors?: boolean; selectedTagType?: string; + tagExpansionDepth?: number; }>(); let expanded = $state(false); @@ -36,6 +38,17 @@ function handleLimitUpdate() { onupdate(); } + + function handleDepthInput(event: Event) { + const input = event.target as HTMLInputElement; + const value = parseInt(input.value); + // Ensure value is between 0 and 10 + if (!isNaN(value) && value >= 0 && value <= 10) { + tagExpansionDepth = value; + } else if (input.value === "") { + tagExpansionDepth = 0; + } + }
@@ -87,25 +100,53 @@

{#if showTagAnchors} -
- - +
+
+ + +
+ +
+
+ + + + (0-10) + +
+

+ Fetch publications sharing tags +

+
{/if}
diff --git a/src/lib/navigator/EventNetwork/index.svelte b/src/lib/navigator/EventNetwork/index.svelte index 58ffffa..b09cc25 100644 --- a/src/lib/navigator/EventNetwork/index.svelte +++ b/src/lib/navigator/EventNetwork/index.svelte @@ -41,7 +41,7 @@ type Selection = any; // Configuration - const DEBUG = false; // Set to true to enable debug logging + const DEBUG = true; // Set to true to enable debug logging const NODE_RADIUS = 20; const LINK_DISTANCE = 10; const ARROW_DISTANCE = 10; @@ -58,9 +58,10 @@ } // Component props - let { events = [], onupdate } = $props<{ + let { events = [], onupdate, onTagExpansionChange } = $props<{ events?: NDKEvent[]; onupdate: () => void; + onTagExpansionChange?: (depth: number, tags: string[]) => void; }>(); // Error state @@ -94,6 +95,9 @@ let svgGroup: Selection; let zoomBehavior: any; let svgElement: Selection; + + // Position cache to preserve node positions across updates + let nodePositions = new Map(); // Track current render level let currentLevels = $derived(levelsToRender); @@ -105,6 +109,14 @@ let showTagAnchors = $state(false); let selectedTagType = $state("t"); // Default to hashtags let tagAnchorInfo = $state([]); + let tagExpansionDepth = $state(0); // Default to no expansion + + // Store initial state to detect if component is being recreated + let componentId = Math.random(); + debug("Component created with ID:", componentId); + + // Event counts by kind + let eventCounts = $state<{ [kind: number]: number }>({}); // Debug function - call from browser console: window.debugTagAnchors() if (typeof window !== "undefined") { @@ -224,6 +236,13 @@ // Enhance with tag anchors if enabled if (showTagAnchors) { + debug("Enhancing graph with tags", { + selectedTagType, + eventCount: events.length, + width, + height + }); + graphData = enhanceGraphWithTags( graphData, events, @@ -233,9 +252,14 @@ ); // Extract tag anchor info for legend - tagAnchorInfo = graphData.nodes - .filter((n) => n.isTagAnchor) - .map((n) => ({ + const tagAnchors = graphData.nodes.filter((n) => n.isTagAnchor); + + debug("Tag anchors created", { + count: tagAnchors.length, + anchors: tagAnchors + }); + + tagAnchorInfo = tagAnchors.map((n) => ({ type: n.tagType, label: n.title, count: n.connectedNodes?.length || 0, @@ -245,12 +269,48 @@ tagAnchorInfo = []; } + // Save current node positions before updating + if (simulation && nodes.length > 0) { + nodes.forEach(node => { + if (node.x != null && node.y != null) { + nodePositions.set(node.id, { + x: node.x, + y: node.y, + vx: node.vx, + vy: node.vy + }); + } + }); + debug("Saved positions for", nodePositions.size, "nodes"); + } + nodes = graphData.nodes; links = graphData.links; + + // Count events by kind + const counts: { [kind: number]: number } = {}; + events.forEach(event => { + counts[event.kind] = (counts[event.kind] || 0) + 1; + }); + eventCounts = counts; + + // Restore positions for existing nodes + let restoredCount = 0; + nodes.forEach(node => { + const savedPos = nodePositions.get(node.id); + if (savedPos && !node.isTagAnchor) { // Don't restore tag anchor positions as they're fixed + node.x = savedPos.x; + node.y = savedPos.y; + node.vx = savedPos.vx || 0; + node.vy = savedPos.vy || 0; + restoredCount++; + } + }); debug("Generated graph data", { nodeCount: nodes.length, linkCount: links.length, + restoredPositions: restoredCount }); if (!nodes.length) { @@ -265,15 +325,24 @@ // Create new simulation debug("Creating new simulation"); + const hasRestoredPositions = restoredCount > 0; + if (starVisualization) { // Use star-specific simulation simulation = createStarSimulation(nodes, links, width, height); - // Apply initial star positioning - applyInitialStarPositions(nodes, links, width, height); + // Apply initial star positioning only if we don't have restored positions + if (!hasRestoredPositions) { + applyInitialStarPositions(nodes, links, width, height); + } } else { // Use regular simulation simulation = createSimulation(nodes, links, NODE_RADIUS, LINK_DISTANCE); } + + // Use gentler alpha for updates with restored positions + if (hasRestoredPositions) { + simulation.alpha(0.3); // Gentler restart + } // Center the nodes when the simulation is done if (simulation) { @@ -643,6 +712,55 @@ } }); + // Track previous values to avoid unnecessary calls + let previousDepth = $state(0); + let previousTagType = $state(selectedTagType); + let isInitialized = $state(false); + + // Mark as initialized after first render + $effect(() => { + if (!isInitialized && svg) { + isInitialized = true; + } + }); + + /** + * Watch for tag expansion depth changes + */ + $effect(() => { + // Skip if not initialized or no callback + if (!isInitialized || !onTagExpansionChange) return; + + // Check if we need to trigger expansion + const depthChanged = tagExpansionDepth !== previousDepth; + const tagTypeChanged = selectedTagType !== previousTagType; + const shouldExpand = showTagAnchors && (depthChanged || tagTypeChanged); + + if (shouldExpand) { + previousDepth = tagExpansionDepth; + previousTagType = selectedTagType; + + // Extract unique tags from current events + const tags = new Set(); + events.forEach(event => { + const eventTags = event.getMatchingTags(selectedTagType); + eventTags.forEach(tag => { + if (tag[1]) tags.add(tag[1]); + }); + }); + + debug("Tag expansion requested", { + depth: tagExpansionDepth, + tagType: selectedTagType, + tags: Array.from(tags), + depthChanged, + tagTypeChanged + }); + + onTagExpansionChange(tagExpansionDepth, Array.from(tags)); + } + }); + /** * Handles tooltip close event */ @@ -724,6 +842,7 @@ starMode={starVisualization} showTags={showTagAnchors} tagAnchors={tagAnchorInfo} + eventCounts={eventCounts} /> @@ -733,6 +852,7 @@ bind:starVisualization bind:showTagAnchors bind:selectedTagType + bind:tagExpansionDepth /> diff --git a/src/lib/navigator/EventNetwork/utils/tagNetworkBuilder.ts b/src/lib/navigator/EventNetwork/utils/tagNetworkBuilder.ts index f41b575..903f2bd 100644 --- a/src/lib/navigator/EventNetwork/utils/tagNetworkBuilder.ts +++ b/src/lib/navigator/EventNetwork/utils/tagNetworkBuilder.ts @@ -73,6 +73,8 @@ export function extractUniqueTagsForType( ): Map> { // Map of tagValue -> Set of event IDs const tagMap = new Map>(); + + console.log(`[TagBuilder] Extracting tags of type: ${tagType} from ${events.length} events`); events.forEach((event) => { if (!event.tags || !event.id) return; @@ -92,6 +94,8 @@ export function extractUniqueTagsForType( tagMap.get(tagValue)!.add(event.id); }); }); + + console.log(`[TagBuilder] Found ${tagMap.size} unique tags of type ${tagType}:`, Array.from(tagMap.keys())); return tagMap; } @@ -108,8 +112,10 @@ export function createTagAnchorNodes( const anchorNodes: NetworkNode[] = []; // Calculate positions for tag anchors randomly within radius + // For single publication view, show all tags. For network view, only show tags with 2+ events + const minEventCount = tagMap.size <= 10 ? 1 : 2; const validTags = Array.from(tagMap.entries()).filter( - ([_, eventIds]) => eventIds.size >= 2, + ([_, eventIds]) => eventIds.size >= minEventCount, ); if (validTags.length === 0) return []; diff --git a/src/routes/visualize/+page.svelte b/src/routes/visualize/+page.svelte index 629199f..fc32d4e 100644 --- a/src/routes/visualize/+page.svelte +++ b/src/routes/visualize/+page.svelte @@ -13,7 +13,7 @@ import { networkFetchLimit } from "$lib/state"; // Configuration - const DEBUG = false; // Set to true to enable debug logging + const DEBUG = true; // Set to true to enable debug logging const INDEX_EVENT_KIND = 30040; const CONTENT_EVENT_KINDS = [30041, 30818]; @@ -31,6 +31,8 @@ let loading = true; let error: string | null = null; let showSettings = false; + let tagExpansionDepth = 0; + let baseEvents: NDKEvent[] = []; // Store original events before expansion /** * Fetches events from the Nostr network @@ -95,6 +97,7 @@ // Step 5: Combine both sets of events events = [...Array.from(validIndexEvents), ...Array.from(contentEvents)]; + baseEvents = [...events]; // Store base events for tag expansion debug("Total events for visualization:", events.length); } catch (e) { console.error("Error fetching events:", e); @@ -105,6 +108,85 @@ } + /** + * Handles tag expansion to fetch related publications + */ + async function handleTagExpansion(depth: number, tags: string[]) { + debug("Handling tag expansion", { depth, tags }); + + if (depth === 0 || tags.length === 0) { + // Reset to base events only + events = [...baseEvents]; + return; + } + + try { + // Don't show loading spinner for incremental updates + error = null; + + // Keep track of existing event IDs to avoid duplicates + const existingEventIds = new Set(baseEvents.map(e => e.id)); + + // Fetch publications that have any of the specified tags + const taggedPublications = await $ndkInstance.fetchEvents({ + kinds: [INDEX_EVENT_KIND], + "#t": tags, // Match any of these tags + limit: 30 * depth // Reasonable limit based on depth + }); + + debug("Found tagged publications:", taggedPublications.size); + + // Filter to avoid duplicates + const newPublications = Array.from(taggedPublications).filter( + event => !existingEventIds.has(event.id) + ); + + // Extract content event IDs from new publications + const contentEventIds = new Set(); + const existingContentIds = new Set( + baseEvents.filter(e => CONTENT_EVENT_KINDS.includes(e.kind)).map(e => e.id) + ); + + newPublications.forEach((event) => { + const aTags = event.getMatchingTags("a"); + aTags.forEach((tag) => { + const eventId = tag[3]; + if (eventId && !existingContentIds.has(eventId)) { + contentEventIds.add(eventId); + } + }); + }); + + // Fetch the content events + let newContentEvents: NDKEvent[] = []; + if (contentEventIds.size > 0) { + const contentEventsSet = await $ndkInstance.fetchEvents({ + kinds: CONTENT_EVENT_KINDS, + ids: Array.from(contentEventIds), + }); + newContentEvents = Array.from(contentEventsSet); + } + + // Combine all events: base events + new publications + new content + events = [ + ...baseEvents, + ...newPublications, + ...newContentEvents + ]; + + debug("Events after expansion:", { + base: baseEvents.length, + newPubs: newPublications.length, + newContent: newContentEvents.length, + total: events.length + }); + + } catch (e) { + console.error("Error expanding tags:", e); + error = e instanceof Error ? e.message : String(e); + } + } + // Fetch events when component mounts onMount(() => { debug("Component mounted"); @@ -159,6 +241,6 @@ {:else} - + {/if}