/*
* SPDX-FileCopyrightText: 2023 The Refinery Authors
*
* SPDX-License-Identifier: EPL-2.0
*/
import * as d3 from 'd3';
import { type Graphviz, graphviz } from 'd3-graphviz';
import type { BaseType, Selection } from 'd3-selection';
import { reaction, type IReactionDisposer } from 'mobx';
import { useCallback, useRef } from 'react';
import { useRootStore } from '../RootStoreProvider';
import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults';
import GraphTheme from './GraphTheme';
import postProcessSvg from './postProcessSVG';
function toGraphviz(
semantics: SemanticsSuccessResult | undefined,
): string | undefined {
if (semantics === undefined) {
return undefined;
}
const lines = [
'digraph {',
'graph [bgcolor=transparent];',
`node [fontsize=12, shape=plain, fontname="OpenSans"];`,
'edge [fontsize=10.5, color=black, fontname="OpenSans"];',
];
const nodeIds = semantics.nodes.map((name, i) => name ?? `n${i}`);
lines.push(
...nodeIds.map(
(id, i) =>
`n${i} [id="${id}", label=<
>];`,
),
);
Object.keys(semantics.partialInterpretation).forEach((relation) => {
if (relation === 'builtin::equals' || relation === 'builtin::contains') {
return;
}
const tuples = semantics.partialInterpretation[relation];
if (tuples === undefined) {
return;
}
const first = tuples[0];
if (first === undefined || first.length !== 3) {
return;
}
const nameFragments = relation.split('::');
const simpleName = nameFragments[nameFragments.length - 1] ?? relation;
lines.push(
...tuples.map(([from, to, value]) => {
if (
typeof from !== 'number' ||
typeof to !== 'number' ||
typeof value !== 'string'
) {
return '';
}
const isUnknown = value === 'UNKNOWN';
return `n${from} -> n${to} [
id="${nodeIds[from]},${nodeIds[to]},${relation}",
xlabel="${simpleName}",
style="${isUnknown ? 'dashed' : 'solid'}",
class="edge-${value}"
];`;
}),
);
});
lines.push('}');
return lines.join('\n');
}
export default function DotGraphVisualizer(): JSX.Element {
const { editorStore } = useRootStore();
const disposerRef = useRef();
const graphvizRef = useRef<
Graphviz | undefined
>();
const setElement = useCallback(
(element: HTMLDivElement | null) => {
if (disposerRef.current !== undefined) {
disposerRef.current();
disposerRef.current = undefined;
}
if (graphvizRef.current !== undefined) {
// `@types/d3-graphviz` does not contain the signature for the `destroy` method.
(graphvizRef.current as unknown as { destroy(): void }).destroy();
graphvizRef.current = undefined;
}
if (element !== null) {
element.replaceChildren();
const renderer = graphviz(element) as Graphviz<
BaseType,
unknown,
null,
undefined
>;
renderer.keyMode('id');
renderer.zoom(false);
renderer.tweenPrecision('5%');
renderer.tweenShapes(false);
renderer.convertEqualSidedPolygons(false);
const transition = () =>
d3.transition().duration(300).ease(d3.easeCubic);
/* eslint-disable-next-line @typescript-eslint/no-unsafe-argument,
@typescript-eslint/no-explicit-any --
Workaround for error in `@types/d3-graphviz`.
*/
renderer.transition(transition as any);
renderer.on(
'postProcessSVG',
// @ts-expect-error Custom `d3-graphviz` hook not covered by typings.
(
svgSelection: Selection,
) => {
const svg = svgSelection.node();
if (svg !== null) {
postProcessSvg(svg);
}
},
);
disposerRef.current = reaction(
() => editorStore?.semantics,
(semantics) => {
const str = toGraphviz(semantics);
if (str !== undefined) {
renderer.renderDot(str);
}
},
{ fireImmediately: true },
);
graphvizRef.current = renderer;
}
},
[editorStore],
);
return ;
}