@centreon/ui 24.4.1-MON-34377-implement-tree-chart-on-centreon-ui.9 → 24.4.1-MON-34377-implement-tree-chart-on-centreon-ui.10
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/package.json +1 -1
- package/src/Graph/Tree/Links.tsx +4 -4
- package/src/Graph/Tree/Tree.cypress.spec.tsx +171 -0
- package/src/Graph/Tree/Tree.tsx +6 -3
- package/src/Graph/Tree/models.ts +4 -2
- package/src/Graph/Tree/stories/datas.ts +2 -2
- package/src/components/Zoom/Minimap.tsx +23 -29
- package/src/components/Zoom/Zoom.cypress.spec.tsx +53 -21
- package/src/components/Zoom/Zoom.stories.tsx +61 -15
- package/src/components/Zoom/ZoomContent.tsx +37 -12
- package/src/components/Zoom/models.ts +9 -0
- package/src/components/Zoom/utils.ts +55 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@centreon/ui",
|
|
3
|
-
"version": "24.4.1-MON-34377-implement-tree-chart-on-centreon-ui.
|
|
3
|
+
"version": "24.4.1-MON-34377-implement-tree-chart-on-centreon-ui.10",
|
|
4
4
|
"description": "Centreon UI Components",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"update:deps": "pnpx npm-check-updates -i --format group",
|
package/src/Graph/Tree/Links.tsx
CHANGED
|
@@ -33,25 +33,25 @@ const Links = <TData extends BaseProp>({
|
|
|
33
33
|
fill="none"
|
|
34
34
|
key={key}
|
|
35
35
|
stroke={
|
|
36
|
-
treeLink
|
|
36
|
+
treeLink?.getStroke?.({
|
|
37
37
|
source: link.source.data.data,
|
|
38
38
|
target: link.target.data.data
|
|
39
39
|
}) || theme.palette.text.primary
|
|
40
40
|
}
|
|
41
41
|
strokeDasharray={
|
|
42
|
-
treeLink
|
|
42
|
+
treeLink?.getStrokeDasharray?.({
|
|
43
43
|
source: link.source.data.data,
|
|
44
44
|
target: link.target.data.data
|
|
45
45
|
}) || '0'
|
|
46
46
|
}
|
|
47
47
|
strokeOpacity={
|
|
48
|
-
treeLink
|
|
48
|
+
treeLink?.getStrokeOpacity?.({
|
|
49
49
|
source: link.source.data.data,
|
|
50
50
|
target: link.target.data.data
|
|
51
51
|
}) || 1
|
|
52
52
|
}
|
|
53
53
|
strokeWidth={
|
|
54
|
-
treeLink
|
|
54
|
+
treeLink?.getStrokeWidth?.({
|
|
55
55
|
source: link.source.data.data,
|
|
56
56
|
target: link.target.data.data
|
|
57
57
|
}) || '2'
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { equals } from 'ramda';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ComplexData,
|
|
5
|
+
complexData,
|
|
6
|
+
SimpleData,
|
|
7
|
+
simpleData
|
|
8
|
+
} from './stories/datas';
|
|
9
|
+
import { ComplexContent, SimpleContent } from './stories/contents';
|
|
10
|
+
|
|
11
|
+
import { Node, StandaloneTree, TreeProps } from '.';
|
|
12
|
+
|
|
13
|
+
const validateTree = (tree): void => {
|
|
14
|
+
if (!tree.children) {
|
|
15
|
+
cy.contains(tree.data.name).should('be.visible');
|
|
16
|
+
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
cy.contains(tree.data.name).should('be.visible');
|
|
21
|
+
tree.children.forEach((child) => {
|
|
22
|
+
validateTree(child);
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
interface InitializeProps
|
|
27
|
+
extends Pick<TreeProps<SimpleData | ComplexData>, 'treeLink' | 'children'> {
|
|
28
|
+
data?: Node<SimpleData | ComplexData>;
|
|
29
|
+
isDefaultExpanded?: (data: SimpleData | ComplexData) => boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const initializeStandaloneTree = ({
|
|
33
|
+
data = simpleData,
|
|
34
|
+
isDefaultExpanded = undefined,
|
|
35
|
+
treeLink,
|
|
36
|
+
children = SimpleContent
|
|
37
|
+
}: InitializeProps): void => {
|
|
38
|
+
cy.mount({
|
|
39
|
+
Component: (
|
|
40
|
+
<div style={{ height: '99vh' }}>
|
|
41
|
+
<StandaloneTree
|
|
42
|
+
node={{ height: 70, isDefaultExpanded, width: 70 }}
|
|
43
|
+
tree={data}
|
|
44
|
+
treeLink={treeLink}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
</StandaloneTree>
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
describe('Simple data tree', () => {
|
|
54
|
+
it('displays the whole tree', () => {
|
|
55
|
+
initializeStandaloneTree({});
|
|
56
|
+
|
|
57
|
+
validateTree(simpleData);
|
|
58
|
+
|
|
59
|
+
cy.makeSnapshot();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("collapses a node's childrens when a node is clicked", () => {
|
|
63
|
+
initializeStandaloneTree({});
|
|
64
|
+
|
|
65
|
+
cy.contains(/^E$/).should('be.visible');
|
|
66
|
+
cy.contains(/^E1$/).should('be.visible');
|
|
67
|
+
|
|
68
|
+
cy.contains(/^C$/).click();
|
|
69
|
+
|
|
70
|
+
cy.contains(/^E$/).should('not.exist');
|
|
71
|
+
cy.contains(/^E1$/).should('not.exist');
|
|
72
|
+
|
|
73
|
+
cy.makeSnapshot();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("expands a node's childrens when a node is clicked", () => {
|
|
77
|
+
initializeStandaloneTree({});
|
|
78
|
+
|
|
79
|
+
cy.contains(/^A$/).click();
|
|
80
|
+
|
|
81
|
+
cy.contains(/^A1$/).should('not.exist');
|
|
82
|
+
cy.contains(/^A2$/).should('not.exist');
|
|
83
|
+
cy.contains(/^A3$/).should('not.exist');
|
|
84
|
+
cy.contains(/^C$/).should('not.exist');
|
|
85
|
+
|
|
86
|
+
cy.contains(/^A$/).click();
|
|
87
|
+
|
|
88
|
+
cy.contains(/^A1$/).should('be.visible');
|
|
89
|
+
cy.contains(/^A2$/).should('be.visible');
|
|
90
|
+
cy.contains(/^A3$/).should('be.visible');
|
|
91
|
+
cy.contains(/^C$/).should('be.visible');
|
|
92
|
+
|
|
93
|
+
cy.makeSnapshot();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('cannot collapses a node when a leaf is clicked', () => {
|
|
97
|
+
initializeStandaloneTree({});
|
|
98
|
+
|
|
99
|
+
cy.contains(/^Z$/).click();
|
|
100
|
+
|
|
101
|
+
cy.contains(/^Z$/).should('be.visible');
|
|
102
|
+
|
|
103
|
+
cy.makeSnapshot();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('expands nodes by default when a prop is set', () => {
|
|
107
|
+
initializeStandaloneTree({
|
|
108
|
+
isDefaultExpanded: (data: SimpleData) => equals('critical', data.status)
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
cy.contains(/^T$/).should('be.visible');
|
|
112
|
+
cy.contains(/^A$/).should('be.visible');
|
|
113
|
+
cy.contains(/^A3$/).should('be.visible');
|
|
114
|
+
cy.contains(/^C$/).should('be.visible');
|
|
115
|
+
cy.contains(/^E$/).should('be.visible');
|
|
116
|
+
cy.contains(/^E1$/).should('be.visible');
|
|
117
|
+
|
|
118
|
+
cy.contains(/^B1$/).should('not.exist');
|
|
119
|
+
cy.contains(/^D1$/).should('not.exist');
|
|
120
|
+
|
|
121
|
+
cy.makeSnapshot();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('displays customized links when a prop is set', () => {
|
|
125
|
+
initializeStandaloneTree({
|
|
126
|
+
treeLink: {
|
|
127
|
+
getStroke: ({ target }) => (target.status === 'ok' ? 'grey' : 'black'),
|
|
128
|
+
getStrokeDasharray: ({ target }) =>
|
|
129
|
+
target.status === 'ok' ? '5,5' : '0',
|
|
130
|
+
getStrokeOpacity: ({ target }) => (target.status === 'ok' ? 0.8 : 1),
|
|
131
|
+
getStrokeWidth: ({ target }) => (target.status === 'ok' ? 1 : 2)
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
cy.contains(/^Z$/).should('be.visible');
|
|
136
|
+
|
|
137
|
+
cy.makeSnapshot();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('Complex data tree', () => {
|
|
142
|
+
it('cannot collapse a node when a node is not clickable', () => {
|
|
143
|
+
initializeStandaloneTree({
|
|
144
|
+
children: ComplexContent,
|
|
145
|
+
data: complexData
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
cy.contains('BA 3').should('be.visible');
|
|
149
|
+
|
|
150
|
+
cy.contains('BA 2').click();
|
|
151
|
+
|
|
152
|
+
cy.contains('BA 3').should('be.visible');
|
|
153
|
+
|
|
154
|
+
cy.makeSnapshot();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('collapses a node when a node is clickable', () => {
|
|
158
|
+
initializeStandaloneTree({
|
|
159
|
+
children: ComplexContent,
|
|
160
|
+
data: complexData
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
cy.contains('BA 3').should('be.visible');
|
|
164
|
+
|
|
165
|
+
cy.contains('2').click();
|
|
166
|
+
|
|
167
|
+
cy.contains('BA 3').should('not.exist');
|
|
168
|
+
|
|
169
|
+
cy.makeSnapshot();
|
|
170
|
+
});
|
|
171
|
+
});
|
package/src/Graph/Tree/Tree.tsx
CHANGED
|
@@ -54,7 +54,7 @@ export const Tree = <TData extends BaseProp>({
|
|
|
54
54
|
|
|
55
55
|
const expandCollapseNode = useCallback(
|
|
56
56
|
(targetNode: Node<TData>): void => {
|
|
57
|
-
changeTree(
|
|
57
|
+
changeTree?.(
|
|
58
58
|
toggleTreeNodesExpanded({ currentTree: formattedTree, targetNode })
|
|
59
59
|
);
|
|
60
60
|
},
|
|
@@ -76,12 +76,15 @@ export const Tree = <TData extends BaseProp>({
|
|
|
76
76
|
);
|
|
77
77
|
|
|
78
78
|
const origin = useMemo(
|
|
79
|
-
() => ({
|
|
79
|
+
() => ({
|
|
80
|
+
x: 0,
|
|
81
|
+
y: containerHeight / 2
|
|
82
|
+
}),
|
|
80
83
|
[containerHeight]
|
|
81
84
|
);
|
|
82
85
|
|
|
83
86
|
return (
|
|
84
|
-
<Group left={node.width
|
|
87
|
+
<Group left={node.width}>
|
|
85
88
|
<VisxTree
|
|
86
89
|
left={0}
|
|
87
90
|
nodeSize={[node.width + nodeMargins.y, node.height + nodeMargins.x]}
|
package/src/Graph/Tree/models.ts
CHANGED
|
@@ -29,17 +29,19 @@ export interface ChildrenProps<TData> {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export interface TreeProps<TData> {
|
|
32
|
-
changeTree
|
|
32
|
+
changeTree?: (newTree: Node<TData>) => void;
|
|
33
33
|
children: (props: ChildrenProps<TData>) => JSX.Element;
|
|
34
34
|
containerHeight: number;
|
|
35
35
|
containerWidth: number;
|
|
36
|
+
contentHeight?: number;
|
|
37
|
+
contentWidth?: number;
|
|
36
38
|
node: {
|
|
37
39
|
height: number;
|
|
38
40
|
isDefaultExpanded?: (data: TData) => boolean;
|
|
39
41
|
width: number;
|
|
40
42
|
};
|
|
41
43
|
tree: Node<TData>;
|
|
42
|
-
treeLink
|
|
44
|
+
treeLink?: {
|
|
43
45
|
getStroke?: (props: LinkProps<TData>) => string | undefined;
|
|
44
46
|
getStrokeDasharray?: (
|
|
45
47
|
props: LinkProps<TData>
|
|
@@ -168,7 +168,7 @@ export const complexData: Node<ComplexData> = {
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
],
|
|
171
|
-
data: { id: 0, name: 'BA
|
|
171
|
+
data: { id: 0, name: 'BA 4', status: 'ok' }
|
|
172
172
|
}
|
|
173
173
|
],
|
|
174
174
|
data: { count: 2, id: 0, name: 'ok', status: 'ok' }
|
|
@@ -294,7 +294,7 @@ export const moreComplexData: Node<ComplexData> = {
|
|
|
294
294
|
{ data: { id: 37, name: 'Indicator 37', status: 'ok' } },
|
|
295
295
|
{ data: { id: 38, name: 'Indicator 38', status: 'ok' } }
|
|
296
296
|
],
|
|
297
|
-
data: { count:
|
|
297
|
+
data: { count: 9, id: 0, name: 'ok', status: 'ok' }
|
|
298
298
|
}
|
|
299
299
|
],
|
|
300
300
|
data: {
|
|
@@ -12,6 +12,10 @@ interface Props extends Omit<UseMinimapProps, 'minimapScale' | 'scale'> {
|
|
|
12
12
|
height: number;
|
|
13
13
|
width: number;
|
|
14
14
|
};
|
|
15
|
+
diffBetweenContentAndSvg: {
|
|
16
|
+
left: number;
|
|
17
|
+
top: number;
|
|
18
|
+
};
|
|
15
19
|
isDraggingFromContainer: boolean;
|
|
16
20
|
}
|
|
17
21
|
|
|
@@ -21,7 +25,8 @@ const Minimap = ({
|
|
|
21
25
|
height,
|
|
22
26
|
width,
|
|
23
27
|
contentClientRect,
|
|
24
|
-
isDraggingFromContainer
|
|
28
|
+
isDraggingFromContainer,
|
|
29
|
+
diffBetweenContentAndSvg
|
|
25
30
|
}: Props): JSX.Element => {
|
|
26
31
|
const { classes } = useZoomStyles();
|
|
27
32
|
|
|
@@ -61,28 +66,20 @@ const Minimap = ({
|
|
|
61
66
|
2 /
|
|
62
67
|
zoom.transformMatrix.scaleY;
|
|
63
68
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
range: [1, 8]
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
return padding - padding / tweakScale(additionalScale);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return padding;
|
|
85
|
-
};
|
|
69
|
+
const translateX = useMemo(
|
|
70
|
+
() =>
|
|
71
|
+
-diffBetweenContentAndSvg.left /
|
|
72
|
+
zoom.transformMatrix.scaleX /
|
|
73
|
+
minimapScale,
|
|
74
|
+
[diffBetweenContentAndSvg.left]
|
|
75
|
+
);
|
|
76
|
+
const translateY = useMemo(
|
|
77
|
+
() =>
|
|
78
|
+
-diffBetweenContentAndSvg.top /
|
|
79
|
+
zoom.transformMatrix.scaleX /
|
|
80
|
+
minimapScale,
|
|
81
|
+
[diffBetweenContentAndSvg.top]
|
|
82
|
+
);
|
|
86
83
|
|
|
87
84
|
return (
|
|
88
85
|
<g className={classes.minimap} clipPath="url(#zoom-clip)">
|
|
@@ -93,16 +90,13 @@ const Minimap = ({
|
|
|
93
90
|
width={finalWidth}
|
|
94
91
|
/>
|
|
95
92
|
<g
|
|
93
|
+
className={classes.movingZone}
|
|
96
94
|
style={{
|
|
97
|
-
transform: `scale(${scaleToUse - additionalScale}) translate(
|
|
95
|
+
transform: `scale(${scaleToUse - additionalScale}) translate(${translateX}px, ${translateY}px)`
|
|
98
96
|
}}
|
|
99
97
|
>
|
|
100
98
|
{children}
|
|
101
|
-
<g
|
|
102
|
-
style={{
|
|
103
|
-
transform: `translate(0px, ${contentClientRect.height / 10}px)`
|
|
104
|
-
}}
|
|
105
|
-
>
|
|
99
|
+
<g>
|
|
106
100
|
<rect
|
|
107
101
|
className={classes.minimapZoom}
|
|
108
102
|
fillOpacity={0.2}
|
|
@@ -1,41 +1,58 @@
|
|
|
1
1
|
import Zoom from './Zoom';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const Content = (): JSX.Element => (
|
|
4
4
|
<g style={{ transform: 'translate(300px, 150px)' }}>
|
|
5
5
|
<circle fill="blue" r={50} stroke="black" />
|
|
6
6
|
</g>
|
|
7
7
|
);
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
<g
|
|
12
|
-
<
|
|
9
|
+
const ContentWithMultipleShapes = (): JSX.Element => {
|
|
10
|
+
return (
|
|
11
|
+
<g>
|
|
12
|
+
<g style={{ transform: 'translate(300px, 150px)' }}>
|
|
13
|
+
<circle fill="blue" r={50} stroke="black" />
|
|
14
|
+
</g>
|
|
15
|
+
<g style={{ transform: 'translate(600px, 500px)' }}>
|
|
16
|
+
<circle fill="green" r={70} />
|
|
17
|
+
</g>
|
|
18
|
+
<g style={{ transform: 'translate(150px, 600px)' }}>
|
|
19
|
+
<circle fill="red" r={70} />
|
|
20
|
+
</g>
|
|
13
21
|
</g>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
const ContentWithMultipleShapesWithNegativeTranslations = (): JSX.Element => {
|
|
25
|
+
return (
|
|
26
|
+
<g>
|
|
27
|
+
<g style={{ transform: 'translate(-300px, -150px)' }}>
|
|
28
|
+
<circle fill="blue" r={50} stroke="black" />
|
|
29
|
+
</g>
|
|
30
|
+
<g style={{ transform: 'translate(600px, 500px)' }}>
|
|
31
|
+
<circle fill="green" r={70} />
|
|
32
|
+
</g>
|
|
33
|
+
<g style={{ transform: 'translate(150px, 600px)' }}>
|
|
34
|
+
<circle fill="red" r={70} />
|
|
35
|
+
</g>
|
|
19
36
|
</g>
|
|
20
|
-
|
|
21
|
-
|
|
37
|
+
);
|
|
38
|
+
};
|
|
22
39
|
|
|
23
40
|
interface Props {
|
|
24
41
|
minimapPosition?;
|
|
25
42
|
showMinimap: boolean;
|
|
26
|
-
|
|
43
|
+
tenplate?: () => JSX.Element;
|
|
27
44
|
}
|
|
28
45
|
|
|
29
46
|
const initialize = ({
|
|
30
47
|
showMinimap,
|
|
31
48
|
minimapPosition,
|
|
32
|
-
|
|
49
|
+
template = Content
|
|
33
50
|
}: Props): void => {
|
|
34
51
|
cy.mount({
|
|
35
52
|
Component: (
|
|
36
53
|
<div style={{ height: '400px', width: '100%' }}>
|
|
37
54
|
<Zoom minimapPosition={minimapPosition} showMinimap={showMinimap}>
|
|
38
|
-
{
|
|
55
|
+
{template}
|
|
39
56
|
</Zoom>
|
|
40
57
|
</div>
|
|
41
58
|
)
|
|
@@ -195,7 +212,25 @@ describe('Zoom', () => {
|
|
|
195
212
|
});
|
|
196
213
|
|
|
197
214
|
it('applies a scale down on the minimap when the content is higher than the original height', () => {
|
|
198
|
-
initialize({ showMinimap: true,
|
|
215
|
+
initialize({ showMinimap: true, template: ContentWithMultipleShapes });
|
|
216
|
+
|
|
217
|
+
cy.get('g[clip-path="url(#zoom-clip)"]').should('be.visible');
|
|
218
|
+
cy.get('svg').should('have.attr', 'height', '400');
|
|
219
|
+
|
|
220
|
+
cy.findByTestId('minimap-interaction')
|
|
221
|
+
.parent()
|
|
222
|
+
.find('g')
|
|
223
|
+
.should('have.attr', 'style')
|
|
224
|
+
.and('include', 'transform: scale(0.684211) translate(-85px, -105px);');
|
|
225
|
+
|
|
226
|
+
cy.makeSnapshot();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('applies a scale down on the minimap when the content has negative translation values', () => {
|
|
230
|
+
initialize({
|
|
231
|
+
showMinimap: true,
|
|
232
|
+
template: ContentWithMultipleShapesWithNegativeTranslations
|
|
233
|
+
});
|
|
199
234
|
|
|
200
235
|
cy.get('g[clip-path="url(#zoom-clip)"]').should('be.visible');
|
|
201
236
|
cy.get('svg').should('have.attr', 'height', '400');
|
|
@@ -203,11 +238,8 @@ describe('Zoom', () => {
|
|
|
203
238
|
cy.findByTestId('minimap-interaction')
|
|
204
239
|
.parent()
|
|
205
240
|
.find('g')
|
|
206
|
-
.should(
|
|
207
|
-
|
|
208
|
-
'style',
|
|
209
|
-
'transform: scale(0.684211) translate(0px, 57px);'
|
|
210
|
-
);
|
|
241
|
+
.should('have.attr', 'style')
|
|
242
|
+
.and('include', 'transform: scale(0.448276) translate(345px, 195px);');
|
|
211
243
|
|
|
212
244
|
cy.makeSnapshot();
|
|
213
245
|
});
|
|
@@ -24,19 +24,21 @@ const meta: Meta<typeof Zoom> = {
|
|
|
24
24
|
export default meta;
|
|
25
25
|
type Story = StoryObj<typeof Zoom>;
|
|
26
26
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
<g style={{ transform: 'translate(
|
|
30
|
-
<
|
|
27
|
+
const Content = (): JSX.Element => {
|
|
28
|
+
return (
|
|
29
|
+
<g style={{ transform: 'translate(-150px, -100px)' }}>
|
|
30
|
+
<g style={{ transform: 'translate(300px, 150px)' }}>
|
|
31
|
+
<circle fill="blue" r={50} stroke="black" />
|
|
32
|
+
</g>
|
|
33
|
+
<g style={{ transform: 'translate(600px, 400px)' }}>
|
|
34
|
+
<circle fill="green" r={70} />
|
|
35
|
+
</g>
|
|
36
|
+
<g style={{ transform: `translate(2400px, 1400px)` }}>
|
|
37
|
+
<circle fill="red" r={70} />
|
|
38
|
+
</g>
|
|
31
39
|
</g>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
</g>
|
|
35
|
-
<g style={{ transform: 'translate(150px, 800px)' }}>
|
|
36
|
-
<circle fill="red" r={70} />
|
|
37
|
-
</g>
|
|
38
|
-
</g>
|
|
39
|
-
);
|
|
40
|
+
);
|
|
41
|
+
};
|
|
40
42
|
|
|
41
43
|
const Template = ({ children, ...args }: ZoomProps): JSX.Element => (
|
|
42
44
|
<div style={{ height: '400px', width: '100%' }}>
|
|
@@ -44,16 +46,44 @@ const Template = ({ children, ...args }: ZoomProps): JSX.Element => (
|
|
|
44
46
|
</div>
|
|
45
47
|
);
|
|
46
48
|
|
|
49
|
+
const TemplateResponsive = ({
|
|
50
|
+
redCircleXPosition,
|
|
51
|
+
redCircleYPosition,
|
|
52
|
+
...args
|
|
53
|
+
}: ZoomProps): JSX.Element => (
|
|
54
|
+
<div style={{ height: '90vh', width: '100%' }}>
|
|
55
|
+
<Zoom {...args}>
|
|
56
|
+
{() => (
|
|
57
|
+
<g>
|
|
58
|
+
<g style={{ transform: 'translate(500px, 100px)' }}>
|
|
59
|
+
<circle fill="blue" r={50} stroke="black" />
|
|
60
|
+
</g>
|
|
61
|
+
<g style={{ transform: 'translate(600px, 400px)' }}>
|
|
62
|
+
<circle fill="green" r={70} />
|
|
63
|
+
</g>
|
|
64
|
+
<g
|
|
65
|
+
style={{
|
|
66
|
+
transform: `translate(${redCircleXPosition || 100}px, ${redCircleYPosition || 100}px)`
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
<circle fill="red" r={70} />
|
|
70
|
+
</g>
|
|
71
|
+
</g>
|
|
72
|
+
)}
|
|
73
|
+
</Zoom>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
|
|
47
77
|
export const WithoutMinimap: Story = {
|
|
48
78
|
args: {
|
|
49
|
-
children:
|
|
79
|
+
children: Content
|
|
50
80
|
},
|
|
51
81
|
render: Template
|
|
52
82
|
};
|
|
53
83
|
|
|
54
84
|
export const WithMinimap: Story = {
|
|
55
85
|
args: {
|
|
56
|
-
children:
|
|
86
|
+
children: Content,
|
|
57
87
|
showMinimap: true
|
|
58
88
|
},
|
|
59
89
|
render: Template
|
|
@@ -61,9 +91,25 @@ export const WithMinimap: Story = {
|
|
|
61
91
|
|
|
62
92
|
export const WithMinimapPosition: Story = {
|
|
63
93
|
args: {
|
|
64
|
-
children:
|
|
94
|
+
children: Content,
|
|
65
95
|
minimapPosition: 'bottom-right',
|
|
66
96
|
showMinimap: true
|
|
67
97
|
},
|
|
68
98
|
render: Template
|
|
69
99
|
};
|
|
100
|
+
|
|
101
|
+
export const Playground: Story = {
|
|
102
|
+
argTypes: {
|
|
103
|
+
redCircleXPosition: {
|
|
104
|
+
control: { max: 2600, min: 400, step: 10, type: 'range' }
|
|
105
|
+
},
|
|
106
|
+
redCircleYPosition: {
|
|
107
|
+
control: { max: 2600, min: 400, step: 10, type: 'range' }
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
args: {
|
|
111
|
+
children: Content,
|
|
112
|
+
showMinimap: true
|
|
113
|
+
},
|
|
114
|
+
render: TemplateResponsive
|
|
115
|
+
};
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react';
|
|
2
2
|
|
|
3
3
|
import { RectClipPath } from '@visx/clip-path';
|
|
4
|
-
import { equals, type } from 'ramda';
|
|
5
|
-
import { Group } from '@visx/group';
|
|
6
4
|
import { ProvidedZoom } from '@visx/zoom/lib/types';
|
|
7
5
|
|
|
8
6
|
import ZoomInIcon from '@mui/icons-material/Add';
|
|
@@ -15,10 +13,10 @@ import { minimapScale, radius } from './constants';
|
|
|
15
13
|
import { useZoom } from './useZoom';
|
|
16
14
|
import { useZoomStyles } from './Zoom.styles';
|
|
17
15
|
import Minimap from './Minimap';
|
|
18
|
-
import { MinimapPosition, ZoomState } from './models';
|
|
16
|
+
import { ChildrenProps, MinimapPosition, ZoomState } from './models';
|
|
19
17
|
|
|
20
18
|
export interface Props {
|
|
21
|
-
children:
|
|
19
|
+
children: ({ width, height, transformMatrix }: ChildrenProps) => JSX.Element;
|
|
22
20
|
height: number;
|
|
23
21
|
minimapPosition: MinimapPosition;
|
|
24
22
|
showMinimap?: boolean;
|
|
@@ -36,16 +34,21 @@ const ZoomContent = ({
|
|
|
36
34
|
}: Props): JSX.Element => {
|
|
37
35
|
const { classes } = useZoomStyles();
|
|
38
36
|
const contentRef = useRef<SVGGElement | null>(null);
|
|
37
|
+
const minimapSvgRef = useRef<SVGSVGElement | null>(null);
|
|
38
|
+
const minimapContentRef = useRef<SVGSVGElement | null>(null);
|
|
39
39
|
const [contentClientRect, setContentClientRect] = useState<{
|
|
40
40
|
height: number;
|
|
41
41
|
width: number;
|
|
42
42
|
} | null>(null);
|
|
43
43
|
|
|
44
44
|
const resizeObserver = new ResizeObserver(() => {
|
|
45
|
+
const contentBoundingClientRect = (
|
|
46
|
+
contentRef.current as SVGGElement
|
|
47
|
+
).getBoundingClientRect();
|
|
48
|
+
|
|
45
49
|
setContentClientRect({
|
|
46
|
-
height:
|
|
47
|
-
|
|
48
|
-
width: (contentRef.current as SVGGElement).getBoundingClientRect().width
|
|
50
|
+
height: contentBoundingClientRect.height,
|
|
51
|
+
width: contentBoundingClientRect.width
|
|
49
52
|
});
|
|
50
53
|
});
|
|
51
54
|
|
|
@@ -62,7 +65,15 @@ const ZoomContent = ({
|
|
|
62
65
|
|
|
63
66
|
const { move, dragEnd, dragStart, isDragging } = useZoom();
|
|
64
67
|
|
|
65
|
-
const
|
|
68
|
+
const diffBetweenContentAndSvg = minimapSvgRef.current &&
|
|
69
|
+
minimapContentRef.current && {
|
|
70
|
+
left:
|
|
71
|
+
minimapContentRef.current.getBoundingClientRect().left -
|
|
72
|
+
minimapSvgRef.current.getBoundingClientRect().left,
|
|
73
|
+
top:
|
|
74
|
+
minimapContentRef.current.getBoundingClientRect().top -
|
|
75
|
+
minimapSvgRef.current.getBoundingClientRect().top
|
|
76
|
+
};
|
|
66
77
|
|
|
67
78
|
return (
|
|
68
79
|
<div style={{ position: 'relative' }}>
|
|
@@ -90,7 +101,12 @@ const ZoomContent = ({
|
|
|
90
101
|
ref={contentRef}
|
|
91
102
|
transform={zoom.toString()}
|
|
92
103
|
>
|
|
93
|
-
{
|
|
104
|
+
{children({
|
|
105
|
+
contentClientRect,
|
|
106
|
+
height,
|
|
107
|
+
transformMatrix: zoom.transformMatrix,
|
|
108
|
+
width
|
|
109
|
+
})}
|
|
94
110
|
</g>
|
|
95
111
|
</svg>
|
|
96
112
|
<div className={classes.actionsAndZoom} data-position={minimapPosition}>
|
|
@@ -99,18 +115,27 @@ const ZoomContent = ({
|
|
|
99
115
|
className={classes.minimapContainer}
|
|
100
116
|
data-testid="minimap"
|
|
101
117
|
height={height * minimapScale}
|
|
118
|
+
ref={minimapSvgRef}
|
|
102
119
|
width={width * minimapScale}
|
|
103
120
|
>
|
|
104
121
|
<Minimap
|
|
105
122
|
contentClientRect={contentClientRect}
|
|
123
|
+
diffBetweenContentAndSvg={
|
|
124
|
+
diffBetweenContentAndSvg || { left: 0, top: 0 }
|
|
125
|
+
}
|
|
106
126
|
height={height}
|
|
107
127
|
isDraggingFromContainer={isDragging}
|
|
108
128
|
width={width}
|
|
109
129
|
zoom={zoom}
|
|
110
130
|
>
|
|
111
|
-
<
|
|
112
|
-
{
|
|
113
|
-
|
|
131
|
+
<g ref={minimapContentRef}>
|
|
132
|
+
{children({
|
|
133
|
+
contentClientRect,
|
|
134
|
+
height,
|
|
135
|
+
transformMatrix: zoom.transformMatrix,
|
|
136
|
+
width
|
|
137
|
+
})}
|
|
138
|
+
</g>
|
|
114
139
|
</Minimap>
|
|
115
140
|
</svg>
|
|
116
141
|
)}
|
|
@@ -14,3 +14,12 @@ export type MinimapPosition =
|
|
|
14
14
|
| 'top-right'
|
|
15
15
|
| 'bottom-left'
|
|
16
16
|
| 'bottom-right';
|
|
17
|
+
|
|
18
|
+
export interface ChildrenProps extends ZoomState {
|
|
19
|
+
contentClientRect: {
|
|
20
|
+
height: number;
|
|
21
|
+
width: number;
|
|
22
|
+
} | null;
|
|
23
|
+
height: number;
|
|
24
|
+
width: number;
|
|
25
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { CSSProperties } from 'react';
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
contentClientRect: {
|
|
5
|
+
height: number;
|
|
6
|
+
width: number;
|
|
7
|
+
} | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const applyTranformStylesForZoom = ({
|
|
11
|
+
contentClientRect
|
|
12
|
+
}: Props): CSSProperties => {
|
|
13
|
+
const contentRect = {
|
|
14
|
+
height: contentClientRect?.height || 1,
|
|
15
|
+
width: contentClientRect?.width || 1
|
|
16
|
+
};
|
|
17
|
+
const isPortrait = contentRect.height > contentRect.width;
|
|
18
|
+
const sizes = isPortrait ? ['width', 'height'] : ['height', 'width'];
|
|
19
|
+
const sizeScale = contentRect[sizes[0]] / contentRect[sizes[1]];
|
|
20
|
+
|
|
21
|
+
const lengthToUse = isPortrait
|
|
22
|
+
? contentRect[sizes[1]] - contentRect[sizes[0]]
|
|
23
|
+
: contentRect[sizes[0]];
|
|
24
|
+
|
|
25
|
+
const t = sizeScale > 0.85 && isPortrait ? sizeScale * 4 : sizeScale / 2;
|
|
26
|
+
const xScaleFactor = sizeScale > 0.7 && !isPortrait ? 10 : 6;
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
transform: `translate(-${isPortrait ? 0 : contentRect.width * (sizeScale / xScaleFactor)}px, -${lengthToUse * (isPortrait ? t + 0.08 : t / 2)}px)`
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// DO NOT REMOVE: As the component is in work in progress, please this code in case we need
|
|
34
|
+
// const getAdditionalPadding = (): number => {
|
|
35
|
+
// if (additionalScale > 0.05) {
|
|
36
|
+
// return 0;
|
|
37
|
+
// }
|
|
38
|
+
|
|
39
|
+
// const padding =
|
|
40
|
+
// additionalScale > 0.012
|
|
41
|
+
// ? (1 / additionalScale) * (1 / zoom.transformMatrix.scaleY)
|
|
42
|
+
// : 1 / additionalScale / zoom.transformMatrix.scaleY;
|
|
43
|
+
|
|
44
|
+
// if (additionalScale < 0.009) {
|
|
45
|
+
// const tweakScale = scaleLinear({
|
|
46
|
+
// clamp: true,
|
|
47
|
+
// domain: [0.009, 0.002],
|
|
48
|
+
// range: [1, 8]
|
|
49
|
+
// });
|
|
50
|
+
|
|
51
|
+
// return padding - padding / tweakScale(additionalScale);
|
|
52
|
+
// }
|
|
53
|
+
|
|
54
|
+
// return padding;
|
|
55
|
+
// };
|