Last active
March 23, 2025 10:26
-
-
Save jim-my/2e6fe181d61e17124de2c98bda0c6d71 to your computer and use it in GitHub Desktop.
Cytoscape.js - Dependency Graph. Cytoscape.js can handle thousands of nodes easily(choice of layout is important). E.g. if viz.js or d3-graphviz doesn't work.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Dependency Graph Visualizer</title> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/dagre/0.8.5/dagre.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape-dagre/2.5.0/cytoscape-dagre.min.js"></script> | |
| <style> | |
| :root { | |
| --primary-color: #4CAF50; | |
| --primary-hover: #45a049; | |
| --download-color: #2196F3; | |
| --download-hover: #0b7dda; | |
| --background-color: #fff; | |
| --border-color: #ccc; | |
| --node-color: #90caf9; | |
| --node-border: #0d47a1; | |
| --highlight-node: #ffeb3b; | |
| --highlight-border: #f57f17; | |
| --highlight-edge: #f44336; | |
| --edge-color: #555; | |
| --text-color: #333; | |
| --status-bg: rgba(0, 0, 0, 0.7); | |
| --status-color: white; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| font-family: Helvetica, Arial, sans-serif; | |
| margin: 0; | |
| padding: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| height: 100vh; | |
| color: var(--text-color); | |
| } | |
| h1 { | |
| margin-top: 0; | |
| margin-bottom: 20px; | |
| } | |
| .container { | |
| display: flex; | |
| flex: 1; | |
| gap: 20px; | |
| height: calc(100vh - 100px); | |
| } | |
| .input-panel { | |
| width: 300px; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .visualization-panel { | |
| flex: 1; | |
| border: 1px solid var(--border-color); | |
| position: relative; | |
| border-radius: 4px; | |
| overflow: hidden; | |
| } | |
| textarea { | |
| flex: 1; | |
| width: 100%; | |
| padding: 10px; | |
| font-size: 16px; | |
| font-family: monospace; | |
| border: 1px solid var(--border-color); | |
| border-radius: 4px; | |
| resize: none; | |
| } | |
| .btn { | |
| margin-top: 10px; | |
| padding: 10px; | |
| background-color: var(--primary-color); | |
| color: white; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 16px; | |
| transition: background-color 0.2s; | |
| } | |
| .btn:hover { | |
| background-color: var(--primary-hover); | |
| } | |
| .btn--download { | |
| background-color: var(--download-color); | |
| } | |
| .btn--download:hover { | |
| background-color: var(--download-hover); | |
| } | |
| #cy { | |
| width: 100%; | |
| height: 100%; | |
| position: absolute; | |
| } | |
| #cy-container { | |
| width: 100%; | |
| height: 100%; | |
| position: relative; | |
| transform: rotate(0deg); | |
| transform-origin: center center; | |
| } | |
| .controls { | |
| position: absolute; | |
| bottom: 20px; | |
| right: 20px; | |
| z-index: 10; | |
| display: flex; | |
| gap: 10px; | |
| } | |
| .control-btn { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 20px; | |
| font-weight: bold; | |
| background-color: var(--primary-color); | |
| color: white; | |
| border: none; | |
| cursor: pointer; | |
| transition: background-color 0.2s, transform 0.1s; | |
| } | |
| .control-btn:hover { | |
| background-color: var(--primary-hover); | |
| transform: scale(1.05); | |
| } | |
| .control-btn--download { | |
| background-color: var(--download-color); | |
| } | |
| .control-btn--download:hover { | |
| background-color: var(--download-hover); | |
| } | |
| .sample-data { | |
| margin-top: 10px; | |
| cursor: pointer; | |
| text-decoration: underline; | |
| color: blue; | |
| } | |
| .fullscreen { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100vw; | |
| height: 100vh; | |
| z-index: 1000; | |
| background: var(--background-color); | |
| padding: 20px; | |
| } | |
| .status-message { | |
| position: absolute; | |
| top: 10px; | |
| left: 10px; | |
| padding: 5px 10px; | |
| background-color: var(--status-bg); | |
| color: var(--status-color); | |
| border-radius: 4px; | |
| font-size: 14px; | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| } | |
| .status-message.visible { | |
| opacity: 1; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Dependency Graph Visualizer</h1> | |
| <div class="container"> | |
| <div class="input-panel"> | |
| <p>Enter dependencies (one per line, format: "a -> b")</p> | |
| <textarea id="input-data" placeholder='a -> b -> c | |
| "Complex Node" -> "Other Node with spaces" | |
| standalone_node | |
| a -> b'></textarea> | |
| <button id="visualize-btn" class="btn">Visualize</button> | |
| <button id="download-svg-btn" class="btn btn--download">Download as SVG</button> | |
| <div class="sample-data" id="load-sample">Load sample data</div> | |
| </div> | |
| <div class="visualization-panel"> | |
| <div id="cy-container"> | |
| <div id="cy"></div> | |
| </div> | |
| <div class="status-message" id="status-message"></div> | |
| <div class="controls"> | |
| <button id="zoom-in" class="control-btn" title="Zoom In">+</button> | |
| <button id="zoom-out" class="control-btn" title="Zoom Out">-</button> | |
| <button id="rotate-cw" class="control-btn" title="Rotate Clockwise">↻</button> | |
| <button id="rotate-ccw" class="control-btn" title="Rotate Counter-Clockwise">↺</button> | |
| <button id="fit-all" class="control-btn" title="Fit All">⟳</button> | |
| <button id="fullscreen-btn" class="control-btn" title="Fullscreen">⛶</button> | |
| <button id="download-svg" class="control-btn control-btn--download" title="Download SVG">↓</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script type="module"> | |
| /** | |
| * Dependency Graph Visualizer | |
| * A tool to visualize dependencies defined in a simple text format. | |
| */ | |
| (function() { | |
| // Modules for better code organization | |
| const App = { | |
| /** | |
| * Configuration settings | |
| */ | |
| Config: { | |
| node: { | |
| shape: 'rectangle', | |
| backgroundColor: '#90caf9', | |
| textValign: 'center', | |
| textHalign: 'center', | |
| padding: 10, | |
| fontSize: 14, | |
| fontFamily: 'Helvetica', | |
| borderWidth: 1, | |
| borderColor: '#0d47a1' | |
| }, | |
| edge: { | |
| width: 2, | |
| lineColor: '#555', | |
| arrowColor: '#555', | |
| arrowShape: 'triangle' | |
| }, | |
| highlight: { | |
| nodeColor: '#ffeb3b', | |
| nodeBorderColor: '#f57f17', | |
| edgeColor: '#f44336', | |
| edgeWidth: 4 | |
| }, | |
| layout: { | |
| name: 'breadthfirst', | |
| directed: true, | |
| padding: 30, | |
| spacingFactor: 1.5, | |
| animationDuration: 500 | |
| }, | |
| rotation: { | |
| step: 15 // degrees to rotate each click | |
| }, | |
| delayTime: 1000, // ms delay for auto-render | |
| download: { | |
| filename: 'dependency-graph.svg' | |
| } | |
| }, | |
| /** | |
| * State management | |
| */ | |
| State: { | |
| currentRotation: 0, | |
| renderTimeout: null, | |
| cy: null, | |
| /** | |
| * Reset the state for a new graph | |
| */ | |
| resetGraph: function() { | |
| if (this.cy) { | |
| this.cy.elements().remove(); | |
| } | |
| } | |
| }, | |
| /** | |
| * UI related functions | |
| */ | |
| UI: { | |
| /** | |
| * Get DOM elements | |
| */ | |
| elements: { | |
| get inputData() { return document.getElementById('input-data'); }, | |
| get visualizeBtn() { return document.getElementById('visualize-btn'); }, | |
| get downloadSvgBtn() { return document.getElementById('download-svg-btn'); }, | |
| get downloadSvg() { return document.getElementById('download-svg'); }, | |
| get loadSample() { return document.getElementById('load-sample'); }, | |
| get zoomIn() { return document.getElementById('zoom-in'); }, | |
| get zoomOut() { return document.getElementById('zoom-out'); }, | |
| get rotateClockwise() { return document.getElementById('rotate-cw'); }, | |
| get rotateCounterClockwise() { return document.getElementById('rotate-ccw'); }, | |
| get fitAll() { return document.getElementById('fit-all'); }, | |
| get fullscreenBtn() { return document.getElementById('fullscreen-btn'); }, | |
| get statusMessage() { return document.getElementById('status-message'); }, | |
| get cyContainer() { return document.getElementById('cy-container'); }, | |
| get cy() { return document.getElementById('cy'); }, | |
| get visualizationPanel() { return document.querySelector('.visualization-panel'); } | |
| }, | |
| /** | |
| * Show a temporary status message | |
| * @param {string} message - The message to display | |
| * @param {number} duration - Duration in ms to show the message (default: 2000) | |
| */ | |
| showStatusMessage: function(message, duration = 2000) { | |
| const statusEl = this.elements.statusMessage; | |
| statusEl.textContent = message; | |
| statusEl.classList.add('visible'); | |
| setTimeout(() => { | |
| statusEl.classList.remove('visible'); | |
| }, duration); | |
| }, | |
| /** | |
| * Toggle fullscreen mode | |
| */ | |
| toggleFullscreen: function() { | |
| const container = this.elements.visualizationPanel; | |
| container.classList.toggle('fullscreen'); | |
| // Resize the graph after toggling fullscreen | |
| setTimeout(() => { | |
| App.State.cy.resize(); | |
| App.State.cy.fit(); | |
| }, 100); | |
| if (container.classList.contains('fullscreen')) { | |
| this.showStatusMessage('Fullscreen mode (press ESC to exit)'); | |
| } | |
| } | |
| }, | |
| /** | |
| * Graph operations | |
| */ | |
| Graph: { | |
| /** | |
| * Create the Cytoscape instance and set up styling | |
| */ | |
| createCytoscapeInstance: function() { | |
| const config = App.Config; | |
| App.State.cy = cytoscape({ | |
| container: App.UI.elements.cy, | |
| style: [ | |
| { | |
| selector: 'node', | |
| style: { | |
| 'shape': config.node.shape, | |
| 'background-color': config.node.backgroundColor, | |
| 'label': 'data(id)', | |
| 'text-valign': config.node.textValign, | |
| 'text-halign': config.node.textHalign, | |
| 'text-wrap': 'wrap', | |
| 'text-max-width': '100px', | |
| 'width': 'label', | |
| 'height': 'label', | |
| 'padding': `${config.node.padding}px`, | |
| 'font-size': `${config.node.fontSize}px`, | |
| 'font-family': config.node.fontFamily, | |
| 'border-width': `${config.node.borderWidth}px`, | |
| 'border-color': config.node.borderColor | |
| } | |
| }, | |
| { | |
| selector: 'edge', | |
| style: { | |
| 'width': config.edge.width, | |
| 'line-color': config.edge.lineColor, | |
| 'target-arrow-color': config.edge.arrowColor, | |
| 'target-arrow-shape': config.edge.arrowShape, | |
| 'curve-style': 'bezier' | |
| } | |
| }, | |
| { | |
| selector: 'node.highlighted', | |
| style: { | |
| 'background-color': config.highlight.nodeColor, | |
| 'border-color': config.highlight.nodeBorderColor, | |
| 'border-width': '2px' | |
| } | |
| }, | |
| { | |
| selector: 'node.faded', | |
| style: { | |
| 'opacity': 0.3 | |
| } | |
| }, | |
| { | |
| selector: 'edge.highlighted', | |
| style: { | |
| 'line-color': config.highlight.edgeColor, | |
| 'target-arrow-color': config.highlight.edgeColor, | |
| 'width': config.highlight.edgeWidth | |
| } | |
| }, | |
| { | |
| selector: 'edge.faded', | |
| style: { | |
| 'opacity': 0.3 | |
| } | |
| } | |
| ] | |
| }); | |
| return App.State.cy; | |
| }, | |
| /** | |
| * Apply layout to the graph | |
| */ | |
| applyLayout: function() { | |
| const cy = App.State.cy; | |
| const config = App.Config; | |
| const layout = cy.layout(config.layout); | |
| layout.run(); | |
| // Fit the graph to the container after animation | |
| setTimeout(() => { | |
| cy.fit(); | |
| cy.zoom(cy.zoom() * 0.9); // Slight zoom out for margin | |
| // Apply rotation if needed | |
| if (App.State.currentRotation !== 0) { | |
| App.UI.elements.cyContainer.style.transform = | |
| `rotate(${App.State.currentRotation}deg)`; | |
| } | |
| }, config.layout.animationDuration + 100); | |
| }, | |
| /** | |
| * Rotate the graph by a given angle | |
| * @param {number} degrees - Degrees to rotate | |
| */ | |
| rotate: function(degrees) { | |
| const cyContainer = App.UI.elements.cyContainer; | |
| // Update the rotation angle | |
| App.State.currentRotation = (App.State.currentRotation + degrees) % 360; | |
| // Ensure positive value | |
| if (App.State.currentRotation < 0) App.State.currentRotation += 360; | |
| // Apply rotation | |
| cyContainer.style.transform = `rotate(${App.State.currentRotation}deg)`; | |
| // Show status message | |
| App.UI.showStatusMessage(`Rotated to ${App.State.currentRotation}°`); | |
| // Refit graph | |
| App.State.cy.fit(); | |
| App.State.cy.zoom(App.State.cy.zoom() * 0.9); | |
| }, | |
| /** | |
| * Highlight a node and its connections | |
| * @param {Object} event - The node click event | |
| */ | |
| highlightNode: function(event) { | |
| const cy = App.State.cy; | |
| const node = event.target; | |
| // Clear previous highlights | |
| cy.elements().removeClass('highlighted faded'); | |
| // Highlight the selected node and its connections | |
| node.addClass('highlighted'); | |
| node.connectedEdges().addClass('highlighted'); | |
| // Get connected nodes | |
| const connectedNodes = node.neighborhood('node'); | |
| connectedNodes.addClass('highlighted'); | |
| // Fade all other elements | |
| cy.elements().not('.highlighted').addClass('faded'); | |
| App.UI.showStatusMessage(`Selected: ${node.id()}`); | |
| } | |
| }, | |
| /** | |
| * Input parsing and data handling | |
| */ | |
| Data: { | |
| /** | |
| * Parse the input text into nodes and edges | |
| * @param {string} input - The input text | |
| * @returns {Object} - Object containing nodes and edges | |
| */ | |
| parseInput: function(input) { | |
| const lines = input.trim().split('\n'); | |
| // Set to track all nodes | |
| const nodes = new Set(); | |
| // Array to store edges | |
| const edges = []; | |
| // Parse lines | |
| lines.forEach(line => { | |
| const trimmedLine = line.trim(); | |
| if (!trimmedLine) return; // Skip empty lines | |
| // Check for quoted node names and -> separators | |
| const parts = this.parseLineIntoParts(trimmedLine); | |
| // Process the parts | |
| if (parts.length === 1) { | |
| // Single node with no connections | |
| let nodeName = parts[0]; | |
| // If quoted, remove the quotes | |
| if (nodeName.startsWith('"') && nodeName.endsWith('"')) { | |
| nodeName = nodeName.substring(1, nodeName.length - 1); | |
| } | |
| nodes.add(nodeName); | |
| } else if (parts.length > 1) { | |
| // Multiple connected nodes | |
| for (let i = 0; i < parts.length - 1; i++) { | |
| let source = parts[i]; | |
| let target = parts[i + 1]; | |
| // If quoted, remove the quotes | |
| if (source.startsWith('"') && source.endsWith('"')) { | |
| source = source.substring(1, source.length - 1); | |
| } | |
| if (target.startsWith('"') && target.endsWith('"')) { | |
| target = target.substring(1, target.length - 1); | |
| } | |
| nodes.add(source); | |
| nodes.add(target); | |
| edges.push({ source, target }); | |
| } | |
| } | |
| }); | |
| return { nodes, edges }; | |
| }, | |
| /** | |
| * Parse a line into parts considering quoted strings | |
| * @param {string} line - The line to parse | |
| * @returns {Array} - Array of parts | |
| */ | |
| parseLineIntoParts: function(line) { | |
| const parts = []; | |
| let currentPart = ''; | |
| let inQuotes = false; | |
| for (let i = 0; i < line.length; i++) { | |
| const char = line[i]; | |
| if (char === '"' && (i === 0 || line[i-1] !== '\\')) { | |
| inQuotes = !inQuotes; | |
| currentPart += char; | |
| } else if (!inQuotes && line.substr(i, 2) === '->') { | |
| parts.push(currentPart.trim()); | |
| currentPart = ''; | |
| i++; // Skip the next character (the '>' of '->') | |
| } else { | |
| currentPart += char; | |
| } | |
| } | |
| if (currentPart.trim()) { | |
| parts.push(currentPart.trim()); | |
| } | |
| return parts; | |
| }, | |
| /** | |
| * Load sample data | |
| */ | |
| loadSampleData: function() { | |
| App.UI.elements.inputData.value = `A -> B -> C | |
| "Complex Node with spaces" -> "Another complex, node." | |
| B -> D | |
| E -> F -> G -> H | |
| standalone_node_1 | |
| "Quoted standalone node" | |
| A -> "Node with spaces" -> D | |
| E -> B`; | |
| App.Controller.parseAndVisualize(); | |
| App.UI.showStatusMessage('Sample data loaded'); | |
| }, | |
| /** | |
| * Load initial data | |
| */ | |
| loadInitialData: function() { | |
| App.UI.elements.inputData.value = `A -> B | |
| B -> C | |
| A -> D | |
| standalone_node`; | |
| } | |
| }, | |
| /** | |
| * Export functionality | |
| */ | |
| Export: { | |
| /** | |
| * Download the graph as an SVG file | |
| */ | |
| downloadAsSVG: function() { | |
| try { | |
| // Get the current graph as SVG | |
| const svgContent = this.generateSVG(); | |
| // Create a Blob object | |
| const blob = new Blob([svgContent], {type: 'image/svg+xml'}); | |
| // Create a URL for the blob | |
| const url = URL.createObjectURL(blob); | |
| // Create a download link | |
| const downloadLink = document.createElement('a'); | |
| downloadLink.href = url; | |
| downloadLink.download = App.Config.download.filename; | |
| // Append to body, click, and remove | |
| document.body.appendChild(downloadLink); | |
| downloadLink.click(); | |
| document.body.removeChild(downloadLink); | |
| // Release the URL object | |
| setTimeout(() => URL.revokeObjectURL(url), 100); | |
| App.UI.showStatusMessage('SVG downloaded successfully'); | |
| } catch (error) { | |
| console.error('Error downloading SVG:', error); | |
| App.UI.showStatusMessage('Error downloading SVG'); | |
| } | |
| }, | |
| /** | |
| * Generate SVG content from the current graph | |
| * @returns {string} - SVG content as string | |
| */ | |
| generateSVG: function() { | |
| const cy = App.State.cy; | |
| const config = App.Config; | |
| // Get the current graph dimensions | |
| const bbox = cy.elements().boundingBox(); | |
| // Get the Cytoscape container dimensions | |
| const width = cy.width(); | |
| const height = cy.height(); | |
| // Adjusted for padding | |
| const padding = 50; | |
| // Create an SVG with viewBox | |
| let svg = `<?xml version="1.0" encoding="UTF-8"?> | |
| <svg xmlns="http://www.w3.org/2000/svg" | |
| width="${width}" | |
| height="${height}" | |
| viewBox="${bbox.x1 - padding} ${bbox.y1 - padding} ${bbox.w + padding*2} ${bbox.h + padding*2}"> | |
| <title>Dependency Graph</title> | |
| <desc>Generated by Dependency Graph Visualizer</desc> | |
| <defs> | |
| <marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" | |
| markerWidth="6" markerHeight="6" orient="auto"> | |
| <path d="M 0 0 L 10 5 L 0 10 z" fill="${config.edge.arrowColor}"/> | |
| </marker> | |
| </defs>`; | |
| // Background | |
| svg += `<rect x="${bbox.x1 - padding}" y="${bbox.y1 - padding}" | |
| width="${bbox.w + padding*2}" height="${bbox.h + padding*2}" fill="white"/>`; | |
| // Add edges (lines) | |
| cy.edges().forEach(edge => { | |
| const edgeData = edge.data(); | |
| const sourceNode = cy.getElementById(edgeData.source); | |
| const targetNode = cy.getElementById(edgeData.target); | |
| if (!sourceNode || !targetNode) return; | |
| const sourcePos = sourceNode.position(); | |
| const targetPos = targetNode.position(); | |
| // Add path for edge | |
| const isHighlighted = edge.hasClass('highlighted'); | |
| const strokeColor = isHighlighted ? config.highlight.edgeColor : config.edge.lineColor; | |
| const strokeWidth = isHighlighted ? config.highlight.edgeWidth : config.edge.width; | |
| const opacity = edge.hasClass('faded') ? 0.3 : 1; | |
| svg += `<line x1="${sourcePos.x}" y1="${sourcePos.y}" | |
| x2="${targetPos.x}" y2="${targetPos.y}" | |
| stroke="${strokeColor}" | |
| stroke-width="${strokeWidth}" | |
| opacity="${opacity}" | |
| marker-end="url(#arrow)"/>`; | |
| }); | |
| // Add nodes (rectangles with text) | |
| cy.nodes().forEach(node => { | |
| const nodeData = node.data(); | |
| const position = node.position(); | |
| const dimensions = node.boundingBox(); | |
| const width = dimensions.w; | |
| const height = dimensions.h; | |
| const isHighlighted = node.hasClass('highlighted'); | |
| const fillColor = isHighlighted ? config.highlight.nodeColor : config.node.backgroundColor; | |
| const borderColor = isHighlighted ? config.highlight.nodeBorderColor : config.node.borderColor; | |
| const opacity = node.hasClass('faded') ? 0.3 : 1; | |
| // Escape HTML special characters in node text | |
| const nodeText = this.escapeHtml(nodeData.id); | |
| // Rectangle for node | |
| svg += `<rect x="${position.x - width/2}" y="${position.y - height/2}" | |
| width="${width}" height="${height}" | |
| rx="3" ry="3" | |
| fill="${fillColor}" | |
| stroke="${borderColor}" | |
| stroke-width="${config.node.borderWidth}" | |
| opacity="${opacity}"/>`; | |
| // Text label for node | |
| svg += `<text x="${position.x}" y="${position.y}" | |
| font-family="${config.node.fontFamily}" | |
| font-size="${config.node.fontSize}" | |
| text-anchor="middle" | |
| dominant-baseline="middle" | |
| opacity="${opacity}">${nodeText}</text>`; | |
| }); | |
| // Close SVG | |
| svg += '</svg>'; | |
| return svg; | |
| }, | |
| /** | |
| * Escape HTML special characters to prevent SVG injection issues | |
| * @param {string} text - Text to escape | |
| * @returns {string} - Escaped text | |
| */ | |
| escapeHtml: function(text) { | |
| return text | |
| .replace(/&/g, '&') | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>') | |
| .replace(/"/g, '"') | |
| .replace(/'/g, '''); | |
| } | |
| }, | |
| /** | |
| * Controller - coordinates actions between modules | |
| */ | |
| Controller: { | |
| /** | |
| * Initialize the application | |
| */ | |
| init: function() { | |
| App.Graph.createCytoscapeInstance(); | |
| this.setupEventListeners(); | |
| // Load initial data | |
| App.Data.loadInitialData(); | |
| // Initial visualization with a short delay | |
| setTimeout(() => this.parseAndVisualize(), 100); | |
| }, | |
| /** | |
| * Set up all event listeners | |
| */ | |
| setupEventListeners: function() { | |
| const ui = App.UI.elements; | |
| const cy = App.State.cy; | |
| // Visualization button | |
| ui.visualizeBtn.addEventListener('click', () => { | |
| clearTimeout(App.State.renderTimeout); | |
| this.parseAndVisualize(); | |
| }); | |
| // Auto-update with delay on text change | |
| ui.inputData.addEventListener('input', () => { | |
| clearTimeout(App.State.renderTimeout); | |
| App.State.renderTimeout = setTimeout( | |
| () => this.parseAndVisualize(), | |
| App.Config.delayTime | |
| ); | |
| }); | |
| // Zoom in button | |
| ui.zoomIn.addEventListener('click', () => { | |
| cy.zoom(cy.zoom() * 1.2); | |
| }); | |
| // Zoom out button | |
| ui.zoomOut.addEventListener('click', () => { | |
| cy.zoom(cy.zoom() * 0.8); | |
| }); | |
| // Fit all button | |
| ui.fitAll.addEventListener('click', () => { | |
| cy.fit(); | |
| cy.zoom(cy.zoom() * 0.9); // Slight zoom out for margin | |
| }); | |
| // Rotate clockwise button | |
| ui.rotateClockwise.addEventListener('click', () => { | |
| App.Graph.rotate(App.Config.rotation.step); | |
| }); | |
| // Rotate counter-clockwise button | |
| ui.rotateCounterClockwise.addEventListener('click', () => { | |
| App.Graph.rotate(-App.Config.rotation.step); | |
| }); | |
| // Fullscreen toggle button | |
| ui.fullscreenBtn.addEventListener('click', () => { | |
| App.UI.toggleFullscreen(); | |
| }); | |
| // Exit fullscreen with Escape key | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'Escape') { | |
| const container = ui.visualizationPanel; | |
| if (container.classList.contains('fullscreen')) { | |
| container.classList.remove('fullscreen'); | |
| // Resize the graph after exiting fullscreen | |
| setTimeout(() => { | |
| cy.resize(); | |
| cy.fit(); | |
| }, 100); | |
| } | |
| } | |
| }); | |
| // Sample data loader | |
| ui.loadSample.addEventListener('click', () => { | |
| App.Data.loadSampleData(); | |
| }); | |
| // Download SVG buttons | |
| ui.downloadSvg.addEventListener('click', () => { | |
| App.Export.downloadAsSVG(); | |
| }); | |
| ui.downloadSvgBtn.addEventListener('click', () => { | |
| App.Export.downloadAsSVG(); | |
| }); | |
| // Node highlighting | |
| cy.on('tap', 'node', (e) => { | |
| App.Graph.highlightNode(e); | |
| }); | |
| // Clear highlights when clicking on the background | |
| cy.on('tap', (e) => { | |
| if (e.target === cy) { | |
| cy.elements().removeClass('highlighted faded'); | |
| } | |
| }); | |
| }, | |
| /** | |
| * Parse the input and visualize the graph | |
| */ | |
| parseAndVisualize: function() { | |
| // Clear previous graph | |
| App.State.resetGraph(); | |
| // Get input text | |
| const input = App.UI.elements.inputData.value; | |
| try { | |
| const { nodes, edges } = App.Data.parseInput(input); | |
| // Add nodes to graph | |
| nodes.forEach(node => { | |
| App.State.cy.add({ | |
| group: 'nodes', | |
| data: { id: node } | |
| }); | |
| }); | |
| // Add edges to graph (deduplicating as needed) | |
| const uniqueEdges = {}; | |
| edges.forEach(edge => { | |
| const edgeId = `${edge.source}-${edge.target}`; | |
| uniqueEdges[edgeId] = edge; | |
| }); | |
| Object.entries(uniqueEdges).forEach(([edgeId, edge]) => { | |
| App.State.cy.add({ | |
| group: 'edges', | |
| data: { | |
| id: edgeId, | |
| source: edge.source, | |
| target: edge.target | |
| } | |
| }); | |
| }); | |
| // Apply layout | |
| App.Graph.applyLayout(); | |
| const uniqueEdgeCount = Object.keys(uniqueEdges).length; | |
| App.UI.showStatusMessage(`Graph created with ${nodes.size} nodes and ${uniqueEdgeCount} edges${uniqueEdgeCount !== edges.length ? ` (${edges.length - uniqueEdgeCount} duplicates removed)` : ''}`); | |
| } catch (error) { | |
| console.error('Error parsing input:', error); | |
| App.UI.showStatusMessage('Error parsing input'); | |
| } | |
| } | |
| } | |
| }; | |
| // Initialize the application | |
| App.Controller.init(); | |
| })(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment