diff --git a/frontend/src/GraphView.jsx b/frontend/src/GraphView.jsx
index fd07c19..1d826de 100644
--- a/frontend/src/GraphView.jsx
+++ b/frontend/src/GraphView.jsx
@@ -3,6 +3,10 @@ import * as d3 from 'd3';
import { IconZoomIn, IconZoomOut, IconMaximize, IconMinimize } from '@tabler/icons-react';
function GraphView({ servers, connections }) {
+ // Debug helper (enable/disable via window.GRAPH_DEBUG = true/false in console)
+ const isDebug = typeof window !== 'undefined' ? (window.GRAPH_DEBUG !== false) : true;
+ const dbg = (...args) => { if (isDebug && typeof console !== 'undefined') console.log('[GraphView]', ...args); };
+
const [nodePositions, setNodePositions] = useState({});
const [draggedNode, setDraggedNode] = useState(null);
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
@@ -80,16 +84,34 @@ function GraphView({ servers, connections }) {
.translateExtent([[-10000, -10000], [10000, 10000]])
.on('zoom', (event) => {
g.attr('transform', event.transform);
+ // dbg('onZoom', { k: event.transform.k, x: event.transform.x, y: event.transform.y });
});
svg.call(zoomRef.current);
+ dbg('zoom initialized', { width, height });
}, []);
// Контролы масштабирования
const zoomBy = (factor) => {
if (!zoomRef.current || !svgRef.current) return;
- const svg = d3.select(svgRef.current);
- // Используем встроенную математику d3.zoom с учётом extent
- svg.transition().duration(220).call(zoomRef.current.scaleBy, factor);
+ const svgEl = svgRef.current;
+ const current = d3.zoomTransform(svgEl);
+ const cx = svgEl.clientWidth / 2;
+ const cy = svgEl.clientHeight / 2;
+ dbg('zoomBy click', { factor, current: { k: current.k, x: current.x, y: current.y }, center: { cx, cy } });
+ const next = d3.zoomIdentity
+ .translate(current.x, current.y)
+ .scale(current.k)
+ .translate(cx, cy)
+ .scale(factor)
+ .translate(-cx, -cy);
+ d3.select(svgEl)
+ .transition()
+ .duration(220)
+ .call(zoomRef.current.transform, next)
+ .on('end', () => {
+ const nt = d3.zoomTransform(svgEl);
+ dbg('zoomBy end', { next: { k: nt.k, x: nt.x, y: nt.y } });
+ });
};
const fitToView = () => {
@@ -127,6 +149,7 @@ function GraphView({ servers, connections }) {
const toggleFullscreen = () => {
const el = containerRef.current;
if (!el) return;
+ dbg('toggleFullscreen', { isFullscreen: Boolean(document.fullscreenElement) });
if (!document.fullscreenElement) {
el.requestFullscreen?.();
} else {
@@ -236,11 +259,13 @@ 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
+ // Флаг страны по ISO-2 с поддержкой нестандартных кодов из списка (SWE->SE)
+ const getFlagEmoji = (code) => {
+ if (!code) return '';
+ const map = { SWE: 'SE', UK: 'GB' };
+ const ccRaw = String(code).trim().toUpperCase();
+ const cc = (map[ccRaw] || ccRaw).slice(0, 2);
+ if (cc.length !== 2) return ccRaw;
return cc.replace(/./g, (ch) => String.fromCodePoint(127397 + ch.charCodeAt()));
};
@@ -429,21 +454,29 @@ function GraphView({ servers, connections }) {
y={badgeY}
rx={8}
ry={8}
- width={36}
- height={18}
+ width={52}
+ height={20}
fill="#fff"
stroke={countryColor}
/>
{flag}
+
+ {String(server.country).toUpperCase().slice(0,3)}
+
);
})()}