diff options
Diffstat (limited to 'subprojects/frontend/src')
-rw-r--r-- | subprojects/frontend/src/graph/GraphArea.tsx | 189 |
1 files changed, 103 insertions, 86 deletions
diff --git a/subprojects/frontend/src/graph/GraphArea.tsx b/subprojects/frontend/src/graph/GraphArea.tsx index aec5d1c6..6ca3bc87 100644 --- a/subprojects/frontend/src/graph/GraphArea.tsx +++ b/subprojects/frontend/src/graph/GraphArea.tsx | |||
@@ -11,6 +11,7 @@ import Box from '@mui/material/Box'; | |||
11 | import IconButton from '@mui/material/IconButton'; | 11 | import IconButton from '@mui/material/IconButton'; |
12 | import Stack from '@mui/material/Stack'; | 12 | import Stack from '@mui/material/Stack'; |
13 | import { useTheme } from '@mui/material/styles'; | 13 | import { useTheme } from '@mui/material/styles'; |
14 | import { CSSProperties } from '@mui/material/styles/createTypography'; | ||
14 | import * as d3 from 'd3'; | 15 | import * as d3 from 'd3'; |
15 | import { type Graphviz, graphviz } from 'd3-graphviz'; | 16 | import { type Graphviz, graphviz } from 'd3-graphviz'; |
16 | import type { BaseType, Selection } from 'd3-selection'; | 17 | import type { BaseType, Selection } from 'd3-selection'; |
@@ -109,15 +110,21 @@ export default function GraphArea(): JSX.Element { | |||
109 | zoomBehavior as unknown as { | 110 | zoomBehavior as unknown as { |
110 | center(callback: (event: MouseEvent) => [number, number]): unknown; | 111 | center(callback: (event: MouseEvent) => [number, number]): unknown; |
111 | } | 112 | } |
112 | ).center((event: MouseEvent) => { | 113 | ).center((event: MouseEvent | Touch) => { |
113 | const { width, height } = element.getBoundingClientRect(); | 114 | const { width, height } = element.getBoundingClientRect(); |
114 | const [x, y] = d3.pointer(event, element); | 115 | const [x, y] = d3.pointer(event, element); |
115 | return [x - width / 2, y - height / 2]; | 116 | return [x - width / 2, y - height / 2]; |
116 | }); | 117 | }); |
118 | // Custom `centroid` method added via patch. | ||
119 | ( | ||
120 | zoomBehavior as unknown as { | ||
121 | centroid(centroid: [number, number]): unknown; | ||
122 | } | ||
123 | ).centroid([0, 0]); | ||
117 | zoomBehavior.on('zoom', (event: d3.D3ZoomEvent<HTMLDivElement, unknown>) => | 124 | zoomBehavior.on('zoom', (event: d3.D3ZoomEvent<HTMLDivElement, unknown>) => |
118 | setZoom(event.transform), | 125 | setZoom(event.transform), |
119 | ); | 126 | ); |
120 | d3.select(element).call(zoomBehavior).on('dblclick.zoom', null); | 127 | d3.select(element).call(zoomBehavior); |
121 | zoomRef.current = zoomBehavior; | 128 | zoomRef.current = zoomBehavior; |
122 | }, []); | 129 | }, []); |
123 | 130 | ||
@@ -213,25 +220,17 @@ export default function GraphArea(): JSX.Element { | |||
213 | [editorStore], | 220 | [editorStore], |
214 | ); | 221 | ); |
215 | 222 | ||
216 | const { | 223 | const changeZoom = useCallback((event: React.MouseEvent, factor: number) => { |
217 | transitions: { | 224 | if (canvasRef.current === undefined || zoomRef.current === undefined) { |
218 | duration: { short: zoomDuration }, | 225 | return; |
219 | }, | 226 | } |
220 | } = theme; | 227 | const selection = d3.select(canvasRef.current); |
221 | const changeZoom = useCallback( | 228 | const zoomTransition = selection.transition().duration(250); |
222 | (event: React.MouseEvent, factor: number) => { | 229 | const center: [number, number] = [0, 0]; |
223 | if (canvasRef.current === undefined || zoomRef.current === undefined) { | 230 | zoomRef.current.scaleBy(zoomTransition, factor, center); |
224 | return; | 231 | event.preventDefault(); |
225 | } | 232 | event.stopPropagation(); |
226 | const selection = d3.select(canvasRef.current); | 233 | }, []); |
227 | const zoomTransition = selection.transition().duration(zoomDuration); | ||
228 | const center: [number, number] = [0, 0]; | ||
229 | zoomRef.current.scaleBy(zoomTransition, factor, center); | ||
230 | event.preventDefault(); | ||
231 | event.stopPropagation(); | ||
232 | }, | ||
233 | [zoomDuration], | ||
234 | ); | ||
235 | 234 | ||
236 | const fitZoom = useCallback((event: React.MouseEvent) => { | 235 | const fitZoom = useCallback((event: React.MouseEvent) => { |
237 | if ( | 236 | if ( |
@@ -255,7 +254,8 @@ export default function GraphArea(): JSX.Element { | |||
255 | (canvasHeight - 64) / height, | 254 | (canvasHeight - 64) / height, |
256 | ); | 255 | ); |
257 | const selection = d3.select(canvasRef.current); | 256 | const selection = d3.select(canvasRef.current); |
258 | zoomRef.current.transform(selection, d3.zoomIdentity.scale(factor)); | 257 | const zoomTransition = selection.transition().duration(250); |
258 | zoomRef.current.transform(zoomTransition, d3.zoomIdentity.scale(factor)); | ||
259 | } | 259 | } |
260 | event.preventDefault(); | 260 | event.preventDefault(); |
261 | event.stopPropagation(); | 261 | event.stopPropagation(); |
@@ -268,79 +268,96 @@ export default function GraphArea(): JSX.Element { | |||
268 | height: '100%', | 268 | height: '100%', |
269 | position: 'relative', | 269 | position: 'relative', |
270 | overflow: 'hidden', | 270 | overflow: 'hidden', |
271 | '& svg': { | ||
272 | userSelect: 'none', | ||
273 | '& .node': { | ||
274 | '& text': { | ||
275 | ...theme.typography.body2, | ||
276 | fill: theme.palette.text.primary, | ||
277 | }, | ||
278 | '& [stroke="black"]': { | ||
279 | stroke: theme.palette.text.primary, | ||
280 | }, | ||
281 | '& [fill="green"]': { | ||
282 | fill: | ||
283 | theme.palette.mode === 'dark' | ||
284 | ? theme.palette.primary.dark | ||
285 | : theme.palette.primary.light, | ||
286 | }, | ||
287 | '& [fill="white"]': { | ||
288 | fill: theme.palette.background.default, | ||
289 | stroke: theme.palette.background.default, | ||
290 | }, | ||
291 | }, | ||
292 | '& .edge': { | ||
293 | '& text': { | ||
294 | ...theme.typography.caption, | ||
295 | fill: theme.palette.text.primary, | ||
296 | }, | ||
297 | '& [stroke="black"]': { | ||
298 | stroke: theme.palette.text.primary, | ||
299 | }, | ||
300 | '& [fill="black"]': { | ||
301 | fill: theme.palette.text.primary, | ||
302 | }, | ||
303 | }, | ||
304 | '& .edge-UNKNOWN': { | ||
305 | '& text': { | ||
306 | fill: theme.palette.text.secondary, | ||
307 | }, | ||
308 | '& [stroke="black"]': { | ||
309 | stroke: theme.palette.text.secondary, | ||
310 | }, | ||
311 | '& [fill="black"]': { | ||
312 | fill: theme.palette.text.secondary, | ||
313 | }, | ||
314 | }, | ||
315 | '& .edge-ERROR': { | ||
316 | '& text': { | ||
317 | fill: theme.palette.error.main, | ||
318 | }, | ||
319 | '& [stroke="black"]': { | ||
320 | stroke: theme.palette.error.main, | ||
321 | }, | ||
322 | '& [fill="black"]': { | ||
323 | fill: theme.palette.error.main, | ||
324 | }, | ||
325 | }, | ||
326 | }, | ||
327 | }} | 271 | }} |
328 | ref={setCanvas} | ||
329 | > | 272 | > |
330 | <Box | 273 | <Box |
331 | sx={{ | 274 | sx={{ |
332 | position: 'absolute', | 275 | position: 'absolute', |
333 | top: '50%', | 276 | overflow: 'hidden', |
334 | left: '50%', | 277 | top: 0, |
335 | transform: ` | 278 | left: 0, |
279 | right: 0, | ||
280 | bottom: 0, | ||
281 | }} | ||
282 | ref={setCanvas} | ||
283 | > | ||
284 | <Box | ||
285 | sx={{ | ||
286 | position: 'absolute', | ||
287 | top: '50%', | ||
288 | left: '50%', | ||
289 | transform: ` | ||
336 | translate(${zoom.x}px, ${zoom.y}px) | 290 | translate(${zoom.x}px, ${zoom.y}px) |
337 | scale(${zoom.k}) | 291 | scale(${zoom.k}) |
338 | translate(-50%, -50%) | 292 | translate(-50%, -50%) |
339 | `, | 293 | `, |
340 | transformOrigin: '0 0', | 294 | transformOrigin: '0 0', |
341 | }} | 295 | '& svg': { |
342 | ref={setElement} | 296 | userSelect: 'none', |
343 | /> | 297 | '& .node': { |
298 | '& text': { | ||
299 | ...(theme.typography.body2 as Omit< | ||
300 | CSSProperties, | ||
301 | '@font-face' | ||
302 | >), | ||
303 | fill: theme.palette.text.primary, | ||
304 | }, | ||
305 | '& [stroke="black"]': { | ||
306 | stroke: theme.palette.text.primary, | ||
307 | }, | ||
308 | '& [fill="green"]': { | ||
309 | fill: | ||
310 | theme.palette.mode === 'dark' | ||
311 | ? theme.palette.primary.dark | ||
312 | : theme.palette.primary.light, | ||
313 | }, | ||
314 | '& [fill="white"]': { | ||
315 | fill: theme.palette.background.default, | ||
316 | stroke: theme.palette.background.default, | ||
317 | }, | ||
318 | }, | ||
319 | '& .edge': { | ||
320 | '& text': { | ||
321 | ...(theme.typography.caption as Omit< | ||
322 | CSSProperties, | ||
323 | '@font-face' | ||
324 | >), | ||
325 | fill: theme.palette.text.primary, | ||
326 | }, | ||
327 | '& [stroke="black"]': { | ||
328 | stroke: theme.palette.text.primary, | ||
329 | }, | ||
330 | '& [fill="black"]': { | ||
331 | fill: theme.palette.text.primary, | ||
332 | }, | ||
333 | }, | ||
334 | '& .edge-UNKNOWN': { | ||
335 | '& text': { | ||
336 | fill: theme.palette.text.secondary, | ||
337 | }, | ||
338 | '& [stroke="black"]': { | ||
339 | stroke: theme.palette.text.secondary, | ||
340 | }, | ||
341 | '& [fill="black"]': { | ||
342 | fill: theme.palette.text.secondary, | ||
343 | }, | ||
344 | }, | ||
345 | '& .edge-ERROR': { | ||
346 | '& text': { | ||
347 | fill: theme.palette.error.main, | ||
348 | }, | ||
349 | '& [stroke="black"]': { | ||
350 | stroke: theme.palette.error.main, | ||
351 | }, | ||
352 | '& [fill="black"]': { | ||
353 | fill: theme.palette.error.main, | ||
354 | }, | ||
355 | }, | ||
356 | }, | ||
357 | }} | ||
358 | ref={setElement} | ||
359 | /> | ||
360 | </Box> | ||
344 | <Stack | 361 | <Stack |
345 | direction="column" | 362 | direction="column" |
346 | p={1} | 363 | p={1} |