spinne-html 0.2.3

HTML representation of component graph for spinne
Documentation
<!DOCTYPE html>
<html>
<head>
    <title>Component Dependency Graph</title>
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <style>
        .node {
            stroke: #fff;
            stroke-width: 1.5px;
        }

        .link {
            stroke: #999;
            stroke-opacity: 0.6;
            stroke-width: 1px;
        }

        .node text {
            font-size: 12px;
            font-family: Arial, sans-serif;
        }

        .tooltip {
            position: absolute;
            padding: 8px;
            background: rgba(0, 0, 0, 0.8);
            color: white;
            border-radius: 4px;
            font-size: 12px;
            pointer-events: none;
        }

        #graph {
            width: 100%;
            height: 100vh;
            overflow: hidden;
        }

        svg {
            width: 100%;
            height: 100%;
        }
    </style>
</head>
<body>
    <div id="graph"></div>
    <script>
        // The graph data will be injected here
        const graphData = {{GRAPH_DATA}};

        // Width and height of the visualization
        const width = window.innerWidth;
        const height = window.innerHeight;

        // Create tooltip div
        const tooltip = d3.select("body")
            .append("div")
            .attr("class", "tooltip")
            .style("opacity", 0);

        // Create SVG container
        const svg = d3.select("#graph")
            .append("svg")
            .attr("width", width)
            .attr("height", height);

        svg.append("defs").append("marker")
          .attr("id", "arrowhead")
          .attr("viewBox", "0 -5 10 10")
          .attr("refX", 0)  // Offset from node
          .attr("refY", 0)
          .attr("markerWidth", 8)
          .attr("markerHeight", 8)
          .attr("orient", "auto")
          .append("path")
          .attr("d", "M0,-5L10,0L0,5")
          .attr("fill", "#999");

        const g = svg.append("g");
        const zoom = d3.zoom().scaleExtent([0.5, 4]).on("zoom", function(event) {
            g.attr("transform", event.transform);
        });
        svg.call(zoom);

        // Create the force simulation
        const simulation = d3.forceSimulation()
            .force("link", d3.forceLink().distance(100))
            .force("charge", d3.forceManyBody().strength(-300))
            .force("center", d3.forceCenter(width / 2, height / 2))
            .force("collision", d3.forceCollide().radius(50));

        // Transform edges data to include full node objects
        const links = graphData.edges.map(edge => ({
            source: edge[0],
            target: edge[1]
        }));

        // Create links
        const link = g.append("g")
            .selectAll("line")
            .data(links)
            .enter()
            .append("line")
            .attr("class", "link")
            .attr("marker-end", "url(#arrowhead)");

        // Create nodes
        const node = g.append("g")
            .selectAll("g")
            .data(graphData.nodes)
            .enter()
            .append("g")
            .call(d3.drag()
                .on("start", dragstarted)
                .on("drag", dragged)
                .on("end", dragended))
            .on("mouseover", function(event, d) {
                const transform = d3.zoomTransform(svg.node());
                tooltip.transition()
                    .duration(200)
                    .style("opacity", .9);
                tooltip.html(`Path: ${d.file_path}<br/>Props: ${Object.entries(d.prop_usage).map(([prop, count]) => `${prop}(${count})`).join(', ')}`)
                    .style("left", (event.pageX + 10) + "px")
                    .style("top", (event.pageY - 28) + "px");
            })
            .on("mouseout", function(d) {
                tooltip.transition()
                    .duration(500)
                    .style("opacity", 0);
            });

        // Add circles to nodes
        node.append("circle")
            .attr("class", "node")
            .attr("r", 5)
            .attr("fill", d => getNodeColor(d));

        // Add labels to nodes
        node.append("text")
            .attr("dx", 12)
            .attr("dy", ".35em")
            .text(d => d.name);

        // Update positions on simulation tick
        simulation
            .nodes(graphData.nodes)
            .on("tick", ticked);

        simulation.force("link")
            .links(links);

        // Tick function to update positions
        function ticked() {
            link
                .attr("x1", d => d.source.x)
                .attr("y1", d => d.source.y)
                .attr("x2", d => {
                  const dx = d.target.x - d.source.x;
                  const dy = d.target.y - d.source.y;
                  const length = Math.sqrt(dx * dx + dy * dy);
                  if (length === 0) return d.target.x;
                  return d.target.x - (dx * 15) / length;  // Offset for arrow
                })
                .attr("y2", d => {
                  const dx = d.target.x - d.source.x;
                  const dy = d.target.y - d.source.y;
                  const length = Math.sqrt(dx * dx + dy * dy);
                  if (length === 0) return d.target.y;
                  return d.target.y - (dy * 15) / length;  // Offset for arrow
                });

            node
                .attr("transform", d => `translate(${d.x},${d.y})`);
        }

        // Helper function to determine node color based on type
        function getNodeColor(node) {
            // Color based on number of props used
            const propCount = Object.keys(node.prop_usage).length;
            if (propCount === 0) return "#69b3a2";
            if (propCount < 3) return "#3498db";
            if (propCount < 5) return "#e67e22";
            return "#e74c3c";
        }

        // Drag functions
        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);
        }

        const zoomControls = svg.append("g")
            .attr("class", "zoom-controls")
            .attr("transform", "translate(20, 20)");

        zoomControls.append("rect")
            .attr("width", 30)
            .attr("height", 60)
            .attr("fill", "white")
            .attr("stroke", "#999");

        zoomControls.append("text")
            .attr("x", 15)
            .attr("y", 20)
            .attr("text-anchor", "middle")
            .style("cursor", "pointer")
            .text("+")
            .on("click", () => {
                svg.transition()
                    .duration(300)
                    .call(zoom.scaleBy, 1.3);
            });

        zoomControls.append("text")
            .attr("x", 15)
            .attr("y", 45)
            .attr("text-anchor", "middle")
            .style("cursor", "pointer")
            .text("-")
            .on("click", () => {
                svg.transition()
                    .duration(300)
                    .call(zoom.scaleBy, 0.7);
            });
    </script>
</body>
</html>