initial commit
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
import * as React from "react";
|
||||
import { observer, disposeOnUnmount } from "mobx-react";
|
||||
import { observable, autorun, trace, computed } from "mobx";
|
||||
import * as monacoTypes from "monaco-editor";
|
||||
import { getLanguageId } from "../monaco-text-visualizer/MonacoEditor";
|
||||
import {
|
||||
TreeWithPathView,
|
||||
TreeViewModel,
|
||||
TreeNodeViewModel,
|
||||
} from "../tree-visualizer/Views";
|
||||
import { sAstTree } from ".";
|
||||
import Measure from "react-measure";
|
||||
import { MonacoEditor, getLoadedMonaco } from "@hediet/monaco-editor-react";
|
||||
import { Theme } from "@hediet/visualization-core";
|
||||
|
||||
export interface NodeInfo {
|
||||
start: number;
|
||||
length: number;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class AstTree extends React.Component<{
|
||||
model: TreeViewModel<NodeInfo>;
|
||||
data: typeof sAstTree.T;
|
||||
nodeInfoToRange: (info: NodeInfo) => monacoTypes.IRange;
|
||||
posToIndex: (pos: monacoTypes.IPosition) => number;
|
||||
theme: Theme;
|
||||
}> {
|
||||
private readonly editorRef = React.createRef<SourceCodeView>();
|
||||
|
||||
render() {
|
||||
const { model, data, theme } = this.props;
|
||||
|
||||
let languageId = "text";
|
||||
if (data.fileName) {
|
||||
languageId = getLanguageId(data.fileName);
|
||||
}
|
||||
return (
|
||||
<Measure client>
|
||||
{({ measureRef, contentRect }) => {
|
||||
return (
|
||||
<div
|
||||
ref={measureRef}
|
||||
className="component-AstTree"
|
||||
style={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection:
|
||||
contentRect.client &&
|
||||
contentRect.client.width > 1100
|
||||
? "row"
|
||||
: "column",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="part-tree2"
|
||||
style={{ flex: 1, minHeight: 0, minWidth: 0 }}
|
||||
>
|
||||
<TreeWithPathView model={model} />
|
||||
</div>
|
||||
<div
|
||||
className="part-editor"
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
minWidth: 0,
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<SourceCodeView
|
||||
theme={theme}
|
||||
ref={this.editorRef}
|
||||
nodeInfoToRange={this.props.nodeInfoToRange}
|
||||
posToIndex={this.props.posToIndex}
|
||||
model={model}
|
||||
languageId={languageId}
|
||||
text={data.text}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Measure>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
export class SourceCodeView extends React.Component<{
|
||||
text: string;
|
||||
languageId: string;
|
||||
model: TreeViewModel<NodeInfo>;
|
||||
theme: Theme;
|
||||
nodeInfoToRange: (info: NodeInfo) => monacoTypes.IRange;
|
||||
posToIndex: (pos: monacoTypes.IPosition) => number;
|
||||
}> {
|
||||
@observable private editor:
|
||||
| monacoTypes.editor.IStandaloneCodeEditor
|
||||
| undefined;
|
||||
|
||||
private markedDecorations: string[] = [];
|
||||
private selectedDecorations: string[] = [];
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
layout() {
|
||||
if (this.editor) {
|
||||
this.editor.layout();
|
||||
}
|
||||
}
|
||||
|
||||
@computed.struct
|
||||
private get markedRanges(): monacoTypes.IRange[] {
|
||||
if (!this.editor || !this.model) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const editorModel = this.model;
|
||||
const ranges = this.props.model.marked.map(s =>
|
||||
this.props.nodeInfoToRange(s.data)
|
||||
);
|
||||
return ranges;
|
||||
}
|
||||
|
||||
@disposeOnUnmount
|
||||
private _updateMarkedDecorations = autorun(
|
||||
() => {
|
||||
if (!this.editor) {
|
||||
return;
|
||||
}
|
||||
const ranges = this.markedRanges;
|
||||
this.markedDecorations = this.editor.deltaDecorations(
|
||||
this.markedDecorations,
|
||||
ranges.map(range => ({
|
||||
range,
|
||||
options: { className: "marked" },
|
||||
}))
|
||||
);
|
||||
if (ranges.length > 0) {
|
||||
this.editor.revealRange(
|
||||
ranges[0],
|
||||
getLoadedMonaco().editor.ScrollType.Smooth
|
||||
);
|
||||
}
|
||||
},
|
||||
{ name: "updateMarkedDecorations" }
|
||||
);
|
||||
|
||||
@computed.struct
|
||||
private get selected(): monacoTypes.IRange | undefined {
|
||||
if (this.editor && this.model) {
|
||||
const selected = this.props.model.selected;
|
||||
if (selected) {
|
||||
const range = this.props.nodeInfoToRange(selected.data);
|
||||
return range;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@disposeOnUnmount
|
||||
private _updateSelectedDecoration = autorun(
|
||||
() => {
|
||||
console.log("_updateSelectedDecoration");
|
||||
if (!this.editor) {
|
||||
return;
|
||||
}
|
||||
const range = this.selected;
|
||||
if (range) {
|
||||
this.selectedDecorations = this.editor.deltaDecorations(
|
||||
this.selectedDecorations,
|
||||
[
|
||||
{
|
||||
range,
|
||||
options: { className: "selected" },
|
||||
},
|
||||
]
|
||||
);
|
||||
this.editor.revealRange(
|
||||
range,
|
||||
getLoadedMonaco().editor.ScrollType.Smooth
|
||||
);
|
||||
} else {
|
||||
this.selectedDecorations = this.editor.deltaDecorations(
|
||||
this.selectedDecorations,
|
||||
[]
|
||||
);
|
||||
}
|
||||
},
|
||||
{ name: "updateDecorations" }
|
||||
);
|
||||
|
||||
@computed.struct
|
||||
private get modelData(): { text: string; languageId: string } {
|
||||
const { text, languageId } = this.props;
|
||||
return { text, languageId };
|
||||
}
|
||||
|
||||
private lastModel: monacoTypes.editor.ITextModel | undefined;
|
||||
@computed get model(): monacoTypes.editor.ITextModel {
|
||||
const last = this.lastModel;
|
||||
this.lastModel = getLoadedMonaco().editor.createModel(
|
||||
this.modelData.text,
|
||||
this.modelData.languageId,
|
||||
undefined
|
||||
);
|
||||
if (last) {
|
||||
setTimeout(() => {
|
||||
// we don't want to dispose the current model
|
||||
last.dispose();
|
||||
});
|
||||
}
|
||||
return this.lastModel;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MonacoEditor
|
||||
model={this.model}
|
||||
theme={this.props.theme.kind === "dark" ? "vs-dark" : "vs"}
|
||||
onEditorLoaded={e => {
|
||||
this.editor = e;
|
||||
e.onDidChangeCursorSelection(e => {
|
||||
const selectionStart = this.props.posToIndex(
|
||||
e.selection.getStartPosition()
|
||||
);
|
||||
const selectionEnd = this.props.posToIndex(
|
||||
e.selection.getEndPosition()
|
||||
);
|
||||
|
||||
const findNode = (
|
||||
m: TreeNodeViewModel<NodeInfo>
|
||||
): TreeNodeViewModel<NodeInfo> | undefined => {
|
||||
const nodeStart = m.data.start;
|
||||
const nodeEnd = nodeStart + m.data.length;
|
||||
|
||||
if (
|
||||
!(
|
||||
nodeStart <= selectionStart &&
|
||||
selectionEnd <= nodeEnd
|
||||
)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const c of m.children) {
|
||||
const r = findNode(c);
|
||||
if (r) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
return m;
|
||||
};
|
||||
|
||||
if (this.props.model.root) {
|
||||
const n = findNode(this.props.model.root);
|
||||
if (n) {
|
||||
this.props.model.select(n);
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
readOnly={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
126
visualization-bundle/src/visualizers/ast-visualizer/index.tsx
Normal file
126
visualization-bundle/src/visualizers/ast-visualizer/index.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import {
|
||||
Serializer,
|
||||
sLazy,
|
||||
sOpenObject,
|
||||
sArrayOf,
|
||||
sString,
|
||||
sOptionalProp,
|
||||
sUnion,
|
||||
sLiteral,
|
||||
sBoolean,
|
||||
sProp,
|
||||
sNumber,
|
||||
} from "@hediet/semantic-json";
|
||||
import { visualizationNs } from "../../consts";
|
||||
import {
|
||||
createVisualizer,
|
||||
ReactVisualization,
|
||||
globalVisualizationFactory,
|
||||
} from "@hediet/visualization-core";
|
||||
import { createTreeViewModelFromTreeNodeData } from "../tree-visualizer";
|
||||
import { AstTree, NodeInfo } from "./AstVisualizer";
|
||||
import React from "react";
|
||||
import LineColumn from "line-column";
|
||||
import * as monacoTypes from "monaco-editor";
|
||||
import { getLoadedMonaco } from "@hediet/monaco-editor-react";
|
||||
|
||||
interface AstTreeNode {
|
||||
children: AstTreeNode[];
|
||||
items: Item[];
|
||||
segment?: string; // like "root", ".name" or "[0]"
|
||||
isMarked?: boolean;
|
||||
span: NodeInfo;
|
||||
}
|
||||
|
||||
interface Item {
|
||||
text: string;
|
||||
emphasis?: "style1" | "style2" | "style3" | string;
|
||||
}
|
||||
|
||||
export const sAstTreeNode: Serializer<AstTreeNode> = sLazy(() =>
|
||||
sOpenObject({
|
||||
children: sArrayOf(sAstTreeNode),
|
||||
items: sArrayOf(
|
||||
sOpenObject({
|
||||
text: sString(),
|
||||
emphasis: sOptionalProp(
|
||||
sUnion(
|
||||
[
|
||||
sLiteral("style1"),
|
||||
sLiteral("style2"),
|
||||
sLiteral("style3"),
|
||||
sString(),
|
||||
],
|
||||
{ inclusive: true }
|
||||
)
|
||||
),
|
||||
}).defineAs(visualizationNs("TreeNodeItem"))
|
||||
),
|
||||
segment: sOptionalProp(sString()),
|
||||
isMarked: sOptionalProp(sBoolean()),
|
||||
span: sOpenObject({
|
||||
start: sNumber(),
|
||||
length: sNumber(),
|
||||
}),
|
||||
}).defineAs(visualizationNs("TreeNode"))
|
||||
);
|
||||
|
||||
export const sAstTree = sOpenObject({
|
||||
kind: sOpenObject({
|
||||
tree: sLiteral(true),
|
||||
text: sLiteral(true),
|
||||
}),
|
||||
root: sAstTreeNode,
|
||||
text: sString(),
|
||||
fileName: sOptionalProp(sString()),
|
||||
});
|
||||
|
||||
export const astVisualizer = createVisualizer({
|
||||
id: "ast",
|
||||
name: "AST",
|
||||
serializer: sAstTree,
|
||||
getVisualization: (data, self) =>
|
||||
new ReactVisualization(self, { priority: 1500 }, theme => {
|
||||
const m = createTreeViewModelFromTreeNodeData(
|
||||
data.root,
|
||||
node => node.span
|
||||
);
|
||||
const l = LineColumn(data.text);
|
||||
function translatePosition(pos: number): monacoTypes.IPosition {
|
||||
let r = l.fromIndex(pos);
|
||||
if (!r) {
|
||||
r = l.fromIndex(data.text.length - 1);
|
||||
r.col++;
|
||||
}
|
||||
return {
|
||||
column: r.col,
|
||||
lineNumber: r.line,
|
||||
};
|
||||
}
|
||||
const nodeInfoToRange = (info: NodeInfo): monacoTypes.IRange => {
|
||||
const start = translatePosition(info.start);
|
||||
const end = translatePosition(info.start + info.length);
|
||||
const range = getLoadedMonaco().Range.fromPositions(start, end);
|
||||
return range;
|
||||
};
|
||||
function posToIndex(pos: monacoTypes.IPosition): number {
|
||||
const i = l.toIndex(pos.lineNumber, pos.column);
|
||||
if (i == -1) {
|
||||
return data.text.length;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
return (
|
||||
<AstTree
|
||||
model={m}
|
||||
theme={theme}
|
||||
data={data}
|
||||
nodeInfoToRange={nodeInfoToRange}
|
||||
posToIndex={posToIndex}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
});
|
||||
|
||||
globalVisualizationFactory.addVisualizer(astVisualizer);
|
||||
@@ -0,0 +1,23 @@
|
||||
.theme-dark {
|
||||
--visualizer-ast-marked-background: rgba(234, 92, 0, 0.33);
|
||||
--visualizer-ast-selected-background: #515c6a;
|
||||
}
|
||||
.theme-light {
|
||||
--visualizer-ast-marked-background: orange;
|
||||
--visualizer-ast-selected-background: yellow;
|
||||
}
|
||||
|
||||
.component-AstTree {
|
||||
.part-editor {
|
||||
//border-top: solid gray 1px;
|
||||
|
||||
.marked {
|
||||
background: var(--visualizer-ast-marked-background);
|
||||
}
|
||||
|
||||
.selected {
|
||||
background: var(--visualizer-ast-selected-background);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
visualization-bundle/src/visualizers/ast-visualizer/types.d.ts
vendored
Normal file
13
visualization-bundle/src/visualizers/ast-visualizer/types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
declare module "line-column" {
|
||||
export = function(
|
||||
text: string
|
||||
): {
|
||||
fromIndex(
|
||||
idx: number
|
||||
): {
|
||||
line: number;
|
||||
col: number;
|
||||
};
|
||||
toIndex(line: number, column: number): number;
|
||||
} {};
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { observer, disposeOnUnmount } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { observable, autorun, runInAction } from "mobx";
|
||||
import { SvgViewer } from "../../svg-visualizer/SvgViewer";
|
||||
|
||||
class VisLoader {
|
||||
private result: any | undefined;
|
||||
|
||||
async getViz(): Promise<any> {
|
||||
if (!this.result) {
|
||||
const Viz = await import("viz.js");
|
||||
const { Module, render } = await import("viz.js/full.render.js");
|
||||
|
||||
const viz = new Viz.default({
|
||||
Module: () => Module({ TOTAL_MEMORY: 1 << 30 }),
|
||||
render,
|
||||
});
|
||||
|
||||
this.result = viz;
|
||||
}
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
|
||||
const vizLoader = new VisLoader();
|
||||
|
||||
@observer
|
||||
export class GraphvizDotViewer 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 dotCode = this.props.dotCode; // to trigger mobx dependency
|
||||
const viz = await vizLoader.getViz();
|
||||
const svg = await viz.renderString(dotCode);
|
||||
runInAction("Update svg", () => (this.svg = svg));
|
||||
});
|
||||
|
||||
render() {
|
||||
if (!this.svg) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
return <SvgViewer svgRef={this.props.svgRef} svgContent={this.svg} />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from "react";
|
||||
import { sOpenObject, sLiteral, sString } from "@hediet/semantic-json";
|
||||
import { GraphvizDotViewer } from "./GraphvizDotVisualizer";
|
||||
import {
|
||||
ReactVisualization,
|
||||
globalVisualizationFactory,
|
||||
createVisualizer,
|
||||
} from "@hediet/visualization-core";
|
||||
|
||||
export const graphvizDotVisualizer = createVisualizer({
|
||||
id: "graphviz-dot",
|
||||
name: "Graphviz (Dot Data)",
|
||||
serializer: sOpenObject({
|
||||
kind: sOpenObject({
|
||||
dotGraph: sLiteral(true),
|
||||
}),
|
||||
text: sString(),
|
||||
}),
|
||||
getVisualization: (data, self) =>
|
||||
new ReactVisualization(self, { priority: 1500 }, () => (
|
||||
<GraphvizDotViewer dotCode={data.text} />
|
||||
)),
|
||||
});
|
||||
|
||||
globalVisualizationFactory.addVisualizer(graphvizDotVisualizer);
|
||||
9
visualization-bundle/src/visualizers/graph/dot-graphviz-visualizer/types.d.ts
vendored
Normal file
9
visualization-bundle/src/visualizers/graph/dot-graphviz-visualizer/types.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
declare module "viz.js" {
|
||||
const x: any;
|
||||
export default x;
|
||||
}
|
||||
|
||||
declare module "viz.js/full.render.js" {
|
||||
export const render: any;
|
||||
export const Module: any;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { GraphvizDotViewer } from "../dot-graphviz-visualizer/GraphvizDotVisualizer";
|
||||
|
||||
@observer
|
||||
export class GraphvizGraphViewer extends React.Component<{
|
||||
nodes: { id: string; label?: string }[];
|
||||
edges: { from: string; to: string; label?: string }[];
|
||||
}> {
|
||||
render() {
|
||||
const { nodes, edges } = this.props;
|
||||
const dotContent = `
|
||||
digraph MyGraph {
|
||||
${nodes
|
||||
.map(
|
||||
n =>
|
||||
`"${n.id}" [ label = ${JSON.stringify(
|
||||
n.label !== undefined ? n.label : n.id
|
||||
)} ];`
|
||||
)
|
||||
.join("\n ")}
|
||||
${edges
|
||||
.map(
|
||||
e =>
|
||||
`"${e.from}" -> "${
|
||||
e.to
|
||||
}" [ label = ${JSON.stringify(
|
||||
e.label !== undefined ? e.label : ""
|
||||
)} ];`
|
||||
)
|
||||
.join("\n")}
|
||||
}
|
||||
`;
|
||||
return <GraphvizDotViewer dotCode={dotContent} />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
import { GraphvizGraphViewer } from "./GraphvizGraphVisualizer";
|
||||
import { sGraph } from "../sGraph";
|
||||
import {
|
||||
createVisualizer,
|
||||
ReactVisualization,
|
||||
globalVisualizationFactory,
|
||||
} from "@hediet/visualization-core";
|
||||
|
||||
export const graphvizGraphVisualizer = createVisualizer({
|
||||
id: "graphviz-graph",
|
||||
name: "Graphviz",
|
||||
serializer: sGraph,
|
||||
getVisualization: (data, self) =>
|
||||
new ReactVisualization(self, { priority: 1000 }, () => (
|
||||
<GraphvizGraphViewer edges={data.edges} nodes={data.nodes} />
|
||||
)),
|
||||
});
|
||||
|
||||
globalVisualizationFactory.addVisualizer(graphvizGraphVisualizer);
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Network, Options, DataSet } from "vis-network/standalone";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { NodeGraphData, EdgeGraphData } from "../sGraph";
|
||||
|
||||
@observer
|
||||
export class VisJsGraphViewer extends React.Component<{
|
||||
nodes: NodeGraphData[];
|
||||
edges: EdgeGraphData[];
|
||||
}> {
|
||||
private readonly divRef = React.createRef<HTMLDivElement>();
|
||||
private readonly nodes = new DataSet<{
|
||||
id: string;
|
||||
label?: string;
|
||||
color?: string;
|
||||
shape?: string;
|
||||
}>();
|
||||
private readonly edges = new DataSet<{
|
||||
id: string;
|
||||
from: string;
|
||||
to: string;
|
||||
label?: string;
|
||||
color?: string;
|
||||
dashes?: boolean;
|
||||
shape?: boolean;
|
||||
}>();
|
||||
|
||||
render() {
|
||||
return <div style={{ height: "100%" }} ref={this.divRef} />;
|
||||
}
|
||||
|
||||
synchronizeData() {
|
||||
const newNodes = new Set<string>();
|
||||
for (const n of this.props.nodes) {
|
||||
newNodes.add(n.id);
|
||||
this.nodes.update({
|
||||
id: n.id,
|
||||
label: n.label !== undefined ? n.label : n.id,
|
||||
color: n.color,
|
||||
shape: n.shape,
|
||||
});
|
||||
}
|
||||
this.nodes.forEach(item => {
|
||||
if (!newNodes.has(item.id)) {
|
||||
this.nodes.remove(item);
|
||||
}
|
||||
});
|
||||
|
||||
function getIdOfEdge(e: EdgeGraphData): string {
|
||||
if (e.id) {
|
||||
return e.id;
|
||||
}
|
||||
return e.from + "####" + e.to + "|" + e.label;
|
||||
}
|
||||
|
||||
const newEdges = new Set<string>();
|
||||
for (const n of this.props.edges) {
|
||||
const id = getIdOfEdge(n);
|
||||
newEdges.add(id);
|
||||
this.edges.update({
|
||||
id: id,
|
||||
label: n.label !== undefined ? n.label : "",
|
||||
from: n.from,
|
||||
to: n.to,
|
||||
color: n.color,
|
||||
dashes: n.dashes,
|
||||
});
|
||||
}
|
||||
this.edges.forEach(item => {
|
||||
if (!newEdges.has(item.id)) {
|
||||
this.edges.remove(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.synchronizeData();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.synchronizeData();
|
||||
|
||||
const data = {
|
||||
nodes: this.nodes,
|
||||
edges: this.edges,
|
||||
};
|
||||
const options: Options = {
|
||||
edges: {
|
||||
arrows: {
|
||||
to: { enabled: true, scaleFactor: 1, type: "arrow" },
|
||||
},
|
||||
},
|
||||
};
|
||||
const network = new Network(this.divRef.current!, data, options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import * as React from "react";
|
||||
import { makeLazyLoadable } from "../../../utils/LazyLoadable";
|
||||
import {
|
||||
createVisualizer,
|
||||
ReactVisualization,
|
||||
globalVisualizationFactory,
|
||||
} from "@hediet/visualization-core";
|
||||
import { sGraph } from "../sGraph";
|
||||
|
||||
const VisJsGraphViewerLazyLoadable = makeLazyLoadable(
|
||||
async () => (await import("./VisJsGraphViewer")).VisJsGraphViewer
|
||||
);
|
||||
|
||||
export const visJsGraphVisualizer = createVisualizer({
|
||||
id: "vis.js-graph",
|
||||
name: "vis.js",
|
||||
serializer: sGraph,
|
||||
getVisualization: (data, self) =>
|
||||
new ReactVisualization(self, { priority: 1500 }, () => (
|
||||
<VisJsGraphViewerLazyLoadable
|
||||
edges={data.edges}
|
||||
nodes={data.nodes}
|
||||
/>
|
||||
)),
|
||||
});
|
||||
|
||||
globalVisualizationFactory.addVisualizer(visJsGraphVisualizer);
|
||||
3
visualization-bundle/src/visualizers/graph/index.ts
Normal file
3
visualization-bundle/src/visualizers/graph/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./dot-graphviz-visualizer";
|
||||
export * from "./graph-graphviz-visualizer";
|
||||
export * from "./graph-visjs-visualizer";
|
||||
35
visualization-bundle/src/visualizers/graph/sGraph.ts
Normal file
35
visualization-bundle/src/visualizers/graph/sGraph.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
sOpenObject,
|
||||
sLiteral,
|
||||
sArrayOf,
|
||||
sString,
|
||||
sOptionalProp,
|
||||
sUnion,
|
||||
sBoolean,
|
||||
} from "@hediet/semantic-json";
|
||||
import { visualizationNs } from "../../consts";
|
||||
|
||||
export type NodeGraphData = typeof sGraphNode.T;
|
||||
export type EdgeGraphData = typeof sGraphEdge.T;
|
||||
|
||||
export const sGraphNode = sOpenObject({
|
||||
id: sString(),
|
||||
label: sOptionalProp(sString(), {}),
|
||||
color: sOptionalProp(sString(), {}),
|
||||
shape: sOptionalProp(sUnion([sLiteral("ellipse"), sLiteral("box")]), {}),
|
||||
}).defineAs(visualizationNs("GraphNode"));
|
||||
|
||||
export const sGraphEdge = sOpenObject({
|
||||
from: sString(),
|
||||
to: sString(),
|
||||
label: sOptionalProp(sString(), {}),
|
||||
id: sOptionalProp(sString(), {}),
|
||||
color: sOptionalProp(sString(), {}),
|
||||
dashes: sOptionalProp(sBoolean(), {}),
|
||||
}).defineAs(visualizationNs("GraphEdge"));
|
||||
|
||||
export const sGraph = sOpenObject({
|
||||
kind: sOpenObject({ graph: sLiteral(true) }),
|
||||
nodes: sArrayOf(sGraphNode),
|
||||
edges: sArrayOf(sGraphEdge),
|
||||
}).defineAs(visualizationNs("Graph"));
|
||||
@@ -0,0 +1,377 @@
|
||||
import * as React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { computed, action, observable } from "mobx";
|
||||
import { Icon } from "@blueprintjs/core";
|
||||
import { sGrid } from ".";
|
||||
|
||||
export class DecoratedGridComponent extends React.Component<{
|
||||
data: typeof sGrid.T;
|
||||
}> {
|
||||
render() {
|
||||
const d = this.props.data;
|
||||
|
||||
const map = new Set<string>();
|
||||
function getUniqueId(tag: string): string {
|
||||
let n = 0;
|
||||
while (true) {
|
||||
const id = tag + (n === 0 ? "" : `-${n}`);
|
||||
if (!map.has(id)) {
|
||||
map.add(id);
|
||||
return id;
|
||||
}
|
||||
n++;
|
||||
}
|
||||
}
|
||||
|
||||
const rows: GridComponent["props"]["rows"] = [];
|
||||
rows.push();
|
||||
let rowIdx = 0;
|
||||
let columnCount = 0;
|
||||
for (const row of d.rows) {
|
||||
columnCount = Math.max(columnCount, row.columns.length);
|
||||
rows.push({
|
||||
columns: row.columns.map((c, colIdx) => ({
|
||||
content: (
|
||||
<Cell>
|
||||
{c.content !== undefined ? c.content : c.tag || ""}
|
||||
</Cell>
|
||||
),
|
||||
id:
|
||||
c.tag !== undefined
|
||||
? getUniqueId(c.tag)
|
||||
: `${rowIdx}-${colIdx}`,
|
||||
color: c.color,
|
||||
kind: "data",
|
||||
})),
|
||||
});
|
||||
rowIdx++;
|
||||
}
|
||||
|
||||
rows.unshift({
|
||||
columns: [...new Array(columnCount)].map((v, idx) => ({
|
||||
id: `column-header-${idx}`,
|
||||
content: <Cell>{idx}</Cell>,
|
||||
kind: "header",
|
||||
})),
|
||||
});
|
||||
|
||||
if (d.markers) {
|
||||
for (const m of d.markers) {
|
||||
rows.push({
|
||||
columns: [],
|
||||
});
|
||||
const r = rows[rows.length - 1];
|
||||
for (let i = 0; i < m.column; i++) {
|
||||
r.columns.push({
|
||||
content: "",
|
||||
kind: "empty",
|
||||
id: `marker-spacer-${i}`,
|
||||
});
|
||||
}
|
||||
|
||||
r.columns.push({
|
||||
id: m.id,
|
||||
kind: "empty",
|
||||
content: (
|
||||
<Cell>
|
||||
<Icon
|
||||
icon="arrow-up"
|
||||
color="var(--vscode-editor-foreground)"
|
||||
/>
|
||||
{m.label || m.id}
|
||||
</Cell>
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: 10,
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<GridComponent rows={rows} markers={[]} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function Cell(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: 10,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface CellData {
|
||||
content: React.ReactNode;
|
||||
id: string;
|
||||
color?: string;
|
||||
kind: "data" | "header" | "empty";
|
||||
}
|
||||
|
||||
@observer
|
||||
class GridComponent extends React.Component<{
|
||||
rows: {
|
||||
columns: CellData[];
|
||||
}[];
|
||||
markers: {
|
||||
id: string;
|
||||
|
||||
row: number;
|
||||
column: number;
|
||||
rows?: number;
|
||||
columns?: number;
|
||||
|
||||
label?: string;
|
||||
color?: string;
|
||||
}[];
|
||||
}> {
|
||||
private readonly cells = new Map<string, CellInfo>();
|
||||
|
||||
@computed get cellsGrid(): { maxWidth: number; grid: CellInfo[][] } {
|
||||
const unusedIds = new Set(this.cells.keys());
|
||||
|
||||
const rows = this.props.rows;
|
||||
|
||||
const grid = new Array<Array<CellInfo>>();
|
||||
|
||||
let maxWidth = 0;
|
||||
for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
|
||||
const row = rows[rowIdx];
|
||||
maxWidth = Math.max(maxWidth, row.columns.length);
|
||||
const cellRow = new Array<CellInfo>();
|
||||
grid.push(cellRow);
|
||||
|
||||
for (let colIdx = 0; colIdx < row.columns.length; colIdx++) {
|
||||
const col = row.columns[colIdx];
|
||||
const key = col.id;
|
||||
unusedIds.delete(key);
|
||||
let c = this.cells.get(key);
|
||||
if (!c) {
|
||||
// New cell
|
||||
c = new CellInfo(col);
|
||||
this.cells.set(key, c);
|
||||
} else {
|
||||
c.updateCellData(col);
|
||||
}
|
||||
cellRow.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
for (const id in unusedIds.values()) {
|
||||
this.cells.delete(id);
|
||||
}
|
||||
|
||||
return { maxWidth, grid };
|
||||
}
|
||||
|
||||
@computed
|
||||
private get layout(): {
|
||||
cells: {
|
||||
cell: CellInfo;
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}[];
|
||||
width: number;
|
||||
height: number;
|
||||
} {
|
||||
const { maxWidth, grid } = this.cellsGrid;
|
||||
|
||||
const maxWidths = new Array<number>();
|
||||
for (let colIdx = 0; colIdx < maxWidth; colIdx++) {
|
||||
let maxWidth = 30;
|
||||
for (const row of grid) {
|
||||
const col = row[colIdx];
|
||||
if (col) {
|
||||
maxWidth = Math.max(maxWidth, col.contentWidth || 0);
|
||||
}
|
||||
}
|
||||
maxWidths.push(maxWidth);
|
||||
}
|
||||
|
||||
const result = new Array<{
|
||||
cell: CellInfo;
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}>();
|
||||
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
let top = 0;
|
||||
let left = 0;
|
||||
|
||||
for (const row of grid) {
|
||||
let maxHeight = 0;
|
||||
for (const col of row) {
|
||||
maxHeight = Math.max(maxHeight, col.contentHeight || 0);
|
||||
}
|
||||
|
||||
for (let colIdx = 0; colIdx < row.length; colIdx++) {
|
||||
const maxWidth = maxWidths[colIdx];
|
||||
const col = row[colIdx];
|
||||
result.push({
|
||||
cell: col,
|
||||
top,
|
||||
left,
|
||||
height: maxHeight,
|
||||
width: maxWidth,
|
||||
});
|
||||
height = Math.max(height, top + maxHeight);
|
||||
width = Math.max(width, left + maxWidth);
|
||||
col.lastTop = col.top;
|
||||
col.lastLeft = col.left;
|
||||
col.top = top;
|
||||
col.left = left;
|
||||
|
||||
col.lastContentArea = col.contentArea;
|
||||
col.contentArea = maxHeight * maxWidth;
|
||||
left += maxWidth;
|
||||
}
|
||||
|
||||
left = 0;
|
||||
top += maxHeight;
|
||||
}
|
||||
|
||||
result.sort((a, b) => a.cell.id.localeCompare(b.cell.id));
|
||||
return { cells: result, height, width };
|
||||
}
|
||||
|
||||
render() {
|
||||
const l = this.layout;
|
||||
return (
|
||||
<div
|
||||
className="component-Grid"
|
||||
style={{
|
||||
position: "relative",
|
||||
height: l.height,
|
||||
width: l.width,
|
||||
}}
|
||||
>
|
||||
{l.cells.map(i => (
|
||||
<div
|
||||
key={i.cell.id}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: i.top,
|
||||
left: i.left,
|
||||
height: i.height,
|
||||
width: i.width,
|
||||
zIndex: i.cell.distance,
|
||||
transition:
|
||||
i.cell.lastContentArea !== 0 ? "all 1s" : "",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`part-${i.cell.kind}`}
|
||||
style={
|
||||
{
|
||||
empty: {},
|
||||
data: {
|
||||
display: "flex",
|
||||
overflow: "hidden",
|
||||
justifyContent: "center",
|
||||
margin: 2,
|
||||
},
|
||||
header: {
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
margin: 2,
|
||||
},
|
||||
}[i.cell.kind]
|
||||
}
|
||||
>
|
||||
<div
|
||||
ref={i.cell.handleRef}
|
||||
style={{
|
||||
width: "fit-content",
|
||||
height: "fit-content",
|
||||
}}
|
||||
>
|
||||
{i.cell.content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateContentSize();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.updateContentSize();
|
||||
}
|
||||
|
||||
@action
|
||||
updateContentSize() {
|
||||
for (const c of this.cells.values()) {
|
||||
if (c.ref) {
|
||||
const r = c.ref.getBoundingClientRect();
|
||||
c.contentHeight = r.height + 6;
|
||||
c.contentWidth = r.width + 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CellInfo {
|
||||
@observable public contentWidth: number | undefined = undefined;
|
||||
@observable public contentHeight: number | undefined = undefined;
|
||||
|
||||
public contentArea = 0;
|
||||
public lastContentArea = 0;
|
||||
|
||||
public lastTop = 0;
|
||||
public top = 0;
|
||||
public lastLeft = 0;
|
||||
public left = 0;
|
||||
|
||||
public get distance(): number {
|
||||
return Math.floor(
|
||||
Math.pow(this.top - this.lastTop, 2) +
|
||||
Math.pow(this.left - this.lastLeft, 2)
|
||||
);
|
||||
}
|
||||
|
||||
content!: React.ReactNode;
|
||||
|
||||
ref: HTMLDivElement | null = null;
|
||||
|
||||
public readonly id: string;
|
||||
public readonly kind: CellData["kind"];
|
||||
|
||||
constructor(cellData: CellData) {
|
||||
this.id = cellData.id;
|
||||
this.kind = cellData.kind;
|
||||
this.updateCellData(cellData);
|
||||
}
|
||||
|
||||
public updateCellData(cellData: CellData) {
|
||||
this.content = cellData.content;
|
||||
}
|
||||
|
||||
public readonly handleRef = (ref: HTMLDivElement | null) => {
|
||||
this.ref = ref;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from "react";
|
||||
import {
|
||||
sOpenObject,
|
||||
sLiteral,
|
||||
sString,
|
||||
sArrayOf,
|
||||
sObject,
|
||||
sOptionalProp,
|
||||
sNumber,
|
||||
} from "@hediet/semantic-json";
|
||||
import { DecoratedGridComponent } from "./GridVisualizer";
|
||||
import {
|
||||
createVisualizer,
|
||||
ReactVisualization,
|
||||
globalVisualizationFactory,
|
||||
} from "@hediet/visualization-core";
|
||||
|
||||
export const sGrid = sOpenObject({
|
||||
kind: sOpenObject({
|
||||
array: sLiteral(true),
|
||||
}),
|
||||
columnLabels: sArrayOf(
|
||||
sOpenObject({
|
||||
label: sOptionalProp(sString(), {}),
|
||||
})
|
||||
),
|
||||
rows: sArrayOf(
|
||||
sOpenObject({
|
||||
label: sOptionalProp(sString(), {}),
|
||||
columns: sArrayOf(
|
||||
sOpenObject({
|
||||
content: sOptionalProp(sString(), {}),
|
||||
tag: sOptionalProp(sString(), {
|
||||
description:
|
||||
"A value to identify this cell. Should be unique.",
|
||||
}),
|
||||
color: sOptionalProp(sString(), {}),
|
||||
})
|
||||
),
|
||||
})
|
||||
),
|
||||
markers: sOptionalProp(
|
||||
sArrayOf(
|
||||
sOpenObject({
|
||||
id: sString(),
|
||||
|
||||
row: sNumber(),
|
||||
column: sNumber(),
|
||||
rows: sOptionalProp(sNumber(), {}),
|
||||
columns: sOptionalProp(sNumber(), {}),
|
||||
|
||||
label: sOptionalProp(sString(), {}),
|
||||
color: sOptionalProp(sString(), {}),
|
||||
})
|
||||
),
|
||||
{}
|
||||
),
|
||||
});
|
||||
|
||||
export const gridVisualizer = createVisualizer({
|
||||
id: "grid",
|
||||
name: "Grid",
|
||||
serializer: sGrid,
|
||||
getVisualization: (data, self) =>
|
||||
new ReactVisualization(self, { priority: 1000 }, () => (
|
||||
<DecoratedGridComponent data={data} />
|
||||
)),
|
||||
});
|
||||
|
||||
globalVisualizationFactory.addVisualizer(gridVisualizer);
|
||||
@@ -0,0 +1,14 @@
|
||||
.component-Grid {
|
||||
.part-data {
|
||||
border: 1px solid lightgray;
|
||||
background: #eeeeee;
|
||||
}
|
||||
|
||||
.part-header {
|
||||
background: transparentize(white, 0.5);
|
||||
}
|
||||
|
||||
.part-empty {
|
||||
color: var(--vscode-editor-foreground);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { sOpenObject, sLiteral, sString } from "@hediet/semantic-json";
|
||||
import React from "react";
|
||||
import {
|
||||
createVisualizer,
|
||||
ReactVisualization,
|
||||
globalVisualizationFactory,
|
||||
} from "@hediet/visualization-core";
|
||||
import { SvgViewer } from "../svg-visualizer/SvgViewer";
|
||||
import { Observer, observer, disposeOnUnmount } from "mobx-react";
|
||||
import { observable, autorun } from "mobx";
|
||||
|
||||
export const imageVisualizer = createVisualizer({
|
||||
id: "image",
|
||||
name: "Image",
|
||||
serializer: sOpenObject({
|
||||
kind: sOpenObject({
|
||||
imagePng: sLiteral(true),
|
||||
}),
|
||||
base64Data: sString(),
|
||||
}),
|
||||
getVisualization: (data, self) =>
|
||||
new ReactVisualization(self, { priority: 1000 }, () => {
|
||||
return <ImageViewer base64Data={data.base64Data} />;
|
||||
}),
|
||||
});
|
||||
|
||||
@observer
|
||||
class ImageViewer extends React.Component<{ base64Data: string }> {
|
||||
@observable private svg: string | undefined = undefined;
|
||||
|
||||
@disposeOnUnmount
|
||||
private readonly updateSvg = autorun(async () => {
|
||||
const src = `data:image/png;base64,${this.props.base64Data}`;
|
||||
|
||||
const { width, height } = await new Promise<{
|
||||
width: number;
|
||||
height: number;
|
||||
}>(res => {
|
||||
const img = new Image();
|
||||
img.onload = function() {
|
||||
res({ width: img.width, height: img.height });
|
||||
};
|
||||
img.src = src;
|
||||
});
|
||||
|
||||
this.svg = `
|
||||
<svg viewBox="0 0 ${width} ${height}">
|
||||
<defs>
|
||||
<pattern id="Pattern" x="0" y="0" width="14" height="14" patternUnits="userSpaceOnUse">
|
||||
<rect x="0" y="0" width="7" height="7" style="fill: lightgray; stroke-width: 0" />
|
||||
<rect x="7" y="7" width="7" height="7" style="fill: lightgray; stroke-width: 0" />
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect fill="url(#Pattern)" style="stroke-width: 0" width="${width}" height="${height}"/>
|
||||
|
||||
<image style="image-rendering: pixelated" xlink:href="${src}" />
|
||||
</svg>`;
|
||||
});
|
||||
|
||||
render() {
|
||||
if (!this.svg) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <SvgViewer svgContent={this.svg} />;
|
||||
}
|
||||
}
|
||||
|
||||
globalVisualizationFactory.addVisualizer(imageVisualizer);
|
||||
11
visualization-bundle/src/visualizers/index.ts
Normal file
11
visualization-bundle/src/visualizers/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export * from "./ast-visualizer";
|
||||
export * from "./graph";
|
||||
export * from "./grid-visualizer";
|
||||
export * from "./image-visualizer";
|
||||
export * from "./monaco-text-visualizer";
|
||||
export * from "./perspective-table-visualizer";
|
||||
export * from "./plotly-visualizer";
|
||||
export * from "./simple-text-visualizer";
|
||||
export * from "./source-visualizer";
|
||||
export * from "./svg-visualizer";
|
||||
export * from "./tree-visualizer";
|
||||
@@ -0,0 +1,103 @@
|
||||
import React from "react";
|
||||
import { observer, disposeOnUnmount } from "mobx-react";
|
||||
import { observable, autorun } from "mobx";
|
||||
import * as monacoTypes from "monaco-editor";
|
||||
import { Theme } from "@hediet/visualization-core";
|
||||
import { getLoadedMonaco } from "@hediet/monaco-editor-react";
|
||||
|
||||
export function getLanguageId(fileName: string): string {
|
||||
const l = getLoadedMonaco().languages.getLanguages();
|
||||
const result = l.find(l => {
|
||||
if (l.filenamePatterns) {
|
||||
for (const p of l.filenamePatterns) {
|
||||
if (new RegExp(p).test(fileName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (l.extensions) {
|
||||
for (const p of l.extensions) {
|
||||
if (fileName.endsWith(p)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (result) {
|
||||
return result.id;
|
||||
}
|
||||
return "text";
|
||||
}
|
||||
|
||||
@observer
|
||||
export class MonacoEditor extends React.Component<{
|
||||
text: string;
|
||||
languageId: string;
|
||||
theme: Theme;
|
||||
}> {
|
||||
@observable private editor:
|
||||
| monacoTypes.editor.IStandaloneCodeEditor
|
||||
| undefined;
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.editor) {
|
||||
this.editor.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private model: monacoTypes.editor.ITextModel | undefined = undefined;
|
||||
|
||||
@disposeOnUnmount
|
||||
private _updateText = autorun(() => {
|
||||
if (this.editor) {
|
||||
const model = getLoadedMonaco().editor.createModel(
|
||||
this.props.text,
|
||||
this.props.languageId,
|
||||
undefined
|
||||
);
|
||||
|
||||
this.editor.setModel(model);
|
||||
|
||||
/* this.editor!.updateOptions({ readOnly: false });
|
||||
|
||||
setTimeout(() => {
|
||||
const a = this.editor!.getAction(
|
||||
"editor.action.formatDocument"
|
||||
);
|
||||
a.run().then(() => {
|
||||
this.editor!.updateOptions({ readOnly: true });
|
||||
});
|
||||
}, 200);*/
|
||||
if (this.model) {
|
||||
this.model.dispose();
|
||||
}
|
||||
this.model = model;
|
||||
}
|
||||
});
|
||||
|
||||
private readonly setEditorDiv = (editorDiv: HTMLDivElement) => {
|
||||
if (!editorDiv) {
|
||||
return;
|
||||
}
|
||||
this.editor = getLoadedMonaco().editor.create(editorDiv, {
|
||||
model: null,
|
||||
automaticLayout: true,
|
||||
scrollBeyondLastLine: false,
|
||||
minimap: { enabled: false },
|
||||
fixedOverflowWidgets: true,
|
||||
readOnly: true,
|
||||
theme: this.props.theme.kind === "dark" ? "vs-dark" : "vs",
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="component-monaco-editor">
|
||||
<div className="part-editor" ref={this.setEditorDiv} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
sOpenObject,
|
||||
sLiteral,
|
||||
sString,
|
||||
sOptionalProp,
|
||||
} from "@hediet/semantic-json";
|
||||
import React from "react";
|
||||
import { getLanguageId, MonacoEditor } from "./MonacoEditor";
|
||||
import {
|
||||
createVisualizer,
|
||||
ReactVisualization,
|
||||
globalVisualizationFactory,
|
||||
} from "@hediet/visualization-core";
|
||||
|
||||
export const monacoTextVisualizer = createVisualizer({
|
||||
id: "monaco-text",
|
||||
name: "Text",
|
||||
serializer: sOpenObject({
|
||||
kind: sOpenObject({
|
||||
text: sLiteral(true),
|
||||
}),
|
||||
text: sString(),
|
||||
fileName: sOptionalProp(sString(), {
|
||||
description:
|
||||
"An optional filename that might be used for chosing a syntax highlighter",
|
||||
}),
|
||||
}),
|
||||
getVisualization: (data, self) =>
|
||||
new ReactVisualization(self, { priority: 500 }, theme => {
|
||||
let id = "text";
|
||||
if (data.fileName) {
|
||||
id = getLanguageId(data.fileName);
|
||||
}
|
||||
return (
|
||||
<MonacoEditor text={data.text} languageId={id} theme={theme} />
|
||||
);
|
||||
}),
|
||||
});
|
||||
|
||||
globalVisualizationFactory.addVisualizer(monacoTextVisualizer);
|
||||
@@ -0,0 +1,22 @@
|
||||
.component-monaco-editor {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
padding: 2px;
|
||||
border: 0.5px solid gray;
|
||||
|
||||
.part-editor {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
|
||||
.current-line {
|
||||
border: none !important;
|
||||
}
|
||||
.selectionHighlight {
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import React from "react";
|
||||
import { sOpenObject, sLiteral, sArrayOf, sAny } from "@hediet/semantic-json";
|
||||
import { toJS } from "mobx";
|
||||
import "@finos/perspective-viewer";
|
||||
import "@finos/perspective-viewer-datagrid";
|
||||
import "@finos/perspective-viewer-d3fc";
|
||||
import "@finos/perspective-viewer/themes/all-themes.css";
|
||||
import { HTMLPerspectiveViewerElement } from "@finos/perspective-viewer";
|
||||
import {
|
||||
createVisualizer,
|
||||
ReactVisualization,
|
||||
globalVisualizationFactory,
|
||||
} from "@hediet/visualization-core";
|
||||
|
||||
export const sTable = sOpenObject({
|
||||
kind: sOpenObject({
|
||||
table: sLiteral(true),
|
||||
}),
|
||||
rows: sArrayOf(
|
||||
sAny() // object
|
||||
),
|
||||
});
|
||||
|
||||
export const perspectiveTableVisualizer = createVisualizer({
|
||||
id: "perspective-table",
|
||||
name: "Perspective Table",
|
||||
serializer: sTable,
|
||||
getVisualization: (data, self) =>
|
||||
new ReactVisualization(self, { priority: 1000 }, theme => (
|
||||
<PerspectiveDataViewer
|
||||
className={
|
||||
{
|
||||
light: "perspective-viewer-material",
|
||||
dark: "perspective-viewer-material-dark",
|
||||
}[theme.kind]
|
||||
}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
data={toJS(data.rows)}
|
||||
/>
|
||||
)),
|
||||
});
|
||||
|
||||
class PerspectiveDataViewer extends React.Component<
|
||||
{ data: object[] } & React.HTMLAttributes<HTMLDivElement>
|
||||
> {
|
||||
private readonly nodeRef = React.createRef<HTMLPerspectiveViewerElement>();
|
||||
|
||||
componentDidUpdate() {
|
||||
this.nodeRef.current!.load(this.props.data);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
setTimeout(() => {
|
||||
// TODO: Why timeout?
|
||||
// It does not work without it.
|
||||
|
||||
this.nodeRef.current!.load(this.props.data);
|
||||
this.nodeRef.current!.toggleConfig();
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<perspective-viewer
|
||||
class={this.props.className}
|
||||
style={this.props.style}
|
||||
ref={this.nodeRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
globalVisualizationFactory.addVisualizer(perspectiveTableVisualizer);
|
||||
@@ -0,0 +1,187 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import Plot from "react-plotly.js";
|
||||
|
||||
@observer
|
||||
export class PlotlyViewer extends React.Component<{
|
||||
data: Plotly.Data[];
|
||||
layout?: Partial<Plotly.Layout>;
|
||||
theme: "light" | "dark";
|
||||
}> {
|
||||
render() {
|
||||
const { theme, data, layout } = this.props;
|
||||
return (
|
||||
<Plot
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
data={data}
|
||||
layout={Object.assign(
|
||||
{},
|
||||
theme === "dark" ? darkLayout : {},
|
||||
layout || {}
|
||||
)}
|
||||
config={{ responsive: true }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// extracted from the plotly website
|
||||
const darkLayout: Partial<Plotly.Layout> = {
|
||||
colorway: [
|
||||
"#636efa",
|
||||
"#EF553B",
|
||||
"#00cc96",
|
||||
"#ab63fa",
|
||||
"#19d3f3",
|
||||
"#e763fa",
|
||||
"#fecb52",
|
||||
"#ffa15a",
|
||||
"#ff6692",
|
||||
"#b6e880",
|
||||
],
|
||||
autosize: true,
|
||||
//margin: { b: 0, t: 0, pad: 0 },
|
||||
//height: 300,
|
||||
/*colorscale: {
|
||||
sequential: [
|
||||
[0, "#0508b8"],
|
||||
[0.0893854748603352, "#1910d8"],
|
||||
[0.1787709497206704, "#3c19f0"],
|
||||
[0.2681564245810056, "#6b1cfb"],
|
||||
[0.3575418994413408, "#981cfd"],
|
||||
[0.44692737430167595, "#bf1cfd"],
|
||||
[0.5363128491620112, "#dd2bfd"],
|
||||
[0.6256983240223464, "#f246fe"],
|
||||
[0.7150837988826816, "#fc67fd"],
|
||||
[0.8044692737430168, "#fe88fc"],
|
||||
[0.8938547486033519, "#fea5fd"],
|
||||
[0.9832402234636871, "#febefe"],
|
||||
[1, "#fec3fe"],
|
||||
],
|
||||
sequentialminus: [
|
||||
[0, "#0508b8"],
|
||||
[0.0893854748603352, "#1910d8"],
|
||||
[0.1787709497206704, "#3c19f0"],
|
||||
[0.2681564245810056, "#6b1cfb"],
|
||||
[0.3575418994413408, "#981cfd"],
|
||||
[0.44692737430167595, "#bf1cfd"],
|
||||
[0.5363128491620112, "#dd2bfd"],
|
||||
[0.6256983240223464, "#f246fe"],
|
||||
[0.7150837988826816, "#fc67fd"],
|
||||
[0.8044692737430168, "#fe88fc"],
|
||||
[0.8938547486033519, "#fea5fd"],
|
||||
[0.9832402234636871, "#febefe"],
|
||||
[1, "#fec3fe"],
|
||||
],
|
||||
diverging: [
|
||||
[0, "#8e0152"],
|
||||
[0.1, "#c51b7d"],
|
||||
[0.2, "#de77ae"],
|
||||
[0.3, "#f1b6da"],
|
||||
[0.4, "#fde0ef"],
|
||||
[0.5, "#f7f7f7"],
|
||||
[0.6, "#e6f5d0"],
|
||||
[0.7, "#b8e186"],
|
||||
[0.8, "#7fbc41"],
|
||||
[0.9, "#4d9221"],
|
||||
[1, "#276419"],
|
||||
],
|
||||
},*/
|
||||
font: { color: "#f2f5fa" },
|
||||
title: { x: 0.05 },
|
||||
paper_bgcolor: "rgb(17,17,17)",
|
||||
plot_bgcolor: "rgb(17,17,17)",
|
||||
hovermode: "closest",
|
||||
polar: {
|
||||
bgcolor: "rgb(17,17,17)",
|
||||
angularaxis: {
|
||||
gridcolor: "#506784",
|
||||
linecolor: "#506784",
|
||||
ticks: "",
|
||||
},
|
||||
radialaxis: {
|
||||
gridcolor: "#506784",
|
||||
linecolor: "#506784",
|
||||
ticks: "",
|
||||
},
|
||||
},
|
||||
ternary: {
|
||||
bgcolor: "rgb(17,17,17)",
|
||||
aaxis: { gridcolor: "#506784", linecolor: "#506784", ticks: "" },
|
||||
baxis: { gridcolor: "#506784", linecolor: "#506784", ticks: "" },
|
||||
caxis: { gridcolor: "#506784", linecolor: "#506784", ticks: "" },
|
||||
},
|
||||
xaxis: {
|
||||
gridcolor: "#283442",
|
||||
linecolor: "#506784",
|
||||
ticks: "",
|
||||
zerolinecolor: "#283442",
|
||||
zerolinewidth: 2,
|
||||
automargin: true,
|
||||
},
|
||||
yaxis: {
|
||||
gridcolor: "#283442",
|
||||
linecolor: "#506784",
|
||||
ticks: "",
|
||||
zerolinecolor: "#283442",
|
||||
zerolinewidth: 2,
|
||||
automargin: true,
|
||||
},
|
||||
scene: {
|
||||
xaxis: {
|
||||
backgroundcolor: "rgb(17,17,17)",
|
||||
gridcolor: "#506784",
|
||||
linecolor: "#506784",
|
||||
showbackground: true,
|
||||
ticks: "",
|
||||
zerolinecolor: "#C8D4E3",
|
||||
gridwidth: 2,
|
||||
},
|
||||
yaxis: {
|
||||
backgroundcolor: "rgb(17,17,17)",
|
||||
gridcolor: "#506784",
|
||||
linecolor: "#506784",
|
||||
showbackground: true,
|
||||
ticks: "",
|
||||
zerolinecolor: "#C8D4E3",
|
||||
gridwidth: 2,
|
||||
},
|
||||
zaxis: {
|
||||
backgroundcolor: "rgb(17,17,17)",
|
||||
gridcolor: "#506784",
|
||||
linecolor: "#506784",
|
||||
showbackground: true,
|
||||
ticks: "",
|
||||
zerolinecolor: "#C8D4E3",
|
||||
gridwidth: 2,
|
||||
},
|
||||
},
|
||||
/*
|
||||
shapedefaults: {
|
||||
fillcolor: "#f2f5fa",
|
||||
line: { width: 0 },
|
||||
opacity: 0.4,
|
||||
},
|
||||
annotationdefaults: {
|
||||
arrowcolor: "#f2f5fa",
|
||||
arrowhead: 0,
|
||||
arrowwidth: 1,
|
||||
},*/
|
||||
geo: {
|
||||
bgcolor: "rgb(17,17,17)",
|
||||
landcolor: "rgb(17,17,17)",
|
||||
subunitcolor: "#506784",
|
||||
showland: true,
|
||||
showlakes: true,
|
||||
lakecolor: "rgb(17,17,17)",
|
||||
},
|
||||
/*
|
||||
updatemenudefaults: { bgcolor: "#506784", borderwidth: 0 },
|
||||
sliderdefaults: {
|
||||
bgcolor: "#C8D4E3",
|
||||
borderwidth: 1,
|
||||
bordercolor: "rgb(17,17,17)",
|
||||
tickwidth: 0,
|
||||
},
|
||||
*/
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
sOpenObject,
|
||||
sLiteral,
|
||||
sArrayOf,
|
||||
sOptionalProp,
|
||||
sAny,
|
||||
sProp,
|
||||
} from "@hediet/semantic-json";
|
||||
import { makeLazyLoadable } from "../../utils/LazyLoadable";
|
||||
import {
|
||||
createVisualizer,
|
||||
ReactVisualization,
|
||||
globalVisualizationFactory,
|
||||
} from "@hediet/visualization-core";
|
||||
|
||||
const PlotlyViewerLazyLoadable = makeLazyLoadable(
|
||||
async () => (await import("./PlotlyViewer")).PlotlyViewer
|
||||
);
|
||||
|
||||
export const plotlyVisualizer = createVisualizer({
|
||||
id: "plotly",
|
||||
name: "plotly",
|
||||
serializer: sOpenObject({
|
||||
kind: sOpenObject({
|
||||
plotly: sLiteral(true),
|
||||
}),
|
||||
data: sProp(sArrayOf(sAny()), {
|
||||
description:
|
||||
"Expecting Plotly.Data[] (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/795ce172038dbafcb9cba030d637d733a7eea19c/types/plotly.js/index.d.ts#L1036)",
|
||||
}),
|
||||
layout: sOptionalProp(sAny(), {
|
||||
description:
|
||||
"Expecting Partial<Plotly.Layout> (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/795ce172038dbafcb9cba030d637d733a7eea19c/types/plotly.js/index.d.ts#L329)",
|
||||
}),
|
||||
}),
|
||||
getVisualization: (data, self) =>
|
||||
new ReactVisualization(self, { priority: 1000 }, theme => (
|
||||
<PlotlyViewerLazyLoadable
|
||||
data={data.data}
|
||||
layout={data.layout}
|
||||
theme={theme.kind}
|
||||
/>
|
||||
)),
|
||||
});
|
||||
|
||||
globalVisualizationFactory.addVisualizer(plotlyVisualizer);
|
||||
@@ -0,0 +1,33 @@
|
||||
import { sOpenObject, sLiteral, sString } from "@hediet/semantic-json";
|
||||
import React from "react";
|
||||
import {
|
||||
createVisualizer,
|
||||
ReactVisualization,
|
||||
globalVisualizationFactory,
|
||||
} from "@hediet/visualization-core";
|
||||
|
||||
export const simpleTextVisualizer = createVisualizer({
|
||||
id: "simple-text",
|
||||
name: "Simple Text",
|
||||
serializer: sOpenObject({
|
||||
kind: sOpenObject({
|
||||
text: sLiteral(true),
|
||||
}),
|
||||
text: sString(),
|
||||
}),
|
||||
getVisualization: (data, self) =>
|
||||
new ReactVisualization(self, { priority: 100 }, () => (
|
||||
<pre
|
||||
className="visualizer-simple-text"
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: 10,
|
||||
color: "var(--visualizer-simple-text-color)",
|
||||
}}
|
||||
>
|
||||
{data.text}
|
||||
</pre>
|
||||
)),
|
||||
});
|
||||
|
||||
globalVisualizationFactory.addVisualizer(simpleTextVisualizer);
|
||||
@@ -0,0 +1,6 @@
|
||||
.theme-dark {
|
||||
--visualizer-simple-text-color: white;
|
||||
}
|
||||
.theme-light {
|
||||
--visualizer-simple-text-color: black;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { sAny } from "@hediet/semantic-json";
|
||||
import React from "react";
|
||||
import {
|
||||
createVisualizer,
|
||||
ReactVisualization,
|
||||
globalVisualizationFactory,
|
||||
} from "@hediet/visualization-core";
|
||||
import { MonacoEditor } from "../monaco-text-visualizer/MonacoEditor";
|
||||
|
||||
export const sourceVisualizer = createVisualizer({
|
||||
id: "source",
|
||||
name: "JSON Source",
|
||||
serializer: sAny(),
|
||||
getVisualization: (data, self) =>
|
||||
new ReactVisualization(self, { priority: -100 }, theme => (
|
||||
<MonacoEditor
|
||||
text={JSON.stringify(data, undefined, 4)}
|
||||
languageId={"json"}
|
||||
theme={theme}
|
||||
/>
|
||||
)),
|
||||
});
|
||||
|
||||
globalVisualizationFactory.addHiddenVisualizer(sourceVisualizer);
|
||||
@@ -0,0 +1,6 @@
|
||||
.theme-dark {
|
||||
--visualizer-simple-text-color: white;
|
||||
}
|
||||
.theme-light {
|
||||
--visualizer-simple-text-color: black;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import Measure, { ContentRect } from "react-measure";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { observable, action } from "mobx";
|
||||
import { Tool, ReactSVGPanZoom, Value } from "react-svg-pan-zoom";
|
||||
|
||||
function widthOrDefault(r: ContentRect): number {
|
||||
if (r.bounds && r.bounds.width) {
|
||||
return r.bounds.width;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
function heightOrDefault(r: ContentRect): number {
|
||||
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 readonly svgPanZoomRef = React.createRef<ReactSVGPanZoom>();
|
||||
|
||||
componentDidMount() {
|
||||
const element = this.svgPanZoomRef.current;
|
||||
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
|
||||
onResize={() => {
|
||||
if (this.svgPanZoomRef.current) {
|
||||
(this.svgPanZoomRef.current.fitToViewer as any)(
|
||||
"center",
|
||||
"center"
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ measureRef, contentRect }) => (
|
||||
<div
|
||||
ref={measureRef}
|
||||
className="svgViewer"
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
>
|
||||
<ReactSVGPanZoom
|
||||
width={widthOrDefault(contentRect)}
|
||||
height={heightOrDefault(contentRect)}
|
||||
tool={tool}
|
||||
value={val as any}
|
||||
onChangeValue={v => (this.value = v)}
|
||||
onChangeTool={this.setTool}
|
||||
ref={this.svgPanZoomRef}
|
||||
toolbarProps={{
|
||||
SVGAlignX: "center",
|
||||
SVGAlignY: "center",
|
||||
}}
|
||||
miniatureProps={{
|
||||
position: "none",
|
||||
height: 0,
|
||||
width: 0,
|
||||
background: "black",
|
||||
}}
|
||||
>
|
||||
<svg width={width} height={height}>
|
||||
<g
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: svgContent,
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
</ReactSVGPanZoom>
|
||||
</div>
|
||||
)}
|
||||
</Measure>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from "react";
|
||||
import { sOpenObject, sLiteral, sString } from "@hediet/semantic-json";
|
||||
import { SvgViewer } from "./SvgViewer";
|
||||
import {
|
||||
createVisualizer,
|
||||
ReactVisualization,
|
||||
globalVisualizationFactory,
|
||||
} from "@hediet/visualization-core";
|
||||
|
||||
export const svgVisualizer = createVisualizer({
|
||||
id: "svg",
|
||||
name: "Svg",
|
||||
serializer: sOpenObject({
|
||||
kind: sOpenObject({
|
||||
svg: sLiteral(true),
|
||||
}),
|
||||
text: sString(),
|
||||
}),
|
||||
getVisualization: (data, self) =>
|
||||
new ReactVisualization(self, { priority: 1500 }, () => (
|
||||
<SvgViewer svgContent={data.text} />
|
||||
)),
|
||||
});
|
||||
|
||||
globalVisualizationFactory.addVisualizer(svgVisualizer);
|
||||
@@ -0,0 +1,5 @@
|
||||
.svgViewer {
|
||||
svg {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
256
visualization-bundle/src/visualizers/tree-visualizer/Point.ts
Normal file
256
visualization-bundle/src/visualizers/tree-visualizer/Point.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
function sqr(a: number) {
|
||||
return a * a;
|
||||
}
|
||||
|
||||
export type PointLike =
|
||||
| Point
|
||||
| { x?: number; y: number }
|
||||
| { x: number; y?: number };
|
||||
|
||||
export function point(data: PointLike) {
|
||||
if (data instanceof Point) {
|
||||
return data;
|
||||
}
|
||||
return new Point(data.x || 0, data.y || 0);
|
||||
}
|
||||
|
||||
export class Point {
|
||||
public static readonly Zero = new Point(0, 0);
|
||||
|
||||
constructor(public readonly x: number, public readonly y: number) {}
|
||||
|
||||
public mapXY(fn: (arg: number) => number): Point {
|
||||
return new Point(fn(this.x), fn(this.y));
|
||||
}
|
||||
|
||||
public distance(other: PointLike = Point.Zero): number {
|
||||
const d = this.sub(other);
|
||||
return Math.sqrt(sqr(d.x) + sqr(d.y));
|
||||
}
|
||||
|
||||
public sub(other: PointLike): Point {
|
||||
const o = point(other);
|
||||
return new Point(this.x - o.x, this.y - o.y);
|
||||
}
|
||||
|
||||
public add(other: PointLike): Point {
|
||||
const o = point(other);
|
||||
return new Point(this.x + o.x, this.y + o.y);
|
||||
}
|
||||
|
||||
public mul(scalar: number): Point {
|
||||
return new Point(this.x * scalar, this.y * scalar);
|
||||
}
|
||||
|
||||
public div(scalar: number): Point {
|
||||
return new Point(this.x / scalar, this.y / scalar);
|
||||
}
|
||||
|
||||
public equals(other: PointLike) {
|
||||
const o = point(other);
|
||||
return this.x === o.x && this.y === o.y;
|
||||
}
|
||||
|
||||
public angle(): number {
|
||||
const angle = Math.atan2(this.x, this.y);
|
||||
return angle;
|
||||
}
|
||||
|
||||
public getPointCloserTo(dest: PointLike, dist: number): Point {
|
||||
if (this.equals(dest)) return this;
|
||||
|
||||
const angle = point(dest)
|
||||
.sub(this)
|
||||
.angle();
|
||||
|
||||
const result = new Point(
|
||||
this.x + Math.sin(angle) * dist,
|
||||
this.y + Math.cos(angle) * dist
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
public toJson(): { x: number; y: number } {
|
||||
return { x: this.x, y: this.y };
|
||||
}
|
||||
}
|
||||
|
||||
function turn(p1: Point, p2: Point, p3: Point): number {
|
||||
const A = (p3.y - p1.y) * (p2.x - p1.x);
|
||||
const B = (p2.y - p1.y) * (p3.x - p1.x);
|
||||
return A > B + Number.MIN_VALUE ? 1 : A + Number.MIN_VALUE < B ? -1 : 0;
|
||||
}
|
||||
|
||||
export function isIntersect(
|
||||
aStart: Point,
|
||||
aEnd: Point,
|
||||
bStart: Point,
|
||||
bEnd: Point
|
||||
): boolean {
|
||||
return (
|
||||
turn(aStart, bStart, bEnd) != turn(aEnd, bStart, bEnd) &&
|
||||
turn(aStart, aEnd, bStart) != turn(aStart, aEnd, bEnd)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liang-Barsky function by Daniel White
|
||||
*
|
||||
* @link http://www.skytopia.com/project/articles/compsci/clipping.html
|
||||
*
|
||||
* @param {number} x0
|
||||
* @param {number} y0
|
||||
* @param {number} x1
|
||||
* @param {number} y1
|
||||
* @param {array<number>} bbox
|
||||
* @return {array<array<number>>|null}
|
||||
*/
|
||||
function liangBarsky(
|
||||
x0: number,
|
||||
y0: number,
|
||||
x1: number,
|
||||
y1: number,
|
||||
bbox: [number, number, number, number]
|
||||
): [[number, number], [number, number]] | null {
|
||||
let [xmin, xmax, ymin, ymax] = bbox;
|
||||
let t0 = 0,
|
||||
t1 = 1;
|
||||
let dx = x1 - x0,
|
||||
dy = y1 - y0;
|
||||
let p = 0,
|
||||
q = 0,
|
||||
r = 0;
|
||||
|
||||
for (let edge: 0 | 1 | 2 | 3 = 0; edge < 4; edge++) {
|
||||
// Traverse through left, right, bottom, top edges.
|
||||
if (edge === 0) {
|
||||
p = -dx;
|
||||
q = -(xmin - x0);
|
||||
} else if (edge === 1) {
|
||||
p = dx;
|
||||
q = xmax - x0;
|
||||
} else if (edge === 2) {
|
||||
p = -dy;
|
||||
q = -(ymin - y0);
|
||||
} else if (edge === 3) {
|
||||
p = dy;
|
||||
q = ymax - y0;
|
||||
}
|
||||
|
||||
r = q / p;
|
||||
|
||||
if (p === 0 && q < 0) return null; // Don't draw line at all. (parallel line outside)
|
||||
|
||||
if (p < 0) {
|
||||
if (r > t1) return null;
|
||||
// Don't draw line at all.
|
||||
else if (r > t0) t0 = r; // Line is clipped!
|
||||
} else if (p > 0) {
|
||||
if (r < t0) return null;
|
||||
// Don't draw line at all.
|
||||
else if (r < t1) t1 = r; // Line is clipped!
|
||||
}
|
||||
}
|
||||
|
||||
return [[x0 + t0 * dx, y0 + t0 * dy], [x0 + t1 * dx, y0 + t1 * dy]];
|
||||
}
|
||||
|
||||
export function intersectRectWithLine(
|
||||
start: Point,
|
||||
end: Point,
|
||||
rect: Rectangle
|
||||
): { first: Point; second: Point } | undefined {
|
||||
const r = liangBarsky(start.x, start.y, end.x, end.y, [
|
||||
rect.topLeft.x,
|
||||
rect.bottomRight.x,
|
||||
rect.topLeft.y,
|
||||
rect.bottomRight.y,
|
||||
]);
|
||||
if (!r) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
first: new Point(r[0][0], r[0][1]),
|
||||
second: new Point(r[1][0], r[1][1]),
|
||||
};
|
||||
}
|
||||
|
||||
// first zoom then translate
|
||||
export function scale(
|
||||
clientOffset: Point,
|
||||
clientSize: Point,
|
||||
viewSize: Point
|
||||
): { clientZoom: number; clientOffset: Point } {
|
||||
const clientRatio = clientSize.x / clientSize.y;
|
||||
const viewRatio = viewSize.x / viewSize.y;
|
||||
|
||||
let zoom = 1;
|
||||
|
||||
if (clientRatio < viewRatio) zoom = viewSize.y / clientSize.y;
|
||||
else zoom = viewSize.x / clientSize.x;
|
||||
|
||||
const clientMid = clientOffset.mul(zoom).add(clientSize.mul(zoom / 2));
|
||||
const viewMid = viewSize.div(2);
|
||||
|
||||
const clientOffset2 = viewMid.sub(clientMid);
|
||||
|
||||
return { clientOffset: clientOffset2, clientZoom: zoom };
|
||||
}
|
||||
|
||||
export class Rectangle {
|
||||
public static ofSize(position: PointLike, size: PointLike): Rectangle {
|
||||
const pos = point(position);
|
||||
return new Rectangle(pos, pos.add(size));
|
||||
}
|
||||
|
||||
public static spanning(first: Point, ...rest: Point[]): Rectangle {
|
||||
let min = first.toJson();
|
||||
let max = first.toJson();
|
||||
for (const p of rest) {
|
||||
min.x = Math.min(min.x, p.x);
|
||||
min.y = Math.min(min.y, p.y);
|
||||
max.x = Math.max(max.x, p.x);
|
||||
max.y = Math.max(max.y, p.y);
|
||||
}
|
||||
|
||||
return new Rectangle(point(min), point(max));
|
||||
}
|
||||
|
||||
constructor(
|
||||
public readonly topLeft: Point,
|
||||
public readonly bottomRight: Point
|
||||
) {}
|
||||
|
||||
get center(): Point {
|
||||
return this.bottomRight.add(this.topLeft).div(2);
|
||||
}
|
||||
|
||||
get size(): Point {
|
||||
return this.bottomRight.sub(this.topLeft);
|
||||
}
|
||||
|
||||
get topRight(): Point {
|
||||
return new Point(this.bottomRight.x, this.topLeft.y);
|
||||
}
|
||||
|
||||
get bottomLeft(): Point {
|
||||
return new Point(this.topLeft.x, this.bottomRight.y);
|
||||
}
|
||||
|
||||
public intersects(selectionRect: Rectangle): boolean {
|
||||
return !(
|
||||
selectionRect.topLeft.x > this.bottomRight.x ||
|
||||
selectionRect.bottomRight.x < this.topLeft.x ||
|
||||
selectionRect.topLeft.y > this.bottomRight.y ||
|
||||
selectionRect.bottomRight.y < this.topLeft.y
|
||||
);
|
||||
}
|
||||
|
||||
equals(other: Rectangle) {
|
||||
return (
|
||||
this.topLeft.equals(other.topLeft) &&
|
||||
this.bottomRight.equals(other.bottomRight)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Point, Rectangle } from "./Point";
|
||||
import * as React from "react";
|
||||
import { Properties } from "csstype";
|
||||
|
||||
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
|
||||
|
||||
export function omit<T, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {
|
||||
const newObj: Omit<T, K> = {} as any;
|
||||
for (const [key, val] of Object.entries(obj)) {
|
||||
if (!keys.includes(key as K)) {
|
||||
(newObj as any)[key] = val;
|
||||
}
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
|
||||
export interface SvgAttributes<T> extends React.DOMAttributes<T> {
|
||||
stroke?: string;
|
||||
className?: string;
|
||||
style?: Properties;
|
||||
pointerEvents?: "none" | "auto" | "initial";
|
||||
}
|
||||
|
||||
export function SvgText(
|
||||
props: {
|
||||
position: Point;
|
||||
children: any;
|
||||
childRef?: React.Ref<SVGTextElement>;
|
||||
textAnchor?: "middle" | "end" | "start";
|
||||
dominantBaseline?: "central" | "middle";
|
||||
} & SvgAttributes<SVGTextElement>
|
||||
) {
|
||||
return (
|
||||
<text
|
||||
x={props.position.x}
|
||||
y={props.position.y}
|
||||
ref={props.childRef}
|
||||
{...omit(props, ["position", "childRef"])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SvgCircle(
|
||||
props: {
|
||||
center: Point;
|
||||
radius: number;
|
||||
childRef?: React.Ref<SVGCircleElement>;
|
||||
} & SvgAttributes<SVGCircleElement>
|
||||
) {
|
||||
return (
|
||||
<circle
|
||||
cx={props.center.x}
|
||||
cy={props.center.y}
|
||||
r={props.radius}
|
||||
ref={props.childRef}
|
||||
{...omit(props, ["center", "radius", "childRef"])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SvgLine(
|
||||
props: { start: Point; end: Point } & SvgAttributes<SVGLineElement>
|
||||
) {
|
||||
return (
|
||||
<line
|
||||
x1={props.start.x}
|
||||
y1={props.start.y}
|
||||
x2={props.end.x}
|
||||
y2={props.end.y}
|
||||
{...omit(props, ["end", "start"])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SvgRect(
|
||||
props: { rectangle: Rectangle; fill?: string } & SvgAttributes<
|
||||
SVGRectElement
|
||||
>
|
||||
) {
|
||||
const r = props.rectangle;
|
||||
return (
|
||||
<rect
|
||||
x={r.topLeft.x}
|
||||
y={r.topLeft.y}
|
||||
width={r.size.x}
|
||||
height={r.size.y}
|
||||
{...omit(props, ["rectangle"])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
545
visualization-bundle/src/visualizers/tree-visualizer/Views.tsx
Normal file
545
visualization-bundle/src/visualizers/tree-visualizer/Views.tsx
Normal file
@@ -0,0 +1,545 @@
|
||||
import { observer, disposeOnUnmount } from "mobx-react";
|
||||
import { observable, autorun, computed, action, runInAction } from "mobx";
|
||||
import * as React from "react";
|
||||
import * as classNames from "classnames";
|
||||
import { Rectangle, Point } from "./Point";
|
||||
import { SvgLine } from "./SvgElements";
|
||||
import { Item } from ".";
|
||||
|
||||
// import "./style.scss";
|
||||
|
||||
export class TreeViewModel<TData = unknown> {
|
||||
private started = true;
|
||||
start() {
|
||||
this.started = true;
|
||||
this.updateOffsets();
|
||||
}
|
||||
stop() {
|
||||
this.started = false;
|
||||
}
|
||||
|
||||
private version = 0;
|
||||
private lastUpdateVersion = -1;
|
||||
|
||||
@observable public root: TreeNodeViewModel<TData> | undefined = undefined;
|
||||
|
||||
public invalidateOffsets() {
|
||||
this.version++;
|
||||
}
|
||||
|
||||
@action
|
||||
public updateOffsets() {
|
||||
if (!this.started) {
|
||||
return;
|
||||
}
|
||||
if (!this.root) {
|
||||
return;
|
||||
}
|
||||
if (this.lastUpdateVersion === this.version) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastUpdateVersion = this.version;
|
||||
console.log("update");
|
||||
this.root.updateOffsets();
|
||||
}
|
||||
|
||||
public get marked(): TreeNodeViewModel<TData>[] {
|
||||
const result = new Array<TreeNodeViewModel<TData>>();
|
||||
function recurse(node: TreeNodeViewModel<TData>) {
|
||||
if (node.isMarked) {
|
||||
result.push(node);
|
||||
}
|
||||
for (const c of node.children) {
|
||||
recurse(c);
|
||||
}
|
||||
}
|
||||
if (this.root) {
|
||||
recurse(this.root);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@observable selected: TreeNodeViewModel<TData> | undefined = undefined;
|
||||
|
||||
@action public select(current: TreeNodeViewModel<TData> | undefined) {
|
||||
if (this.selected === current) {
|
||||
return;
|
||||
}
|
||||
if (this.selected) {
|
||||
this.selected.isSelected = false;
|
||||
}
|
||||
if (current) {
|
||||
current.isSelected = true;
|
||||
}
|
||||
this.selected = current;
|
||||
}
|
||||
|
||||
@action public toggleSelect(current: TreeNodeViewModel<TData>) {
|
||||
if (this.selected === current) {
|
||||
this.select(undefined);
|
||||
} else {
|
||||
this.select(current);
|
||||
}
|
||||
}
|
||||
|
||||
private selectOnHoverEnabled = false;
|
||||
private selectedBeforeHover:
|
||||
| TreeNodeViewModel<TData>
|
||||
| undefined = undefined;
|
||||
|
||||
@action public handleHoverEvent(
|
||||
current: TreeNodeViewModel<TData> | undefined
|
||||
) {
|
||||
if (this.selectOnHoverEnabled) {
|
||||
if (current) {
|
||||
this.select(current);
|
||||
} else {
|
||||
this.select(this.selectedBeforeHover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setSelectOnHover(enable: boolean) {
|
||||
if (this.selectOnHoverEnabled === enable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (enable) {
|
||||
this.selectedBeforeHover = this.selected;
|
||||
} else if (!this.selected) {
|
||||
this.select(this.selectedBeforeHover);
|
||||
}
|
||||
|
||||
this.selectOnHoverEnabled = enable;
|
||||
}
|
||||
}
|
||||
|
||||
export class TreeNodeViewModel<TData = unknown> {
|
||||
public parent: TreeNodeViewModel<TData> | null = null;
|
||||
|
||||
constructor(
|
||||
public readonly treeViewModel: TreeViewModel<TData>,
|
||||
public readonly children: TreeNodeViewModel<TData>[],
|
||||
public readonly items: Item[],
|
||||
public readonly segment: string,
|
||||
public readonly data: TData
|
||||
) {}
|
||||
|
||||
@observable expanderOffsetRect: Rectangle | undefined = undefined;
|
||||
private expanderDiv: HTMLDivElement | null = null;
|
||||
|
||||
@action.bound
|
||||
setExpanderDiv(div: HTMLDivElement | null) {
|
||||
this.expanderDiv = div;
|
||||
this.treeViewModel.invalidateOffsets();
|
||||
}
|
||||
|
||||
@observable public expanded: boolean = false;
|
||||
@observable public isMarked: boolean = false;
|
||||
@observable public isSelected: boolean = false;
|
||||
|
||||
@action
|
||||
public toggleExpanded() {
|
||||
this.expanded = !this.expanded;
|
||||
this.treeViewModel.invalidateOffsets();
|
||||
}
|
||||
|
||||
@computed public get isChildOrThisMarked(): boolean {
|
||||
if (this.isMarked) return true;
|
||||
return this.children.some(c => c.isChildOrThisMarked);
|
||||
}
|
||||
|
||||
@computed public get isChildOrThisSelected(): boolean {
|
||||
if (this.isSelected) return true;
|
||||
return this.children.some(c => c.isChildOrThisSelected);
|
||||
}
|
||||
|
||||
@computed public get isParentOrThisSelected(): boolean {
|
||||
if (this.isSelected) return true;
|
||||
if (this.parent) return this.parent.isParentOrThisSelected;
|
||||
return false;
|
||||
}
|
||||
|
||||
get path(): string[] {
|
||||
const result = new Array<string>();
|
||||
let c: TreeNodeViewModel = this;
|
||||
while (c.parent) {
|
||||
result.unshift(c.segment);
|
||||
c = c.parent;
|
||||
}
|
||||
if (result.length === 0) {
|
||||
result.push("root");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
updateOffsets() {
|
||||
for (const c of this.children) {
|
||||
c.updateOffsets();
|
||||
}
|
||||
|
||||
if (!this.expanderDiv) {
|
||||
return;
|
||||
}
|
||||
const newRect = Rectangle.ofSize(
|
||||
new Point(this.expanderDiv.offsetLeft, this.expanderDiv.offsetTop),
|
||||
new Point(
|
||||
this.expanderDiv.offsetWidth,
|
||||
this.expanderDiv.offsetHeight
|
||||
)
|
||||
);
|
||||
if (
|
||||
!this.expanderOffsetRect ||
|
||||
!newRect.equals(this.expanderOffsetRect)
|
||||
) {
|
||||
this.expanderOffsetRect = newRect;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isValidFunctionName = (function() {
|
||||
var validName = /^[$A-Z_][0-9A-Z_$]*$/i;
|
||||
var reserved = {
|
||||
abstract: true,
|
||||
boolean: true,
|
||||
// ...
|
||||
with: true,
|
||||
} as any;
|
||||
return function(s: string) {
|
||||
// Ensure a valid name and not reserved.
|
||||
return validName.test(s) && !reserved[s];
|
||||
};
|
||||
})();
|
||||
|
||||
@observer
|
||||
export class TreeWithPathView extends React.Component<{
|
||||
model: TreeViewModel;
|
||||
}> {
|
||||
render() {
|
||||
const model = this.props.model;
|
||||
return (
|
||||
<div className="component-TreeWithPathView">
|
||||
<div className="part-path">
|
||||
<span>
|
||||
{model.selected ? (
|
||||
<span>
|
||||
{model.selected.path.reduce((acc, v) => {
|
||||
acc = acc.slice();
|
||||
function add() {
|
||||
acc.push(
|
||||
<span
|
||||
className="part-path-item"
|
||||
key={acc.length}
|
||||
>
|
||||
{v}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (isValidFunctionName(v)) {
|
||||
if (acc.length > 0) {
|
||||
acc.push(
|
||||
<span key={acc.length}>.</span>
|
||||
);
|
||||
}
|
||||
add();
|
||||
} else {
|
||||
acc.push(
|
||||
<span key={acc.length}>[</span>
|
||||
);
|
||||
add();
|
||||
acc.push(
|
||||
<span key={acc.length}>]</span>
|
||||
);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, new Array<React.ReactElement>())}
|
||||
</span>
|
||||
) : (
|
||||
"(Nothing Selected)"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="part-tree">
|
||||
<TreeView model={model} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
export class TreeView extends React.Component<{ model: TreeViewModel }> {
|
||||
componentWillUpdate(newProps: this["props"]) {
|
||||
newProps.model.stop();
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.props.model.stop();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.props.model.start();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.model.start();
|
||||
}
|
||||
|
||||
render() {
|
||||
const model = this.props.model;
|
||||
return (
|
||||
<div
|
||||
tabIndex={0}
|
||||
className="component-TreeView"
|
||||
onKeyDown={e => {
|
||||
if (e.keyCode === 18) {
|
||||
// alt
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
model.setSelectOnHover(true);
|
||||
}
|
||||
}}
|
||||
onKeyUp={e => {
|
||||
if (e.keyCode === 18) {
|
||||
// alt
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
model.setSelectOnHover(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{model.root && (
|
||||
<>
|
||||
<svg className="part-svg">
|
||||
<TreeNodeSvgView model={model.root} />
|
||||
</svg>
|
||||
<div className="part-node">
|
||||
<TreeNodeView model={model.root} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
export class TreeNodeSvgView extends React.Component<{
|
||||
model: TreeNodeViewModel;
|
||||
}> {
|
||||
render() {
|
||||
const model = this.props.model;
|
||||
if (
|
||||
!model.expanderOffsetRect ||
|
||||
model.children.length === 0 ||
|
||||
model.expanded
|
||||
) {
|
||||
return <g />;
|
||||
}
|
||||
|
||||
function findLastExpanderOffset(
|
||||
predicate: (v: TreeNodeViewModel) => boolean
|
||||
): Rectangle | undefined {
|
||||
const all = model.children
|
||||
.filter(predicate)
|
||||
.map(c => c.expanderOffsetRect);
|
||||
const last = all[all.length - 1];
|
||||
return last;
|
||||
}
|
||||
|
||||
const lastOffset = findLastExpanderOffset(() => true);
|
||||
const selectedOffset = findLastExpanderOffset(
|
||||
v => v.isChildOrThisSelected
|
||||
);
|
||||
const markedOffset = findLastExpanderOffset(v => v.isChildOrThisMarked);
|
||||
|
||||
if (!lastOffset) {
|
||||
return <g />;
|
||||
}
|
||||
|
||||
const start = model.expanderOffsetRect.center.add({ y: 10 });
|
||||
|
||||
return (
|
||||
<g className="component-TreeNodeSvgView">
|
||||
{markedOffset && (
|
||||
<SvgLine
|
||||
className="path childOrThisMarked"
|
||||
start={start}
|
||||
end={new Point(start.x, markedOffset.center.y)}
|
||||
/>
|
||||
)}
|
||||
{selectedOffset && (
|
||||
<SvgLine
|
||||
className="path childOrThisSelected"
|
||||
start={start}
|
||||
end={new Point(start.x, selectedOffset.center.y)}
|
||||
/>
|
||||
)}
|
||||
<SvgLine
|
||||
className="path"
|
||||
start={start}
|
||||
end={new Point(start.x, lastOffset.center.y)}
|
||||
/>
|
||||
{model.children.map((c, idx) => {
|
||||
const end = c.expanderOffsetRect!.center.sub({ x: 5 });
|
||||
|
||||
return (
|
||||
<g key={idx}>
|
||||
{c.isChildOrThisMarked && (
|
||||
<SvgLine
|
||||
className={classNames(
|
||||
"path",
|
||||
"childOrThisMarked"
|
||||
)}
|
||||
start={new Point(start.x, end.y)}
|
||||
end={end}
|
||||
/>
|
||||
)}
|
||||
<SvgLine
|
||||
className={classNames(
|
||||
"path",
|
||||
c.isChildOrThisSelected &&
|
||||
"childOrThisSelected"
|
||||
)}
|
||||
start={new Point(start.x, end.y)}
|
||||
end={end}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
{model.children.map((c, idx) => (
|
||||
<TreeNodeSvgView key={idx} model={c} />
|
||||
))}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
export class TreeNodeView extends React.Component<{
|
||||
model: TreeNodeViewModel;
|
||||
}> {
|
||||
@action.bound
|
||||
private clickHandler() {
|
||||
this.props.model.treeViewModel.toggleSelect(this.props.model);
|
||||
}
|
||||
|
||||
private handleMouseEnterOrLeave(enter: boolean) {
|
||||
this.props.model.treeViewModel.handleHoverEvent(
|
||||
enter ? this.props.model : undefined
|
||||
);
|
||||
}
|
||||
|
||||
@observable
|
||||
private rootDiv: HTMLElement | null = null;
|
||||
@action.bound
|
||||
private setRootDiv(div: HTMLElement | null) {
|
||||
this.rootDiv = div;
|
||||
}
|
||||
|
||||
@disposeOnUnmount
|
||||
private readonly _autorun = autorun(() => {
|
||||
if (this.props.model.isMarked && this.rootDiv) {
|
||||
this.rootDiv.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
});
|
||||
|
||||
public componentDidMount() {
|
||||
this.props.model.treeViewModel.updateOffsets();
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
this.props.model.treeViewModel.updateOffsets();
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const model = this.props.model;
|
||||
const collapsed = model.expanded;
|
||||
console.log("render");
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"component-TreeNodeView",
|
||||
model.expanded && "collapsed",
|
||||
model.isMarked && "selected",
|
||||
model.isSelected && "hovered",
|
||||
model.isChildOrThisSelected &&
|
||||
!model.isSelected &&
|
||||
"childHovered",
|
||||
model.isParentOrThisSelected &&
|
||||
!model.isSelected &&
|
||||
"parentHovered"
|
||||
)}
|
||||
ref={this.setRootDiv}
|
||||
>
|
||||
<div
|
||||
className="part-header"
|
||||
onMouseOver={e => {
|
||||
this.handleMouseEnterOrLeave(true);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onMouseOut={e => {
|
||||
this.handleMouseEnterOrLeave(false);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={model.setExpanderDiv}
|
||||
onClick={() => model.toggleExpanded()}
|
||||
className={classNames(
|
||||
"part-collapser",
|
||||
model.children.length > 0 &&
|
||||
"decorator-collapse-icon",
|
||||
!collapsed && "expanded"
|
||||
)}
|
||||
/>
|
||||
|
||||
<span className="part-text" onClick={this.clickHandler}>
|
||||
{renderItems(model.items)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{model.children.length > 0 && !model.expanded && (
|
||||
<div className="part-children">
|
||||
{model.children.map((c, idx) => (
|
||||
<TreeNodeView model={c} key={idx} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderItems(items: Item[]): React.ReactNode {
|
||||
return items.map((i, idx) => {
|
||||
switch (i.emphasis) {
|
||||
case "style1":
|
||||
return (
|
||||
<span key={idx} className="style1">
|
||||
{i.text}
|
||||
</span>
|
||||
);
|
||||
case "style2":
|
||||
return (
|
||||
<span key={idx} className="style2">
|
||||
{i.text}
|
||||
</span>
|
||||
);
|
||||
case "style3":
|
||||
return (
|
||||
<span key={idx} className="style3">
|
||||
{i.text}
|
||||
</span>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<span key={idx} className="unstyled">
|
||||
{i.text}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
121
visualization-bundle/src/visualizers/tree-visualizer/index.tsx
Normal file
121
visualization-bundle/src/visualizers/tree-visualizer/index.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import {
|
||||
TreeViewModel,
|
||||
TreeNodeViewModel,
|
||||
TreeView,
|
||||
TreeWithPathView,
|
||||
} from "./Views";
|
||||
import * as React from "react";
|
||||
import {
|
||||
sOpenObject,
|
||||
sLiteral,
|
||||
sLazy,
|
||||
sString,
|
||||
sBoolean,
|
||||
sArrayOf,
|
||||
Serializer,
|
||||
sOptionalProp,
|
||||
sUnion,
|
||||
} from "@hediet/semantic-json";
|
||||
import {
|
||||
createVisualizer,
|
||||
ReactVisualization,
|
||||
globalVisualizationFactory,
|
||||
} from "@hediet/visualization-core";
|
||||
import { visualizationNs } from "../../consts";
|
||||
|
||||
type TreeNode<T = {}> = {
|
||||
children: TreeNode<T>[];
|
||||
items: Item[];
|
||||
segment?: string; // like "root", ".name" or "[0]"
|
||||
isMarked?: boolean;
|
||||
} & T;
|
||||
|
||||
export interface Item {
|
||||
text: string;
|
||||
emphasis?: "style1" | "style2" | "style3" | string;
|
||||
}
|
||||
|
||||
const sTreeNode: Serializer<TreeNode> = sLazy(() =>
|
||||
sOpenObject({
|
||||
children: sArrayOf(sTreeNode),
|
||||
items: sArrayOf(
|
||||
sOpenObject({
|
||||
text: sString(),
|
||||
emphasis: sOptionalProp(
|
||||
sUnion(
|
||||
[
|
||||
sLiteral("style1"),
|
||||
sLiteral("style2"),
|
||||
sLiteral("style3"),
|
||||
sString(),
|
||||
],
|
||||
{ inclusive: true }
|
||||
)
|
||||
),
|
||||
}).defineAs(visualizationNs("TreeNodeItem"))
|
||||
),
|
||||
segment: sOptionalProp(sString()),
|
||||
isMarked: sOptionalProp(sBoolean()),
|
||||
}).defineAs(visualizationNs("TreeNode"))
|
||||
);
|
||||
|
||||
const sTree = sOpenObject({
|
||||
kind: sOpenObject({
|
||||
tree: sLiteral(true),
|
||||
}),
|
||||
root: sTreeNode,
|
||||
});
|
||||
|
||||
export const treeVisualizer = createVisualizer({
|
||||
id: "tree",
|
||||
name: "Tree",
|
||||
serializer: sTree,
|
||||
getVisualization: (data, self) =>
|
||||
new ReactVisualization(self, { priority: 1200 }, () => {
|
||||
const m = createTreeViewModelFromTreeNodeData(
|
||||
data.root,
|
||||
() => undefined
|
||||
);
|
||||
return <TreeWithPathView model={m} />;
|
||||
}),
|
||||
});
|
||||
|
||||
globalVisualizationFactory.addVisualizer(treeVisualizer);
|
||||
|
||||
export function createTreeViewModelFromTreeNodeData<TData, TNodeData>(
|
||||
root: TreeNode<TNodeData>,
|
||||
dataSelector: (node: TreeNode<TNodeData>) => TData
|
||||
): TreeViewModel<TData> {
|
||||
const m = new TreeViewModel<TData>();
|
||||
m.root = recurse(root, m);
|
||||
return m;
|
||||
|
||||
function recurse(
|
||||
node: TreeNode<TNodeData>,
|
||||
viewModel: TreeViewModel<TData>
|
||||
): TreeNodeViewModel<TData> {
|
||||
const children: TreeNodeViewModel<TData>[] = node.children.map(c =>
|
||||
recurse(c, viewModel)
|
||||
);
|
||||
|
||||
let segment = node.segment;
|
||||
if (!segment && node.items.length > 0) {
|
||||
segment = node.items[0].text;
|
||||
} else {
|
||||
segment = "";
|
||||
}
|
||||
|
||||
const model = new TreeNodeViewModel<TData>(
|
||||
viewModel,
|
||||
children,
|
||||
node.items,
|
||||
segment,
|
||||
dataSelector(node)
|
||||
);
|
||||
model.isMarked = !!node.isMarked;
|
||||
for (const c of children) {
|
||||
c.parent = model;
|
||||
}
|
||||
return model;
|
||||
}
|
||||
}
|
||||
162
visualization-bundle/src/visualizers/tree-visualizer/style.scss
Normal file
162
visualization-bundle/src/visualizers/tree-visualizer/style.scss
Normal file
@@ -0,0 +1,162 @@
|
||||
.theme-dark {
|
||||
--visualizer-tree-childOrThisMarked-background: orange;
|
||||
--visualizer-tree-childOrThisSelected-background: white;
|
||||
--visualizer-tree-pathColor: gray;
|
||||
|
||||
--visualizer-tree-style1-foreground: #5594ea;
|
||||
--visualizer-tree-style2-foreground: #c88544; //#38b968;
|
||||
--visualizer-tree-style3-foreground: darkred;
|
||||
--visualizer-tree-style3-border: black;
|
||||
--visualizer-tree-unstyled-foreground: lightgray;
|
||||
--visualizer-tree-path-foreground: white;
|
||||
}
|
||||
|
||||
.theme-light {
|
||||
--visualizer-tree-childOrThisMarked-background: orange;
|
||||
--visualizer-tree-childOrThisSelected-background: black;
|
||||
--visualizer-tree-pathColor: gray;
|
||||
|
||||
--visualizer-tree-style1-foreground: blue;
|
||||
--visualizer-tree-style2-foreground: darkgreen;
|
||||
--visualizer-tree-style3-foreground: darkred;
|
||||
--visualizer-tree-style3-border: black;
|
||||
--visualizer-tree-unstyled-foreground: black;
|
||||
--visualizer-tree-path-foreground: black;
|
||||
}
|
||||
|
||||
.component-TreeView {
|
||||
overflow: auto;
|
||||
scroll-behavior: smooth;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: grid;
|
||||
|
||||
& > .part-svg,
|
||||
& > .part-node {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.component-TreeNodeSvgView {
|
||||
.path {
|
||||
stroke: var(--visualizer-tree-pathColor);
|
||||
stroke-dasharray: 1;
|
||||
|
||||
&.childOrThisMarked {
|
||||
stroke: var(--visualizer-tree-childOrThisMarked-background);
|
||||
stroke-width: 5;
|
||||
stroke-dasharray: 0;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
&.childOrThisSelected {
|
||||
stroke: var(--visualizer-tree-childOrThisSelected-background);
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.component-TreeNodeView {
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe WPC, Segoe UI,
|
||||
HelveticaNeue-Light, Ubuntu, Droid Sans, sans-serif;
|
||||
|
||||
.part-header {
|
||||
padding: 2px;
|
||||
padding-left: 5px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.part-collapser {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.part-text {
|
||||
cursor: pointer;
|
||||
span.style1 {
|
||||
color: var(--visualizer-tree-style1-foreground);
|
||||
}
|
||||
span.style2 {
|
||||
color: var(--visualizer-tree-style2-foreground);
|
||||
text-decoration: underline;
|
||||
padding: 1px 3px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
span.style3 {
|
||||
color: var(--visualizer-tree-style3-foreground);
|
||||
border: var(--visualizer-tree-style3-border) solid 1px;
|
||||
padding: 1px 3px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
span.unstyled {
|
||||
color: var(--visualizer-tree-unstyled-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.part-children {
|
||||
margin-left: 0px;
|
||||
margin-top: 0px;
|
||||
|
||||
padding: 5px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
& > .part-header {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
&.hovered {
|
||||
& > .part-header > span {
|
||||
font-weight: bold;
|
||||
}
|
||||
& > .part-children {
|
||||
background: transparentize(gray, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.component-TreeWithPathView {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.part-path {
|
||||
overflow: hidden;
|
||||
white-space: pre;
|
||||
padding: 5px 10px;
|
||||
border-bottom: solid gray 1px;
|
||||
|
||||
color: lightgray;
|
||||
|
||||
.part-path-item {
|
||||
color: var(--visualizer-tree-path-foreground);
|
||||
margin-left: 1px;
|
||||
margin-right: 1px;
|
||||
}
|
||||
}
|
||||
.part-tree {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.decorator-collapse-icon {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath fill='%23646465' d='M6 4v8l4-4-4-4zm1 2.414L8.586 8 7 9.586V6.414z'/%3E%3C/svg%3E");
|
||||
|
||||
background-position: 50%;
|
||||
background-position-x: 50%;
|
||||
background-position-y: center;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
&.expanded {
|
||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath fill='%23646465' d='M11 10.07H5.344L11 4.414v5.656z'/%3E%3C/svg%3E");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user