aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src/graph/DotGraphVisualizer.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects/frontend/src/graph/DotGraphVisualizer.tsx')
-rw-r--r--subprojects/frontend/src/graph/DotGraphVisualizer.tsx86
1 files changed, 23 insertions, 63 deletions
diff --git a/subprojects/frontend/src/graph/DotGraphVisualizer.tsx b/subprojects/frontend/src/graph/DotGraphVisualizer.tsx
index 29e750f5..291314ec 100644
--- a/subprojects/frontend/src/graph/DotGraphVisualizer.tsx
+++ b/subprojects/frontend/src/graph/DotGraphVisualizer.tsx
@@ -8,76 +8,24 @@ import * as d3 from 'd3';
8import { type Graphviz, graphviz } from 'd3-graphviz'; 8import { type Graphviz, graphviz } from 'd3-graphviz';
9import type { BaseType, Selection } from 'd3-selection'; 9import type { BaseType, Selection } from 'd3-selection';
10import { reaction, type IReactionDisposer } from 'mobx'; 10import { reaction, type IReactionDisposer } from 'mobx';
11import { observer } from 'mobx-react-lite';
11import { useCallback, useRef } from 'react'; 12import { useCallback, useRef } from 'react';
12 13
13import { useRootStore } from '../RootStoreProvider'; 14import { useRootStore } from '../RootStoreProvider';
14import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; 15import getLogger from '../utils/getLogger';
15 16
16import GraphTheme from './GraphTheme'; 17import GraphTheme from './GraphTheme';
17import { FitZoomCallback } from './ZoomCanvas'; 18import { FitZoomCallback } from './ZoomCanvas';
19import dotSource from './dotSource';
18import postProcessSvg from './postProcessSVG'; 20import postProcessSvg from './postProcessSVG';
19 21
20function toGraphviz( 22const LOG = getLogger('graph.DotGraphVisualizer');
21 semantics: SemanticsSuccessResult | undefined,
22): string | undefined {
23 if (semantics === undefined) {
24 return undefined;
25 }
26 const lines = [
27 'digraph {',
28 'graph [bgcolor=transparent];',
29 `node [fontsize=12, shape=plain, fontname="OpenSans"];`,
30 'edge [fontsize=10.5, color=black, fontname="OpenSans"];',
31 ];
32 const nodeIds = semantics.nodes.map((name, i) => name ?? `n${i}`);
33 lines.push(
34 ...nodeIds.map(
35 (id, i) =>
36 `n${i} [id="${id}", label=<<table border="1" cellborder="0" cellspacing="0" cellpadding="4.5" style="rounded" bgcolor="green"><tr><td>${id}</td></tr><hr/><tr><td bgcolor="white">node</td></tr></table>>];`,
37 ),
38 );
39 Object.keys(semantics.partialInterpretation).forEach((relation) => {
40 if (relation === 'builtin::equals' || relation === 'builtin::contains') {
41 return;
42 }
43 const tuples = semantics.partialInterpretation[relation];
44 if (tuples === undefined) {
45 return;
46 }
47 const first = tuples[0];
48 if (first === undefined || first.length !== 3) {
49 return;
50 }
51 const nameFragments = relation.split('::');
52 const simpleName = nameFragments[nameFragments.length - 1] ?? relation;
53 lines.push(
54 ...tuples.map(([from, to, value]) => {
55 if (
56 typeof from !== 'number' ||
57 typeof to !== 'number' ||
58 typeof value !== 'string'
59 ) {
60 return '';
61 }
62 const isUnknown = value === 'UNKNOWN';
63 return `n${from} -> n${to} [
64 id="${nodeIds[from]},${nodeIds[to]},${relation}",
65 xlabel="${simpleName}",
66 style="${isUnknown ? 'dashed' : 'solid'}",
67 class="edge-${value}"
68 ];`;
69 }),
70 );
71 });
72 lines.push('}');
73 return lines.join('\n');
74}
75 23
76function ptToPx(pt: number): number { 24function ptToPx(pt: number): number {
77 return (pt * 4) / 3; 25 return (pt * 4) / 3;
78} 26}
79 27
80export default function DotGraphVisualizer({ 28function DotGraphVisualizer({
81 fitZoom, 29 fitZoom,
82 transitionTime, 30 transitionTime,
83}: { 31}: {
@@ -88,6 +36,7 @@ export default function DotGraphVisualizer({
88 transitionTime ?? DotGraphVisualizer.defaultProps.transitionTime; 36 transitionTime ?? DotGraphVisualizer.defaultProps.transitionTime;
89 37
90 const { editorStore } = useRootStore(); 38 const { editorStore } = useRootStore();
39 const graph = editorStore?.graph;
91 const disposerRef = useRef<IReactionDisposer | undefined>(); 40 const disposerRef = useRef<IReactionDisposer | undefined>();
92 const graphvizRef = useRef< 41 const graphvizRef = useRef<
93 Graphviz<BaseType, unknown, null, undefined> | undefined 42 Graphviz<BaseType, unknown, null, undefined> | undefined
@@ -113,6 +62,9 @@ export default function DotGraphVisualizer({
113 undefined 62 undefined
114 >; 63 >;
115 renderer.keyMode('id'); 64 renderer.keyMode('id');
65 ['TRUE', 'UNKNOWN', 'ERROR'].forEach((icon) =>
66 renderer.addImage(`#${icon}`, 16, 16),
67 );
116 renderer.zoom(false); 68 renderer.zoom(false);
117 renderer.tweenPrecision('5%'); 69 renderer.tweenPrecision('5%');
118 renderer.tweenShapes(false); 70 renderer.tweenShapes(false);
@@ -125,6 +77,7 @@ export default function DotGraphVisualizer({
125 */ 77 */
126 renderer.transition(transition as any); 78 renderer.transition(transition as any);
127 let newViewBox = { width: 0, height: 0 }; 79 let newViewBox = { width: 0, height: 0 };
80 renderer.onerror(LOG.error.bind(LOG));
128 renderer.on( 81 renderer.on(
129 'postProcessSVG', 82 'postProcessSVG',
130 // @ts-expect-error Custom `d3-graphviz` hook not covered by typings. 83 // @ts-expect-error Custom `d3-graphviz` hook not covered by typings.
@@ -139,19 +92,24 @@ export default function DotGraphVisualizer({
139 height: ptToPx(svg.viewBox.baseVal.height), 92 height: ptToPx(svg.viewBox.baseVal.height),
140 }; 93 };
141 } else { 94 } else {
95 // Do not trigger fit zoom.
142 newViewBox = { width: 0, height: 0 }; 96 newViewBox = { width: 0, height: 0 };
143 } 97 }
144 }, 98 },
145 ); 99 );
100 renderer.on('renderEnd', () => {
101 // `d3-graphviz` uses `<title>` elements for traceability,
102 // so we only remove them after the rendering is finished.
103 d3.select(element).selectAll('title').remove();
104 });
146 if (fitZoom !== undefined) { 105 if (fitZoom !== undefined) {
147 renderer.on('transitionStart', () => fitZoom(newViewBox)); 106 renderer.on('transitionStart', () => fitZoom(newViewBox));
148 } 107 }
149 disposerRef.current = reaction( 108 disposerRef.current = reaction(
150 () => editorStore?.semantics, 109 () => dotSource(graph),
151 (semantics) => { 110 (source) => {
152 const str = toGraphviz(semantics); 111 if (source !== undefined) {
153 if (str !== undefined) { 112 renderer.renderDot(source);
154 renderer.renderDot(str);
155 } 113 }
156 }, 114 },
157 { fireImmediately: true }, 115 { fireImmediately: true },
@@ -159,7 +117,7 @@ export default function DotGraphVisualizer({
159 graphvizRef.current = renderer; 117 graphvizRef.current = renderer;
160 } 118 }
161 }, 119 },
162 [editorStore, fitZoom, transitionTimeOrDefault], 120 [graph, fitZoom, transitionTimeOrDefault],
163 ); 121 );
164 122
165 return <GraphTheme ref={setElement} />; 123 return <GraphTheme ref={setElement} />;
@@ -169,3 +127,5 @@ DotGraphVisualizer.defaultProps = {
169 fitZoom: undefined, 127 fitZoom: undefined,
170 transitionTime: 250, 128 transitionTime: 250,
171}; 129};
130
131export default observer(DotGraphVisualizer);