@cyber-harbour/ui 1.0.32 → 1.0.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +14 -38
- package/dist/index.d.ts +14 -38
- package/dist/index.js +266 -265
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +145 -144
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/Core/Box/Box.tsx +10 -4
- package/src/Core/Line/Line.tsx +1 -1
- package/src/Core/Typography/Typography.tsx +6 -1
- package/src/Graph2D/Graph2D.tsx +248 -38
- package/src/Graph2D/types.ts +6 -4
- package/src/Layouts/Container/Container.tsx +14 -8
- package/src/Theme/theme.ts +3 -4
- package/src/Theme/types.ts +1 -0
- package/src/Theme/utils.ts +6 -2
- package/tsup.config.ts +4 -2
package/package.json
CHANGED
package/src/Core/Box/Box.tsx
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { createComponent, FabricComponent } from '../../Theme';
|
|
2
2
|
import { styled } from 'styled-components';
|
|
3
3
|
|
|
4
|
-
type BoxProps = FabricComponent<
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
type BoxProps = FabricComponent<
|
|
5
|
+
{
|
|
6
|
+
children: any;
|
|
7
|
+
} & React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
|
|
8
|
+
>;
|
|
7
9
|
|
|
8
|
-
export const Box =
|
|
10
|
+
export const Box = ({ children, ...props }: BoxProps) => {
|
|
11
|
+
return <StyledBox {...props}>{children}</StyledBox>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const StyledBox = styled(createComponent('div'))(
|
|
9
15
|
({ theme }) => `
|
|
10
16
|
padding: ${theme.box.padding};
|
|
11
17
|
border-radius: ${theme.box.borderRadius};
|
package/src/Core/Line/Line.tsx
CHANGED
|
@@ -12,6 +12,7 @@ type TypographyProps = FabricComponent<{
|
|
|
12
12
|
fontStyle?: CSSProperties['fontStyle'];
|
|
13
13
|
color?: ColorVariant | string;
|
|
14
14
|
className?: string;
|
|
15
|
+
ellipsis?: boolean;
|
|
15
16
|
}>;
|
|
16
17
|
|
|
17
18
|
// Create a styled component that can be dynamically rendered as different HTML elements
|
|
@@ -20,7 +21,8 @@ const StyledTypography = styled(createComponent('div'))<{
|
|
|
20
21
|
$weight?: CSSProperties['fontWeight'];
|
|
21
22
|
$style?: CSSProperties['fontStyle'];
|
|
22
23
|
$color?: ColorVariant | string;
|
|
23
|
-
|
|
24
|
+
$ellipsis?: boolean;
|
|
25
|
+
}>(({ theme, $variant, $color, $weight = '400', $style = 'initial', $ellipsis }) => {
|
|
24
26
|
// Resolve color from theme if it's a theme color path, or use the direct color value
|
|
25
27
|
|
|
26
28
|
return `
|
|
@@ -28,6 +30,7 @@ const StyledTypography = styled(createComponent('div'))<{
|
|
|
28
30
|
font-weight: ${$weight};
|
|
29
31
|
font-style: ${$style};
|
|
30
32
|
color: ${resolveThemeColor(theme, $color) || theme.colors.text.main};
|
|
33
|
+
${$ellipsis ? 'overflow: hidden; text-overflow: ellipsis; white-space: nowrap;' : ''}
|
|
31
34
|
`;
|
|
32
35
|
});
|
|
33
36
|
|
|
@@ -40,6 +43,7 @@ export const Typography = ({
|
|
|
40
43
|
color,
|
|
41
44
|
className,
|
|
42
45
|
style,
|
|
46
|
+
ellipsis = false,
|
|
43
47
|
...props
|
|
44
48
|
}: TypographyProps) => {
|
|
45
49
|
// Determine which HTML element to render based on the variant if not explicitly specified
|
|
@@ -52,6 +56,7 @@ export const Typography = ({
|
|
|
52
56
|
$weight={weight}
|
|
53
57
|
$style={fontStyle}
|
|
54
58
|
$color={color}
|
|
59
|
+
$ellipsis={ellipsis}
|
|
55
60
|
className={className}
|
|
56
61
|
style={style}
|
|
57
62
|
{...props}
|
package/src/Graph2D/Graph2D.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import ForceGraph2D, { ForceGraphMethods, LinkObject, NodeObject } from 'react-force-graph-2d';
|
|
2
2
|
import { Graph2DProps } from './types';
|
|
3
|
-
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
3
|
+
import { useEffect, useRef, useState, useCallback, useLayoutEffect } from 'react';
|
|
4
4
|
import { forceCollide } from 'd3-force';
|
|
5
5
|
import { styled } from 'styled-components';
|
|
6
6
|
import eyeLightIcon from './eye_light.png';
|
|
@@ -8,6 +8,8 @@ import eyeLightHoverIcon from './eye_light_hover.png';
|
|
|
8
8
|
import crossLightIcon from './cross_light.png';
|
|
9
9
|
import crossLightHoverIcon from './cross_light_hover.png';
|
|
10
10
|
|
|
11
|
+
const ALPHA_MIN = 0.5;
|
|
12
|
+
|
|
11
13
|
// Створюємо та налаштовуємо об'єкти зображень
|
|
12
14
|
const imgEyeLightIcon = new Image();
|
|
13
15
|
imgEyeLightIcon.src = eyeLightIcon;
|
|
@@ -37,20 +39,35 @@ export const Graph2D = ({
|
|
|
37
39
|
maxZoom: 4, // Максимальний зум
|
|
38
40
|
},
|
|
39
41
|
onNodeClick,
|
|
42
|
+
onNodeHover,
|
|
43
|
+
onLinkHover,
|
|
40
44
|
onLinkClick,
|
|
45
|
+
onBackgroundClick,
|
|
41
46
|
}: Graph2DProps) => {
|
|
42
47
|
// Стан для підсвічування вузлів і зв'язків
|
|
43
48
|
const [highlightNodes, setHighlightNodes] = useState(new Set());
|
|
44
49
|
const [highlightLinks, setHighlightLinks] = useState(new Set());
|
|
45
50
|
const [hoverNode, setHoverNode] = useState<any>(null);
|
|
51
|
+
const [selectedNode, setSelectedNode] = useState<any>(null);
|
|
52
|
+
const [unVisibleNodes, setUnVisibleNodes] = useState(new Set<string>());
|
|
46
53
|
const [hiddenNodes, setHiddenNodes] = useState(new Set<string>());
|
|
47
54
|
const [collapsedNodes, setCollapsedNodes] = useState(new Set<string>());
|
|
48
55
|
// Стани для відстеження наведення на кнопки
|
|
49
56
|
const [hoverTopButton, setHoverTopButton] = useState(false);
|
|
50
57
|
const [hoverBottomButton, setHoverBottomButton] = useState(false);
|
|
58
|
+
const [isRendering, setIsRendering] = useState(true);
|
|
51
59
|
|
|
52
|
-
const fgRef = useRef<
|
|
60
|
+
const fgRef = useRef<
|
|
61
|
+
ForceGraphMethods & {
|
|
62
|
+
tick?: number;
|
|
63
|
+
}
|
|
64
|
+
>(null) as React.MutableRefObject<
|
|
65
|
+
ForceGraphMethods<NodeObject, LinkObject> & {
|
|
66
|
+
tick?: number;
|
|
67
|
+
}
|
|
68
|
+
>;
|
|
53
69
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
70
|
+
const tickTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
54
71
|
|
|
55
72
|
// Функція для реверсивного масштабування тексту
|
|
56
73
|
// При максимальному зумі текст має розмір config.fontSize
|
|
@@ -84,6 +101,8 @@ export const Graph2D = ({
|
|
|
84
101
|
}
|
|
85
102
|
}
|
|
86
103
|
|
|
104
|
+
onNodeHover?.(node);
|
|
105
|
+
|
|
87
106
|
setHoverNode(node || null);
|
|
88
107
|
setHighlightNodes(newHighlightNodes);
|
|
89
108
|
setHighlightLinks(newHighlightLinks);
|
|
@@ -98,14 +117,42 @@ export const Graph2D = ({
|
|
|
98
117
|
newHighlightLinks.add(link);
|
|
99
118
|
newHighlightNodes.add(link.source);
|
|
100
119
|
newHighlightNodes.add(link.target);
|
|
120
|
+
onLinkHover?.(link);
|
|
101
121
|
}
|
|
102
122
|
|
|
103
123
|
setHighlightNodes(newHighlightNodes);
|
|
104
124
|
setHighlightLinks(newHighlightLinks);
|
|
105
125
|
}, []);
|
|
106
126
|
|
|
127
|
+
const handleEngineTick = useCallback(() => {
|
|
128
|
+
if (isRendering)
|
|
129
|
+
if (
|
|
130
|
+
graphData &&
|
|
131
|
+
fgRef.current &&
|
|
132
|
+
fgRef.current.tick &&
|
|
133
|
+
graphData.nodes.length > 0 &&
|
|
134
|
+
graphData.nodes.length * ALPHA_MIN <= fgRef.current.tick
|
|
135
|
+
) {
|
|
136
|
+
if (tickTimerRef.current) {
|
|
137
|
+
clearTimeout(tickTimerRef.current);
|
|
138
|
+
}
|
|
139
|
+
fgRef.current.zoomToFit(0, 20);
|
|
140
|
+
setIsRendering(false);
|
|
141
|
+
} else {
|
|
142
|
+
fgRef.current.tick = fgRef.current.tick ? (fgRef.current.tick = fgRef.current.tick + 1) : 1;
|
|
143
|
+
if (tickTimerRef.current) {
|
|
144
|
+
clearTimeout(tickTimerRef.current);
|
|
145
|
+
}
|
|
146
|
+
tickTimerRef.current = setTimeout(() => {
|
|
147
|
+
//force tick check
|
|
148
|
+
fgRef.current.zoomToFit(0, 20);
|
|
149
|
+
setIsRendering(false);
|
|
150
|
+
}, 1500);
|
|
151
|
+
}
|
|
152
|
+
}, [graphData]);
|
|
153
|
+
|
|
107
154
|
// Створення взаємозв'язків між вузлами
|
|
108
|
-
|
|
155
|
+
useLayoutEffect(() => {
|
|
109
156
|
if (!graphData) return;
|
|
110
157
|
|
|
111
158
|
// Прив'язка вузлів до їхніх сусідів та зв'язків
|
|
@@ -175,13 +222,10 @@ export const Graph2D = ({
|
|
|
175
222
|
.iterations(3) // Більше ітерацій для точнішого розрахунку
|
|
176
223
|
.strength(1); // Максимальна сила (1 - тверде обмеження)
|
|
177
224
|
|
|
178
|
-
fgRef.current.d3Force('collide', collideForce);
|
|
225
|
+
fgRef.current.pauseAnimation().d3Force('collide', collideForce).resumeAnimation();
|
|
179
226
|
} catch (err) {
|
|
180
227
|
console.error('Error setting up collision force:', err);
|
|
181
228
|
}
|
|
182
|
-
|
|
183
|
-
// Перезапустити симуляцію для застосування змін
|
|
184
|
-
fgRef.current.resumeAnimation();
|
|
185
229
|
}
|
|
186
230
|
}, [graphData]);
|
|
187
231
|
|
|
@@ -292,15 +336,35 @@ export const Graph2D = ({
|
|
|
292
336
|
[config, hoverTopButton, hoverBottomButton]
|
|
293
337
|
);
|
|
294
338
|
|
|
295
|
-
const
|
|
296
|
-
if (node && node.id && !
|
|
339
|
+
const hideNode = (unvisibles: Set<string>, node: NodeObject) => {
|
|
340
|
+
if (node && node.id && !unvisibles.has(`${node.id}`) && graphData) {
|
|
297
341
|
// Прив'язка вузлів до їхніх сусідів та зв'язків
|
|
298
342
|
const targets = graphData.links.filter((link: any) => {
|
|
299
343
|
return link.source.id === node.id && link.label !== 'MATCH';
|
|
300
344
|
});
|
|
301
345
|
targets.forEach((link: any) => {
|
|
302
|
-
|
|
303
|
-
|
|
346
|
+
unvisibles.add(`${link.target.id}`);
|
|
347
|
+
hideNode(unvisibles, link.target);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const showNode = (unvisibles: Set<string>, node: NodeObject) => {
|
|
353
|
+
if (node && node.id && graphData) {
|
|
354
|
+
// Прив'язка вузлів до їхніх сусідів та зв'язків
|
|
355
|
+
const targets = graphData.links.filter((link: any) => {
|
|
356
|
+
return link.source.id === node.id && link.label !== 'MATCH';
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
targets.forEach((link: any) => {
|
|
360
|
+
if (unvisibles.has(`${link.target.id}`)) {
|
|
361
|
+
if (!hiddenNodes.has(`${link.target.id}`)) {
|
|
362
|
+
unvisibles.delete(`${link.target.id}`);
|
|
363
|
+
if (!collapsedNodes.has(`${link.target.id}`)) {
|
|
364
|
+
showNode(unvisibles, link.target);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
304
368
|
});
|
|
305
369
|
}
|
|
306
370
|
};
|
|
@@ -308,18 +372,31 @@ export const Graph2D = ({
|
|
|
308
372
|
// Функція для обробки кліку на кнопку "сховати вузол"
|
|
309
373
|
const handleHideNode = (node: any) => {
|
|
310
374
|
const newHiddenNodes = new Set(hiddenNodes);
|
|
311
|
-
|
|
312
|
-
newHiddenNodes.
|
|
313
|
-
|
|
375
|
+
const newUnVisibleNodes = new Set(unVisibleNodes);
|
|
376
|
+
if (newHiddenNodes.has(node.id)) {
|
|
377
|
+
newHiddenNodes.delete(node.id);
|
|
378
|
+
showNode(newUnVisibleNodes, node);
|
|
379
|
+
} else {
|
|
380
|
+
newHiddenNodes.add(node.id);
|
|
381
|
+
hideNode(newUnVisibleNodes, node);
|
|
382
|
+
}
|
|
314
383
|
setHiddenNodes(newHiddenNodes);
|
|
384
|
+
setUnVisibleNodes(newUnVisibleNodes);
|
|
315
385
|
};
|
|
316
386
|
|
|
317
387
|
// Функція для обробки кліку на кнопку "згорнути дочірні вузли"
|
|
318
388
|
const handleCollapseChildren = (node: any) => {
|
|
319
389
|
const newCollapsedNodes = new Set(collapsedNodes);
|
|
320
|
-
|
|
321
|
-
|
|
390
|
+
const newUnVisibleNodes = new Set(unVisibleNodes);
|
|
391
|
+
if (newCollapsedNodes.has(node.id)) {
|
|
392
|
+
newCollapsedNodes.delete(node.id);
|
|
393
|
+
showNode(newUnVisibleNodes, node);
|
|
394
|
+
} else {
|
|
395
|
+
newCollapsedNodes.add(node.id);
|
|
396
|
+
hideNode(newUnVisibleNodes, node);
|
|
397
|
+
}
|
|
322
398
|
setCollapsedNodes(newCollapsedNodes);
|
|
399
|
+
setUnVisibleNodes(newUnVisibleNodes);
|
|
323
400
|
};
|
|
324
401
|
|
|
325
402
|
// Функція для визначення, чи знаходиться точка в межах сектора кола (кнопки)
|
|
@@ -381,7 +458,6 @@ export const Graph2D = ({
|
|
|
381
458
|
return;
|
|
382
459
|
}
|
|
383
460
|
|
|
384
|
-
const nodeSize = config.nodeSizeBase;
|
|
385
461
|
const buttonRadius = (config.nodeSizeBase * config.nodeAreaFactor) / 2;
|
|
386
462
|
const zoom = fgRef.current.zoom() || 1; // Отримуємо поточний зум
|
|
387
463
|
|
|
@@ -466,6 +542,10 @@ export const Graph2D = ({
|
|
|
466
542
|
};
|
|
467
543
|
}, [hoverNode, config, isPointInButtonArea, hoverTopButton, hoverBottomButton]);
|
|
468
544
|
|
|
545
|
+
useEffect(() => {
|
|
546
|
+
if (fgRef.current) fgRef.current.zoomToFit(0, 20); // Автоматичне масштабування графа при першому рендері
|
|
547
|
+
}, [width, height]);
|
|
548
|
+
|
|
469
549
|
const truncateText = (text: string, maxWidth: number, ctx: CanvasRenderingContext2D): string => {
|
|
470
550
|
if (!text) return '';
|
|
471
551
|
|
|
@@ -488,6 +568,7 @@ export const Graph2D = ({
|
|
|
488
568
|
};
|
|
489
569
|
|
|
490
570
|
const renderGrid = (ctx: CanvasRenderingContext2D, globalScale: number) => {
|
|
571
|
+
if (isRendering) return; // Не малюємо сітку під час рендерингу
|
|
491
572
|
// This will be called before each rendering frame
|
|
492
573
|
ctx.getTransform();
|
|
493
574
|
ctx.save();
|
|
@@ -521,7 +602,7 @@ export const Graph2D = ({
|
|
|
521
602
|
globalScale: number
|
|
522
603
|
) => {
|
|
523
604
|
const { x, y } = node;
|
|
524
|
-
const radius = config.nodeSizeBase;
|
|
605
|
+
const radius = selectedNode === node ? (config.nodeSizeBase * config.nodeAreaFactor) / 2 : config.nodeSizeBase / 2;
|
|
525
606
|
|
|
526
607
|
ctx.beginPath();
|
|
527
608
|
ctx.arc(x as number, y as number, radius, 0, 2 * Math.PI);
|
|
@@ -535,14 +616,14 @@ export const Graph2D = ({
|
|
|
535
616
|
// Якщо вузол підсвічений, малюємо кільце
|
|
536
617
|
if (highlightNodes.has(node)) {
|
|
537
618
|
// Якщо це наведений вузол, малюємо кнопки
|
|
538
|
-
if (node
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
619
|
+
if (node !== selectedNode) paintRing(node, ctx, globalScale);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (node === selectedNode) {
|
|
623
|
+
paintNodeButtons(node, ctx, globalScale);
|
|
543
624
|
}
|
|
544
625
|
|
|
545
|
-
const { x, y, color, label } = node;
|
|
626
|
+
const { x, y, color, fontColor, label } = node;
|
|
546
627
|
|
|
547
628
|
const size = config.nodeSizeBase;
|
|
548
629
|
const radius = config.nodeSizeBase / 2;
|
|
@@ -550,7 +631,8 @@ export const Graph2D = ({
|
|
|
550
631
|
// Малюємо коло
|
|
551
632
|
ctx.beginPath();
|
|
552
633
|
ctx.arc(x as number, y as number, radius, 0, 2 * Math.PI);
|
|
553
|
-
ctx.fillStyle = color; // Колір контуру
|
|
634
|
+
ctx.fillStyle = collapsedNodes.has(node.id as string) ? `${color}50` : color; // Колір контуру з opacity для згорнутих вузлів
|
|
635
|
+
|
|
554
636
|
ctx.fill();
|
|
555
637
|
|
|
556
638
|
// пігтовока до малювання тексту
|
|
@@ -565,7 +647,7 @@ export const Graph2D = ({
|
|
|
565
647
|
ctx.font = `${scaledFontSize}px Sans-Serif`;
|
|
566
648
|
ctx.textAlign = 'center';
|
|
567
649
|
ctx.textBaseline = 'middle';
|
|
568
|
-
ctx.fillStyle =
|
|
650
|
+
ctx.fillStyle = fontColor;
|
|
569
651
|
|
|
570
652
|
const truncatedLabel = truncateText(label, maxWidth, ctx);
|
|
571
653
|
ctx.fillText(truncatedLabel, 0, 0);
|
|
@@ -577,11 +659,127 @@ export const Graph2D = ({
|
|
|
577
659
|
// Отримуємо позиції початку і кінця зв'язку
|
|
578
660
|
const { source, target, label } = link;
|
|
579
661
|
|
|
580
|
-
if (!label) return; // Пропускаємо, якщо немає мітки
|
|
581
|
-
|
|
582
662
|
// Координати початку і кінця зв'язку
|
|
583
663
|
const start = { x: source.x, y: source.y };
|
|
584
664
|
const end = { x: target.x, y: target.y };
|
|
665
|
+
|
|
666
|
+
// Відстань між вузлами
|
|
667
|
+
const dx = end.x - start.x;
|
|
668
|
+
const dy = end.y - start.y;
|
|
669
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
670
|
+
|
|
671
|
+
// Розмір вузла
|
|
672
|
+
const sourceSize = config.nodeSizeBase;
|
|
673
|
+
const targetSize = config.nodeSizeBase;
|
|
674
|
+
|
|
675
|
+
// Нормалізовані вектори для напрямку
|
|
676
|
+
const unitDx = dx / distance;
|
|
677
|
+
const unitDy = dy / distance;
|
|
678
|
+
|
|
679
|
+
// Скоригований початок і кінець (щоб стрілка не починалася з центру вузла і не закінчувалася в центрі вузла)
|
|
680
|
+
const startRadius = sourceSize / 2;
|
|
681
|
+
const endRadius = targetSize / 2;
|
|
682
|
+
|
|
683
|
+
// Зміщені позиції початку і кінця
|
|
684
|
+
const adjustedStart = {
|
|
685
|
+
x: start.x + unitDx * startRadius,
|
|
686
|
+
y: start.y + unitDy * startRadius,
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
// Для кінцевої точки віднімаємо невелику відстань, щоб стрілка не заходила в вузол
|
|
690
|
+
const arrowHeadLength = 4;
|
|
691
|
+
const adjustedEnd = {
|
|
692
|
+
x: end.x - unitDx * (endRadius + arrowHeadLength),
|
|
693
|
+
y: end.y - unitDy * (endRadius + arrowHeadLength),
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
// Позиція для стрілки (трохи ближче до кінцевого вузла)
|
|
697
|
+
const adjusteArrowdEnd = {
|
|
698
|
+
x: end.x - unitDx * (endRadius + 1),
|
|
699
|
+
y: end.y - unitDy * (endRadius + 1),
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
// Малюємо лінію зв'язку з урахуванням місця для тексту, якщо він є
|
|
703
|
+
const lineColor = highlightLinks.has(link) ? '#ff9900' : '#999';
|
|
704
|
+
const lineWidth = highlightLinks.has(link) ? 1.5 : 0.5;
|
|
705
|
+
|
|
706
|
+
if (label) {
|
|
707
|
+
// Розраховуємо ширину тексту для визначення розміру проміжку
|
|
708
|
+
const scaledFontSize = calculateFontSize(globalScale);
|
|
709
|
+
ctx.font = `${scaledFontSize}px Sans-Serif`;
|
|
710
|
+
const textWidth = ctx.measureText(label).width;
|
|
711
|
+
|
|
712
|
+
// Розраховуємо довжину проміжку вздовж лінії
|
|
713
|
+
const gapLength = Math.sqrt(textWidth * textWidth + scaledFontSize * scaledFontSize);
|
|
714
|
+
|
|
715
|
+
// Загальна довжина лінії між вузлами
|
|
716
|
+
const lineLength = distance - startRadius - endRadius - arrowHeadLength;
|
|
717
|
+
|
|
718
|
+
// Розрахунок відстані від початку до середини і від середини до кінця
|
|
719
|
+
const halfLineLength = lineLength / 2;
|
|
720
|
+
const gapHalf = gapLength / 2;
|
|
721
|
+
|
|
722
|
+
// Малюємо першу частину лінії (від початку до проміжку)
|
|
723
|
+
if (halfLineLength > gapHalf) {
|
|
724
|
+
// Розрахунок точок перед проміжком
|
|
725
|
+
const gapStart = {
|
|
726
|
+
x: adjustedStart.x + unitDx * (halfLineLength - gapHalf),
|
|
727
|
+
y: adjustedStart.y + unitDy * (halfLineLength - gapHalf),
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
ctx.beginPath();
|
|
731
|
+
ctx.moveTo(adjustedStart.x, adjustedStart.y);
|
|
732
|
+
ctx.lineTo(gapStart.x, gapStart.y);
|
|
733
|
+
ctx.strokeStyle = lineColor;
|
|
734
|
+
ctx.lineWidth = lineWidth;
|
|
735
|
+
ctx.stroke();
|
|
736
|
+
|
|
737
|
+
// Розрахунок точок після проміжку
|
|
738
|
+
const gapEnd = {
|
|
739
|
+
x: adjustedStart.x + unitDx * (halfLineLength + gapHalf),
|
|
740
|
+
y: adjustedStart.y + unitDy * (halfLineLength + gapHalf),
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
ctx.beginPath();
|
|
744
|
+
ctx.moveTo(gapEnd.x, gapEnd.y);
|
|
745
|
+
ctx.lineTo(adjustedEnd.x, adjustedEnd.y);
|
|
746
|
+
ctx.strokeStyle = lineColor;
|
|
747
|
+
ctx.lineWidth = lineWidth;
|
|
748
|
+
ctx.stroke();
|
|
749
|
+
}
|
|
750
|
+
} else {
|
|
751
|
+
// Якщо немає тексту, малюємо повну лінію
|
|
752
|
+
ctx.beginPath();
|
|
753
|
+
ctx.moveTo(adjustedStart.x, adjustedStart.y);
|
|
754
|
+
ctx.lineTo(adjustedEnd.x, adjustedEnd.y);
|
|
755
|
+
ctx.strokeStyle = lineColor;
|
|
756
|
+
ctx.lineWidth = lineWidth;
|
|
757
|
+
ctx.stroke();
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Малюємо стрілку
|
|
761
|
+
const arrowHeadWidth = 2;
|
|
762
|
+
const angle = Math.atan2(dy, dx);
|
|
763
|
+
|
|
764
|
+
ctx.save();
|
|
765
|
+
ctx.translate(adjusteArrowdEnd.x, adjusteArrowdEnd.y);
|
|
766
|
+
ctx.rotate(angle);
|
|
767
|
+
|
|
768
|
+
// Малюємо наконечник стрілки
|
|
769
|
+
ctx.beginPath();
|
|
770
|
+
ctx.moveTo(0, 0);
|
|
771
|
+
ctx.lineTo(-arrowHeadLength, arrowHeadWidth);
|
|
772
|
+
ctx.lineTo(-arrowHeadLength, 0); // Стрілка трохи вдавлена всередину
|
|
773
|
+
ctx.lineTo(-arrowHeadLength, -arrowHeadWidth);
|
|
774
|
+
ctx.closePath();
|
|
775
|
+
|
|
776
|
+
ctx.fillStyle = highlightLinks.has(link) ? '#ff9900' : '#999';
|
|
777
|
+
ctx.fill();
|
|
778
|
+
ctx.restore();
|
|
779
|
+
|
|
780
|
+
// Якщо немає мітки, не малюємо текст
|
|
781
|
+
if (!label) return;
|
|
782
|
+
|
|
585
783
|
// Знаходимо середину лінії для розміщення тексту
|
|
586
784
|
const middleX = start.x + (end.x - start.x) / 2;
|
|
587
785
|
const middleY = start.y + (end.y - start.y) / 2;
|
|
@@ -594,7 +792,6 @@ export const Graph2D = ({
|
|
|
594
792
|
ctx.textBaseline = 'middle';
|
|
595
793
|
|
|
596
794
|
// Визначення кута нахилу лінії для повороту тексту
|
|
597
|
-
const angle = Math.atan2(end.y - start.y, end.x - start.x);
|
|
598
795
|
ctx.save();
|
|
599
796
|
// Переміщення до центру лінії та поворот тексту
|
|
600
797
|
ctx.translate(middleX, middleY);
|
|
@@ -717,9 +914,13 @@ export const Graph2D = ({
|
|
|
717
914
|
event.stopPropagation();
|
|
718
915
|
return;
|
|
719
916
|
}
|
|
720
|
-
|
|
721
917
|
// Якщо клік не на кнопках, обробляємо клік на вузлі
|
|
722
|
-
|
|
918
|
+
setSelectedNode(node);
|
|
919
|
+
onNodeClick?.(node);
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
const handleBackgroundClick = (event: MouseEvent) => {
|
|
923
|
+
setSelectedNode(null);
|
|
723
924
|
};
|
|
724
925
|
|
|
725
926
|
return (
|
|
@@ -733,39 +934,48 @@ export const Graph2D = ({
|
|
|
733
934
|
linkSource={linkSource}
|
|
734
935
|
onLinkClick={onLinkClick}
|
|
735
936
|
onNodeClick={handleNodeClick}
|
|
937
|
+
onBackgroundClick={handleBackgroundClick}
|
|
736
938
|
nodeLabel={(node: any) => `${node.label || ''}`} // Показуємо повний текст у тултіпі
|
|
737
939
|
linkLabel={(link: any) => link.label}
|
|
738
940
|
nodeAutoColorBy="label"
|
|
739
|
-
linkDirectionalArrowLength={3.5}
|
|
740
|
-
linkDirectionalArrowRelPos={1}
|
|
741
941
|
linkCurvature={0}
|
|
942
|
+
// Вимикаємо вбудовані стрілки, оскільки використовуємо свою реалізацію
|
|
943
|
+
linkDirectionalArrowLength={0}
|
|
742
944
|
// Обмеження максимального зуму
|
|
743
|
-
maxZoom={config.maxZoom}
|
|
744
|
-
minZoom={
|
|
945
|
+
//maxZoom={config.maxZoom}
|
|
946
|
+
minZoom={0.01}
|
|
745
947
|
// Додавання обробників наведення
|
|
746
948
|
onNodeHover={handleNodeHover}
|
|
747
949
|
onLinkHover={handleLinkHover}
|
|
950
|
+
onEngineTick={handleEngineTick}
|
|
951
|
+
d3AlphaMin={ALPHA_MIN}
|
|
952
|
+
d3VelocityDecay={0.4}
|
|
953
|
+
d3AlphaDecay={0.038}
|
|
748
954
|
// Виділення зв'язків при наведенні
|
|
749
955
|
linkWidth={(link: any) => (highlightLinks.has(link) ? 3 : 1)}
|
|
750
956
|
linkColor={(link: any) => (highlightLinks.has(link) ? '#ff9900' : '#999')}
|
|
751
957
|
onRenderFramePre={renderGrid}
|
|
752
958
|
nodePointerAreaPaint={renderNodePointerAreaPaint}
|
|
753
959
|
nodeCanvasObject={renderNodeCanvasObject}
|
|
754
|
-
linkCanvasObjectMode={() => 'after'
|
|
960
|
+
linkCanvasObjectMode={() => 'replace'} // 'replace' замість 'after' для повної заміни стандартного рендерингу зв'язків
|
|
755
961
|
linkCanvasObject={renderLinkCanvasObject}
|
|
756
962
|
linkVisibility={(link: any) => {
|
|
963
|
+
if (isRendering) return false; // Не показуємо вузол, якщо граф ще рендериться
|
|
964
|
+
|
|
757
965
|
// Перевіряємо, чи вузол прихований
|
|
758
966
|
if (hiddenNodes.has(link.source.id) || hiddenNodes.has(link.target.id)) return false;
|
|
759
967
|
// Перевіряємо, чи вузол згорнутий
|
|
760
|
-
if (collapsedNodes.has(link.source.id)
|
|
968
|
+
if (collapsedNodes.has(link.source.id)) return false;
|
|
969
|
+
if (unVisibleNodes.has(link.source.id) || unVisibleNodes.has(link.target.id)) return false;
|
|
761
970
|
|
|
762
971
|
return true; // Показуємо вузол, якщо не прихований і не згорнутий
|
|
763
972
|
}}
|
|
764
973
|
nodeVisibility={(node: NodeObject) => {
|
|
974
|
+
if (isRendering) return false; // Не показуємо вузол, якщо граф ще рендериться
|
|
765
975
|
// Перевіряємо, чи вузол прихований
|
|
766
976
|
if (hiddenNodes.has(node.id as string)) return false;
|
|
767
977
|
// Перевіряємо, чи вузол згорнутий
|
|
768
|
-
if (
|
|
978
|
+
if (unVisibleNodes.has(node.id as string)) return false;
|
|
769
979
|
return true; // Показуємо вузол, якщо не прихований і не згорнутий
|
|
770
980
|
}}
|
|
771
981
|
/>
|
package/src/Graph2D/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GraphData } from 'react-force-graph-2d';
|
|
1
|
+
import { GraphData, NodeObject } from 'react-force-graph-2d';
|
|
2
2
|
|
|
3
3
|
export interface Graph2DProps {
|
|
4
4
|
graphData?: GraphData;
|
|
@@ -19,7 +19,9 @@ export interface Graph2DProps {
|
|
|
19
19
|
gridSpacing: number;
|
|
20
20
|
dotSize: number;
|
|
21
21
|
};
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
onNodeClick?: (node: NodeObject) => void;
|
|
23
|
+
onLinkClick?: (link: NodeObject) => void;
|
|
24
|
+
onNodeHover?: (node: NodeObject | null) => void;
|
|
25
|
+
onLinkHover?: (link: NodeObject | null) => void;
|
|
26
|
+
onBackgroundClick?: () => void;
|
|
25
27
|
}
|
|
@@ -6,13 +6,19 @@ type ContainerProps = FabricComponent<{
|
|
|
6
6
|
maxWidth?: string | number;
|
|
7
7
|
}>;
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
type StyledContainerProps = {
|
|
10
|
+
$maxWidth?: string | number;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
export const Container = ({ maxWidth, ...props }: ContainerProps) => {
|
|
14
|
+
return <StyledContainer {...props} $maxWidth={maxWidth} />;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const StyledContainer = styled(createComponent<StyledContainerProps>('div'))(({ theme, $maxWidth }) => {
|
|
18
|
+
return `
|
|
19
|
+
padding-inline: ${pxToRem(20, theme.baseSize)};
|
|
20
|
+
width: 100%;
|
|
21
|
+
min-width: 0;
|
|
22
|
+
max-width: ${typeof $maxWidth === 'number' ? pxToRem($maxWidth, theme.baseSize) : $maxWidth || '100%'};
|
|
23
|
+
`;
|
|
24
|
+
});
|
package/src/Theme/theme.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { Theme } from './types';
|
|
|
10
10
|
*/
|
|
11
11
|
export const lightThemePx: Theme = {
|
|
12
12
|
mode: 'light',
|
|
13
|
+
baseSize: 14, // Базовий розмір шрифту для конвертації px в rem
|
|
13
14
|
// Секція кольорів з теми
|
|
14
15
|
colors: {
|
|
15
16
|
background: '#ffffff',
|
|
@@ -708,7 +709,5 @@ export const lightThemePx: Theme = {
|
|
|
708
709
|
};
|
|
709
710
|
|
|
710
711
|
// Конвертуємо всі розміри з px в rem
|
|
711
|
-
export const lightTheme = convertPaletteToRem(lightThemePx) as DefaultTheme;
|
|
712
|
-
export const darkTheme = convertPaletteToRem(lightThemePx) as DefaultTheme;
|
|
713
|
-
|
|
714
|
-
console.log('Light theme:', lightTheme.contextMenu);
|
|
712
|
+
export const lightTheme = convertPaletteToRem(lightThemePx, lightThemePx.baseSize) as DefaultTheme;
|
|
713
|
+
export const darkTheme = convertPaletteToRem(lightThemePx, lightThemePx.baseSize) as DefaultTheme;
|
package/src/Theme/types.ts
CHANGED
package/src/Theme/utils.ts
CHANGED
|
@@ -73,8 +73,9 @@ export const pxToRem = (pxValue: number | string, baseSize: number = 16): string
|
|
|
73
73
|
return `${remValue}rem`;
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
-
const IGNORE_CONVERT_KEYS: Record<string, string[]> = {
|
|
76
|
+
const IGNORE_CONVERT_KEYS: Record<string, string[] | boolean> = {
|
|
77
77
|
contextMenu: ['padding'],
|
|
78
|
+
baseSize: true,
|
|
78
79
|
};
|
|
79
80
|
|
|
80
81
|
/**
|
|
@@ -107,7 +108,10 @@ export const convertPaletteToRem = (
|
|
|
107
108
|
key.toLowerCase().includes(prop.toLowerCase())
|
|
108
109
|
)
|
|
109
110
|
) {
|
|
110
|
-
if (
|
|
111
|
+
if (
|
|
112
|
+
!(parentKey && Array.isArray(IGNORE_CONVERT_KEYS[parentKey]) && IGNORE_CONVERT_KEYS[parentKey].includes(key)) &&
|
|
113
|
+
!IGNORE_CONVERT_KEYS[key]
|
|
114
|
+
) {
|
|
111
115
|
result[key] = pxToRem(value, baseSize);
|
|
112
116
|
} else {
|
|
113
117
|
result[key] = value; // Keep original value if it's in the ignore list
|
package/tsup.config.ts
CHANGED
|
@@ -19,8 +19,10 @@ export default defineConfig({
|
|
|
19
19
|
platform: 'browser',
|
|
20
20
|
skipNodeModulesBundle: true,
|
|
21
21
|
esbuildOptions(options) {
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
if (process.env.NODE_ENV === 'development') {
|
|
23
|
+
options.drop = ['console', 'debugger'];
|
|
24
|
+
options.pure = ['console.log', 'console.info', 'console.debug', 'console.warn'];
|
|
25
|
+
}
|
|
24
26
|
},
|
|
25
27
|
shims: true,
|
|
26
28
|
keepNames: true,
|