@centreon/ui 24.4.1-MON-34377-implement-tree-chart-on-centreon-ui.6 → 24.4.1-MON-34377-implement-tree-chart-on-centreon-ui.8

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 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.6",
3
+ "version": "24.4.1-MON-34377-implement-tree-chart-on-centreon-ui.8",
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,
@@ -6,7 +6,7 @@ import { isNil } from 'ramda';
6
6
 
7
7
  import { useDeepCompare } from '../../utils';
8
8
 
9
- import { margins, nodeMargins } from './constants';
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} top={margins.top}>
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}>
@@ -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: 10, id: 0, name: 'ok', status: 'ok' }
297
+ data: { count: 2, id: 0, name: 'ok', status: 'ok' }
298
298
  }
299
299
  ],
300
300
  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,16 +25,64 @@ const Minimap = ({
21
25
  }: Props): JSX.Element => {
22
26
  const { classes } = useZoomStyles();
23
27
 
24
- const { transformTo, move, zoomInOut } = useMinimap({
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: (invertedScale > 1 ? 1 : scale) || 1,
28
46
  width,
29
47
  zoom
30
48
  });
31
49
 
32
- const finalHeight = Math.max(contentClientRect?.height || 0, height);
33
- const finalWidth = Math.max(contentClientRect?.width || 0, width);
50
+ const finalHeight = height;
51
+ const finalWidth = width;
52
+
53
+ const additionalScaleScale = scaleLinear({
54
+ clamp: true,
55
+ domain: [contentClientRect.height, 0],
56
+ range: [0, 0.05]
57
+ });
58
+
59
+ const additionalScale =
60
+ additionalScaleScale(contentClientRect.height - height) /
61
+ 2 /
62
+ zoom.transformMatrix.scaleY;
63
+
64
+ const getAdditionalPadding = (): number => {
65
+ if (additionalScale > 0.05) {
66
+ return 0;
67
+ }
68
+
69
+ const padding =
70
+ additionalScale > 0.012
71
+ ? (1 / additionalScale) * (1 / zoom.transformMatrix.scaleY)
72
+ : 1 / additionalScale / zoom.transformMatrix.scaleY;
73
+
74
+ if (additionalScale < 0.009) {
75
+ const test = scaleLinear({
76
+ clamp: true,
77
+ domain: [0.005, 0.002],
78
+ range: [1, 5]
79
+ });
80
+
81
+ return padding - padding / test(additionalScale);
82
+ }
83
+
84
+ return padding;
85
+ };
34
86
 
35
87
  return (
36
88
  <g className={classes.minimap} clipPath="url(#zoom-clip)">
@@ -40,23 +92,38 @@ const Minimap = ({
40
92
  rx={radius}
41
93
  width={finalWidth}
42
94
  />
43
- {children}
44
- <rect
45
- className={classes.minimapZoom}
46
- fillOpacity={0.2}
47
- height={height}
48
- rx={radius}
49
- transform={zoom.toStringInvert()}
50
- width={width}
51
- />
95
+ <g
96
+ style={{
97
+ transform: `scale(${scaleToUse - additionalScale}) translate(0px, ${getAdditionalPadding()}px)`
98
+ }}
99
+ >
100
+ {children}
101
+ <g
102
+ style={{
103
+ transform: `translate(0px, ${contentClientRect.height / 10}px)`
104
+ }}
105
+ >
106
+ <rect
107
+ className={classes.minimapZoom}
108
+ fillOpacity={0.2}
109
+ height={height}
110
+ rx={radius}
111
+ transform={zoom.toStringInvert()}
112
+ width={width}
113
+ />
114
+ </g>
115
+ </g>
52
116
  <rect
53
117
  data-testid="minimap-interaction"
54
118
  fill="transparent"
55
119
  height={finalHeight}
56
120
  rx={radius}
57
121
  width={finalWidth}
58
- onMouseDown={transformTo}
122
+ onMouseDown={dragStart}
123
+ onMouseEnter={dragStart}
124
+ onMouseLeave={dragEnd}
59
125
  onMouseMove={move}
126
+ onMouseUp={dragEnd}
60
127
  onWheel={zoomInOut}
61
128
  />
62
129
  </g>
@@ -6,16 +6,36 @@ const content = (
6
6
  </g>
7
7
  );
8
8
 
9
- const labels = {
10
- clear: 'Clear'
11
- };
9
+ const contentWithMultipleShapes = (
10
+ <g style={{ transform: 'translate(0px, -300px)' }}>
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
- const initialize = ({ showMinimap }): void => {
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 labels={labels} showMinimap={showMinimap}>
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.contains('Clear').click();
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,48 @@ describe('Zoom', () => {
146
166
  cy.makeSnapshot();
147
167
  });
148
168
 
149
- it('moves the view when the minimap is clicked', () => {
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('minimap-interaction').click(20, 20);
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, -100, -100)'
182
+ 'matrix(1, 0, 0, 1, 200, 0)'
161
183
  );
162
184
 
163
185
  cy.makeSnapshot();
164
186
  });
165
187
 
166
- it('moves the view when the mouse is hover the minimap with the corresponding button pressed down', () => {
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('moves the view when the mouse is hover the content with the corresponding button pressed down', () => {
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('zoom-container')
192
- .trigger('mousedown', 400, 200)
193
- .trigger('mousemove', 600, 200);
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(
207
+ 'have.attr',
208
+ 'style',
209
+ 'transform: scale(0.684211) translate(0px, 57px);'
210
+ );
200
211
 
201
212
  cy.makeSnapshot();
202
213
  });
@@ -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(300px, 150px)' }}>
25
- <circle fill="blue" r={50} stroke="black" />
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 labels = {
36
- clear: 'Clear'
47
+ export const WithoutMinimap: Story = {
48
+ args: {
49
+ children: content
50
+ },
51
+ render: Template
37
52
  };
38
53
 
39
- export const WithoutMinimap: Story = {
54
+ export const WithMinimap: Story = {
40
55
  args: {
41
56
  children: content,
42
- labels
57
+ showMinimap: true
43
58
  },
44
59
  render: Template
45
60
  };
46
61
 
47
- export const WithMinimap: Story = {
62
+ export const WithMinimapPosition: Story = {
48
63
  args: {
49
64
  children: content,
50
- labels,
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
- gap: theme.spacing(1),
15
- left: 0,
16
- padding: theme.spacing(1),
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
- transition: 'top 0.15s ease-out'
40
+ width: 'fit-content'
19
41
  },
20
42
  minimap: {
21
43
  transform: `scale(${minimapScale})`
22
44
  },
23
45
  minimapBackground: {
24
- fill: theme.palette.background.default,
25
- stroke: theme.palette.background.default
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.text.primary
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 { useZoomStyles } from './Zoom.styles';
15
- import Minimap from './Minimap';
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
- labels: {
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
- labels
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
- <div style={{ position: 'relative' }}>
94
- <svg
95
- className={classes.svg}
96
- data-is-grabbing={isDragging}
97
- data-testid="zoom-container"
98
- height={height}
99
- width={width}
100
- onMouseDown={dragStart(zoom)}
101
- onMouseMove={move(zoom)}
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,142 @@
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
+ onMouseEnter={dragStart(zoom)}
77
+ onMouseLeave={dragEnd}
78
+ onMouseMove={move(zoom)}
79
+ onMouseUp={dragEnd}
80
+ onWheel={zoom.handleWheel}
81
+ >
82
+ <RectClipPath
83
+ height={Math.max(contentClientRect?.height || 0, height)}
84
+ id="zoom-clip"
85
+ rx={radius}
86
+ width={Math.max(contentClientRect?.width || 0, width)}
87
+ />
88
+ <g
89
+ data-testid="zoom-content"
90
+ ref={contentRef}
91
+ transform={zoom.toString()}
92
+ >
93
+ {isChildrenObject ? children : children({ height, width })}
94
+ </g>
95
+ </svg>
96
+ <div className={classes.actionsAndZoom} data-position={minimapPosition}>
97
+ {showMinimap && contentClientRect && (
98
+ <svg
99
+ className={classes.minimapContainer}
100
+ data-testid="minimap"
101
+ height={height * minimapScale}
102
+ width={width * minimapScale}
103
+ >
104
+ <Minimap
105
+ contentClientRect={contentClientRect}
106
+ height={height}
107
+ isDraggingFromContainer={isDragging}
108
+ width={width}
109
+ zoom={zoom}
110
+ >
111
+ <Group left={0} top={contentClientRect.height / 10}>
112
+ {isChildrenObject ? children : children({ height, width })}
113
+ </Group>
114
+ </Minimap>
115
+ </svg>
116
+ )}
117
+ <div className={classes.actions}>
118
+ <IconButton
119
+ data-testid="zoom in"
120
+ icon={<ZoomInIcon />}
121
+ size="small"
122
+ onClick={() => zoom.scale({ scaleX: 1.2, scaleY: 1.2 })}
123
+ />
124
+ <IconButton
125
+ data-testid="zoom out"
126
+ icon={<ZoomOutIcon />}
127
+ size="small"
128
+ onClick={() => zoom.scale({ scaleX: 0.8, scaleY: 0.8 })}
129
+ />
130
+ <IconButton
131
+ data-testid="clear"
132
+ icon={<ReplayIcon />}
133
+ size="small"
134
+ onClick={zoom.reset}
135
+ />
136
+ </div>
137
+ </div>
138
+ </div>
139
+ );
140
+ };
141
+
142
+ export default ZoomContent;
@@ -1,2 +1,2 @@
1
- export const radius = 30;
1
+ export const radius = 16;
2
2
  export const minimapScale = 0.2;
@@ -8,3 +8,9 @@ export interface ZoomState {
8
8
  translateY: number;
9
9
  };
10
10
  }
11
+
12
+ export type MinimapPosition =
13
+ | 'top-left'
14
+ | 'top-right'
15
+ | 'bottom-left'
16
+ | 'bottom-right';
@@ -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,42 @@ 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
- (event): { x: number; y: number } => {
40
+ (event, newScale?: number): { 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 = -(
48
+ point.x * (newScale || zoom.transformMatrix.scaleX) -
49
+ width / 2
50
+ );
51
+ const dy = -(
52
+ point.y * (newScale || zoom.transformMatrix.scaleY) -
53
+ height / 2
54
+ );
55
+
36
56
  return {
37
- x: -(point.x * zoom.transformMatrix.scaleX - width / 2),
38
- y: -(point.y * zoom.transformMatrix.scaleY - height / 2)
57
+ x: !hasScale ? dx : dx * scale - width / 2,
58
+ y: !hasScale ? dy : dy * scale - height / 2
39
59
  };
40
60
  },
41
- [zoom.transformMatrix]
61
+ [zoom.transformMatrix, scale, width, height, minimapScale]
42
62
  );
43
63
 
44
64
  const transformTo = useCallback(
45
65
  (e): void => {
46
- if (!equals(e.nativeEvent.which, 1)) {
66
+ if (!isNil(e.nativeEvent.which) && !equals(e.nativeEvent.which, 1)) {
47
67
  return;
48
68
  }
49
69
  const { x, y } = getMatrixPoint(e);
@@ -53,40 +73,82 @@ export const useMinimap = ({
53
73
  translateY: y
54
74
  });
55
75
  },
56
- [zoom.transformMatrix]
76
+ [zoom.transformMatrix, scale]
57
77
  );
58
78
 
79
+ const dragStart = (e): void => {
80
+ if (
81
+ (!isNil(e.nativeEvent.which) && !equals(e.nativeEvent.which, 1)) ||
82
+ isDraggingFromContainer
83
+ ) {
84
+ return;
85
+ }
86
+ setStartPoint(getMatrixPoint(e));
87
+ setStartTranslate(pick(['translateX', 'translateY'], zoom.transformMatrix));
88
+ };
89
+
90
+ const dragEnd = (): void => {
91
+ setStartPoint(null);
92
+ setStartTranslate(null);
93
+ };
94
+
59
95
  const move = useCallback(
60
96
  (e): void => {
61
- if (!equals(e.nativeEvent.which, 1) || isDraggingFromContainer) {
97
+ if (!startPoint || !startTranslate) {
62
98
  return;
63
99
  }
64
- transformTo(e);
100
+ const { x, y } = getMatrixPoint(e);
101
+
102
+ const diffX = startPoint.x - x;
103
+ const diffY = startPoint.y - y;
104
+
105
+ zoom.setTransformMatrix({
106
+ ...zoom.transformMatrix,
107
+ translateX: startTranslate.translateX - diffX,
108
+ translateY: startTranslate.translateY - diffY
109
+ });
65
110
  },
66
- [zoom.transformMatrix, isDraggingFromContainer]
111
+
112
+ [zoom.transformMatrix, isDraggingFromContainer, scale, startPoint]
67
113
  );
68
114
 
69
115
  const zoomInOut = useCallback(
70
116
  (e): void => {
71
117
  const isZoomIn = gt(0, e.deltaY);
72
- const { x, y } = getMatrixPoint(e);
118
+
119
+ const newScaleX = isZoomIn
120
+ ? zoom.transformMatrix.scaleX + 0.1
121
+ : zoom.transformMatrix.scaleX - 0.1;
122
+
123
+ const newScaleY = isZoomIn
124
+ ? zoom.transformMatrix.scaleX + 0.1
125
+ : zoom.transformMatrix.scaleX - 0.1;
126
+ const { x, y } = getMatrixPoint(e, newScaleX);
127
+
128
+ const diffX = x - zoom.transformMatrix.translateX;
129
+ const diffY = y - zoom.transformMatrix.translateY;
73
130
 
74
131
  zoom.setTransformMatrix({
75
132
  ...zoom.transformMatrix,
76
- scaleX: isZoomIn
77
- ? zoom.transformMatrix.scaleX + 0.1
78
- : zoom.transformMatrix.scaleX - 0.1,
79
- scaleY: isZoomIn
80
- ? zoom.transformMatrix.scaleY + 0.1
81
- : zoom.transformMatrix.scaleY - 0.1,
82
- translateX: x,
83
- translateY: y
133
+ scaleX: newScaleX,
134
+ scaleY: newScaleY,
135
+ translateX: zoom.transformMatrix.translateX + diffX / 4,
136
+ translateY: zoom.transformMatrix.translateY + diffY / 4
84
137
  });
85
138
  },
86
- [zoom.transformMatrix]
139
+ [
140
+ zoom.transformMatrix,
141
+ width,
142
+ height,
143
+ isDraggingFromContainer,
144
+ scale,
145
+ startPoint
146
+ ]
87
147
  );
88
148
 
89
149
  return {
150
+ dragEnd,
151
+ dragStart,
90
152
  move,
91
153
  transformTo,
92
154
  zoomInOut