aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src/graph/GraphArea.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/frontend/src/graph/GraphArea.tsx')
-rw-r--r--subprojects/frontend/src/graph/GraphArea.tsx189
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';
11import IconButton from '@mui/material/IconButton'; 11import IconButton from '@mui/material/IconButton';
12import Stack from '@mui/material/Stack'; 12import Stack from '@mui/material/Stack';
13import { useTheme } from '@mui/material/styles'; 13import { useTheme } from '@mui/material/styles';
14import { CSSProperties } from '@mui/material/styles/createTypography';
14import * as d3 from 'd3'; 15import * as d3 from 'd3';
15import { type Graphviz, graphviz } from 'd3-graphviz'; 16import { type Graphviz, graphviz } from 'd3-graphviz';
16import type { BaseType, Selection } from 'd3-selection'; 17import 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}