From fa921d7bece203a7ce453c35206e30756505653d Mon Sep 17 00:00:00 2001 From: limina1 Date: Sun, 8 Dec 2024 14:45:29 -0500 Subject: [PATCH 01/27] Add d3, visualize, EventNetwork.svelte --- package.json | 1 + src/lib/components/EventNetwork.svelte | 395 +++++++++++++++++++++++++ src/routes/visualize/+page.svelte | 98 +++++- 3 files changed, 485 insertions(+), 9 deletions(-) create mode 100644 src/lib/components/EventNetwork.svelte diff --git a/package.json b/package.json index 99658c9..afef0bf 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.10", "asciidoctor": "^3.0.4", + "d3": "^7.9.0", "he": "^1.2.0", "markdown-it": "^14.0.0", "markdown-it-plain-text": "^0.3.0", diff --git a/src/lib/components/EventNetwork.svelte b/src/lib/components/EventNetwork.svelte new file mode 100644 index 0000000..8ad44b6 --- /dev/null +++ b/src/lib/components/EventNetwork.svelte @@ -0,0 +1,395 @@ + + +# /lib/components/EventNetwork.svelte +
+ +
+

+ Legend +

+
    +
  • +
    + + + I +
    + Index events (kind 30040) - Each with a unique pastel color +
  • +
  • +
    + + C +
    + Content events (kind 30041) - Publication sections +
  • +
  • + + + + Arrows indicate reading/sequence order +
  • +
+
+
+ + diff --git a/src/routes/visualize/+page.svelte b/src/routes/visualize/+page.svelte index 9afab24..3bf515e 100644 --- a/src/routes/visualize/+page.svelte +++ b/src/routes/visualize/+page.svelte @@ -1,12 +1,92 @@ - -
-
- Visualize -

Coming soon.

-
-
\ No newline at end of file +
+

Publication Network

+ + {#if loading} +
+

Loading network data...

+
+ {:else if error} +
+

Error loading network: {error}

+ +
+ {:else} + + +
+ +

About This Visualization

+ +
+ {/if} +
From cc2380bc17ca01ddf24d0b75f937dd73915e731d Mon Sep 17 00:00:00 2001 From: limina1 <131614677+limina1@users.noreply.github.com> Date: Sat, 14 Dec 2024 17:19:48 -0500 Subject: [PATCH 02/27] Update src/lib/components/EventNetwork.svelte Co-authored-by: Michael J <37635304+buttercat1791@users.noreply.github.com> --- src/lib/components/EventNetwork.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/EventNetwork.svelte b/src/lib/components/EventNetwork.svelte index 8ad44b6..65bcb4b 100644 --- a/src/lib/components/EventNetwork.svelte +++ b/src/lib/components/EventNetwork.svelte @@ -27,7 +27,7 @@ return `hsl(${hue}, ${saturation}%, ${lightness}%)`; } - function generateGraph(events: NDKEvent[]) { + function generateGraph(events: NDKEvent[]): [object[], object[]] { const nodes = []; const links = []; const nodeMap = new Map(); From 56dd896f655276cfd4d2161da8fdff1d56c35534 Mon Sep 17 00:00:00 2001 From: limina1 Date: Tue, 17 Dec 2024 00:41:32 -0500 Subject: [PATCH 03/27] remove updateTheme --- src/lib/components/EventNetwork.svelte | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/lib/components/EventNetwork.svelte b/src/lib/components/EventNetwork.svelte index 65bcb4b..ac2c6dd 100644 --- a/src/lib/components/EventNetwork.svelte +++ b/src/lib/components/EventNetwork.svelte @@ -13,12 +13,6 @@ const dragRadius = 45; const linkDistance = 120; - function updateTheme() { - isDarkMode = !isDarkMode; - document.body.classList.toggle("dark"); - if (svg) drawNetwork(); - } - function getEventColor(eventId: string): string { const num = parseInt(eventId.slice(0, 4), 16); const hue = num % 360; From 34921906b44c40557e3e4e14dbe5b498b8e78fcc Mon Sep 17 00:00:00 2001 From: limina1 Date: Tue, 17 Dec 2024 00:44:51 -0500 Subject: [PATCH 04/27] add NetworkNode interface --- src/lib/components/EventNetwork.svelte | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/lib/components/EventNetwork.svelte b/src/lib/components/EventNetwork.svelte index ac2c6dd..c02e4c3 100644 --- a/src/lib/components/EventNetwork.svelte +++ b/src/lib/components/EventNetwork.svelte @@ -12,6 +12,16 @@ const nodeRadius = 20; const dragRadius = 45; const linkDistance = 120; + interface NetworkNode { + id: string; + event?: NDKEvent; + index?: number; + isContainer: boolean; + title: string; + content: string; + author: string; + type: "Index" | "Content"; + } function getEventColor(eventId: string): string { const num = parseInt(eventId.slice(0, 4), 16); From 614dddf2b159ae32227a935807403515871035e5 Mon Sep 17 00:00:00 2001 From: limina1 Date: Tue, 17 Dec 2024 00:59:37 -0500 Subject: [PATCH 05/27] modified observer --- src/lib/components/EventNetwork.svelte | 32 ++++++++++++++++++-------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/lib/components/EventNetwork.svelte b/src/lib/components/EventNetwork.svelte index c02e4c3..66e14b4 100644 --- a/src/lib/components/EventNetwork.svelte +++ b/src/lib/components/EventNetwork.svelte @@ -7,11 +7,12 @@ let svg; let isDarkMode = false; - const width = 1200; - const height = 600; const nodeRadius = 20; const dragRadius = 45; const linkDistance = 120; + let container: HTMLDivElement; + let width: number; + let height: number; interface NetworkNode { id: string; event?: NDKEvent; @@ -309,7 +310,7 @@ isDarkMode = document.body.classList.contains("dark"); // Watch for theme changes - const observer = new MutationObserver((mutations) => { + const themeObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.attributeName === "class") { const newIsDarkMode = document.body.classList.contains("dark"); @@ -321,22 +322,33 @@ }); }); - observer.observe(document.documentElement, { + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + width = entry.contentRect.width; + height = entry.contentRect.height || width * 0.6; + } + if (svg) drawNetwork(); + }); + + // Start observers + themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["class"], }); - - return () => observer.disconnect(); + resizeObserver.observe(container); + // Clean up + return () => { + themeObserver.disconnect(); + resizeObserver.disconnect(); + }; }); # /lib/components/EventNetwork.svelte -
+
Date: Tue, 17 Dec 2024 02:21:28 -0500 Subject: [PATCH 06/27] Add node and link interfaces --- src/lib/components/EventNetwork.svelte | 104 ++++++++++++++----------- src/routes/visualize/+page.svelte | 6 +- 2 files changed, 59 insertions(+), 51 deletions(-) diff --git a/src/lib/components/EventNetwork.svelte b/src/lib/components/EventNetwork.svelte index 66e14b4..9bf88c0 100644 --- a/src/lib/components/EventNetwork.svelte +++ b/src/lib/components/EventNetwork.svelte @@ -5,14 +5,20 @@ export let events: NDKEvent[] = []; - let svg; + let svg: SVGSVGElement; let isDarkMode = false; const nodeRadius = 20; const dragRadius = 45; const linkDistance = 120; let container: HTMLDivElement; + let width: number; let height: number; + + $: if (container) { + width = container.clientWidth || 800; + height = container.clientHeight || 600; + } interface NetworkNode { id: string; event?: NDKEvent; @@ -24,6 +30,35 @@ type: "Index" | "Content"; } + interface NetworkLink { + source: NetworkNode; + target: NetworkNode; + isSequential: boolean; + } + + function getNode( + id: string, + nodeMap: Map, + event?: NDKEvent, + index?: number, + ): NetworkNode | null { + if (!id) return null; + + if (!nodeMap.has(id)) { + const node: NetworkNode = { + id, + event, + index, + isContainer: event?.kind === 30040, + title: event?.getMatchingTags("title")?.[0]?.[1] || "Untitled", + content: event?.content || "", + author: event?.pubkey || "", + type: event?.kind === 30040 ? "Index" : "Content", + }; + nodeMap.set(id, node); + } + return nodeMap.get(id) || null; + } function getEventColor(eventId: string): string { const num = parseInt(eventId.slice(0, 4), 16); const hue = num % 360; @@ -31,51 +66,33 @@ const lightness = 75; return `hsl(${hue}, ${saturation}%, ${lightness}%)`; } + function generateGraph(events: NDKEvent[]): { + nodes: NetworkNode[]; + links: NetworkLink[]; + } { + const nodes: NetworkNode[] = []; + const links: NetworkLink[] = []; + const nodeMap = new Map(); - function generateGraph(events: NDKEvent[]): [object[], object[]] { - const nodes = []; - const links = []; - const nodeMap = new Map(); - - function getNode(id: string, event?: NDKEvent, index?: number) { - if (!id) return null; - - if (!nodeMap.has(id)) { - const node = { - id, - event, - index, - isContainer: event?.kind === 30040, - title: event?.getMatchingTags("title")?.[0]?.[1] || "Untitled", - content: event?.content || "", - author: event?.pubkey, - type: event?.kind === 30040 ? "Index" : "Content", - }; - nodes.push(node); - nodeMap.set(id, node); - } - return nodeMap.get(id); - } - - // Process index events first const indexEvents = events.filter((e) => e.kind === 30040); indexEvents.forEach((index) => { if (!index.id) return; const contentRefs = index.getMatchingTags("e"); - const sourceNode = getNode(index.id, index); + const sourceNode = getNode(index.id, nodeMap, index); if (!sourceNode) return; + nodes.push(sourceNode); - // Create a linear chain of content events contentRefs.forEach((tag, idx) => { if (!tag[1]) return; const targetEvent = events.find((e) => e.id === tag[1]); if (!targetEvent) return; - const targetNode = getNode(tag[1], targetEvent, idx); + const targetNode = getNode(tag[1], nodeMap, targetEvent, idx); if (!targetNode) return; + nodes.push(targetNode); const prevNodeId = idx === 0 ? sourceNode.id : contentRefs[idx - 1]?.[1]; @@ -93,7 +110,6 @@ return { nodes, links }; } - function drawNetwork() { if (!svg || !events?.length) return; @@ -102,14 +118,7 @@ const { nodes, links } = generateGraph(events); if (!nodes.length) return; - const svgElement = d3 - .select(svg) - .attr( - "class", - "network-leather w-full border border-gray-300 dark:border-gray-700 rounded", - ) - .attr("viewBox", [0, 0, width, height]); - + const svgElement = d3.select(svg).attr("viewBox", `0 0 ${width} ${height}`); // Set up zoom behavior const zoom = d3 .zoom() @@ -124,19 +133,22 @@ // Force simulation setup const simulation = d3 - .forceSimulation(nodes) + .forceSimulation(nodes) .force( "link", d3 - .forceLink(links) + .forceLink(links) .id((d) => d.id) .distance(linkDistance), ) - .force("charge", d3.forceManyBody().strength(-500)) + .force("charge", d3.forceManyBody().strength(-500)) .force("center", d3.forceCenter(width / 2, height / 2)) - .force("x", d3.forceX(width / 2).strength(0.1)) - .force("y", d3.forceY(height / 2).strength(0.1)) - .force("collision", d3.forceCollide().radius(nodeRadius * 2.5)); + .force("x", d3.forceX(width / 2).strength(0.1)) + .force("y", d3.forceY(height / 2).strength(0.1)) + .force( + "collision", + d3.forceCollide().radius(nodeRadius * 2.5), + ); // Define arrow marker with black fill const marker = g @@ -322,7 +334,7 @@ }); }); - const resizeObserver = new ResizeObserver((entries) => { + let resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { width = entry.contentRect.width; height = entry.contentRect.height || width * 0.6; diff --git a/src/routes/visualize/+page.svelte b/src/routes/visualize/+page.svelte index 3bf515e..c1560ba 100644 --- a/src/routes/visualize/+page.svelte +++ b/src/routes/visualize/+page.svelte @@ -83,10 +83,6 @@ {:else} -
- -

About This Visualization

- -
+
{/if}
From c59eefe6c1dae1cb889d889242ea57d60da0abc4 Mon Sep 17 00:00:00 2001 From: limina1 Date: Tue, 17 Dec 2024 02:27:07 -0500 Subject: [PATCH 07/27] Modify event lookup --- src/lib/components/EventNetwork.svelte | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/lib/components/EventNetwork.svelte b/src/lib/components/EventNetwork.svelte index 9bf88c0..4538660 100644 --- a/src/lib/components/EventNetwork.svelte +++ b/src/lib/components/EventNetwork.svelte @@ -66,6 +66,7 @@ const lightness = 75; return `hsl(${hue}, ${saturation}%, ${lightness}%)`; } + function generateGraph(events: NDKEvent[]): { nodes: NetworkNode[]; links: NetworkLink[]; @@ -74,6 +75,12 @@ const links: NetworkLink[] = []; const nodeMap = new Map(); + // Create event lookup map - O(n) operation done once + const eventMap = new Map(); + events.forEach((event) => { + if (event.id) eventMap.set(event.id, event); + }); + const indexEvents = events.filter((e) => e.kind === 30040); indexEvents.forEach((index) => { @@ -87,7 +94,8 @@ contentRefs.forEach((tag, idx) => { if (!tag[1]) return; - const targetEvent = events.find((e) => e.id === tag[1]); + // O(1) lookup instead of O(n) search + const targetEvent = eventMap.get(tag[1]); if (!targetEvent) return; const targetNode = getNode(tag[1], nodeMap, targetEvent, idx); @@ -110,6 +118,7 @@ return { nodes, links }; } + function drawNetwork() { if (!svg || !events?.length) return; From a20cfc1409f58ef49bac04c0408904a00d1b4b91 Mon Sep 17 00:00:00 2001 From: limina1 Date: Tue, 17 Dec 2024 12:57:50 -0500 Subject: [PATCH 08/27] add eventMap, fix redrawing --- src/lib/components/EventNetwork.svelte | 377 +++++++++++++------------ 1 file changed, 190 insertions(+), 187 deletions(-) diff --git a/src/lib/components/EventNetwork.svelte b/src/lib/components/EventNetwork.svelte index 4538660..2a76b5a 100644 --- a/src/lib/components/EventNetwork.svelte +++ b/src/lib/components/EventNetwork.svelte @@ -11,15 +11,17 @@ const dragRadius = 45; const linkDistance = 120; let container: HTMLDivElement; - let width: number; let height: number; + // Reactive statement for container dimensions $: if (container) { width = container.clientWidth || 800; height = container.clientHeight || 600; } - interface NetworkNode { + + // Type definitions for network components + interface NetworkNode extends d3.SimulationNodeDatum { id: string; event?: NDKEvent; index?: number; @@ -28,14 +30,23 @@ content: string; author: string; type: "Index" | "Content"; + x?: number; + y?: number; + fx?: number | null; + fy?: number | null; } - interface NetworkLink { + interface NetworkLink extends d3.SimulationLinkDatum { source: NetworkNode; target: NetworkNode; isSequential: boolean; } + // Create an efficient event map for O(1) lookups + function createEventMap(events: NDKEvent[]): Map { + return new Map(events.map((event) => [event.id, event])); + } + function getNode( id: string, nodeMap: Map, @@ -59,12 +70,11 @@ } return nodeMap.get(id) || null; } + function getEventColor(eventId: string): string { const num = parseInt(eventId.slice(0, 4), 16); const hue = num % 360; - const saturation = 70; - const lightness = 75; - return `hsl(${hue}, ${saturation}%, ${lightness}%)`; + return `hsl(${hue}, 70%, 75%)`; } function generateGraph(events: NDKEvent[]): { @@ -74,12 +84,7 @@ const nodes: NetworkNode[] = []; const links: NetworkLink[] = []; const nodeMap = new Map(); - - // Create event lookup map - O(n) operation done once - const eventMap = new Map(); - events.forEach((event) => { - if (event.id) eventMap.set(event.id, event); - }); + const eventMap = createEventMap(events); const indexEvents = events.filter((e) => e.kind === 30040); @@ -94,7 +99,7 @@ contentRefs.forEach((tag, idx) => { if (!tag[1]) return; - // O(1) lookup instead of O(n) search + // Use O(1) lookup instead of O(n) find operation const targetEvent = eventMap.get(tag[1]); if (!targetEvent) return; @@ -119,28 +124,96 @@ return { nodes, links }; } + function setupDragHandlers( + simulation: d3.Simulation, + ) { + // Create drag behavior with proper typing + const dragBehavior = d3 + .drag() + .on( + "start", + ( + event: d3.D3DragEvent, + d: NetworkNode, + ) => { + // Warm up simulation when drag starts + if (!event.active) simulation.alphaTarget(0.3).restart(); + // Fix node position during drag + d.fx = d.x; + d.fy = d.y; + }, + ) + .on( + "drag", + ( + event: d3.D3DragEvent, + d: NetworkNode, + ) => { + // Update fixed position to drag position + d.fx = event.x; + d.fy = event.y; + }, + ) + .on( + "end", + ( + event: d3.D3DragEvent, + d: NetworkNode, + ) => { + // Cool down simulation when drag ends + if (!event.active) simulation.alphaTarget(0); + // Release fixed position, allowing forces to take over + d.fx = null; + d.fy = null; + }, + ); + + return dragBehavior; + } + function drawNetwork() { if (!svg || !events?.length) return; - d3.select(svg).selectAll("*").remove(); - const { nodes, links } = generateGraph(events); if (!nodes.length) return; const svgElement = d3.select(svg).attr("viewBox", `0 0 ${width} ${height}`); - // Set up zoom behavior - const zoom = d3 - .zoom() - .scaleExtent([0.1, 4]) - .on("zoom", (event) => { - g.attr("transform", event.transform); - }); - - svgElement.call(zoom); - const g = svgElement.append("g"); + let g = svgElement.select("g"); + + // Only create the base group and zoom behavior if it doesn't exist + if (g.empty()) { + const zoom = d3 + .zoom() + .scaleExtent([0.1, 4]) + .on("zoom", (event) => { + g.attr("transform", event.transform); + }); + + svgElement.call(zoom); + g = svgElement.append("g"); + + // Define arrow marker only once + const marker = g + .append("defs") + .selectAll("marker") + .data(["arrowhead"]) + .join("marker") + .attr("id", "arrowhead") + .attr("viewBox", "0 -5 20 20") + .attr("refX", nodeRadius + 10) + .attr("refY", 0) + .attr("markerWidth", 8) + .attr("markerHeight", 8) + .attr("orient", "auto"); + + marker + .append("path") + .attr("d", "M -8,-5 L 0, 0 L -8, 5 Z") + .attr("class", "network-link-leather"); + } - // Force simulation setup + // Set up force simulation const simulation = d3 .forceSimulation(nodes) .force( @@ -150,183 +223,104 @@ .id((d) => d.id) .distance(linkDistance), ) - .force("charge", d3.forceManyBody().strength(-500)) + .force("charge", d3.forceManyBody().strength(-500)) .force("center", d3.forceCenter(width / 2, height / 2)) - .force("x", d3.forceX(width / 2).strength(0.1)) - .force("y", d3.forceY(height / 2).strength(0.1)) - .force( - "collision", - d3.forceCollide().radius(nodeRadius * 2.5), - ); + .force("x", d3.forceX(width / 2).strength(0.1)) + .force("y", d3.forceY(height / 2).strength(0.1)) + .force("collision", d3.forceCollide().radius(nodeRadius * 2.5)); + + // Create drag handler + const dragHandler = setupDragHandlers(simulation); - // Define arrow marker with black fill - const marker = g - .append("defs") - .selectAll("marker") - .data(["arrowhead"]) - .join("marker") - .attr("id", "arrowhead") - .attr("viewBox", "0 -5 20 20") - .attr("refX", nodeRadius + 10) - .attr("refY", 0) - .attr("markerWidth", 8) - .attr("markerHeight", 8) - .attr("orient", "auto"); - - marker - .append("path") - .attr("d", "M -8,-5 L 0, 0 L -8, 5 Z") - .attr("class", "network-link-leather"); // Black fill for arrowhead - - // Create links + // Update links with enter/update/exit pattern const link = g - .selectAll("path") - .data(links) - .join("path") - .attr("class", "network-link-leather") - .attr("stroke-width", 2) - .attr("fill", "none") - .attr("marker-end", "url(#arrow)") - .attr("fill", "none"); - - // Create nodes - const node = g - .selectAll("g") - .data(nodes) - .join("g") - .attr("class", "node network-node-leather") - .call( - d3 - .drag() - .on("start", dragstarted) - .on("drag", dragged) - .on("end", dragended), + .selectAll("path.link") + .data(links, (d: NetworkLink) => `${d.source.id}-${d.target.id}`) + .join( + (enter) => + enter + .append("path") + .attr("class", "network-link-leather link") + .attr("stroke-width", 2) + .attr("fill", "none") + .attr("marker-end", "url(#arrowhead)"), + (update) => update, + (exit) => exit.remove(), ); - // Add invisible larger circle for better drag handling - node - .append("circle") - .attr("r", dragRadius) - .attr("fill", "transparent") - .style("cursor", "move"); - - // Add visible node circle - node - .append("circle") - .attr("r", nodeRadius) - .attr("fill", (d) => { - if (!d.isContainer) { - return isDarkMode ? "#342718" : "#d6c1a8"; // primary-800 : primary-100 - } - return getEventColor(d.id); - }) - .attr("stroke", "#000000") // Black outline for all nodes - .attr("stroke-width", 2); + // Update nodes with enter/update/exit pattern + const node = g + .selectAll("g.node") + .data(nodes, (d: NetworkNode) => d.id) + .join( + (enter) => { + const nodeEnter = enter + .append("g") + .attr("class", "node network-node-leather") + .call(dragHandler); + + // Add drag circle + nodeEnter + .append("circle") + .attr("r", dragRadius) + .attr("fill", "transparent") + .style("cursor", "move"); + + // Add visible node circle + nodeEnter + .append("circle") + .attr("r", nodeRadius) + .attr("stroke", "#000000") + .attr("stroke-width", 2); + + // Add text labels + nodeEnter + .append("text") + .attr("dy", "0.35em") + .attr("text-anchor", "middle") + .attr("fill", "#000000") + .attr("font-size", "12px") + .attr("font-weight", "bold"); + + return nodeEnter; + }, + (update) => update, + (exit) => exit.remove(), + ); - // Add text labels + // Update node appearances node - .append("text") - .attr("dy", "0.35em") - .attr("text-anchor", "middle") - .attr("fill", "#000000") // Always black - .attr("font-size", "12px") - .attr("font-weight", "bold") // Making it bold for better contrast - .text((d) => (d.isContainer ? "I" : "C")); - - // Add tooltips - const tooltip = d3 - .select("body") - .append("div") - .attr( - "class", - "fixed hidden bg-primary-0 dark:bg-primary-1000 " + - "text-gray-800 dark:text-gray-300 " + - "p-4 rounded shadow-lg border border-gray-200 dark:border-gray-800 " + - "transition-colors duration-200", - ) - .style("z-index", 1000); + .select("circle:nth-child(2)") + .attr("fill", (d) => + !d.isContainer + ? isDarkMode + ? "#342718" + : "#d6c1a8" + : getEventColor(d.id), + ); - node - .on("mouseover", function (event, d) { - tooltip - .style("display", "block") - .html( - ` -
-
${d.title}
-
- ${d.type} (${d.isContainer ? "30040" : "30041"}) -
-
- ID: ${d.id} -
- ${ - d.content - ? ` -
- ${d.content} -
- ` - : "" - } -
- `, - ) - .style("left", event.pageX + 10 + "px") - .style("top", event.pageY - 10 + "px"); - }) - .on("mousemove", function (event) { - tooltip - .style("left", event.pageX + 10 + "px") - .style("top", event.pageY - 10 + "px"); - }) - .on("mouseout", () => { - tooltip.style("display", "none"); - }); + node.select("text").text((d) => (d.isContainer ? "I" : "C")); - // Handle simulation ticks + // Handle simulation updates simulation.on("tick", () => { + // Update link positions link.attr("d", (d) => { - const dx = d.target.x - d.source.x; - const dy = d.target.y - d.source.y; + const dx = d.target.x! - d.source.x!; + const dy = d.target.y! - d.source.y!; const angle = Math.atan2(dy, dx); - - // Adjust start and end points to prevent overlap with nodes - const startX = d.source.x + nodeRadius * Math.cos(angle); - const startY = d.source.y + nodeRadius * Math.sin(angle); - const endX = d.target.x - nodeRadius * Math.cos(angle); - const endY = d.target.y - nodeRadius * Math.sin(angle); - + const startX = d.source.x! + nodeRadius * Math.cos(angle); + const startY = d.source.y! + nodeRadius * Math.sin(angle); + const endX = d.target.x! - nodeRadius * Math.cos(angle); + const endY = d.target.y! - nodeRadius * Math.sin(angle); return `M${startX},${startY}L${endX},${endY}`; }); + + // Update node positions node.attr("transform", (d) => `translate(${d.x},${d.y})`); }); - - // Drag handlers - function dragstarted(event, d) { - if (!event.active) simulation.alphaTarget(0.3).restart(); - d.fx = d.x; - d.fy = d.y; - } - - function dragged(event, d) { - d.fx = event.x; - d.fy = event.y; - } - - function dragended(event, d) { - if (!event.active) simulation.alphaTarget(0); - d.fx = null; - d.fy = null; - } - } - - $: { - if (svg && events?.length) { - drawNetwork(); - } } + // Setup and cleanup onMount(() => { isDarkMode = document.body.classList.contains("dark"); @@ -343,7 +337,8 @@ }); }); - let resizeObserver = new ResizeObserver((entries) => { + // Watch for container size changes + const resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { width = entry.contentRect.width; height = entry.contentRect.height || width * 0.6; @@ -357,12 +352,20 @@ attributeFilter: ["class"], }); resizeObserver.observe(container); - // Clean up + + // Cleanup function return () => { themeObserver.disconnect(); resizeObserver.disconnect(); }; }); + + // Reactive redraw + $: { + if (svg && events?.length) { + drawNetwork(); + } + } # /lib/components/EventNetwork.svelte From 6d6298f2040bc10cb99f50ad52f68b8cfb355f91 Mon Sep 17 00:00:00 2001 From: limina1 Date: Tue, 17 Dec 2024 13:04:13 -0500 Subject: [PATCH 09/27] removed global style --- src/app.css | 19 +++++++++++++------ src/lib/components/EventNetwork.svelte | 10 ---------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/app.css b/src/app.css index 41e84e0..a0ea8e8 100644 --- a/src/app.css +++ b/src/app.css @@ -62,7 +62,7 @@ div.note-leather:hover:not(:has(.note-leather:hover)), p.note-leather:hover:not(:has(.note-leather:hover)), section.note-leather:hover:not(:has(.note-leather:hover)) { - @apply hover:bg-primary-100 dark:hover:bg-primary-800 ; + @apply hover:bg-primary-100 dark:hover:bg-primary-800; } /* Heading */ @@ -79,11 +79,11 @@ @apply bg-primary-0 dark:bg-primary-1000 border-b-[1px] border-gray-800 dark:border-gray-500; } - div.modal-leather > div > h1, - div.modal-leather > div > h2, - div.modal-leather > div > h3, - div.modal-leather > div > h4, - div.modal-leather > div > h5, + div.modal-leather > div > h1, + div.modal-leather > div > h2, + div.modal-leather > div > h3, + div.modal-leather > div > h4, + div.modal-leather > div > h5, div.modal-leather > div > h6 { @apply text-gray-800 hover:text-gray-800 dark:text-gray-300 dark:hover:text-gray-300; } @@ -147,4 +147,11 @@ .ul-leather li a { @apply text-gray-800 hover:text-primary-400 dark:text-gray-300 dark:hover:text-primary-500; } + .network-link-leather { + @apply stroke-gray-400 dark:stroke-gray-600 fill-gray-400 dark:fill-gray-600; + } + + .text-leather { + @apply fill-gray-800 dark:fill-gray-300; + } } diff --git a/src/lib/components/EventNetwork.svelte b/src/lib/components/EventNetwork.svelte index 2a76b5a..eb232fb 100644 --- a/src/lib/components/EventNetwork.svelte +++ b/src/lib/components/EventNetwork.svelte @@ -423,13 +423,3 @@
- - From de919ebfa193c8196a2842c069ccbc2c2e65e515 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Mon, 9 Dec 2024 08:13:58 -0600 Subject: [PATCH 10/27] Configure for static site generation Build currently fails because some routes are dynamic. --- package-lock.json | 11 +++++++++++ package.json | 1 + src/routes/+layout.ts | 1 + svelte.config.js | 10 ++++++++-- 4 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 src/routes/+layout.ts diff --git a/package-lock.json b/package-lock.json index 212fe6c..d09c1bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ }, "devDependencies": { "@sveltejs/adapter-auto": "^3.1.1", + "@sveltejs/adapter-static": "^3.0.6", "@sveltejs/kit": "^2.4.3", "@types/he": "^1.2.3", "@types/markdown-it": "^13.0.7", @@ -658,6 +659,16 @@ "@sveltejs/kit": "^2.0.0" } }, + "node_modules/@sveltejs/adapter-static": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.6.tgz", + "integrity": "sha512-MGJcesnJWj7FxDcB/GbrdYD3q24Uk0PIL4QIX149ku+hlJuj//nxUbb0HxUTpjkecWfHjVveSUnUaQWnPRXlpg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, "node_modules/@sveltejs/kit": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.0.tgz", diff --git a/package.json b/package.json index afef0bf..ecfdc35 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ }, "devDependencies": { "@sveltejs/adapter-auto": "^3.1.1", + "@sveltejs/adapter-static": "^3.0.6", "@sveltejs/kit": "^2.4.3", "@types/he": "^1.2.3", "@types/markdown-it": "^13.0.7", diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts new file mode 100644 index 0000000..189f71e --- /dev/null +++ b/src/routes/+layout.ts @@ -0,0 +1 @@ +export const prerender = true; diff --git a/svelte.config.js b/svelte.config.js index 1f5ee97..aeddb48 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -1,4 +1,4 @@ -import adapter from '@sveltejs/adapter-auto'; +import adapter from '@sveltejs/adapter-static'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; /** @type {import('@sveltejs/kit').Config} */ @@ -11,7 +11,13 @@ const config = { // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. // If your environment is not supported or you settled on a specific environment, switch out the adapter. // See https://kit.svelte.dev/docs/adapters for more information about adapters. - adapter: adapter(), + adapter: adapter({ + pages: 'build', + assets: 'build', + fallback: null, // TODO: Create a 404.html page. + precompress: false, + strict: true, + }), alias: { $lib: 'src/lib', $components: 'src/lib/components', From 5dcbe45cecfd476c9c0864f1eb65ee3789e1c840 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Mon, 9 Dec 2024 08:41:43 -0600 Subject: [PATCH 11/27] Fix parameter name --- src/routes/[id]/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/[id]/+page.svelte b/src/routes/[id]/+page.svelte index ac0fe07..81ebaef 100644 --- a/src/routes/[id]/+page.svelte +++ b/src/routes/[id]/+page.svelte @@ -8,5 +8,5 @@
-
+
From 95575b335f1a6dcc9af4b306caa8e71c8101993c Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Mon, 9 Dec 2024 09:06:23 -0600 Subject: [PATCH 12/27] Make page viewer route prerenderable --- src/routes/[id]/+page.svelte | 12 ------------ src/routes/[id]/+page.ts | 26 -------------------------- src/routes/article/+page.svelte | 26 ++++++++++++++++++++++++++ 3 files changed, 26 insertions(+), 38 deletions(-) delete mode 100644 src/routes/[id]/+page.svelte delete mode 100644 src/routes/[id]/+page.ts create mode 100644 src/routes/article/+page.svelte diff --git a/src/routes/[id]/+page.svelte b/src/routes/[id]/+page.svelte deleted file mode 100644 index 81ebaef..0000000 --- a/src/routes/[id]/+page.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - -
-
-
diff --git a/src/routes/[id]/+page.ts b/src/routes/[id]/+page.ts deleted file mode 100644 index 504be65..0000000 --- a/src/routes/[id]/+page.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { getNdkInstance, ndk } from '$lib/ndk'; -import type { NDKEvent } from '@nostr-dev-kit/ndk'; -import { error } from '@sveltejs/kit'; - -// MichaelJ - 23 July 2024 - Disable server-side rendering so that the load function can use the -// browser's local storage to retrieve saved relays and the cache adapter for the NDK instance. -export const ssr = false; - -export const load = async ({ params }) => { - const ndk = getNdkInstance(); - const { id } = params; - - let event: NDKEvent | null | undefined; - - try { - event = await ndk.fetchEvent(id); - } catch (err) { - console.error(err); - } - - if (!event) { - error(404, 'No event found with the given ID.'); - } - - return { event }; -}; diff --git a/src/routes/article/+page.svelte b/src/routes/article/+page.svelte new file mode 100644 index 0000000..b49baf7 --- /dev/null +++ b/src/routes/article/+page.svelte @@ -0,0 +1,26 @@ + + +
+
+
From 057b2760ebe8a7dee286fcbce1cc90589c4f956b Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sun, 15 Dec 2024 08:58:40 -0600 Subject: [PATCH 13/27] Bump patch version in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ecfdc35..2f108f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "alexandria", - "version": "0.0.5", + "version": "0.0.6", "private": true, "type": "module", "scripts": { From 750cc5ab66467d3874e0465d255b31a07173cbaa Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sun, 15 Dec 2024 08:59:26 -0600 Subject: [PATCH 14/27] Use static routes for article viewer --- src/lib/components/ArticleHeader.svelte | 4 +-- src/routes/article/+page.svelte | 26 --------------- src/routes/d/[tag]/+page.svelte | 20 ------------ src/routes/d/[tag]/+page.ts | 11 ------- src/routes/publication/+page.svelte | 42 +++++++++++++++++++++++++ 5 files changed, 44 insertions(+), 59 deletions(-) delete mode 100644 src/routes/article/+page.svelte delete mode 100644 src/routes/d/[tag]/+page.svelte delete mode 100644 src/routes/d/[tag]/+page.ts create mode 100644 src/routes/publication/+page.svelte diff --git a/src/lib/components/ArticleHeader.svelte b/src/lib/components/ArticleHeader.svelte index 0e758cd..a5d054e 100644 --- a/src/lib/components/ArticleHeader.svelte +++ b/src/lib/components/ArticleHeader.svelte @@ -20,9 +20,9 @@ const d = event.getMatchingTags('d')[0][1]; if (d != null) { - href = `d/${d}`; + href = `publication?d=${d}`; } else { - href = neventEncode(event, relays); + href = `publication?id=${neventEncode(event, relays)}`; } } catch (e) { console.warn(e); diff --git a/src/routes/article/+page.svelte b/src/routes/article/+page.svelte deleted file mode 100644 index b49baf7..0000000 --- a/src/routes/article/+page.svelte +++ /dev/null @@ -1,26 +0,0 @@ - - -
-
-
diff --git a/src/routes/d/[tag]/+page.svelte b/src/routes/d/[tag]/+page.svelte deleted file mode 100644 index 5534c5c..0000000 --- a/src/routes/d/[tag]/+page.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - -
- {#await getIndexEvent(data.event.d)} - - {:then index} -
- {/await} -
diff --git a/src/routes/d/[tag]/+page.ts b/src/routes/d/[tag]/+page.ts deleted file mode 100644 index 79712f2..0000000 --- a/src/routes/d/[tag]/+page.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { PageLoad } from './$types'; - -export const load: PageLoad = async ({ params }) => { - const { tag } = params; - - return { - event: { - d: tag, - } - }; -}; diff --git a/src/routes/publication/+page.svelte b/src/routes/publication/+page.svelte new file mode 100644 index 0000000..0f288e6 --- /dev/null +++ b/src/routes/publication/+page.svelte @@ -0,0 +1,42 @@ + + +
+ {#await event} + + {:then ev} +
+ {/await} +
From 7d52b3b3b3f8d4340288d1426406865fd7871420 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sun, 15 Dec 2024 09:21:39 -0600 Subject: [PATCH 15/27] Change outdated comment --- svelte.config.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/svelte.config.js b/svelte.config.js index aeddb48..4a3459b 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -8,9 +8,7 @@ const config = { preprocess: [vitePreprocess()], kit: { - // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. - // If your environment is not supported or you settled on a specific environment, switch out the adapter. - // See https://kit.svelte.dev/docs/adapters for more information about adapters. + // Static adapter adapter: adapter({ pages: 'build', assets: 'build', From b2d53d626f34333739931297a03aa9d54bd673a2 Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sun, 15 Dec 2024 12:57:57 -0600 Subject: [PATCH 16/27] Only handle 30041 events as zettels --- src/lib/consts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/consts.ts b/src/lib/consts.ts index fbe3477..2b114fd 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -1,6 +1,6 @@ export const wikiKind = 30818; export const indexKind = 30040; -export const zettelKinds = [ 1, 30024, 30041, 30818]; +export const zettelKinds = [ 30041 ]; export const standardRelays = [ "wss://thecitadel.nostr1.com", "wss://relay.noswhere.com" ]; export enum FeedType { From 3106c43d37b2f1bfe2cea5ed99b66ccbb519f7aa Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sun, 15 Dec 2024 12:58:45 -0600 Subject: [PATCH 17/27] Enable parser to fetch publications from relays --- src/lib/parser.ts | 79 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/src/lib/parser.ts b/src/lib/parser.ts index 0c0b0a8..874bb34 100644 --- a/src/lib/parser.ts +++ b/src/lib/parser.ts @@ -12,6 +12,7 @@ import asciidoctor, { } from 'asciidoctor'; import he from 'he'; import { writable, type Writable } from 'svelte/store'; +import { indexKind, zettelKinds } from './consts'; interface IndexMetadata { authors?: string[]; @@ -154,6 +155,28 @@ export default class Pharos { } } + /** + * Fetches and parses the event tree for a publication given the event or event ID of the + * publication's root index. + * @param event The event or event ID of the publication's root index. + */ + async fetch(event: NDKEvent | string): Promise { + let content: string; + + if (typeof event === 'string') { + const index = await this.ndk.fetchEvent({ ids: [event] }); + if (!index) { + throw new Error('Failed to fetch publication.'); + } + + content = await this.getPublicationContent(index); + } else { + content = await this.getPublicationContent(event); + } + + this.parse(content); + } + /** * Generates and stores Nostr events from the parsed AsciiDoc document. The events can be * modified via the parser's API and retrieved via the `getEvents()` method. @@ -558,6 +581,62 @@ export default class Pharos { } } + /** + * Uses the NDK to crawl the event tree of a publication and return its content as a string. + * @param event The root index event of the publication. + * @returns The content of the publication as a string. + * @remarks This function does a depth-first crawl of the event tree using the relays specified + * on the NDK instance. + */ + private async getPublicationContent(event: NDKEvent, depth: number = 0): Promise { + let content: string = ''; + + // Format title into AsciiDoc header. + const title = event.getMatchingTags('title')[0][1]; + let titleLevel = ''; + for (let i = 0; i <= depth; i++) { + titleLevel += '='; + } + content += `${titleLevel} ${title}\n\n`; + + // TODO: Deprecate `e` tags in favor of `a` tags required by NIP-62. + let tags = event.getMatchingTags('a'); + if (tags.length === 0) { + tags = event.getMatchingTags('e'); + } + + // Base case: The event is a zettel. + if (zettelKinds.includes(event.kind ?? -1)) { + content += event.content; + return content; + } + + // Recursive case: The event is an index. + const childEvents = await Promise.all( + tags.map(tag => this.ndk.fetchEventFromTag(tag, event)) + ); + + // Michael J - 15 December 2024 - This could be further parallelized by recursively fetching + // children of index events before processing them for content. We won't make that change now, + // as it would increase complexity, but if performance suffers, we can revisit this option. + const childContentPromises: Promise[] = []; + for (let i = 0; i < childEvents.length; i++) { + const childEvent = childEvents[i]; + + if (!childEvent) { + console.warn(`NDK could not find event ${tags[i][1]}.`); + continue; + } + + childContentPromises.push(this.getPublicationContent(childEvent, depth + 1)); + } + + const childContents = await Promise.all(childContentPromises); + content += childContents.join('\n\n'); + + return content; + } + // #endregion // #region NDKEvent Generation From c57469478756c89edfa5db91609b95536a4fcb4b Mon Sep 17 00:00:00 2001 From: buttercat1791 Date: Sun, 15 Dec 2024 12:59:35 -0600 Subject: [PATCH 18/27] Use Preview component for reader view --- src/lib/components/Article.svelte | 116 +++++++++++++----------------- 1 file changed, 48 insertions(+), 68 deletions(-) diff --git a/src/lib/components/Article.svelte b/src/lib/components/Article.svelte index 032865c..2935342 100644 --- a/src/lib/components/Article.svelte +++ b/src/lib/components/Article.svelte @@ -3,36 +3,24 @@ import type { NDKEvent } from '@nostr-dev-kit/ndk'; import { page } from '$app/stores'; import { Button, Heading, Sidebar, SidebarGroup, SidebarItem, SidebarWrapper, Skeleton, TextPlaceholder, Tooltip } from 'flowbite-svelte'; - import showdown from 'showdown'; import { onMount } from 'svelte'; import { BookOutline } from 'flowbite-svelte-icons'; - import { zettelKinds } from '../consts'; + import Pharos, { parser } from '$lib/parser'; + import Preview from './Preview.svelte'; export let index: NDKEvent | null | undefined; + $parser ??= new Pharos($ndk); + $: activeHash = $page.url.hash; - const getEvents = async (index?: NDKEvent | null | undefined): Promise> => { - if (index == null) { - // TODO: Add error handling. + const getContentRoot = async (index?: NDKEvent | null | undefined): Promise => { + if (!index) { + return null; } - const eventIds = index!.getMatchingTags('e').map((value) => value[1]); - const events = await $ndk.fetchEvents( - { - // @ts-ignore - kinds: zettelKinds, - ids: eventIds, - }, - { - groupable: false, - skipVerification: false, - skipValidation: false - } - ); - - console.debug(`Fetched ${events.size} events from ${eventIds.length} references.`); - return events; + await $parser.fetch(index); + return $parser.getRootIndexId(); }; function normalizeHashPath(str: string): string { @@ -104,62 +92,54 @@ window.removeEventListener('click', hideTocOnClick); }; }); - - const converter = new showdown.Converter(); -{#await getEvents(index)} +{#await getContentRoot(index)} -{:then events} - {#if showTocButton && !showToc} - - - Show Table of Contents - - {/if} - {#if showToc} - - - - {#each events as event} - - {/each} - - - +{:then rootId} + {#if rootId} + {#if showTocButton && !showToc} + + + Show Table of Contents + + {/if} + + +
+ +
+ {:else} + {/if} -
- {#each events as event} -
- - {event.getMatchingTags('title')[0][1]} - - {@html converter.makeHtml(event.content)} -
- {/each} -
{/await} From d50ac5d58b8d44c423d68275bb6c29efd01323fa Mon Sep 17 00:00:00 2001 From: limina1 Date: Thu, 19 Dec 2024 16:45:42 -0500 Subject: [PATCH 20/27] fix styling, reactive graph display --- src/app.css | 9 ++ src/lib/components/EventNetwork.svelte | 109 ++++++++++++++++--------- 2 files changed, 79 insertions(+), 39 deletions(-) diff --git a/src/app.css b/src/app.css index 8a26f05..1953be8 100644 --- a/src/app.css +++ b/src/app.css @@ -157,3 +157,12 @@ @apply fill-[#d6c1a8]; } } +@layer components { + .leather-legend { + @apply flex-shrink-0 p-4 bg-primary-0 dark:bg-primary-1000 rounded-lg shadow + border border-gray-200 dark:border-gray-800; + } + .tooltip-leather { + @apply bg-primary-0 dark:bg-primary-1000 text-gray-800 dark:text-gray-300; + } +} diff --git a/src/lib/components/EventNetwork.svelte b/src/lib/components/EventNetwork.svelte index 920f977..f872068 100644 --- a/src/lib/components/EventNetwork.svelte +++ b/src/lib/components/EventNetwork.svelte @@ -8,13 +8,16 @@ let svg: SVGSVGElement; let isDarkMode = false; const nodeRadius = 20; - const dragRadius = 10; + const dragRadiusMultiplier = 2; const linkDistance = 5; const warmupClickEnergy = 0.9; // Energy to restart simulation on drag let container: HTMLDivElement; let width: number; let height: number; + let windowHeight: number; + + $: graphHeight = windowHeight ? Math.max(windowHeight * 0.2, 400) : 400; $: if (container) { width = container.clientWidth || 800; @@ -199,7 +202,7 @@ .join("marker") .attr("id", "arrowhead") .attr("viewBox", "0 -5 20 20") - .attr("refX", nodeRadius + 10) + .attr("refX", nodeRadius * dragRadiusMultiplier) .attr("refY", 0) .attr("markerWidth", 8) .attr("markerHeight", 8) @@ -309,8 +312,9 @@ .append("div") .attr( "class", - "fixed hidden bg-primary-0 dark:bg-primary-1000 " + - "text-gray-800 dark:text-gray-300 " + + "tooltip-leather fixed hidden p-4 rounded shadow-lg " + + "bg-primary-0 dark:bg-primary-800 " + + "border border-gray-200 dark:border-gray-800 " + "p-4 rounded shadow-lg border border-gray-200 dark:border-gray-800 " + "transition-colors duration-200", ) @@ -375,6 +379,14 @@ onMount(() => { isDarkMode = document.body.classList.contains("dark"); + // Add window resize listener + const handleResize = () => { + windowHeight = window.innerHeight; + }; + + // Initial resize + windowHeight = window.innerHeight; + window.addEventListener("resize", handleResize); // Watch for theme changes const themeObserver = new MutationObserver((mutations) => { @@ -383,7 +395,7 @@ const newIsDarkMode = document.body.classList.contains("dark"); if (newIsDarkMode !== isDarkMode) { isDarkMode = newIsDarkMode; - drawNetwork(); + // drawNetwork(); } } }); @@ -392,9 +404,12 @@ const resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { width = entry.contentRect.width; - height = entry.contentRect.height * 1 || width * 1.0; + height = graphHeight; } - // if (svg) drawNetwork(); + + // first remove all nodes and links + d3.select(svg).selectAll("*").remove(); + drawNetwork(); }); // Start observers @@ -417,47 +432,39 @@ } -# /lib/components/EventNetwork.svelte -
- -
-

- Legend -

-
    -
  • -
    - +
    +
    + +
    + + +
    +

    Legend

    +
      +
    • +
      - I + + I
      Index events (kind 30040) - Each with a unique pastel color
    • -
    • -
      - - C + +
    • +
      + + C
      Content events (kind 30041) - Publication sections
    • -
    • + +
    • From cb5ebc4fe231bac780971ef55890aebe31360a8a Mon Sep 17 00:00:00 2001 From: limina1 Date: Thu, 19 Dec 2024 16:55:25 -0500 Subject: [PATCH 21/27] Added flowbite components for loading error --- src/routes/visualize/+page.svelte | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/routes/visualize/+page.svelte b/src/routes/visualize/+page.svelte index c1560ba..d947104 100644 --- a/src/routes/visualize/+page.svelte +++ b/src/routes/visualize/+page.svelte @@ -68,15 +68,37 @@ {#if loading}
      -

      Loading network data...

      +
      + + Loading... +
      {:else if error} From 48c21030a807cce10fd3008721ca0156c73178c8 Mon Sep 17 00:00:00 2001 From: limina1 Date: Thu, 19 Dec 2024 19:11:02 -0500 Subject: [PATCH 22/27] Add arrows point to next node --- src/app.css | 2 +- src/lib/components/EventNetwork.svelte | 79 ++++++++++++++------------ 2 files changed, 45 insertions(+), 36 deletions(-) diff --git a/src/app.css b/src/app.css index 1953be8..4a63456 100644 --- a/src/app.css +++ b/src/app.css @@ -148,7 +148,7 @@ @apply text-gray-800 hover:text-primary-400 dark:text-gray-300 dark:hover:text-primary-500; } .network-link-leather { - @apply stroke-gray-400 dark:stroke-gray-600 fill-gray-400 dark:fill-gray-600; + @apply stroke-gray-400 fill-gray-400; } .network-node-leather { @apply stroke-gray-800; diff --git a/src/lib/components/EventNetwork.svelte b/src/lib/components/EventNetwork.svelte index f872068..1834dbb 100644 --- a/src/lib/components/EventNetwork.svelte +++ b/src/lib/components/EventNetwork.svelte @@ -8,8 +8,8 @@ let svg: SVGSVGElement; let isDarkMode = false; const nodeRadius = 20; - const dragRadiusMultiplier = 2; const linkDistance = 5; + const arrowDistance = 3; const warmupClickEnergy = 0.9; // Energy to restart simulation on drag let container: HTMLDivElement; @@ -195,24 +195,25 @@ g = svgElement.append("g"); // Define arrow marker with black fill - const marker = g - .append("defs") - .selectAll("marker") - .data(["arrowhead"]) - .join("marker") - .attr("id", "arrowhead") - .attr("viewBox", "0 -5 20 20") - .attr("refX", nodeRadius * dragRadiusMultiplier) - .attr("refY", 0) - .attr("markerWidth", 8) - .attr("markerHeight", 8) - .attr("orient", "auto"); - - marker - .append("path") - .attr("d", "M -8,-5 L 0, 0 L -8, 5 Z") - .attr("class", "network-link-leather"); // Black fill for arrowhead } + svgElement.select("defs").remove(); + const defs = svgElement.append("defs"); + defs + .append("marker") + .attr("id", "arrowhead") + .attr("markerUnits", "strokeWidth") // Added this + .attr("viewBox", "-10 -5 10 10") + .attr("refX", 0) + .attr("refY", 0) + .attr("markerWidth", 5) + .attr("markerHeight", 5) + .attr("orient", "auto") + .append("path") + .attr("d", "M -10 -5 L 0 0 L -10 5 z") + .attr("class", "network-link-leather") + .attr("fill", "none") + .attr("stroke-width", 1); // Added stroke + // Force simulation setup const simulation = d3 .forceSimulation(nodes) @@ -234,21 +235,30 @@ const dragHandler = setupDragHandlers(simulation); // Create links + // First, make sure we're selecting and creating links correctly const link = g - .selectAll("path.link") - .data(links, (d: NetworkLink) => `${d.source.id}-${d.target.id}`) + .selectAll("path") // Changed from "path.link" to just "path" + .data(links) .join( (enter) => enter .append("path") - .attr("class", "network-link-leather") .attr("stroke-width", 2) - .attr("fill", "transparent") - .attr("marker-end", "url(#arrowhead)"), + .attr("marker-end", "url(#arrowhead)") // This should now be applied + .attr("class", "network-link-leather"), // Add class if needed (update) => update, (exit) => exit.remove(), ); + // Let's verify the links are being created + console.log( + "Number of paths created:", + document.querySelectorAll("path").length, + ); + console.log( + "Paths with marker-end:", + document.querySelectorAll("path[marker-end]").length, + ); // Create nodes const node = g .selectAll("g.node") @@ -365,17 +375,22 @@ const dy = d.target.y! - d.source.y!; const angle = Math.atan2(dy, dx); - // Adjust start and end points to prevent overlap with nodes - const startX = d.source.x! + nodeRadius * Math.cos(angle); - const startY = d.source.y! + nodeRadius * Math.sin(angle); - const endX = d.target.x! - nodeRadius * Math.cos(angle); - const endY = d.target.y! - nodeRadius * Math.sin(angle); + // Adjust these values to fine-tune the gap + const sourceGap = nodeRadius; + const targetGap = nodeRadius + arrowDistance; // Increased gap for arrowhead + + const startX = d.source.x! + sourceGap * Math.cos(angle); + const startY = d.source.y! + sourceGap * Math.sin(angle); + const endX = d.target.x! - targetGap * Math.cos(angle); + const endY = d.target.y! - targetGap * Math.sin(angle); return `M${startX},${startY}L${endX},${endY}`; }); node.attr("transform", (d) => `translate(${d.x},${d.y})`); }); } + console.log("Marker definition:", document.querySelector("defs marker")); + console.log("Path with marker:", document.querySelector("path[marker-end]")); onMount(() => { isDarkMode = document.body.classList.contains("dark"); @@ -466,13 +481,7 @@
    • - + Arrows indicate reading/sequence order
    • From 2fa3e067afb444c4ab20d62bdd3cd615d9ee09e2 Mon Sep 17 00:00:00 2001 From: limina1 Date: Sat, 21 Dec 2024 19:19:53 -0500 Subject: [PATCH 23/27] Reactive graph & legend without default scroll bar --- src/lib/components/EventNetwork.svelte | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/lib/components/EventNetwork.svelte b/src/lib/components/EventNetwork.svelte index 1834dbb..0563348 100644 --- a/src/lib/components/EventNetwork.svelte +++ b/src/lib/components/EventNetwork.svelte @@ -20,7 +20,7 @@ $: graphHeight = windowHeight ? Math.max(windowHeight * 0.2, 400) : 400; $: if (container) { - width = container.clientWidth || 800; + width = container.clientWidth || 1000; height = container.clientHeight || 600; } interface NetworkNode extends d3.SimulationNodeDatum { @@ -447,14 +447,15 @@ } -
      -
      +
      +
      -

      Legend

      From c697059fc4b2242dca3bba31800edebec8392af0 Mon Sep 17 00:00:00 2001 From: limina1 Date: Sat, 21 Dec 2024 19:50:58 -0500 Subject: [PATCH 24/27] Add log gravity for graph Stops events from pulling to center of graph, can drag outwards --- src/lib/components/EventNetwork.svelte | 68 ++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/src/lib/components/EventNetwork.svelte b/src/lib/components/EventNetwork.svelte index 0563348..7061628 100644 --- a/src/lib/components/EventNetwork.svelte +++ b/src/lib/components/EventNetwork.svelte @@ -8,7 +8,7 @@ let svg: SVGSVGElement; let isDarkMode = false; const nodeRadius = 20; - const linkDistance = 5; + const linkDistance = 1; const arrowDistance = 3; const warmupClickEnergy = 0.9; // Energy to restart simulation on drag let container: HTMLDivElement; @@ -46,6 +46,25 @@ function createEventMap(events: NDKEvent[]): Map { return new Map(events.map((event) => [event.id, event])); } + const logGravity = (alpha: number) => { + const centerX = width / 2; + const centerY = height / 2; + const gravityStrength = 0.1; // Adjustable parameter + + return function (d: NetworkNode) { + const dx = d.x! - centerX; + const dy = d.y! - centerY; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance === 0) return; + + // Logarithmic falloff - force gets stronger the further out you go + const force = Math.log(distance + 1) * gravityStrength * alpha; + + d.vx! -= (dx / distance) * force; + d.vy! -= (dy / distance) * force; + }; + }; function getNode( id: string, @@ -185,7 +204,7 @@ const zoom = d3 .zoom() - .scaleExtent([0.4, 9]) + .scaleExtent([0.1, 9]) .on("zoom", (event) => { g.attr("transform", event.transform); }); @@ -224,14 +243,42 @@ .id((d) => d.id) .distance(linkDistance), ) - .force("charge", d3.forceManyBody().strength(-1000)) - .force("center", d3.forceCenter(width / 2, height / 2)) - .force("x", d3.forceX(width / 2).strength(0.1)) - .force("y", d3.forceY(height / 2).strength(0.1)) - .force( - "collision", - d3.forceCollide().radius(nodeRadius * 2.5), - ); + .force("gravity", logGravity) + .force("collide", d3.forceCollide().radius(nodeRadius * 4)); + // .force("charge", d3.forceManyBody().strength(-1000)) + // .force("center", d3.forceCenter(width / 2, height / 2)) + // .force("x", d3.forceX(width / 2).strength(0.1)) + // .force("y", d3.forceY(height / 2).strength(0.1)) + // .force( + // "collision", + // d3.forceCollide().radius(nodeRadius * 2), + // ); + simulation.on("end", () => { + // Get the bounds of the graph + const bounds = g.node()?.getBBox(); + if (bounds) { + const dx = bounds.width; + const dy = bounds.height; + const x = bounds.x; + const y = bounds.y; + + // Calculate scale to fit + const scale = 1.25 / Math.max(dx / width, dy / height); + const translate = [ + width / 2 - scale * (x + dx / 2), + height / 2 - scale * (y + dy / 2), + ]; + + // Apply the initial transform + svgElement + .transition() + .duration(750) + .call( + zoom.transform, + d3.zoomIdentity.translate(translate[0], translate[1]).scale(scale), + ); + } + }); const dragHandler = setupDragHandlers(simulation); // Create links @@ -370,6 +417,7 @@ // Handle simulation ticks simulation.on("tick", () => { + logGravity(simulation.alpha())(nodes); link.attr("d", (d) => { const dx = d.target.x! - d.source.x!; const dy = d.target.y! - d.source.y!; From 4ceffc5b6034d1e57a5d0271864f98f630a32e83 Mon Sep 17 00:00:00 2001 From: limina1 Date: Sat, 21 Dec 2024 21:32:58 -0500 Subject: [PATCH 25/27] Change to function instead of const --- src/app.css | 9 --- src/lib/components/EventNetwork.svelte | 91 +++++++++++++++----------- 2 files changed, 53 insertions(+), 47 deletions(-) diff --git a/src/app.css b/src/app.css index 4a63456..3f197fa 100644 --- a/src/app.css +++ b/src/app.css @@ -8,15 +8,6 @@ @apply bg-primary-0 dark:bg-primary-1000 text-gray-800 dark:text-gray-300; } - /* Button */ - button.btn-leather.text-white { - @apply text-primary-0; - } - - .btn-leather span svg { - @apply fill-gray-800 hover:fill-primary-400 dark:fill-gray-300 dark:hover:fill-primary-500; - } - .btn-leather.text-xs { @apply w-7 h-7; } diff --git a/src/lib/components/EventNetwork.svelte b/src/lib/components/EventNetwork.svelte index 7061628..c761639 100644 --- a/src/lib/components/EventNetwork.svelte +++ b/src/lib/components/EventNetwork.svelte @@ -8,8 +8,8 @@ let svg: SVGSVGElement; let isDarkMode = false; const nodeRadius = 20; - const linkDistance = 1; - const arrowDistance = 3; + const linkDistance = 10; + const arrowDistance = 10; const warmupClickEnergy = 0.9; // Energy to restart simulation on drag let container: HTMLDivElement; @@ -23,6 +23,7 @@ width = container.clientWidth || 1000; height = container.clientHeight || 600; } + interface NetworkNode extends d3.SimulationNodeDatum { id: string; event?: NDKEvent; @@ -43,28 +44,58 @@ target: NetworkNode; isSequential: boolean; } + function createEventMap(events: NDKEvent[]): Map { return new Map(events.map((event) => [event.id, event])); } - const logGravity = (alpha: number) => { - const centerX = width / 2; - const centerY = height / 2; - const gravityStrength = 0.1; // Adjustable parameter + function applyGlobalLogGravity( + node: NetworkNode, + centerX: number, + centerY: number, + alpha: number, + ) { + const globalGravityStrength = 0.05; + const dx = node.x! - centerX; + const dy = node.y! - centerY; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance === 0) return; - return function (d: NetworkNode) { - const dx = d.x! - centerX; - const dy = d.y! - centerY; - const distance = Math.sqrt(dx * dx + dy * dy); + const force = Math.log(distance + 1) * globalGravityStrength * alpha; + node.vx! -= (dx / distance) * force; + node.vy! -= (dy / distance) * force; + } - if (distance === 0) return; + function applyConnectedGravity( + node: NetworkNode, + links: NetworkLink[], + alpha: number, + ) { + const connectedGravityStrength = 0.3; - // Logarithmic falloff - force gets stronger the further out you go - const force = Math.log(distance + 1) * gravityStrength * alpha; + const connectedNodes = links + .filter( + (link) => link.source.id === node.id || link.target.id === node.id, + ) + .map((link) => (link.source.id === node.id ? link.target : link.source)); - d.vx! -= (dx / distance) * force; - d.vy! -= (dy / distance) * force; - }; - }; + if (connectedNodes.length === 0) return; + + const cogX = d3.mean(connectedNodes, (node) => node.x); + const cogY = d3.mean(connectedNodes, (node) => node.y); + + if (cogX === undefined || cogY === undefined) return; + + const dx = node.x! - cogX; + const dy = node.y! - cogY; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance === 0) return; + + const force = distance * connectedGravityStrength * alpha; + node.vx! -= (dx / distance) * force; + node.vy! -= (dy / distance) * force; + } function getNode( id: string, @@ -241,18 +272,9 @@ d3 .forceLink(links) .id((d) => d.id) - .distance(linkDistance), + .distance(linkDistance * 0.1), ) - .force("gravity", logGravity) .force("collide", d3.forceCollide().radius(nodeRadius * 4)); - // .force("charge", d3.forceManyBody().strength(-1000)) - // .force("center", d3.forceCenter(width / 2, height / 2)) - // .force("x", d3.forceX(width / 2).strength(0.1)) - // .force("y", d3.forceY(height / 2).strength(0.1)) - // .force( - // "collision", - // d3.forceCollide().radius(nodeRadius * 2), - // ); simulation.on("end", () => { // Get the bounds of the graph const bounds = g.node()?.getBBox(); @@ -297,15 +319,6 @@ (exit) => exit.remove(), ); - // Let's verify the links are being created - console.log( - "Number of paths created:", - document.querySelectorAll("path").length, - ); - console.log( - "Paths with marker-end:", - document.querySelectorAll("path[marker-end]").length, - ); // Create nodes const node = g .selectAll("g.node") @@ -417,13 +430,15 @@ // Handle simulation ticks simulation.on("tick", () => { - logGravity(simulation.alpha())(nodes); + nodes.forEach((node) => { + applyGlobalLogGravity(node, width / 2, height / 2, simulation.alpha()); + applyConnectedGravity(node, links, simulation.alpha()); + }); link.attr("d", (d) => { const dx = d.target.x! - d.source.x!; const dy = d.target.y! - d.source.y!; const angle = Math.atan2(dy, dx); - // Adjust these values to fine-tune the gap const sourceGap = nodeRadius; const targetGap = nodeRadius + arrowDistance; // Increased gap for arrowhead From 9647c41007c847bdc43521cc87aa7ba6a40b37e1 Mon Sep 17 00:00:00 2001 From: limina1 Date: Tue, 31 Dec 2024 15:16:49 -0500 Subject: [PATCH 26/27] fix vx and vy --- src/lib/components/EventNetwork.svelte | 70 ++++++++++++-------------- 1 file changed, 31 insertions(+), 39 deletions(-) diff --git a/src/lib/components/EventNetwork.svelte b/src/lib/components/EventNetwork.svelte index c761639..7ad63ba 100644 --- a/src/lib/components/EventNetwork.svelte +++ b/src/lib/components/EventNetwork.svelte @@ -37,6 +37,8 @@ y?: number; fx?: number | null; fy?: number | null; + vx?: number; + vy?: number; } interface NetworkLink extends d3.SimulationLinkDatum { @@ -48,55 +50,45 @@ function createEventMap(events: NDKEvent[]): Map { return new Map(events.map((event) => [event.id, event])); } - function applyGlobalLogGravity( - node: NetworkNode, - centerX: number, - centerY: number, - alpha: number, - ) { - const globalGravityStrength = 0.05; - const dx = node.x! - centerX; - const dy = node.y! - centerY; - const distance = Math.sqrt(dx * dx + dy * dy); - - if (distance === 0) return; - - const force = Math.log(distance + 1) * globalGravityStrength * alpha; - node.vx! -= (dx / distance) * force; - node.vy! -= (dy / distance) * force; +function updateNodeVelocity(node: NetworkNode, deltaVx: number, deltaVy: number) { + if (typeof node.vx === 'number' && typeof node.vy === 'number') { + node.vx = node.vx - deltaVx; + node.vy = node.vy - deltaVy; } +} - function applyConnectedGravity( - node: NetworkNode, - links: NetworkLink[], - alpha: number, - ) { - const connectedGravityStrength = 0.3; +function applyGlobalLogGravity(node: NetworkNode, centerX: number, centerY: number, alpha: number) { + const dx = (node.x ?? 0) - centerX; + const dy = (node.y ?? 0) - centerY; + const distance = Math.sqrt(dx * dx + dy * dy); - const connectedNodes = links - .filter( - (link) => link.source.id === node.id || link.target.id === node.id, - ) - .map((link) => (link.source.id === node.id ? link.target : link.source)); + if (distance === 0) return; - if (connectedNodes.length === 0) return; + const force = Math.log(distance + 1) * 0.05 * alpha; + updateNodeVelocity(node, (dx / distance) * force, (dy / distance) * force); +} - const cogX = d3.mean(connectedNodes, (node) => node.x); - const cogY = d3.mean(connectedNodes, (node) => node.y); +function applyConnectedGravity(node: NetworkNode, links: NetworkLink[], alpha: number) { + const connectedNodes = links + .filter(link => link.source.id === node.id || link.target.id === node.id) + .map(link => link.source.id === node.id ? link.target : link.source); - if (cogX === undefined || cogY === undefined) return; + if (connectedNodes.length === 0) return; - const dx = node.x! - cogX; - const dy = node.y! - cogY; - const distance = Math.sqrt(dx * dx + dy * dy); + const cogX = d3.mean(connectedNodes, n => n.x); + const cogY = d3.mean(connectedNodes, n => n.y); - if (distance === 0) return; + if (cogX === undefined || cogY === undefined) return; - const force = distance * connectedGravityStrength * alpha; - node.vx! -= (dx / distance) * force; - node.vy! -= (dy / distance) * force; - } + const dx = (node.x ?? 0) - cogX; + const dy = (node.y ?? 0) - cogY; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance === 0) return; + const force = distance * 0.3 * alpha; + updateNodeVelocity(node, (dx / distance) * force, (dy / distance) * force); +} function getNode( id: string, nodeMap: Map, From ccb6106a92d1350b445a4b8203e99a5508ff5ec7 Mon Sep 17 00:00:00 2001 From: limina1 Date: Tue, 31 Dec 2024 15:46:28 -0500 Subject: [PATCH 27/27] fix update scaling --- src/lib/components/EventNetwork.svelte | 83 +++++++++++++++----------- 1 file changed, 49 insertions(+), 34 deletions(-) diff --git a/src/lib/components/EventNetwork.svelte b/src/lib/components/EventNetwork.svelte index 7ad63ba..9cb90c9 100644 --- a/src/lib/components/EventNetwork.svelte +++ b/src/lib/components/EventNetwork.svelte @@ -13,15 +13,15 @@ const warmupClickEnergy = 0.9; // Energy to restart simulation on drag let container: HTMLDivElement; - let width: number; - let height: number; + let width: number = 1000; + let height: number = 600; let windowHeight: number; $: graphHeight = windowHeight ? Math.max(windowHeight * 0.2, 400) : 400; $: if (container) { - width = container.clientWidth || 1000; - height = container.clientHeight || 600; + width = container.clientWidth || width; + height = container.clientHeight || height; } interface NetworkNode extends d3.SimulationNodeDatum { @@ -50,45 +50,60 @@ function createEventMap(events: NDKEvent[]): Map { return new Map(events.map((event) => [event.id, event])); } -function updateNodeVelocity(node: NetworkNode, deltaVx: number, deltaVy: number) { - if (typeof node.vx === 'number' && typeof node.vy === 'number') { - node.vx = node.vx - deltaVx; - node.vy = node.vy - deltaVy; + function updateNodeVelocity( + node: NetworkNode, + deltaVx: number, + deltaVy: number, + ) { + if (typeof node.vx === "number" && typeof node.vy === "number") { + node.vx = node.vx - deltaVx; + node.vy = node.vy - deltaVy; + } } -} -function applyGlobalLogGravity(node: NetworkNode, centerX: number, centerY: number, alpha: number) { - const dx = (node.x ?? 0) - centerX; - const dy = (node.y ?? 0) - centerY; - const distance = Math.sqrt(dx * dx + dy * dy); + function applyGlobalLogGravity( + node: NetworkNode, + centerX: number, + centerY: number, + alpha: number, + ) { + const dx = (node.x ?? 0) - centerX; + const dy = (node.y ?? 0) - centerY; + const distance = Math.sqrt(dx * dx + dy * dy); - if (distance === 0) return; + if (distance === 0) return; - const force = Math.log(distance + 1) * 0.05 * alpha; - updateNodeVelocity(node, (dx / distance) * force, (dy / distance) * force); -} + const force = Math.log(distance + 1) * 0.05 * alpha; + updateNodeVelocity(node, (dx / distance) * force, (dy / distance) * force); + } -function applyConnectedGravity(node: NetworkNode, links: NetworkLink[], alpha: number) { - const connectedNodes = links - .filter(link => link.source.id === node.id || link.target.id === node.id) - .map(link => link.source.id === node.id ? link.target : link.source); + function applyConnectedGravity( + node: NetworkNode, + links: NetworkLink[], + alpha: number, + ) { + const connectedNodes = links + .filter( + (link) => link.source.id === node.id || link.target.id === node.id, + ) + .map((link) => (link.source.id === node.id ? link.target : link.source)); - if (connectedNodes.length === 0) return; + if (connectedNodes.length === 0) return; - const cogX = d3.mean(connectedNodes, n => n.x); - const cogY = d3.mean(connectedNodes, n => n.y); + const cogX = d3.mean(connectedNodes, (n) => n.x); + const cogY = d3.mean(connectedNodes, (n) => n.y); - if (cogX === undefined || cogY === undefined) return; + if (cogX === undefined || cogY === undefined) return; - const dx = (node.x ?? 0) - cogX; - const dy = (node.y ?? 0) - cogY; - const distance = Math.sqrt(dx * dx + dy * dy); + const dx = (node.x ?? 0) - cogX; + const dy = (node.y ?? 0) - cogY; + const distance = Math.sqrt(dx * dx + dy * dy); - if (distance === 0) return; + if (distance === 0) return; - const force = distance * 0.3 * alpha; - updateNodeVelocity(node, (dx / distance) * force, (dy / distance) * force); -} + const force = distance * 0.3 * alpha; + updateNodeVelocity(node, (dx / distance) * force, (dy / distance) * force); + } function getNode( id: string, nodeMap: Map, @@ -107,6 +122,8 @@ function applyConnectedGravity(node: NetworkNode, links: NetworkLink[], alpha: n content: event?.content || "", author: event?.pubkey || "", type: event?.kind === 30040 ? "Index" : "Content", + x: width / 2 + (Math.random() - 0.5) * 100, + y: height / 2 + (Math.random() - 0.5) * 100, }; nodeMap.set(id, node); } @@ -444,8 +461,6 @@ function applyConnectedGravity(node: NetworkNode, links: NetworkLink[], alpha: n node.attr("transform", (d) => `translate(${d.x},${d.y})`); }); } - console.log("Marker definition:", document.querySelector("defs marker")); - console.log("Path with marker:", document.querySelector("path[marker-end]")); onMount(() => { isDarkMode = document.body.classList.contains("dark");