From 896f2e668c6c0634c02750112f2fae472795e3c5 Mon Sep 17 00:00:00 2001 From: Denis Shatskiy Date: Fri, 8 Aug 2025 15:48:31 +0700 Subject: [PATCH] feat: Add zoom controls and country flag display in GraphView for enhanced interactivity and visualization --- frontend/src/GraphView.jsx | 105 +++++++++++++++++++++++++++++-------- 1 file changed, 84 insertions(+), 21 deletions(-) diff --git a/frontend/src/GraphView.jsx b/frontend/src/GraphView.jsx index fa5cdac..1243e67 100644 --- a/frontend/src/GraphView.jsx +++ b/frontend/src/GraphView.jsx @@ -1,5 +1,6 @@ import React, { useState, useRef, useEffect } from 'react'; import * as d3 from 'd3'; +import { IconZoomIn, IconZoomOut, IconMaximize } from '@tabler/icons-react'; function GraphView({ servers, connections }) { const [nodePositions, setNodePositions] = useState({}); @@ -69,13 +70,44 @@ function GraphView({ servers, connections }) { const g = d3.select(gRef.current); zoomRef.current = d3 .zoom() - .scaleExtent([0.5, 2]) + .scaleExtent([0.3, 3]) .on('zoom', (event) => { g.attr('transform', event.transform); }); svg.call(zoomRef.current); }, []); + // Контролы масштабирования + const zoomBy = (factor) => { + const svg = d3.select(svgRef.current); + svg.transition().duration(200).call(zoomRef.current.scaleBy, factor); + }; + + const fitToView = () => { + const svgEl = svgRef.current; + if (!svgEl) return; + const box = svgEl.getBoundingClientRect(); + const padding = 40; + const positions = nodePositions; + const ids = Object.keys(positions); + if (ids.length === 0) return; + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + ids.forEach((id) => { + const p = positions[id]; + minX = Math.min(minX, p.x - NODE_WIDTH / 2); + minY = Math.min(minY, p.y - NODE_HEIGHT / 2); + maxX = Math.max(maxX, p.x + NODE_WIDTH / 2); + maxY = Math.max(maxY, p.y + NODE_HEIGHT / 2); + }); + const contentW = Math.max(1, maxX - minX); + const contentH = Math.max(1, maxY - minY); + const scale = Math.max(0.5, Math.min(1.6, Math.min((box.width - padding) / contentW, (box.height - padding) / contentH))); + const tx = (box.width - scale * (minX + maxX)) / 2; + const ty = (box.height - scale * (minY + maxY)) / 2; + const t = d3.zoomIdentity.translate(tx, ty).scale(scale); + d3.select(svgEl).transition().duration(250).call(zoomRef.current.transform, t); + }; + if (servers.length === 0) { return (
@@ -178,8 +210,16 @@ function GraphView({ servers, connections }) { return map[tunnelType] || { color: '#667382', dash: '5,5', width: 2 }; }; + // Флаг страны по ISO-2 (emoji). Для не-ISO-2 вернём код как есть + const getFlagEmoji = (countryCode) => { + if (!countryCode) return ''; + const cc = String(countryCode).trim().toUpperCase(); + if (cc.length !== 2) return cc; // например, SWE + return cc.replace(/./g, (ch) => String.fromCodePoint(127397 + ch.charCodeAt())); + }; + return ( -
+
handleMouseDown(e, server.ip)} /> {/* Плашка страны */} - - - {server.country} - + {(() => { + const badgeX = pos.x - NODE_WIDTH / 2 + 8; + const badgeY = pos.y - NODE_HEIGHT / 2 - 10; + const flag = getFlagEmoji(server.country); + return ( + + + + {flag} + + + ); + })()} {/* Название */} + {/* Контролы масштабирования */} +
+
+ + + +
+
); }