aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2023-08-23 03:36:25 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2023-08-23 03:36:25 +0200
commit0e54d399424374d497d08a8631c4761dece57ceb (patch)
treebd0873080b4bc3b81984852def5e435e51292d0d /subprojects/frontend/src
parentfix: predicate value translation (diff)
downloadrefinery-0e54d399424374d497d08a8631c4761dece57ceb.tar.gz
refinery-0e54d399424374d497d08a8631c4761dece57ceb.tar.zst
refinery-0e54d399424374d497d08a8631c4761dece57ceb.zip
feat: dot visualization
Diffstat (limited to 'subprojects/frontend/src')
-rw-r--r--subprojects/frontend/src/editor/EditorStore.ts5
-rw-r--r--subprojects/frontend/src/graph/GraphArea.tsx318
-rw-r--r--subprojects/frontend/src/graph/GraphPane.tsx30
-rw-r--r--subprojects/frontend/src/xtext/SemanticsService.ts12
-rw-r--r--subprojects/frontend/src/xtext/xtextServiceResults.ts17
5 files changed, 358 insertions, 24 deletions
diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts
index 563725bb..10f01099 100644
--- a/subprojects/frontend/src/editor/EditorStore.ts
+++ b/subprojects/frontend/src/editor/EditorStore.ts
@@ -28,6 +28,7 @@ import { nanoid } from 'nanoid';
28import type PWAStore from '../PWAStore'; 28import type PWAStore from '../PWAStore';
29import getLogger from '../utils/getLogger'; 29import getLogger from '../utils/getLogger';
30import type XtextClient from '../xtext/XtextClient'; 30import type XtextClient from '../xtext/XtextClient';
31import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults';
31 32
32import EditorErrors from './EditorErrors'; 33import EditorErrors from './EditorErrors';
33import LintPanelStore from './LintPanelStore'; 34import LintPanelStore from './LintPanelStore';
@@ -65,7 +66,7 @@ export default class EditorStore {
65 66
66 semanticsError: string | undefined; 67 semanticsError: string | undefined;
67 68
68 semantics: unknown = {}; 69 semantics: SemanticsSuccessResult | undefined;
69 70
70 constructor(initialValue: string, pwaStore: PWAStore) { 71 constructor(initialValue: string, pwaStore: PWAStore) {
71 this.id = nanoid(); 72 this.id = nanoid();
@@ -295,7 +296,7 @@ export default class EditorStore {
295 this.semanticsError = semanticsError; 296 this.semanticsError = semanticsError;
296 } 297 }
297 298
298 setSemantics(semantics: unknown) { 299 setSemantics(semantics: SemanticsSuccessResult) {
299 this.semanticsError = undefined; 300 this.semanticsError = undefined;
300 this.semantics = semantics; 301 this.semantics = semantics;
301 } 302 }
diff --git a/subprojects/frontend/src/graph/GraphArea.tsx b/subprojects/frontend/src/graph/GraphArea.tsx
new file mode 100644
index 00000000..b55245d8
--- /dev/null
+++ b/subprojects/frontend/src/graph/GraphArea.tsx
@@ -0,0 +1,318 @@
1/*
2 * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import Box from '@mui/material/Box';
8import * as d3 from 'd3';
9import { type Graphviz, graphviz } from 'd3-graphviz';
10import type { BaseType, Selection } from 'd3-selection';
11import { reaction, type IReactionDisposer } from 'mobx';
12import { useCallback, useRef, useState } from 'react';
13import { useResizeDetector } from 'react-resize-detector';
14
15import { useRootStore } from '../RootStoreProvider';
16import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults';
17
18function toGraphviz(
19 semantics: SemanticsSuccessResult | undefined,
20): string | undefined {
21 if (semantics === undefined) {
22 return undefined;
23 }
24 const lines = [
25 'digraph {',
26 'graph [bgcolor=transparent];',
27 'node [fontsize=16, shape=plain];',
28 'edge [fontsize=12, color=black];',
29 ];
30 const nodeIds = semantics.nodes.map((name, i) => name ?? `n${i}`);
31 lines.push(
32 ...nodeIds.map(
33 (id, i) =>
34 `n${i} [id="${id}", label=<<table border="1" cellborder="0" cellspacing="0" cellpadding="4" style="rounded" bgcolor="green"><tr><td>${id}</td></tr><hr/><tr><td bgcolor="white">node</td></tr></table>>];`,
35 ),
36 );
37 Object.keys(semantics.partialInterpretation).forEach((relation) => {
38 if (relation === 'builtin::equals' || relation === 'builtin::contains') {
39 return;
40 }
41 const tuples = semantics.partialInterpretation[relation];
42 if (tuples === undefined) {
43 return;
44 }
45 const first = tuples[0];
46 if (first === undefined || first.length !== 3) {
47 return;
48 }
49 const nameFragments = relation.split('::');
50 const simpleName = nameFragments[nameFragments.length - 1] ?? relation;
51 lines.push(
52 ...tuples.map(([from, to, value]) => {
53 if (
54 typeof from !== 'number' ||
55 typeof to !== 'number' ||
56 typeof value !== 'string'
57 ) {
58 return '';
59 }
60 const isUnknown = value === 'UNKNOWN';
61 return `n${from} -> n${to} [
62 id="${nodeIds[from]},${nodeIds[to]},${relation}",
63 xlabel="${simpleName}",
64 style="${isUnknown ? 'dashed' : 'solid'}",
65 class="edge-${value}"
66 ];`;
67 }),
68 );
69 });
70 lines.push('}');
71 return lines.join('\n');
72}
73
74interface Transform {
75 x: number;
76 y: number;
77 k: number;
78}
79
80export default function GraphArea(): JSX.Element {
81 const { editorStore } = useRootStore();
82 const disposerRef = useRef<IReactionDisposer | undefined>();
83 const graphvizRef = useRef<
84 Graphviz<BaseType, unknown, null, undefined> | undefined
85 >();
86 const canvasRef = useRef<HTMLDivElement | undefined>();
87 const zoomRef = useRef<
88 d3.ZoomBehavior<HTMLDivElement, unknown> | undefined
89 >();
90 const [zoom, setZoom] = useState<Transform>({ x: 0, y: 0, k: 1 });
91 const widthRef = useRef<number | undefined>();
92 const heightRef = useRef<number | undefined>();
93
94 const onResize = useCallback(
95 (width: number | undefined, height: number | undefined) => {
96 if (canvasRef.current === undefined || zoomRef.current === undefined) {
97 return;
98 }
99 let moveX = 0;
100 let moveY = 0;
101 if (widthRef.current !== undefined && width !== undefined) {
102 moveX = (width - widthRef.current) / 2;
103 }
104 if (heightRef.current !== undefined && height !== undefined) {
105 moveY = (height - heightRef.current) / 2;
106 }
107 widthRef.current = width;
108 heightRef.current = height;
109 if (moveX === 0 && moveY === 0) {
110 return;
111 }
112 const currentTransform = d3.zoomTransform(canvasRef.current);
113 zoomRef.current.translateBy(
114 d3.select(canvasRef.current),
115 moveX / currentTransform.k - moveX,
116 moveY / currentTransform.k - moveY,
117 );
118 },
119 [],
120 );
121
122 const { ref: setCanvasResize } = useResizeDetector({
123 onResize,
124 });
125
126 const setCanvas = useCallback(
127 (element: HTMLDivElement | null) => {
128 canvasRef.current = element ?? undefined;
129 setCanvasResize(element);
130 if (element === null) {
131 return;
132 }
133 const zoomBehavior = d3.zoom<HTMLDivElement, unknown>();
134 zoomBehavior.on(
135 'zoom',
136 (event: d3.D3ZoomEvent<HTMLDivElement, unknown>) =>
137 setZoom(event.transform),
138 );
139 d3.select(element).call(zoomBehavior);
140 zoomRef.current = zoomBehavior;
141 },
142 [setCanvasResize],
143 );
144
145 const setElement = useCallback(
146 (element: HTMLDivElement | null) => {
147 if (disposerRef.current !== undefined) {
148 disposerRef.current();
149 disposerRef.current = undefined;
150 }
151 if (graphvizRef.current !== undefined) {
152 // `@types/d3-graphviz` does not contain the signature for the `destroy` method.
153 (graphvizRef.current as unknown as { destroy(): void }).destroy();
154 graphvizRef.current = undefined;
155 }
156 if (element !== null) {
157 element.replaceChildren();
158 const renderer = graphviz(element) as Graphviz<
159 BaseType,
160 unknown,
161 null,
162 undefined
163 >;
164 renderer.keyMode('id');
165 renderer.zoom(false);
166 renderer.tweenPrecision('5%');
167 renderer.tweenShapes(false);
168 renderer.convertEqualSidedPolygons(false);
169 const transition = () =>
170 d3.transition().duration(300).ease(d3.easeCubic);
171 /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument,
172 @typescript-eslint/no-explicit-any --
173 Workaround for error in `@types/d3-graphviz`.
174 */
175 renderer.transition(transition as any);
176 renderer.on(
177 'postProcessSVG',
178 // @ts-expect-error Custom `d3-graphviz` hook not covered by typings.
179 (
180 svgSelection: Selection<SVGSVGElement, unknown, BaseType, unknown>,
181 ) => {
182 svgSelection.selectAll('title').remove();
183 const svg = svgSelection.node();
184 if (svg === null) {
185 return;
186 }
187 svg.querySelectorAll('.node').forEach((node) => {
188 node.querySelectorAll('path').forEach((path) => {
189 const d = path.getAttribute('d') ?? '';
190 const points = d.split(/[A-Z ]/);
191 points.shift();
192 const x = points.map((p) => {
193 return Number(p.split(',')[0] ?? 0);
194 });
195 const y = points.map((p) => {
196 return Number(p.split(',')[1] ?? 0);
197 });
198 const xmin = Math.min.apply(null, x);
199 const xmax = Math.max.apply(null, x);
200 const ymin = Math.min.apply(null, y);
201 const ymax = Math.max.apply(null, y);
202 const rect = document.createElementNS(
203 'http://www.w3.org/2000/svg',
204 'rect',
205 );
206 rect.setAttribute('fill', path.getAttribute('fill') ?? '');
207 rect.setAttribute('stroke', path.getAttribute('stroke') ?? '');
208 rect.setAttribute('x', String(xmin));
209 rect.setAttribute('y', String(ymin));
210 rect.setAttribute('width', String(xmax - xmin));
211 rect.setAttribute('height', String(ymax - ymin));
212 rect.setAttribute('height', String(ymax - ymin));
213 rect.setAttribute('rx', '12');
214 rect.setAttribute('ry', '12');
215 node.replaceChild(rect, path);
216 });
217 });
218 },
219 );
220 disposerRef.current = reaction(
221 () => editorStore?.semantics,
222 (semantics) => {
223 const str = toGraphviz(semantics);
224 if (str !== undefined) {
225 renderer.renderDot(str);
226 }
227 },
228 { fireImmediately: true },
229 );
230 graphvizRef.current = renderer;
231 }
232 },
233 [editorStore],
234 );
235
236 return (
237 <Box
238 sx={(theme) => ({
239 width: '100%',
240 height: '100%',
241 position: 'relative',
242 overflow: 'hidden',
243 '& svg': {
244 userSelect: 'none',
245 '& .node': {
246 '& text': {
247 ...theme.typography.body2,
248 fill: theme.palette.text.primary,
249 },
250 '& [stroke="black"]': {
251 stroke: theme.palette.text.primary,
252 },
253 '& [fill="green"]': {
254 fill:
255 theme.palette.mode === 'dark'
256 ? theme.palette.primary.dark
257 : theme.palette.primary.light,
258 },
259 '& [fill="white"]': {
260 fill: theme.palette.background.default,
261 stroke: theme.palette.background.default,
262 },
263 },
264 '& .edge': {
265 '& text': {
266 ...theme.typography.caption,
267 fill: theme.palette.text.primary,
268 },
269 '& [stroke="black"]': {
270 stroke: theme.palette.text.primary,
271 },
272 '& [fill="black"]': {
273 fill: theme.palette.text.primary,
274 },
275 },
276 '& .edge-UNKNOWN': {
277 '& text': {
278 fill: theme.palette.text.secondary,
279 },
280 '& [stroke="black"]': {
281 stroke: theme.palette.text.secondary,
282 },
283 '& [fill="black"]': {
284 fill: theme.palette.text.secondary,
285 },
286 },
287 '& .edge-ERROR': {
288 '& text': {
289 fill: theme.palette.error.main,
290 },
291 '& [stroke="black"]': {
292 stroke: theme.palette.error.main,
293 },
294 '& [fill="black"]': {
295 fill: theme.palette.error.main,
296 },
297 },
298 },
299 })}
300 ref={setCanvas}
301 >
302 <Box
303 sx={{
304 position: 'absolute',
305 top: `${50 * zoom.k}%`,
306 left: `${50 * zoom.k}%`,
307 transform: `
308 translate(${zoom.x}px, ${zoom.y}px)
309 scale(${zoom.k})
310 translate(-50%, -50%)
311 `,
312 transformOrigin: '0 0',
313 }}
314 ref={setElement}
315 />
316 </Box>
317 );
318}
diff --git a/subprojects/frontend/src/graph/GraphPane.tsx b/subprojects/frontend/src/graph/GraphPane.tsx
index f69f52a6..f04b9931 100644
--- a/subprojects/frontend/src/graph/GraphPane.tsx
+++ b/subprojects/frontend/src/graph/GraphPane.tsx
@@ -5,24 +5,24 @@
5 */ 5 */
6 6
7import Stack from '@mui/material/Stack'; 7import Stack from '@mui/material/Stack';
8import { styled } from '@mui/material/styles'; 8import { Suspense, lazy } from 'react';
9import stringify from 'json-stringify-pretty-compact';
10import { observer } from 'mobx-react-lite';
11 9
12import { useRootStore } from '../RootStoreProvider'; 10import Loading from '../Loading';
13 11
14const StyledCode = styled('code')(({ theme }) => ({ 12const GraphArea = lazy(() => import('./GraphArea'));
15 ...theme.typography.editor,
16 fontWeight: theme.typography.fontWeightEditorNormal,
17 margin: theme.spacing(2),
18 whiteSpace: 'pre',
19}));
20 13
21export default observer(function GraphPane(): JSX.Element { 14export default function GraphPane(): JSX.Element {
22 const { editorStore } = useRootStore();
23 return ( 15 return (
24 <Stack direction="column" height="100%" overflow="auto"> 16 <Stack
25 <StyledCode>{stringify(editorStore?.semantics ?? {})}</StyledCode> 17 direction="column"
18 height="100%"
19 overflow="auto"
20 alignItems="center"
21 justifyContent="center"
22 >
23 <Suspense fallback={<Loading />}>
24 <GraphArea />
25 </Suspense>
26 </Stack> 26 </Stack>
27 ); 27 );
28}); 28}
diff --git a/subprojects/frontend/src/xtext/SemanticsService.ts b/subprojects/frontend/src/xtext/SemanticsService.ts
index 50ec371a..d68b87a9 100644
--- a/subprojects/frontend/src/xtext/SemanticsService.ts
+++ b/subprojects/frontend/src/xtext/SemanticsService.ts
@@ -17,11 +17,15 @@ export default class SemanticsService {
17 17
18 onPush(push: unknown): void { 18 onPush(push: unknown): void {
19 const result = SemanticsResult.parse(push); 19 const result = SemanticsResult.parse(push);
20 this.validationService.setSemanticsIssues(result.issues ?? []); 20 if ('issues' in result) {
21 if (result.error !== undefined) { 21 this.validationService.setSemanticsIssues(result.issues);
22 this.store.setSemanticsError(result.error);
23 } else { 22 } else {
24 this.store.setSemantics(push); 23 this.validationService.setSemanticsIssues([]);
24 if ('error' in result) {
25 this.store.setSemanticsError(result.error);
26 } else {
27 this.store.setSemantics(result);
28 }
25 } 29 }
26 this.store.analysisCompleted(); 30 this.store.analysisCompleted();
27 } 31 }
diff --git a/subprojects/frontend/src/xtext/xtextServiceResults.ts b/subprojects/frontend/src/xtext/xtextServiceResults.ts
index cae95771..12f87b26 100644
--- a/subprojects/frontend/src/xtext/xtextServiceResults.ts
+++ b/subprojects/frontend/src/xtext/xtextServiceResults.ts
@@ -126,9 +126,20 @@ export const FormattingResult = DocumentStateResult.extend({
126 126
127export type FormattingResult = z.infer<typeof FormattingResult>; 127export type FormattingResult = z.infer<typeof FormattingResult>;
128 128
129export const SemanticsResult = z.object({ 129export const SemanticsSuccessResult = z.object({
130 error: z.string().optional(), 130 nodes: z.string().nullable().array(),
131 issues: Issue.array().optional(), 131 partialInterpretation: z.record(
132 z.string(),
133 z.union([z.number(), z.string()]).array().array(),
134 ),
132}); 135});
133 136
137export type SemanticsSuccessResult = z.infer<typeof SemanticsSuccessResult>;
138
139export const SemanticsResult = z.union([
140 z.object({ error: z.string() }),
141 z.object({ issues: Issue.array() }),
142 SemanticsSuccessResult,
143]);
144
134export type SemanticsResult = z.infer<typeof SemanticsResult>; 145export type SemanticsResult = z.infer<typeof SemanticsResult>;