@centreon/ui 24.4.1-MON-34377-implement-tree-chart-on-centreon-ui.5 → 24.4.1-MON-34377-implement-tree-chart-on-centreon-ui.7
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/Tree.stories.tsx +0 -12
- package/src/Graph/Tree/Tree.tsx +4 -2
- package/src/Graph/Tree/stories/datas.ts +2 -7
- package/src/components/Zoom/Minimap.tsx +56 -16
- package/src/components/Zoom/Zoom.cypress.spec.tsx +40 -33
- package/src/components/Zoom/Zoom.stories.tsx +23 -8
- package/src/components/Zoom/Zoom.styles.tsx +38 -11
- package/src/components/Zoom/Zoom.tsx +13 -129
- package/src/components/Zoom/ZoomContent.tsx +140 -0
- package/src/components/Zoom/constants.ts +1 -1
- package/src/components/Zoom/localPoint.ts +1 -11
- package/src/components/Zoom/models.ts +6 -0
- package/src/components/Zoom/useMinimap.ts +54 -12
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.7",
|
|
4
4
|
"description": "Centreon UI Components",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"update:deps": "pnpx npm-check-updates -i --format group",
|
|
@@ -101,18 +101,6 @@ export const WithCustomLinks: Story = {
|
|
|
101
101
|
render: StandaloneTreeTemplate
|
|
102
102
|
};
|
|
103
103
|
|
|
104
|
-
export const WithResetButton: Story = {
|
|
105
|
-
args: {
|
|
106
|
-
children: SimpleContent,
|
|
107
|
-
node: {
|
|
108
|
-
height: 90,
|
|
109
|
-
width: 90
|
|
110
|
-
},
|
|
111
|
-
tree: simpleData
|
|
112
|
-
},
|
|
113
|
-
render: StandaloneTreeTemplate
|
|
114
|
-
};
|
|
115
|
-
|
|
116
104
|
export const WithComplexData: Story = {
|
|
117
105
|
args: {
|
|
118
106
|
children: ComplexContent,
|
package/src/Graph/Tree/Tree.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import { isNil } from 'ramda';
|
|
|
6
6
|
|
|
7
7
|
import { useDeepCompare } from '../../utils';
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { nodeMargins } from './constants';
|
|
10
10
|
import { BaseProp, Node, TreeProps } from './models';
|
|
11
11
|
import { updateNodeFromTree } from './utils';
|
|
12
12
|
import Links from './Links';
|
|
@@ -81,12 +81,14 @@ export const Tree = <TData extends BaseProp>({
|
|
|
81
81
|
);
|
|
82
82
|
|
|
83
83
|
return (
|
|
84
|
-
<Group left={node.width / 2}
|
|
84
|
+
<Group left={node.width / 2}>
|
|
85
85
|
<VisxTree
|
|
86
|
+
left={0}
|
|
86
87
|
nodeSize={[node.width + nodeMargins.y, node.height + nodeMargins.x]}
|
|
87
88
|
root={hierarchy(formattedTree, getExpanded)}
|
|
88
89
|
separation={() => 1}
|
|
89
90
|
size={[containerWidth, containerHeight]}
|
|
91
|
+
top={0}
|
|
90
92
|
>
|
|
91
93
|
{(subTree) => (
|
|
92
94
|
<Group left={origin.x} top={origin.y}>
|
|
@@ -287,14 +287,9 @@ export const moreComplexData: Node<ComplexData> = {
|
|
|
287
287
|
{ data: { id: 30, name: 'Indicator 30', status: 'ok' } },
|
|
288
288
|
{ data: { id: 31, name: 'Indicator 31', status: 'ok' } },
|
|
289
289
|
{ data: { id: 32, name: 'Indicator 32', status: 'ok' } },
|
|
290
|
-
{ data: { id: 33, name: 'Indicator 33', status: 'ok' } }
|
|
291
|
-
{ data: { id: 34, name: 'Indicator 34', status: 'ok' } },
|
|
292
|
-
{ data: { id: 35, name: 'Indicator 35', status: 'ok' } },
|
|
293
|
-
{ data: { id: 36, name: 'Indicator 36', status: 'ok' } },
|
|
294
|
-
{ data: { id: 37, name: 'Indicator 37', status: 'ok' } },
|
|
295
|
-
{ data: { id: 38, name: 'Indicator 38', status: 'ok' } }
|
|
290
|
+
{ data: { id: 33, name: 'Indicator 33', status: 'ok' } }
|
|
296
291
|
],
|
|
297
|
-
data: { count:
|
|
292
|
+
data: { count: 2, id: 0, name: 'ok', status: 'ok' }
|
|
298
293
|
}
|
|
299
294
|
],
|
|
300
295
|
data: {
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import { scaleLinear } from '@visx/scale';
|
|
4
|
+
|
|
1
5
|
import { minimapScale, radius } from './constants';
|
|
2
6
|
import { UseMinimapProps, useMinimap } from './useMinimap';
|
|
3
7
|
import { useZoomStyles } from './Zoom.styles';
|
|
4
8
|
|
|
5
|
-
interface Props extends Omit<UseMinimapProps, 'minimapScale'> {
|
|
9
|
+
interface Props extends Omit<UseMinimapProps, 'minimapScale' | 'scale'> {
|
|
6
10
|
children: JSX.Element;
|
|
7
11
|
contentClientRect: {
|
|
8
12
|
height: number;
|
|
@@ -21,39 +25,75 @@ const Minimap = ({
|
|
|
21
25
|
}: Props): JSX.Element => {
|
|
22
26
|
const { classes } = useZoomStyles();
|
|
23
27
|
|
|
24
|
-
const
|
|
28
|
+
const yMinimapScale = useMemo(
|
|
29
|
+
() => contentClientRect.height / zoom.transformMatrix.scaleY / height,
|
|
30
|
+
[contentClientRect.height, height]
|
|
31
|
+
);
|
|
32
|
+
const xMinimapScale = useMemo(
|
|
33
|
+
() => contentClientRect.width / zoom.transformMatrix.scaleX / width,
|
|
34
|
+
[contentClientRect.width, width]
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const scale = Math.max(yMinimapScale, xMinimapScale);
|
|
38
|
+
const invertedScale = 1 / scale;
|
|
39
|
+
const scaleToUse = (invertedScale > 1 ? 1 : invertedScale) || 1;
|
|
40
|
+
|
|
41
|
+
const { move, zoomInOut, dragStart, dragEnd } = useMinimap({
|
|
25
42
|
height,
|
|
26
43
|
isDraggingFromContainer,
|
|
27
44
|
minimapScale,
|
|
45
|
+
scale: (1 / scale > 1 ? 1 : scale) || 1,
|
|
28
46
|
width,
|
|
29
47
|
zoom
|
|
30
48
|
});
|
|
31
49
|
|
|
50
|
+
const finalHeight = height;
|
|
51
|
+
const finalWidth = width;
|
|
52
|
+
|
|
53
|
+
const additionalScale = scaleLinear({
|
|
54
|
+
clamp: true,
|
|
55
|
+
domain: [contentClientRect.height, 0],
|
|
56
|
+
range: [0, 0.05]
|
|
57
|
+
});
|
|
58
|
+
|
|
32
59
|
return (
|
|
33
60
|
<g className={classes.minimap} clipPath="url(#zoom-clip)">
|
|
34
61
|
<rect
|
|
35
62
|
className={classes.minimapBackground}
|
|
36
|
-
height={
|
|
37
|
-
rx={radius}
|
|
38
|
-
width={Math.max(contentClientRect?.width || 0, width)}
|
|
39
|
-
/>
|
|
40
|
-
{children}
|
|
41
|
-
<rect
|
|
42
|
-
className={classes.minimapZoom}
|
|
43
|
-
fillOpacity={0.2}
|
|
44
|
-
height={height}
|
|
63
|
+
height={finalHeight}
|
|
45
64
|
rx={radius}
|
|
46
|
-
|
|
47
|
-
width={width}
|
|
65
|
+
width={finalWidth}
|
|
48
66
|
/>
|
|
67
|
+
<g
|
|
68
|
+
style={{
|
|
69
|
+
transform: `scale(${scaleToUse - additionalScale(contentClientRect.height - height) / 2})`
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
{children}
|
|
73
|
+
<g
|
|
74
|
+
style={{
|
|
75
|
+
transform: `translate(0px, ${contentClientRect.height / 10}px)`
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
<rect
|
|
79
|
+
className={classes.minimapZoom}
|
|
80
|
+
fillOpacity={0.2}
|
|
81
|
+
height={height}
|
|
82
|
+
rx={radius}
|
|
83
|
+
transform={zoom.toStringInvert()}
|
|
84
|
+
width={width}
|
|
85
|
+
/>
|
|
86
|
+
</g>
|
|
87
|
+
</g>
|
|
49
88
|
<rect
|
|
50
89
|
data-testid="minimap-interaction"
|
|
51
90
|
fill="transparent"
|
|
52
|
-
height={
|
|
91
|
+
height={finalHeight}
|
|
53
92
|
rx={radius}
|
|
54
|
-
width={
|
|
55
|
-
onMouseDown={
|
|
93
|
+
width={finalWidth}
|
|
94
|
+
onMouseDown={dragStart}
|
|
56
95
|
onMouseMove={move}
|
|
96
|
+
onMouseUp={dragEnd}
|
|
57
97
|
onWheel={zoomInOut}
|
|
58
98
|
/>
|
|
59
99
|
</g>
|
|
@@ -6,16 +6,36 @@ const content = (
|
|
|
6
6
|
</g>
|
|
7
7
|
);
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
}
|
|
9
|
+
const contentWithMultipleShapes = (
|
|
10
|
+
<g style={{ transform: 'translate(0px, -200px)' }}>
|
|
11
|
+
<g style={{ transform: 'translate(300px, 150px)' }}>
|
|
12
|
+
<circle fill="blue" r={50} stroke="black" />
|
|
13
|
+
</g>
|
|
14
|
+
<g style={{ transform: 'translate(600px, 500px)' }}>
|
|
15
|
+
<circle fill="green" r={70} />
|
|
16
|
+
</g>
|
|
17
|
+
<g style={{ transform: 'translate(150px, 600px)' }}>
|
|
18
|
+
<circle fill="red" r={70} />
|
|
19
|
+
</g>
|
|
20
|
+
</g>
|
|
21
|
+
);
|
|
12
22
|
|
|
13
|
-
|
|
23
|
+
interface Props {
|
|
24
|
+
minimapPosition?;
|
|
25
|
+
showMinimap: boolean;
|
|
26
|
+
useMultipleShapes?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const initialize = ({
|
|
30
|
+
showMinimap,
|
|
31
|
+
minimapPosition,
|
|
32
|
+
useMultipleShapes
|
|
33
|
+
}: Props): void => {
|
|
14
34
|
cy.mount({
|
|
15
35
|
Component: (
|
|
16
36
|
<div style={{ height: '400px', width: '100%' }}>
|
|
17
|
-
<Zoom
|
|
18
|
-
{content}
|
|
37
|
+
<Zoom minimapPosition={minimapPosition} showMinimap={showMinimap}>
|
|
38
|
+
{useMultipleShapes ? contentWithMultipleShapes : content}
|
|
19
39
|
</Zoom>
|
|
20
40
|
</div>
|
|
21
41
|
)
|
|
@@ -102,7 +122,7 @@ describe('Zoom', () => {
|
|
|
102
122
|
.should('have.attr', 'transform')
|
|
103
123
|
.and('include', '1.1, 0, 0, 1.1');
|
|
104
124
|
|
|
105
|
-
cy.
|
|
125
|
+
cy.findByTestId('clear').click();
|
|
106
126
|
|
|
107
127
|
cy.get('g[transform="matrix(1, 0, 0, 1, 0, 0)"]');
|
|
108
128
|
|
|
@@ -146,57 +166,44 @@ describe('Zoom', () => {
|
|
|
146
166
|
cy.makeSnapshot();
|
|
147
167
|
});
|
|
148
168
|
|
|
149
|
-
it('moves the view when the
|
|
169
|
+
it('moves the view when the mouse is hover the content with the corresponding button pressed down', () => {
|
|
150
170
|
initialize({ showMinimap: true });
|
|
151
171
|
|
|
152
172
|
cy.get('g[clip-path="url(#zoom-clip)"]').should('be.visible');
|
|
153
173
|
cy.get('svg').should('have.attr', 'height', '400');
|
|
154
174
|
|
|
155
|
-
cy.findByTestId('
|
|
175
|
+
cy.findByTestId('zoom-container')
|
|
176
|
+
.trigger('mousedown', 400, 200)
|
|
177
|
+
.trigger('mousemove', 600, 200);
|
|
156
178
|
|
|
157
179
|
cy.findByTestId('zoom-content').should(
|
|
158
180
|
'have.attr',
|
|
159
181
|
'transform',
|
|
160
|
-
'matrix(1, 0, 0, 1,
|
|
182
|
+
'matrix(1, 0, 0, 1, 200, 0)'
|
|
161
183
|
);
|
|
162
184
|
|
|
163
185
|
cy.makeSnapshot();
|
|
164
186
|
});
|
|
165
187
|
|
|
166
|
-
it('
|
|
167
|
-
initialize({ showMinimap: true });
|
|
188
|
+
it('displays the minimap in the bottom right when the prop to the corresponding value', () => {
|
|
189
|
+
initialize({ minimapPosition: 'bottom-right', showMinimap: true });
|
|
168
190
|
|
|
169
191
|
cy.get('g[clip-path="url(#zoom-clip)"]').should('be.visible');
|
|
170
192
|
cy.get('svg').should('have.attr', 'height', '400');
|
|
171
193
|
|
|
172
|
-
cy.findByTestId('minimap-interaction')
|
|
173
|
-
.realMouseDown()
|
|
174
|
-
.realMouseMove(40, 40);
|
|
175
|
-
|
|
176
|
-
cy.findByTestId('zoom-content').should(
|
|
177
|
-
'have.attr',
|
|
178
|
-
'transform',
|
|
179
|
-
'matrix(1, 0, 0, 1, -640, -200)'
|
|
180
|
-
);
|
|
181
|
-
|
|
182
194
|
cy.makeSnapshot();
|
|
183
195
|
});
|
|
184
196
|
|
|
185
|
-
it('
|
|
186
|
-
initialize({ showMinimap: true });
|
|
197
|
+
it('applies a scale down on the minimap when the content is higher than the original height', () => {
|
|
198
|
+
initialize({ showMinimap: true, useMultipleShapes: true });
|
|
187
199
|
|
|
188
200
|
cy.get('g[clip-path="url(#zoom-clip)"]').should('be.visible');
|
|
189
201
|
cy.get('svg').should('have.attr', 'height', '400');
|
|
190
202
|
|
|
191
|
-
cy.findByTestId('
|
|
192
|
-
.
|
|
193
|
-
.
|
|
194
|
-
|
|
195
|
-
cy.findByTestId('zoom-content').should(
|
|
196
|
-
'have.attr',
|
|
197
|
-
'transform',
|
|
198
|
-
'matrix(1, 0, 0, 1, 200, 0)'
|
|
199
|
-
);
|
|
203
|
+
cy.findByTestId('minimap-interaction')
|
|
204
|
+
.parent()
|
|
205
|
+
.find('g')
|
|
206
|
+
.should('have.attr', 'style', 'transform: scale(0.684211);');
|
|
200
207
|
|
|
201
208
|
cy.makeSnapshot();
|
|
202
209
|
});
|
|
@@ -4,6 +4,10 @@ import Zoom, { ZoomProps } from './Zoom';
|
|
|
4
4
|
|
|
5
5
|
const meta: Meta<typeof Zoom> = {
|
|
6
6
|
argTypes: {
|
|
7
|
+
minimapPosition: {
|
|
8
|
+
control: 'select',
|
|
9
|
+
options: ['top-left', 'top-right', 'bottom-left', 'bottom-right']
|
|
10
|
+
},
|
|
7
11
|
scaleMax: {
|
|
8
12
|
control: { max: 16, min: 0.4, step: 0.2, type: 'range' }
|
|
9
13
|
},
|
|
@@ -21,8 +25,16 @@ export default meta;
|
|
|
21
25
|
type Story = StoryObj<typeof Zoom>;
|
|
22
26
|
|
|
23
27
|
const content = (
|
|
24
|
-
<g style={{ transform: 'translate(
|
|
25
|
-
<
|
|
28
|
+
<g style={{ transform: 'translate(0px, -200px)' }}>
|
|
29
|
+
<g style={{ transform: 'translate(300px, 150px)' }}>
|
|
30
|
+
<circle fill="blue" r={50} stroke="black" />
|
|
31
|
+
</g>
|
|
32
|
+
<g style={{ transform: 'translate(600px, 400px)' }}>
|
|
33
|
+
<circle fill="green" r={70} />
|
|
34
|
+
</g>
|
|
35
|
+
<g style={{ transform: 'translate(150px, 800px)' }}>
|
|
36
|
+
<circle fill="red" r={70} />
|
|
37
|
+
</g>
|
|
26
38
|
</g>
|
|
27
39
|
);
|
|
28
40
|
|
|
@@ -32,22 +44,25 @@ const Template = ({ children, ...args }: ZoomProps): JSX.Element => (
|
|
|
32
44
|
</div>
|
|
33
45
|
);
|
|
34
46
|
|
|
35
|
-
const
|
|
36
|
-
|
|
47
|
+
export const WithoutMinimap: Story = {
|
|
48
|
+
args: {
|
|
49
|
+
children: content
|
|
50
|
+
},
|
|
51
|
+
render: Template
|
|
37
52
|
};
|
|
38
53
|
|
|
39
|
-
export const
|
|
54
|
+
export const WithMinimap: Story = {
|
|
40
55
|
args: {
|
|
41
56
|
children: content,
|
|
42
|
-
|
|
57
|
+
showMinimap: true
|
|
43
58
|
},
|
|
44
59
|
render: Template
|
|
45
60
|
};
|
|
46
61
|
|
|
47
|
-
export const
|
|
62
|
+
export const WithMinimapPosition: Story = {
|
|
48
63
|
args: {
|
|
49
64
|
children: content,
|
|
50
|
-
|
|
65
|
+
minimapPosition: 'bottom-right',
|
|
51
66
|
showMinimap: true
|
|
52
67
|
},
|
|
53
68
|
render: Template
|
|
@@ -7,26 +7,53 @@ import { minimapScale } from './constants';
|
|
|
7
7
|
export const useZoomStyles = makeStyles()((theme) => ({
|
|
8
8
|
actions: {
|
|
9
9
|
backgroundColor: alpha(theme.palette.background.paper, 0.8),
|
|
10
|
-
borderRadius: `0 ${theme.shape.borderRadius}px 0 0`,
|
|
11
|
-
bottom: 0,
|
|
12
10
|
display: 'flex',
|
|
13
|
-
flexDirection: 'row'
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
flexDirection: 'row'
|
|
12
|
+
},
|
|
13
|
+
actionsAndZoom: {
|
|
14
|
+
'&[data-position="bottom-left"]': {
|
|
15
|
+
alignItems: 'flex-start',
|
|
16
|
+
bottom: 0,
|
|
17
|
+
flexDirection: 'column-reverse',
|
|
18
|
+
left: 0
|
|
19
|
+
},
|
|
20
|
+
'&[data-position="bottom-right"]': {
|
|
21
|
+
alignItems: 'flex-end',
|
|
22
|
+
bottom: 0,
|
|
23
|
+
flexDirection: 'column-reverse',
|
|
24
|
+
right: 0
|
|
25
|
+
},
|
|
26
|
+
'&[data-position="top-left"]': {
|
|
27
|
+
alignItems: 'flex-start',
|
|
28
|
+
flexDirection: 'column',
|
|
29
|
+
left: 0,
|
|
30
|
+
top: 0
|
|
31
|
+
},
|
|
32
|
+
'&[data-position="top-right"]': {
|
|
33
|
+
alignItems: 'flex-end',
|
|
34
|
+
flexDirection: 'column',
|
|
35
|
+
right: 0,
|
|
36
|
+
top: 0
|
|
37
|
+
},
|
|
38
|
+
display: 'flex',
|
|
17
39
|
position: 'absolute',
|
|
18
|
-
|
|
40
|
+
width: 'fit-content'
|
|
19
41
|
},
|
|
20
42
|
minimap: {
|
|
21
43
|
transform: `scale(${minimapScale})`
|
|
22
44
|
},
|
|
23
45
|
minimapBackground: {
|
|
24
|
-
fill: theme.palette.background.
|
|
25
|
-
stroke: theme.palette.background.
|
|
46
|
+
fill: theme.palette.background.paper,
|
|
47
|
+
stroke: theme.palette.background.paper
|
|
48
|
+
},
|
|
49
|
+
minimapContainer: {
|
|
50
|
+
border: `1px solid ${theme.palette.divider}`,
|
|
51
|
+
borderRadius: theme.shape.borderRadius
|
|
26
52
|
},
|
|
27
|
-
minimapContainer: { left: 0, position: 'absolute', top: 0 },
|
|
28
53
|
minimapZoom: {
|
|
29
|
-
fill: theme.palette.
|
|
54
|
+
fill: theme.palette.primary.main,
|
|
55
|
+
stroke: theme.palette.primary.main,
|
|
56
|
+
strokeWidth: 10
|
|
30
57
|
},
|
|
31
58
|
movingZone: {
|
|
32
59
|
transition: 'transform 0.1s linear'
|
|
@@ -1,26 +1,13 @@
|
|
|
1
|
-
import { useState, useRef, useEffect } from 'react';
|
|
2
|
-
|
|
3
1
|
import { Zoom as VisxZoom } from '@visx/zoom';
|
|
4
|
-
import { RectClipPath } from '@visx/clip-path';
|
|
5
|
-
import { Group } from '@visx/group';
|
|
6
|
-
import { equals, type } from 'ramda';
|
|
7
|
-
|
|
8
|
-
import ZoomInIcon from '@mui/icons-material/Add';
|
|
9
|
-
import ZoomOutIcon from '@mui/icons-material/Remove';
|
|
10
2
|
|
|
11
3
|
import { ParentSize } from '../..';
|
|
12
|
-
import { Button, IconButton } from '../Button';
|
|
13
4
|
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
import { useZoom } from './useZoom';
|
|
17
|
-
import { minimapScale } from './constants';
|
|
5
|
+
import ZoomContent from './ZoomContent';
|
|
6
|
+
import { MinimapPosition } from './models';
|
|
18
7
|
|
|
19
8
|
export interface ZoomProps {
|
|
20
9
|
children: JSX.Element | (({ width, height }) => JSX.Element);
|
|
21
|
-
|
|
22
|
-
clear: string;
|
|
23
|
-
};
|
|
10
|
+
minimapPosition?: MinimapPosition;
|
|
24
11
|
scaleMax?: number;
|
|
25
12
|
scaleMin?: number;
|
|
26
13
|
showMinimap?: boolean;
|
|
@@ -34,49 +21,14 @@ const initialTransform = {
|
|
|
34
21
|
translateX: 0,
|
|
35
22
|
translateY: 0
|
|
36
23
|
};
|
|
37
|
-
const radius = 30;
|
|
38
24
|
|
|
39
25
|
const Zoom = ({
|
|
40
26
|
children,
|
|
41
27
|
scaleMin = 0.5,
|
|
42
28
|
scaleMax = 4,
|
|
43
29
|
showMinimap = false,
|
|
44
|
-
|
|
30
|
+
minimapPosition = 'top-left'
|
|
45
31
|
}: ZoomProps): JSX.Element => {
|
|
46
|
-
const { classes } = useZoomStyles();
|
|
47
|
-
|
|
48
|
-
const [contentClientRect, setContentClientRect] = useState<{
|
|
49
|
-
height: number;
|
|
50
|
-
width: number;
|
|
51
|
-
} | null>(null);
|
|
52
|
-
const contentRef = useRef<SVGGElement | null>(null);
|
|
53
|
-
|
|
54
|
-
const { move, dragEnd, dragStart, isDragging } = useZoom();
|
|
55
|
-
|
|
56
|
-
const resizeObserver = new ResizeObserver(() => {
|
|
57
|
-
setContentClientRect({
|
|
58
|
-
height: contentRef.current?.getBoundingClientRect().height || 0,
|
|
59
|
-
width: contentRef.current?.getBoundingClientRect().width || 0
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
useEffect(() => {
|
|
64
|
-
if (contentRef.current) {
|
|
65
|
-
resizeObserver.disconnect();
|
|
66
|
-
setContentClientRect({
|
|
67
|
-
height: contentRef.current?.getBoundingClientRect().height || 0,
|
|
68
|
-
width: contentRef.current?.getBoundingClientRect().width || 0
|
|
69
|
-
});
|
|
70
|
-
resizeObserver.observe(contentRef.current);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return () => {
|
|
74
|
-
resizeObserver.disconnect();
|
|
75
|
-
};
|
|
76
|
-
}, [contentRef.current]);
|
|
77
|
-
|
|
78
|
-
const isChildrenObject = equals(type(children), 'Object');
|
|
79
|
-
|
|
80
32
|
return (
|
|
81
33
|
<ParentSize>
|
|
82
34
|
{({ width, height }) => (
|
|
@@ -90,83 +42,15 @@ const Zoom = ({
|
|
|
90
42
|
width={width}
|
|
91
43
|
>
|
|
92
44
|
{(zoom) => (
|
|
93
|
-
<
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
onMouseUp={dragEnd}
|
|
103
|
-
onWheel={zoom.handleWheel}
|
|
104
|
-
>
|
|
105
|
-
<RectClipPath
|
|
106
|
-
height={Math.max(contentClientRect?.height || 0, height)}
|
|
107
|
-
id="zoom-clip"
|
|
108
|
-
rx={radius}
|
|
109
|
-
width={Math.max(contentClientRect?.width || 0, width)}
|
|
110
|
-
/>
|
|
111
|
-
<g
|
|
112
|
-
data-testid="zoom-content"
|
|
113
|
-
ref={contentRef}
|
|
114
|
-
transform={zoom.toString()}
|
|
115
|
-
>
|
|
116
|
-
{isChildrenObject ? children : children({ height, width })}
|
|
117
|
-
</g>
|
|
118
|
-
</svg>
|
|
119
|
-
<svg
|
|
120
|
-
className={classes.minimapContainer}
|
|
121
|
-
data-testid="minimap"
|
|
122
|
-
height={
|
|
123
|
-
Math.max(contentClientRect?.height || 0, height) *
|
|
124
|
-
minimapScale
|
|
125
|
-
}
|
|
126
|
-
width={
|
|
127
|
-
Math.max(contentClientRect?.width || 0, width) * minimapScale
|
|
128
|
-
}
|
|
129
|
-
>
|
|
130
|
-
{showMinimap && contentClientRect && (
|
|
131
|
-
<Minimap
|
|
132
|
-
contentClientRect={contentClientRect}
|
|
133
|
-
height={height}
|
|
134
|
-
isDraggingFromContainer={isDragging}
|
|
135
|
-
width={width}
|
|
136
|
-
zoom={zoom}
|
|
137
|
-
>
|
|
138
|
-
<Group left={0} top={contentClientRect.height / 10}>
|
|
139
|
-
{isChildrenObject
|
|
140
|
-
? children
|
|
141
|
-
: children({ height, width })}
|
|
142
|
-
</Group>
|
|
143
|
-
</Minimap>
|
|
144
|
-
)}
|
|
145
|
-
</svg>
|
|
146
|
-
<div className={classes.actions}>
|
|
147
|
-
<IconButton
|
|
148
|
-
data-testid="zoom in"
|
|
149
|
-
icon={<ZoomInIcon />}
|
|
150
|
-
size="small"
|
|
151
|
-
onClick={() => zoom.scale({ scaleX: 1.2, scaleY: 1.2 })}
|
|
152
|
-
/>
|
|
153
|
-
<IconButton
|
|
154
|
-
data-testid="zoom out"
|
|
155
|
-
icon={<ZoomOutIcon />}
|
|
156
|
-
size="small"
|
|
157
|
-
onClick={() => zoom.scale({ scaleX: 0.8, scaleY: 0.8 })}
|
|
158
|
-
/>
|
|
159
|
-
|
|
160
|
-
<Button
|
|
161
|
-
data-testid="clear"
|
|
162
|
-
size="small"
|
|
163
|
-
variant="ghost"
|
|
164
|
-
onClick={zoom.clear}
|
|
165
|
-
>
|
|
166
|
-
{labels.clear}
|
|
167
|
-
</Button>
|
|
168
|
-
</div>
|
|
169
|
-
</div>
|
|
45
|
+
<ZoomContent
|
|
46
|
+
height={height}
|
|
47
|
+
minimapPosition={minimapPosition}
|
|
48
|
+
showMinimap={showMinimap}
|
|
49
|
+
width={width}
|
|
50
|
+
zoom={zoom}
|
|
51
|
+
>
|
|
52
|
+
{children}
|
|
53
|
+
</ZoomContent>
|
|
170
54
|
)}
|
|
171
55
|
</VisxZoom>
|
|
172
56
|
)}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { RectClipPath } from '@visx/clip-path';
|
|
4
|
+
import { equals, type } from 'ramda';
|
|
5
|
+
import { Group } from '@visx/group';
|
|
6
|
+
import { ProvidedZoom } from '@visx/zoom/lib/types';
|
|
7
|
+
|
|
8
|
+
import ZoomInIcon from '@mui/icons-material/Add';
|
|
9
|
+
import ZoomOutIcon from '@mui/icons-material/Remove';
|
|
10
|
+
import ReplayIcon from '@mui/icons-material/Replay';
|
|
11
|
+
|
|
12
|
+
import { IconButton } from '../Button';
|
|
13
|
+
|
|
14
|
+
import { minimapScale, radius } from './constants';
|
|
15
|
+
import { useZoom } from './useZoom';
|
|
16
|
+
import { useZoomStyles } from './Zoom.styles';
|
|
17
|
+
import Minimap from './Minimap';
|
|
18
|
+
import { MinimapPosition, ZoomState } from './models';
|
|
19
|
+
|
|
20
|
+
export interface Props {
|
|
21
|
+
children: JSX.Element | (({ width, height }) => JSX.Element);
|
|
22
|
+
height: number;
|
|
23
|
+
minimapPosition: MinimapPosition;
|
|
24
|
+
showMinimap?: boolean;
|
|
25
|
+
width: number;
|
|
26
|
+
zoom: ProvidedZoom<SVGSVGElement> & ZoomState;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ZoomContent = ({
|
|
30
|
+
zoom,
|
|
31
|
+
width,
|
|
32
|
+
height,
|
|
33
|
+
children,
|
|
34
|
+
showMinimap,
|
|
35
|
+
minimapPosition
|
|
36
|
+
}: Props): JSX.Element => {
|
|
37
|
+
const { classes } = useZoomStyles();
|
|
38
|
+
const contentRef = useRef<SVGGElement | null>(null);
|
|
39
|
+
const [contentClientRect, setContentClientRect] = useState<{
|
|
40
|
+
height: number;
|
|
41
|
+
width: number;
|
|
42
|
+
} | null>(null);
|
|
43
|
+
|
|
44
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
45
|
+
setContentClientRect({
|
|
46
|
+
height: (contentRef.current as SVGGElement).getBoundingClientRect()
|
|
47
|
+
.height,
|
|
48
|
+
width: (contentRef.current as SVGGElement).getBoundingClientRect().width
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (contentRef.current) {
|
|
54
|
+
resizeObserver.disconnect();
|
|
55
|
+
resizeObserver.observe(contentRef.current);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return () => {
|
|
59
|
+
resizeObserver.disconnect();
|
|
60
|
+
};
|
|
61
|
+
}, [contentRef.current]);
|
|
62
|
+
|
|
63
|
+
const { move, dragEnd, dragStart, isDragging } = useZoom();
|
|
64
|
+
|
|
65
|
+
const isChildrenObject = equals(type(children), 'Object');
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div style={{ position: 'relative' }}>
|
|
69
|
+
<svg
|
|
70
|
+
className={classes.svg}
|
|
71
|
+
data-is-grabbing={isDragging}
|
|
72
|
+
data-testid="zoom-container"
|
|
73
|
+
height={height}
|
|
74
|
+
width={width}
|
|
75
|
+
onMouseDown={dragStart(zoom)}
|
|
76
|
+
onMouseMove={move(zoom)}
|
|
77
|
+
onMouseUp={dragEnd}
|
|
78
|
+
onWheel={zoom.handleWheel}
|
|
79
|
+
>
|
|
80
|
+
<RectClipPath
|
|
81
|
+
height={Math.max(contentClientRect?.height || 0, height)}
|
|
82
|
+
id="zoom-clip"
|
|
83
|
+
rx={radius}
|
|
84
|
+
width={Math.max(contentClientRect?.width || 0, width)}
|
|
85
|
+
/>
|
|
86
|
+
<g
|
|
87
|
+
data-testid="zoom-content"
|
|
88
|
+
ref={contentRef}
|
|
89
|
+
transform={zoom.toString()}
|
|
90
|
+
>
|
|
91
|
+
{isChildrenObject ? children : children({ height, width })}
|
|
92
|
+
</g>
|
|
93
|
+
</svg>
|
|
94
|
+
<div className={classes.actionsAndZoom} data-position={minimapPosition}>
|
|
95
|
+
{showMinimap && contentClientRect && (
|
|
96
|
+
<svg
|
|
97
|
+
className={classes.minimapContainer}
|
|
98
|
+
data-testid="minimap"
|
|
99
|
+
height={height * minimapScale}
|
|
100
|
+
width={width * minimapScale}
|
|
101
|
+
>
|
|
102
|
+
<Minimap
|
|
103
|
+
contentClientRect={contentClientRect}
|
|
104
|
+
height={height}
|
|
105
|
+
isDraggingFromContainer={isDragging}
|
|
106
|
+
width={width}
|
|
107
|
+
zoom={zoom}
|
|
108
|
+
>
|
|
109
|
+
<Group left={0} top={contentClientRect.height / 10}>
|
|
110
|
+
{isChildrenObject ? children : children({ height, width })}
|
|
111
|
+
</Group>
|
|
112
|
+
</Minimap>
|
|
113
|
+
</svg>
|
|
114
|
+
)}
|
|
115
|
+
<div className={classes.actions}>
|
|
116
|
+
<IconButton
|
|
117
|
+
data-testid="zoom in"
|
|
118
|
+
icon={<ZoomInIcon />}
|
|
119
|
+
size="small"
|
|
120
|
+
onClick={() => zoom.scale({ scaleX: 1.2, scaleY: 1.2 })}
|
|
121
|
+
/>
|
|
122
|
+
<IconButton
|
|
123
|
+
data-testid="zoom out"
|
|
124
|
+
icon={<ZoomOutIcon />}
|
|
125
|
+
size="small"
|
|
126
|
+
onClick={() => zoom.scale({ scaleX: 0.8, scaleY: 0.8 })}
|
|
127
|
+
/>
|
|
128
|
+
<IconButton
|
|
129
|
+
data-testid="clear"
|
|
130
|
+
icon={<ReplayIcon />}
|
|
131
|
+
size="small"
|
|
132
|
+
onClick={zoom.reset}
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export default ZoomContent;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const radius =
|
|
1
|
+
export const radius = 16;
|
|
2
2
|
export const minimapScale = 0.2;
|
|
@@ -4,7 +4,6 @@ import type {
|
|
|
4
4
|
TouchEvent as ReactTouchEvent
|
|
5
5
|
} from 'react';
|
|
6
6
|
|
|
7
|
-
import { has } from 'ramda';
|
|
8
7
|
import { Point } from '@visx/point';
|
|
9
8
|
|
|
10
9
|
type EventType =
|
|
@@ -44,16 +43,7 @@ const getXAndYFromEvent = (event?: EventType): PointCoords => {
|
|
|
44
43
|
};
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
? event.target.getBoundingClientRect()
|
|
49
|
-
: null;
|
|
50
|
-
|
|
51
|
-
if (!boundingClientRect) return { ...DEFAULT_POINT };
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
x: boundingClientRect.x + boundingClientRect.width / 2,
|
|
55
|
-
y: boundingClientRect.y + boundingClientRect.height / 2
|
|
56
|
-
};
|
|
46
|
+
return { ...DEFAULT_POINT };
|
|
57
47
|
};
|
|
58
48
|
|
|
59
49
|
export const localPoint = (event: EventType): PointCoords | null => {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { useCallback } from 'react';
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
2
|
|
|
3
|
-
import { ProvidedZoom } from '@visx/zoom/lib/types';
|
|
4
|
-
import { equals, gt } from 'ramda';
|
|
3
|
+
import { ProvidedZoom, Translate } from '@visx/zoom/lib/types';
|
|
4
|
+
import { equals, gt, isNil, pick } from 'ramda';
|
|
5
|
+
import { Point } from '@visx/point';
|
|
5
6
|
|
|
6
7
|
import { ZoomState } from './models';
|
|
7
8
|
|
|
@@ -9,11 +10,14 @@ export interface UseMinimapProps {
|
|
|
9
10
|
height: number;
|
|
10
11
|
isDraggingFromContainer: boolean;
|
|
11
12
|
minimapScale: number;
|
|
13
|
+
scale: number;
|
|
12
14
|
width: number;
|
|
13
15
|
zoom: ProvidedZoom<SVGSVGElement> & ZoomState;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
interface UseMinimapState {
|
|
19
|
+
dragEnd: (e) => void;
|
|
20
|
+
dragStart: (e) => void;
|
|
17
21
|
move: (e) => void;
|
|
18
22
|
transformTo: (e) => void;
|
|
19
23
|
zoomInOut: (e) => void;
|
|
@@ -24,26 +28,36 @@ export const useMinimap = ({
|
|
|
24
28
|
height,
|
|
25
29
|
zoom,
|
|
26
30
|
minimapScale,
|
|
27
|
-
isDraggingFromContainer
|
|
31
|
+
isDraggingFromContainer,
|
|
32
|
+
scale
|
|
28
33
|
}: UseMinimapProps): UseMinimapState => {
|
|
34
|
+
const [startPoint, setStartPoint] = useState<Pick<Point, 'x' | 'y'> | null>(
|
|
35
|
+
null
|
|
36
|
+
);
|
|
37
|
+
const [startTranslate, setStartTranslate] = useState<Translate | null>(null);
|
|
38
|
+
|
|
29
39
|
const getMatrixPoint = useCallback(
|
|
30
40
|
(event): { x: number; y: number } => {
|
|
41
|
+
const hasScale = scale > 1;
|
|
31
42
|
const point = {
|
|
32
43
|
x: event.nativeEvent.offsetX * (1 / minimapScale),
|
|
33
44
|
y: event.nativeEvent.offsetY * (1 / minimapScale)
|
|
34
45
|
};
|
|
35
46
|
|
|
47
|
+
const dx = -(point.x * zoom.transformMatrix.scaleX - width / 2);
|
|
48
|
+
const dy = -(point.y * zoom.transformMatrix.scaleY - height / 2);
|
|
49
|
+
|
|
36
50
|
return {
|
|
37
|
-
x:
|
|
38
|
-
y:
|
|
51
|
+
x: !hasScale ? dx : dx * scale - width / 2,
|
|
52
|
+
y: !hasScale ? dy : dy * scale - height / 2
|
|
39
53
|
};
|
|
40
54
|
},
|
|
41
|
-
[zoom.transformMatrix]
|
|
55
|
+
[zoom.transformMatrix, scale, width, height, minimapScale]
|
|
42
56
|
);
|
|
43
57
|
|
|
44
58
|
const transformTo = useCallback(
|
|
45
59
|
(e): void => {
|
|
46
|
-
if (!equals(e.nativeEvent.which, 1)) {
|
|
60
|
+
if (!isNil(e.nativeEvent.which) && !equals(e.nativeEvent.which, 1)) {
|
|
47
61
|
return;
|
|
48
62
|
}
|
|
49
63
|
const { x, y } = getMatrixPoint(e);
|
|
@@ -53,17 +67,43 @@ export const useMinimap = ({
|
|
|
53
67
|
translateY: y
|
|
54
68
|
});
|
|
55
69
|
},
|
|
56
|
-
[zoom.transformMatrix]
|
|
70
|
+
[zoom.transformMatrix, scale]
|
|
57
71
|
);
|
|
58
72
|
|
|
73
|
+
const dragStart = (e): void => {
|
|
74
|
+
if (
|
|
75
|
+
(!equals(e.buttons, 0) && !equals(e.nativeEvent.which, 1)) ||
|
|
76
|
+
isDraggingFromContainer
|
|
77
|
+
) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
setStartPoint(getMatrixPoint(e));
|
|
81
|
+
setStartTranslate(pick(['translateX', 'translateY'], zoom.transformMatrix));
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const dragEnd = (): void => {
|
|
85
|
+
setStartPoint(null);
|
|
86
|
+
setStartTranslate(null);
|
|
87
|
+
};
|
|
88
|
+
|
|
59
89
|
const move = useCallback(
|
|
60
90
|
(e): void => {
|
|
61
|
-
if (!
|
|
91
|
+
if (!startPoint || !startTranslate) {
|
|
62
92
|
return;
|
|
63
93
|
}
|
|
64
|
-
|
|
94
|
+
const { x, y } = getMatrixPoint(e);
|
|
95
|
+
|
|
96
|
+
const diffX = startPoint.x - x;
|
|
97
|
+
const diffY = startPoint.y - y;
|
|
98
|
+
|
|
99
|
+
zoom.setTransformMatrix({
|
|
100
|
+
...zoom.transformMatrix,
|
|
101
|
+
translateX: startTranslate.translateX - diffX,
|
|
102
|
+
translateY: startTranslate.translateY - diffY
|
|
103
|
+
});
|
|
65
104
|
},
|
|
66
|
-
|
|
105
|
+
|
|
106
|
+
[zoom.transformMatrix, isDraggingFromContainer, scale, startPoint]
|
|
67
107
|
);
|
|
68
108
|
|
|
69
109
|
const zoomInOut = useCallback(
|
|
@@ -87,6 +127,8 @@ export const useMinimap = ({
|
|
|
87
127
|
);
|
|
88
128
|
|
|
89
129
|
return {
|
|
130
|
+
dragEnd,
|
|
131
|
+
dragStart,
|
|
90
132
|
move,
|
|
91
133
|
transformTo,
|
|
92
134
|
zoomInOut
|