aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src/graph/DotGraphVisualizer.tsx
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2023-08-24 00:06:37 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2023-08-24 01:17:45 +0200
commit2e2ebbf75b12784ac664d864865f01729b3eb8c4 (patch)
tree6002405f0759015c57d161775c94b9e0df138872 /subprojects/frontend/src/graph/DotGraphVisualizer.tsx
parentrefactor(web): move d3-zoom patch into repo (diff)
downloadrefinery-2e2ebbf75b12784ac664d864865f01729b3eb8c4.tar.gz
refinery-2e2ebbf75b12784ac664d864865f01729b3eb8c4.tar.zst
refinery-2e2ebbf75b12784ac664d864865f01729b3eb8c4.zip
refactor(web): clean up graphviz visualization
Diffstat (limited to 'subprojects/frontend/src/graph/DotGraphVisualizer.tsx')
-rw-r--r--subprojects/frontend/src/graph/DotGraphVisualizer.tsx142
1 files changed, 142 insertions, 0 deletions
diff --git a/subprojects/frontend/src/graph/DotGraphVisualizer.tsx b/subprojects/frontend/src/graph/DotGraphVisualizer.tsx
new file mode 100644
index 00000000..7c25488a
--- /dev/null
+++ b/subprojects/frontend/src/graph/DotGraphVisualizer.tsx
@@ -0,0 +1,142 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import * as d3 from 'd3';
8import { type Graphviz, graphviz } from 'd3-graphviz';
9import type { BaseType, Selection } from 'd3-selection';
10import { reaction, type IReactionDisposer } from 'mobx';
11import { useCallback, useRef } from 'react';
12
13import { useRootStore } from '../RootStoreProvider';
14import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults';
15
16import GraphTheme from './GraphTheme';
17import postProcessSvg from './postProcessSVG';
18
19function toGraphviz(
20 semantics: SemanticsSuccessResult | undefined,
21): string | undefined {
22 if (semantics === undefined) {
23 return undefined;
24 }
25 const lines = [
26 'digraph {',
27 'graph [bgcolor=transparent];',
28 `node [fontsize=12, shape=plain, fontname="OpenSans"];`,
29 'edge [fontsize=10.5, color=black, fontname="OpenSans"];',
30 ];
31 const nodeIds = semantics.nodes.map((name, i) => name ?? `n${i}`);
32 lines.push(
33 ...nodeIds.map(
34 (id, i) =>
35 `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>>];`,
36 ),
37 );
38 Object.keys(semantics.partialInterpretation).forEach((relation) => {
39 if (relation === 'builtin::equals' || relation === 'builtin::contains') {
40 return;
41 }
42 const tuples = semantics.partialInterpretation[relation];
43 if (tuples === undefined) {
44 return;
45 }
46 const first = tuples[0];
47 if (first === undefined || first.length !== 3) {
48 return;
49 }
50 const nameFragments = relation.split('::');
51 const simpleName = nameFragments[nameFragments.length - 1] ?? relation;
52 lines.push(
53 ...tuples.map(([from, to, value]) => {
54 if (
55 typeof from !== 'number' ||
56 typeof to !== 'number' ||
57 typeof value !== 'string'
58 ) {
59 return '';
60 }
61 const isUnknown = value === 'UNKNOWN';
62 return `n${from} -> n${to} [
63 id="${nodeIds[from]},${nodeIds[to]},${relation}",
64 xlabel="${simpleName}",
65 style="${isUnknown ? 'dashed' : 'solid'}",
66 class="edge-${value}"
67 ];`;
68 }),
69 );
70 });
71 lines.push('}');
72 return lines.join('\n');
73}
74
75export default function DotGraphVisualizer(): JSX.Element {
76 const { editorStore } = useRootStore();
77 const disposerRef = useRef<IReactionDisposer | undefined>();
78 const graphvizRef = useRef<
79 Graphviz<BaseType, unknown, null, undefined> | undefined
80 >();
81
82 const setElement = useCallback(
83 (element: HTMLDivElement | null) => {
84 if (disposerRef.current !== undefined) {
85 disposerRef.current();
86 disposerRef.current = undefined;
87 }
88 if (graphvizRef.current !== undefined) {
89 // `@types/d3-graphviz` does not contain the signature for the `destroy` method.
90 (graphvizRef.current as unknown as { destroy(): void }).destroy();
91 graphvizRef.current = undefined;
92 }
93 if (element !== null) {
94 element.replaceChildren();
95 const renderer = graphviz(element) as Graphviz<
96 BaseType,
97 unknown,
98 null,
99 undefined
100 >;
101 renderer.keyMode('id');
102 renderer.zoom(false);
103 renderer.tweenPrecision('5%');
104 renderer.tweenShapes(false);
105 renderer.convertEqualSidedPolygons(false);
106 const transition = () =>
107 d3.transition().duration(300).ease(d3.easeCubic);
108 /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument,
109 @typescript-eslint/no-explicit-any --
110 Workaround for error in `@types/d3-graphviz`.
111 */
112 renderer.transition(transition as any);
113 renderer.on(
114 'postProcessSVG',
115 // @ts-expect-error Custom `d3-graphviz` hook not covered by typings.
116 (
117 svgSelection: Selection<SVGSVGElement, unknown, BaseType, unknown>,
118 ) => {
119 const svg = svgSelection.node();
120 if (svg !== null) {
121 postProcessSvg(svg);
122 }
123 },
124 );
125 disposerRef.current = reaction(
126 () => editorStore?.semantics,
127 (semantics) => {
128 const str = toGraphviz(semantics);
129 if (str !== undefined) {
130 renderer.renderDot(str);
131 }
132 },
133 { fireImmediately: true },
134 );
135 graphvizRef.current = renderer;
136 }
137 },
138 [editorStore],
139 );
140
141 return <GraphTheme ref={setElement} />;
142}