UNPKG

@wavequery/conductor

Version:
243 lines 8.7 kB
import * as d3 from "d3"; export class GraphRenderer { constructor(containerId, config) { const container = document.getElementById(containerId); if (!container) throw new Error("Container not found"); this.container = container; this.config = config; this.initialize(); } initialize() { this.setupSVG(); this.setupSimulation(); this.setupEventListeners(); } setupSVG() { const { width, height } = this.container.getBoundingClientRect(); this.svg = d3 .select(this.container) .append("svg") .attr("width", width) .attr("height", height) .attr("class", `theme-${this.config.theme}`); // Create defs for markers and filters const defs = this.svg.append("defs"); this.createMarkers(defs); this.createFilters(defs); this.mainGroup = this.svg.append("g").attr("class", "main-group"); if (this.config.interactive) { this.setupZoom(); } } createMarkers(defs) { const types = ["flow", "data", "control"]; types.forEach((type) => { defs .append("marker") .attr("id", `arrow-${type}`) .attr("viewBox", "0 -5 10 10") .attr("refX", 20) .attr("refY", 0) .attr("markerWidth", 6) .attr("markerHeight", 6) .attr("orient", "auto") .append("path") .attr("d", "M0,-5L10,0L0,5") .attr("class", `arrow-${type}`); }); } createFilters(defs) { const filter = defs .append("filter") .attr("id", "glow") .attr("x", "-50%") .attr("y", "-50%") .attr("width", "200%") .attr("height", "200%"); filter .append("feGaussianBlur") .attr("stdDeviation", "3") .attr("result", "coloredBlur"); const feMerge = filter.append("feMerge"); feMerge.append("feMergeNode").attr("in", "coloredBlur"); feMerge.append("feMergeNode").attr("in", "SourceGraphic"); } setupSimulation() { const { width, height } = this.container.getBoundingClientRect(); this.simulation = d3 .forceSimulation() .force("link", d3.forceLink().id((d) => d.id)) .force("charge", d3.forceManyBody().strength(-1000)) .force("center", d3.forceCenter(width / 2, height / 2)) .on("tick", () => this.handleTick()); } setupZoom() { const zoom = d3 .zoom() .scaleExtent([0.1, 4]) .on("zoom", (event) => { this.mainGroup.attr("transform", event.transform.toString()); }); this.svg.call(zoom); } setupEventListeners() { window.addEventListener("resize", this.handleResize.bind(this)); } handleResize() { const { width, height } = this.container.getBoundingClientRect(); this.svg.attr("width", width).attr("height", height); if (this.config.fitView) { this.fitViewToContent(); } } handleTick() { // Update edge positions this.edges .select("path") .attr("d", (d) => this.calculateEdgePath(d)); // Update node positions this.nodes.attr("transform", (d) => `translate(${d.x || 0},${d.y || 0})`); // Update edge labels this.edges.select("text").attr("transform", (d) => { const source = d.source; const target = d.target; return `translate(${(source.x + target.x) / 2},${(source.y + target.y) / 2})`; }); } calculateEdgePath(edge) { const source = edge.source; const target = edge.target; const dx = target.x - source.x; const dy = target.y - source.y; const dr = Math.sqrt(dx * dx + dy * dy); return `M${source.x},${source.y}A${dr},${dr} 0 0,1 ${target.x},${target.y}`; } render(graph) { // Clear existing elements this.mainGroup.selectAll("*").remove(); const edgeGroup = this.mainGroup.append("g").attr("class", "edges"); const nodeGroup = this.mainGroup.append("g").attr("class", "nodes"); this.renderEdges(graph.edges, edgeGroup); this.renderNodes(graph.nodes, nodeGroup); // Update simulation with new data this.simulation.nodes(graph.nodes).force("link", d3 .forceLink(graph.edges) .id((d) => d.id) .distance(100) .strength(0.5)); if (this.config.autoLayout) { this.simulation.alpha(1).restart(); } // Fit view after initial render if (this.config.fitView) { setTimeout(() => this.fitViewToContent(), 100); } } renderNodes(nodes, container) { this.nodes = container .selectAll(".node") .data(nodes, (d) => d.id) .join((enter) => this.createNodes(enter), (update) => this.updateNodes(update), (exit) => this.removeNodes(exit)); } createNodes(enter) { const nodeGroups = enter .append("g") .attr("class", (d) => `node node-type-${d.type}`) .call(this.setupDragBehavior()); // Add circle nodeGroups.append("circle").attr("r", 20).attr("class", "node-circle"); // Add label nodeGroups .append("text") .attr("dy", 30) .attr("text-anchor", "middle") .attr("class", "node-label") .text((d) => d.label); // Add status indicator nodeGroups .append("circle") .attr("class", "status-indicator") .attr("r", 5) .attr("cy", -20); return nodeGroups; } updateNodes(update) { update.attr("class", (d) => `node node-type-${d.type}`); update.select(".node-label").text((d) => d.label); update .select(".status-indicator") .attr("class", (d) => `status-indicator status-${d.status}`); return update; } removeNodes(exit) { exit.transition().duration(300).style("opacity", 0).remove(); } setupDragBehavior() { return d3 .drag() .on("start", (event, d) => { if (!event.active) this.simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; }) .on("drag", (event, d) => { d.fx = event.x; d.fy = event.y; }) .on("end", (event, d) => { if (!event.active) this.simulation.alphaTarget(0); }); } renderEdges(edges, container) { this.edges = container .selectAll(".edge") .data(edges, (d) => d.id) .join((enter) => this.createEdges(enter), (update) => this.updateEdges(update), (exit) => this.removeEdges(exit)); } createEdges(enter) { const edgeGroups = enter .append("g") .attr("class", (d) => `edge edge-type-${d.type}`); edgeGroups .append("path") .attr("class", "edge-path") .attr("marker-end", (d) => `url(#arrow-${d.type})`); edgeGroups .append("text") .attr("class", "edge-label") .attr("text-anchor", "middle") .text((d) => d.label || ""); return edgeGroups; } updateEdges(update) { update.attr("class", (d) => `edge edge-type-${d.type}`); update.select(".edge-label").text((d) => d.label || ""); return update; } removeEdges(exit) { exit.transition().duration(300).style("opacity", 0).remove(); } fitViewToContent() { const bounds = this.mainGroup.node()?.getBBox(); if (!bounds) return; const { width, height } = this.container.getBoundingClientRect(); const scale = Math.min(width / bounds.width, height / bounds.height) * 0.9; const transform = d3.zoomIdentity .translate(width / 2 - (bounds.x + bounds.width / 2) * scale, height / 2 - (bounds.y + bounds.height / 2) * scale) .scale(scale); this.svg .transition() .duration(750) .call(d3.zoom().transform, transform); } dispose() { this.simulation.stop(); this.svg.remove(); window.removeEventListener("resize", this.handleResize); } } //# sourceMappingURL=graph-renderer.js.map