Files
vscode-debug-visualizer/ui/src/Components/GraphViz.tsx
Henning Dieterichs 922eb0b845 Next Iteration
2019-07-31 11:41:10 +02:00

150 lines
3.4 KiB
TypeScript

import { observer, disposeOnUnmount } from "mobx-react";
import * as classNames from "classnames";
import * as React from "react";
import * as ReactDOM from "react-dom";
import { ReactSVGPanZoom, Tool, Value } from "react-svg-pan-zoom";
import { computed, observable, autorun, runInAction, action } from "mobx";
import Measure, { ContentRect } from "react-measure";
import Viz from "viz.js";
import { Module, render } from "viz.js/full.render.js";
export interface GraphData {
kind: "GraphData";
nodes: { id: string; label: string }[];
edges: { from: string; to: string; label: string }[];
}
@observer
export class DotGraphViewer extends React.Component<{ data: GraphData }> {
render() {
const { nodes, edges } = this.props.data;
const dotContent = `
digraph MyGraph {
${nodes
.map(n => `${n.id} [ label = ${JSON.stringify(n.label)} ];`)
.join("\n ")}
${edges
.map(
e =>
`${e.from} -> ${e.to} [ label = ${JSON.stringify(
e.label
)} ];`
)
.join("\n")}
}
`;
return <DotViewer dotCode={dotContent} />;
}
}
const viz: any = new Viz({
Module: () => Module({ TOTAL_MEMORY: 1 << 30 }),
render,
});
@observer
export class DotViewer extends React.Component<{
dotCode: string;
svgRef?: (element: SVGSVGElement | null) => void;
}> {
@observable private svg: string | null = null;
@disposeOnUnmount
// @ts-ignore
private readonly _updateSvgAutorun = autorun(async () => {
const svg = await viz.renderString(this.props.dotCode);
runInAction("Update svg", () => (this.svg = svg));
});
render() {
if (!this.svg) {
return <div>Loading...</div>;
}
return <SvgViewer svgRef={this.props.svgRef} svgContent={this.svg} />;
}
}
function widthOrDefault(r: ContentRect): number {
if (r.bounds && r.bounds.width) {
return r.bounds.width;
}
return 1;
}
function heightOrDefault(r: ContentRect): number {
console.log(r);
if (r.bounds && r.bounds.height) {
return r.bounds.height;
}
return 1;
}
@observer
export class SvgViewer extends React.Component<{
svgContent: string;
svgRef?: (element: SVGSVGElement | null) => void;
}> {
@observable tool: Tool = "pan";
@action.bound
private setTool(tool: Tool): void {
this.tool = tool;
}
private setRef(element: ReactSVGPanZoom | null) {
if (this.props.svgRef) {
if (!element) {
this.props.svgRef(null);
return;
}
const svg = (element as any).ViewerDOM as SVGSVGElement;
this.props.svgRef(svg);
}
}
@observable value: Value | {} = {};
render() {
let { svgContent } = this.props;
let width: number = 0;
let height: number = 0;
svgContent = svgContent.replace(
/viewBox="[0-9\.]+ [0-9\.]+ ([0-9\.]+) ([0-9\.]+)"/,
(r, w, h) => {
width = parseFloat(w);
height = parseFloat(h);
return "";
}
);
const tool = this.tool;
const val = this.value;
return (
<Measure bounds>
{({ measureRef, contentRect }) => (
<div ref={measureRef} className="svgViewer">
<ReactSVGPanZoom
width={widthOrDefault(contentRect)}
height={heightOrDefault(contentRect)}
tool={tool}
value={val}
onChangeValue={v => (this.value = v)}
onChangeTool={this.setTool}
ref={e => this.setRef(e)}
>
<svg width={width} height={height}>
<g
dangerouslySetInnerHTML={{
__html: svgContent,
}}
/>
</svg>
</ReactSVGPanZoom>
</div>
)}
</Measure>
);
}
}