Browse Source

Add node and link interfaces

master
limina1 1 year ago
parent
commit
5b790c1279
  1. 104
      src/lib/components/EventNetwork.svelte
  2. 6
      src/routes/visualize/+page.svelte

104
src/lib/components/EventNetwork.svelte

@ -5,14 +5,20 @@
export let events: NDKEvent[] = []; export let events: NDKEvent[] = [];
let svg; let svg: SVGSVGElement;
let isDarkMode = false; let isDarkMode = false;
const nodeRadius = 20; const nodeRadius = 20;
const dragRadius = 45; const dragRadius = 45;
const linkDistance = 120; const linkDistance = 120;
let container: HTMLDivElement; let container: HTMLDivElement;
let width: number; let width: number;
let height: number; let height: number;
$: if (container) {
width = container.clientWidth || 800;
height = container.clientHeight || 600;
}
interface NetworkNode { interface NetworkNode {
id: string; id: string;
event?: NDKEvent; event?: NDKEvent;
@ -24,6 +30,35 @@
type: "Index" | "Content"; type: "Index" | "Content";
} }
interface NetworkLink {
source: NetworkNode;
target: NetworkNode;
isSequential: boolean;
}
function getNode(
id: string,
nodeMap: Map<string, NetworkNode>,
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 { function getEventColor(eventId: string): string {
const num = parseInt(eventId.slice(0, 4), 16); const num = parseInt(eventId.slice(0, 4), 16);
const hue = num % 360; const hue = num % 360;
@ -31,51 +66,33 @@
const lightness = 75; const lightness = 75;
return `hsl(${hue}, ${saturation}%, ${lightness}%)`; return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
} }
function generateGraph(events: NDKEvent[]): {
nodes: NetworkNode[];
links: NetworkLink[];
} {
const nodes: NetworkNode[] = [];
const links: NetworkLink[] = [];
const nodeMap = new Map<string, NetworkNode>();
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); const indexEvents = events.filter((e) => e.kind === 30040);
indexEvents.forEach((index) => { indexEvents.forEach((index) => {
if (!index.id) return; if (!index.id) return;
const contentRefs = index.getMatchingTags("e"); const contentRefs = index.getMatchingTags("e");
const sourceNode = getNode(index.id, index); const sourceNode = getNode(index.id, nodeMap, index);
if (!sourceNode) return; if (!sourceNode) return;
nodes.push(sourceNode);
// Create a linear chain of content events
contentRefs.forEach((tag, idx) => { contentRefs.forEach((tag, idx) => {
if (!tag[1]) return; if (!tag[1]) return;
const targetEvent = events.find((e) => e.id === tag[1]); const targetEvent = events.find((e) => e.id === tag[1]);
if (!targetEvent) return; if (!targetEvent) return;
const targetNode = getNode(tag[1], targetEvent, idx); const targetNode = getNode(tag[1], nodeMap, targetEvent, idx);
if (!targetNode) return; if (!targetNode) return;
nodes.push(targetNode);
const prevNodeId = const prevNodeId =
idx === 0 ? sourceNode.id : contentRefs[idx - 1]?.[1]; idx === 0 ? sourceNode.id : contentRefs[idx - 1]?.[1];
@ -93,7 +110,6 @@
return { nodes, links }; return { nodes, links };
} }
function drawNetwork() { function drawNetwork() {
if (!svg || !events?.length) return; if (!svg || !events?.length) return;
@ -102,14 +118,7 @@
const { nodes, links } = generateGraph(events); const { nodes, links } = generateGraph(events);
if (!nodes.length) return; if (!nodes.length) return;
const svgElement = d3 const svgElement = d3.select(svg).attr("viewBox", `0 0 ${width} ${height}`);
.select(svg)
.attr(
"class",
"network-leather w-full border border-gray-300 dark:border-gray-700 rounded",
)
.attr("viewBox", [0, 0, width, height]);
// Set up zoom behavior // Set up zoom behavior
const zoom = d3 const zoom = d3
.zoom() .zoom()
@ -124,19 +133,22 @@
// Force simulation setup // Force simulation setup
const simulation = d3 const simulation = d3
.forceSimulation(nodes) .forceSimulation<NetworkNode>(nodes)
.force( .force(
"link", "link",
d3 d3
.forceLink(links) .forceLink<NetworkNode, NetworkLink>(links)
.id((d) => d.id) .id((d) => d.id)
.distance(linkDistance), .distance(linkDistance),
) )
.force("charge", d3.forceManyBody().strength(-500)) .force("charge", d3.forceManyBody<NetworkNode>().strength(-500))
.force("center", d3.forceCenter(width / 2, height / 2)) .force("center", d3.forceCenter(width / 2, height / 2))
.force("x", d3.forceX(width / 2).strength(0.1)) .force("x", d3.forceX<NetworkNode>(width / 2).strength(0.1))
.force("y", d3.forceY(height / 2).strength(0.1)) .force("y", d3.forceY<NetworkNode>(height / 2).strength(0.1))
.force("collision", d3.forceCollide().radius(nodeRadius * 2.5)); .force(
"collision",
d3.forceCollide<NetworkNode>().radius(nodeRadius * 2.5),
);
// Define arrow marker with black fill // Define arrow marker with black fill
const marker = g const marker = g
@ -322,7 +334,7 @@
}); });
}); });
const resizeObserver = new ResizeObserver((entries) => { let resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) { for (const entry of entries) {
width = entry.contentRect.width; width = entry.contentRect.width;
height = entry.contentRect.height || width * 0.6; height = entry.contentRect.height || width * 0.6;

6
src/routes/visualize/+page.svelte

@ -83,10 +83,6 @@
{:else} {:else}
<EventNetwork {events} /> <EventNetwork {events} />
<div class="mt-8 prose dark:prose-invert max-w-none"> <div class="mt-8 prose dark:prose-invert max-w-none"></div>
<!-- Legend section with proper styling -->
<h2 class="h-leather">About This Visualization</h2>
<!-- ... rest of the content ... -->
</div>
{/if} {/if}
</div> </div>

Loading…
Cancel
Save