coma 0.2.0

Coma is a lightweight command-line tool designed for crawling websites
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Directed Graph Visualization</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <style>
    body,
    html {
      margin: 0;
      padding: 0;
      width: 100%;
      height: 100%;
      overflow: hidden;
    }

    svg {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
    }
  </style>
</head>

<body class="bg-gray-100 dark:bg-gray-900">
  <svg></svg>
  <div
    class="absolute top-4 right-4 w-80 p-4 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-800 dark:border-gray-700">
    <h2 class="text-lg font-semibold mb-2 text-gray-900 dark:text-gray-100">Node Details</h2>
    <div id="node-details" class="text-gray-800 dark:text-gray-300">
      <p>Click on a node to see more details.</p>
    </div>
  </div>
  <div
    class="absolute top-4 left-4 w-80 p-4 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-800 dark:border-gray-700">
    <h2 class="text-lg font-semibold mb-2 text-gray-900 dark:text-gray-100">Settings</h2>
    <label class="block mb-2 text-gray-900 dark:text-gray-100">
      <input type="radio" name="linkType" value="unidirectional" checked class="mr-2"> Unidirectional Links
    </label>
    <label class="block text-gray-900 dark:text-gray-100">
      <input type="radio" name="linkType" value="bidirectional" class="mr-2"> Bidirectional Links
    </label>
  </div>
  <div class="absolute top-4 left-96 flex flex-col gap-2">
    <button
      class="px-4 py-2 bg-blue-500 text-white rounded-lg shadow-md hover:bg-blue-600 dark:bg-blue-700 dark:hover:bg-blue-800"
      id="zoom-in">Zoom In</button>
    <button
      class="px-4 py-2 bg-blue-500 text-white rounded-lg shadow-md hover:bg-blue-600 dark:bg-blue-700 dark:hover:bg-blue-800"
      id="zoom-out">Zoom Out</button>
  </div>
  <script src="https://d3js.org/d3.v7.min.js"></script>
  <script>
    document.addEventListener('DOMContentLoaded', () => {
      const svg = d3.select('svg');
      const zoom = d3.zoom().scaleExtent([0.5, 5]).on('zoom', (event) => {
        svg.select('g').attr('transform', event.transform);
      });
      svg.call(zoom);

      let linkType = 'unidirectional';


      const data = {{graph| json | safe}}
      const width = window.innerWidth;
      const height = window.innerHeight;

      // Define arrow markers
      svg.append('defs').selectAll('marker')
        .data(['end'])
        .enter().append('marker')
        .attr('id', d => `arrow-${d}`)
        .attr('viewBox', '0 -5 10 10')
        .attr('refX', 15)
        .attr('refY', 0)
        .attr('markerWidth', 6)
        .attr('markerHeight', 6)
        .attr('orient', 'auto')
        .append('path')
        .attr('d', 'M0,-5L10,0L0,5')
        .attr('class', 'link-arrow');

      const g = svg.append('g');
      const simulation = d3.forceSimulation(data.nodes)
        .force('link', d3.forceLink(data.edges).id(d => d.id))
        .force('charge', d3.forceManyBody())
        .force('center', d3.forceCenter(width / 2, height / 2));

      let link = g.append('g').attr('class', 'links')
        .selectAll('line').data(data.edges)
        .enter().append('line')
        .attr('class', 'link')
        .attr('stroke', '#999')  // Default stroke color
        .attr('stroke-width', 1);  // Default stroke width

      const node = g.append('g').attr('class', 'nodes')
        .selectAll('g').data(data.nodes)
        .enter().append('g');

      node.append('circle').attr('class', 'node')
        .attr('r', 5)
        .attr('fill', 'grey')
        .call(d3.drag()
          .on('start', dragstarted)
          .on('drag', dragged)
          .on('end', dragended))
        .on('mouseover', handleMouseOver)
        .on('mouseout', handleMouseOut)
        .on('click', handleClick);

      node.append('text').attr('x', 8).attr('y', 3)
        .text(d => d.label);

      simulation.on('tick', () => {
        link.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
          .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
        node.attr('transform', d => `translate(${d.x},${d.y})`);
      });

      function updateLinks() {
        link.attr('marker-end', linkType === 'unidirectional' ? 'url(#arrow-end)' : null);
      }

      document.querySelectorAll('input[name="linkType"]').forEach(input => {
        input.addEventListener('change', () => {
          linkType = document.querySelector('input[name="linkType"]:checked').value;
          updateLinks();
        });
      });

      document.getElementById('zoom-in').addEventListener('click', () => svg.transition().duration(300).call(zoom.scaleBy, 1.5));
      document.getElementById('zoom-out').addEventListener('click', () => svg.transition().duration(300).call(zoom.scaleBy, 0.5));

      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;
      }

      function handleMouseOver(event, d) {
        d3.select(this).select('circle').attr('r', 10);
        link.filter(l => l.source.id === d.id || l.target.id === d.id)
          .attr('stroke', 'red').attr('stroke-width', 2);
      }

      function handleMouseOut(event, d) {
        d3.select(this).select('circle').attr('r', 5);
        link.filter(l => l.source.id === d.id || l.target.id === d.id)
          .attr('stroke', '#999').attr('stroke-width', 1);
      }

      function handleClick(event, d) {
        const details = document.getElementById('node-details');
        details.innerHTML = `<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">${d.label}</h3><p class="text-gray-800 dark:text-gray-300">ID: ${d.id}</p>`;
      }

      updateLinks();
    });
  </script>
</body>

</html>