/
komarovdd
/
GraphViz
Обзор
Документация
Войти
/
komarovdd
/
GraphViz
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
scripts/main.js
333 строки
12 KB
komarovdd
new project
09 фев 2026, 14:29
Верифицирован
09 фев 2026, 14:29
966388e
Код
Авторство
О чём код?
import { Graph } from './graph.js'; import { Renderer } from './renderer.js'; import { PhysicsEngine } from './physics.js'; import { clusteringRegistry } from './clustering.js'; class App { constructor() { this.graph = new Graph(); this.canvas = document.getElementById('graphCanvas'); this.renderer = new Renderer(this.canvas); this.physics = new PhysicsEngine(); this.isSimulating = false; this.lastTime = 0; this.init(); } init() { this.initTheme(); this.renderer.resize(); window.addEventListener('resize', () => this.renderer.resize()); this.setupEventListeners(); this.populateClusteringSelect(); this.requestRender(); } initTheme() { const savedTheme = localStorage.getItem('theme') || 'dark-theme'; document.body.className = savedTheme; this.updateThemeIcons(); } toggleTheme() { const current = document.body.className; const next = current === 'dark-theme' ? 'light-theme' : 'dark-theme'; document.body.className = next; localStorage.setItem('theme', next); this.updateThemeIcons(); this.requestRender(); } updateThemeIcons() { const isDark = document.body.classList.contains('dark-theme'); document.querySelector('.theme-icon-sun').style.display = isDark ? 'none' : 'block'; document.querySelector('.theme-icon-moon').style.display = isDark ? 'block' : 'none'; this.renderer.isDark = isDark; } populateClusteringSelect() { const select = document.getElementById('clusteringAlgo'); clusteringRegistry.forEach((algo, index) => { const opt = document.createElement('option'); opt.value = index; opt.textContent = algo.name; select.appendChild(opt); }); } setupEventListeners() { // --- Data Loading --- document.getElementById('loadGraph').onclick = () => { const text = document.getElementById('adjacencyList').value; this.graph = Graph.parseAdjacencyList(text); this.updateStats(); this.centerGraph(); }; document.getElementById('exportJSON').onclick = () => { const data = JSON.stringify(this.graph.toJSON(), null, 2); const blob = new Blob([data], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = 'graph.json'; link.click(); }; document.getElementById('importJSON').onclick = () => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = e => { const file = e.target.files[0]; const reader = new FileReader(); reader.onload = event => { const json = JSON.parse(event.target.result); this.graph = Graph.fromJSON(json); this.updateStats(); this.requestRender(); }; reader.readAsText(file); }; input.click(); }; // --- View Controls --- document.getElementById('randomizeLayout').onclick = () => { this.graph.nodes.forEach(node => { node.x = (Math.random() - 0.5) * this.canvas.width; node.y = (Math.random() - 0.5) * this.canvas.height; }); this.requestRender(); }; document.getElementById('centerGraph').onclick = () => this.centerGraph(); document.getElementById('resetScale').onclick = () => { this.renderer.transform.scale = 1.0; this.updateZoomUI(); this.requestRender(); }; const simBtn = document.getElementById('toggleSimulation'); const simIcon = document.getElementById('simIcon'); const simText = document.getElementById('simText'); simBtn.onclick = () => { this.isSimulating = !this.isSimulating; simText.textContent = this.isSimulating ? 'Стоп' : 'Старт'; simIcon.innerHTML = this.isSimulating ? '<rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect>' // Pause : '<polygon points="5 3 19 12 5 21 5 3"></polygon>'; // Play if (this.isSimulating) { this.lastTime = performance.now(); requestAnimationFrame(this.loop.bind(this)); } }; document.getElementById('stepSimulation').onclick = () => { this.physics.update(this.graph, 0.016); this.requestRender(); }; const simSpeed = document.getElementById('simSpeed'); simSpeed.oninput = () => { const val = parseFloat(simSpeed.value); document.getElementById('simSpeedVal').textContent = val.toFixed(1); this.physics.params.timeScale = val; }; // Physics Settings const updatePhysicsParam = (id, param) => { document.getElementById(id).oninput = e => { this.physics.params[param] = parseFloat(e.target.value); }; }; updatePhysicsParam('paramAttract', 'attraction'); updatePhysicsParam('paramRepulse', 'repulsion'); updatePhysicsParam('paramGravity', 'gravity'); updatePhysicsParam('paramDamping', 'damping'); // --- Clustering --- document.getElementById('applyClustering').onclick = () => { const idx = document.getElementById('clusteringAlgo').value; const algo = clusteringRegistry[idx]; const clustering = algo.run(this.graph); this.graph.nodes.forEach((node, id) => { node.clusterId = clustering.get(id); }); this.updateClusteringLegend(clustering); this.requestRender(); }; document.getElementById('resetClustering').onclick = () => { this.graph.nodes.forEach(node => { node.clusterId = null; }); document.getElementById('clusteringLegend').innerHTML = ''; this.requestRender(); }; // --- Utility --- document.getElementById('toggleTheme').onclick = () => this.toggleTheme(); document.getElementById('zoomIn').onclick = () => this.zoom(1.1); document.getElementById('zoomOut').onclick = () => this.zoom(0.9); document.getElementById('savePNG').onclick = () => { const link = document.createElement('a'); link.download = 'graph.png'; link.href = this.canvas.toDataURL(); link.click(); }; // --- Mouse Interaction --- this.setupMouseEvents(); } setupMouseEvents() { let isPanning = false; let lastMousePos = { x: 0, y: 0 }; let draggedNode = null; this.canvas.onmousedown = e => { const rect = this.canvas.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const mouseY = e.clientY - rect.top; const worldPos = this.renderer.screenToWorld(mouseX, mouseY); // Check for node under mouse for (const node of this.graph.nodes.values()) { const dx = node.x - worldPos.x; const dy = node.y - worldPos.y; if (Math.sqrt(dx * dx + dy * dy) < 20) { draggedNode = node; node.isDragging = true; this.renderer.selectedNodeId = node.id; break; } } if (!draggedNode) { if (e.button === 2 || (e.button === 0 && e.shiftKey)) { isPanning = true; } } lastMousePos = { x: e.clientX, y: e.clientY }; this.requestRender(); }; window.onmousemove = e => { const dx = e.clientX - lastMousePos.x; const dy = e.clientY - lastMousePos.y; if (draggedNode) { draggedNode.x += dx / this.renderer.transform.scale; draggedNode.y += dy / this.renderer.transform.scale; } else if (isPanning) { this.renderer.transform.x += dx; this.renderer.transform.y += dy; } else { // Hover check const rect = this.canvas.getBoundingClientRect(); const worldPos = this.renderer.screenToWorld(e.clientX - rect.left, e.clientY - rect.top); let foundHover = null; for (const node of this.graph.nodes.values()) { const ndx = node.x - worldPos.x; const ndy = node.y - worldPos.y; if (Math.sqrt(ndx * ndx + ndy * ndy) < 20) { foundHover = node.id; break; } } if (this.renderer.hoveredNodeId !== foundHover) { this.renderer.hoveredNodeId = foundHover; } } lastMousePos = { x: e.clientX, y: e.clientY }; this.requestRender(); }; window.onmouseup = () => { if (draggedNode) draggedNode.isDragging = false; draggedNode = null; isPanning = false; this.requestRender(); }; this.canvas.onwheel = e => { e.preventDefault(); const factor = e.deltaY > 0 ? 0.9 : 1.1; this.zoom(factor); }; this.canvas.oncontextmenu = e => e.preventDefault(); } zoom(factor) { this.renderer.transform.scale *= factor; this.renderer.transform.scale = Math.max(0.1, Math.min(10, this.renderer.transform.scale)); this.updateZoomUI(); this.requestRender(); } updateZoomUI() { document.getElementById('zoomPercent').textContent = Math.round(this.renderer.transform.scale * 100) + '%'; } centerGraph() { if (!this.graph.nodes.size) return; let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; this.graph.nodes.forEach(n => { minX = Math.min(minX, n.x); maxX = Math.max(maxX, n.x); minY = Math.min(minY, n.y); maxY = Math.max(maxY, n.y); }); const centerX = (minX + maxX) / 2; const centerY = (minY + maxY) / 2; this.renderer.transform.x = this.canvas.width / 2 - centerX * this.renderer.transform.scale; this.renderer.transform.y = this.canvas.height / 2 - centerY * this.renderer.transform.scale; this.requestRender(); } updateStats() { document.getElementById('statNodes').textContent = this.graph.nodes.size; document.getElementById('statEdges').textContent = this.graph.edges.length; } updateClusteringLegend(clustering) { const counts = new Map(); clustering.forEach(cid => counts.set(cid, (counts.get(cid) || 0) + 1)); const legend = document.getElementById('clusteringLegend'); legend.innerHTML = '<strong>Кластеры:</strong><br>'; counts.forEach((count, cid) => { const color = this.renderer.palette[cid % this.renderer.palette.length]; legend.innerHTML += `<span style="color:${color}">●</span> Кластер ${cid}: ${count} узлов<br>`; }); } loop(time) { if (!this.isSimulating) return; const dt = (time - this.lastTime) / 1000; this.lastTime = time; this.physics.update(this.graph, Math.min(dt, 0.05)); this.renderer.render(this.graph); requestAnimationFrame(this.loop.bind(this)); } requestRender() { if (!this.isSimulating) { this.renderer.render(this.graph); } } } // Start the app new App();