Browse Source

add eventMap, fix redrawing

master
limina1 1 year ago
parent
commit
a20cfc1409
  1. 377
      src/lib/components/EventNetwork.svelte

377
src/lib/components/EventNetwork.svelte

@ -11,15 +11,17 @@ @@ -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 @@ @@ -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<NetworkNode> {
source: NetworkNode;
target: NetworkNode;
isSequential: boolean;
}
// Create an efficient event map for O(1) lookups
function createEventMap(events: NDKEvent[]): Map<string, NDKEvent> {
return new Map(events.map((event) => [event.id, event]));
}
function getNode(
id: string,
nodeMap: Map<string, NetworkNode>,
@ -59,12 +70,11 @@ @@ -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 @@ @@ -74,12 +84,7 @@
const nodes: NetworkNode[] = [];
const links: NetworkLink[] = [];
const nodeMap = new Map<string, NetworkNode>();
// Create event lookup map - O(n) operation done once
const eventMap = new Map<string, NDKEvent>();
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 @@ @@ -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 @@ @@ -119,28 +124,96 @@
return { nodes, links };
}
function setupDragHandlers(
simulation: d3.Simulation<NetworkNode, undefined>,
) {
// Create drag behavior with proper typing
const dragBehavior = d3
.drag<SVGGElement, NetworkNode>()
.on(
"start",
(
event: d3.D3DragEvent<SVGGElement, NetworkNode, NetworkNode>,
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<SVGGElement, NetworkNode, NetworkNode>,
d: NetworkNode,
) => {
// Update fixed position to drag position
d.fx = event.x;
d.fy = event.y;
},
)
.on(
"end",
(
event: d3.D3DragEvent<SVGGElement, NetworkNode, NetworkNode>,
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<SVGSVGElement, unknown>()
.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<NetworkNode>(nodes)
.force(
@ -150,183 +223,104 @@ @@ -150,183 +223,104 @@
.id((d) => d.id)
.distance(linkDistance),
)
.force("charge", d3.forceManyBody<NetworkNode>().strength(-500))
.force("charge", d3.forceManyBody().strength(-500))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("x", d3.forceX<NetworkNode>(width / 2).strength(0.1))
.force("y", d3.forceY<NetworkNode>(height / 2).strength(0.1))
.force(
"collision",
d3.forceCollide<NetworkNode>().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<SVGPathElement, NetworkLink>("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<SVGGElement, NetworkNode>("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(
`
<div class="space-y-2">
<div class="font-bold text-base">${d.title}</div>
<div class="text-gray-600 dark:text-gray-400 text-sm">
${d.type} (${d.isContainer ? "30040" : "30041"})
</div>
<div class="text-gray-600 dark:text-gray-400 text-sm overflow-hidden text-ellipsis">
ID: ${d.id}
</div>
${
d.content
? `
<div class="mt-2 text-xs bg-gray-100 dark:bg-gray-800 p-2 rounded overflow-auto max-h-40">
${d.content}
</div>
`
: ""
}
</div>
`,
)
.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 @@ @@ -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 @@ @@ -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();
}
}
</script>
# /lib/components/EventNetwork.svelte

Loading…
Cancel
Save