@hpcc-js/graph 3.8.3 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{dagre-D_vAZIQ-.js.map → dagre-Ch4XIoOw.js.map} +1 -1
- package/dist/assets/graphviz-C6pqwU-h.js.map +1 -0
- package/dist/index.js +252 -22
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +4 -4
- package/dist/index.umd.cjs.map +1 -1
- package/package.json +10 -10
- package/src/graphviz/Widget.css +138 -0
- package/src/graphviz/Widget.ts +426 -0
- package/src/graphviz/index.ts +1 -0
- package/src/index.ts +1 -0
- package/types/graphviz/Widget.d.ts +49 -0
- package/types/graphviz/index.d.ts +1 -0
- package/types/index.d.ts +1 -0
- package/dist/assets/graphviz-CE2MKOxL.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hpcc-js/graph",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.0",
|
|
4
4
|
"description": "hpcc-js - Viz Graph",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.umd.cjs",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"bundle-workers": "vite build -c ./workers/vite.config.ts",
|
|
27
27
|
"bundle-watch": "vite build --watch",
|
|
28
28
|
"bundle-serve": "vite --port 5509",
|
|
29
|
-
"gen-types": "
|
|
29
|
+
"gen-types": "tsc --project tsconfig.json",
|
|
30
30
|
"gen-types-watch": "npm run gen-types -- --watch",
|
|
31
31
|
"build": "run-p gen-types bundle",
|
|
32
32
|
"watch": "run-p gen-types-watch bundle-watch",
|
|
@@ -40,18 +40,18 @@
|
|
|
40
40
|
"update-major": "npx --yes npm-check-updates -u"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@hpcc-js/api": "^3.5.
|
|
44
|
-
"@hpcc-js/common": "^3.
|
|
45
|
-
"@hpcc-js/html": "^3.4.
|
|
46
|
-
"@hpcc-js/react": "^3.5.
|
|
47
|
-
"@hpcc-js/util": "^3.6.
|
|
43
|
+
"@hpcc-js/api": "^3.5.5",
|
|
44
|
+
"@hpcc-js/common": "^3.9.0",
|
|
45
|
+
"@hpcc-js/html": "^3.4.5",
|
|
46
|
+
"@hpcc-js/react": "^3.5.5",
|
|
47
|
+
"@hpcc-js/util": "^3.6.5"
|
|
48
48
|
},
|
|
49
49
|
"optionalPeerDependencies": {
|
|
50
50
|
"react": ">=17.0.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@hpcc-js/esbuild-plugins": "^1.9.
|
|
54
|
-
"@hpcc-js/wasm-graphviz": "1.
|
|
53
|
+
"@hpcc-js/esbuild-plugins": "^1.9.5",
|
|
54
|
+
"@hpcc-js/wasm-graphviz": "1.26.0",
|
|
55
55
|
"@types/d3-transition": "1.3.6",
|
|
56
56
|
"@types/dagre": "0.7.54",
|
|
57
57
|
"d3-force": "^1",
|
|
@@ -76,5 +76,5 @@
|
|
|
76
76
|
"url": "https://github.com/hpcc-systems/Visualization/issues"
|
|
77
77
|
},
|
|
78
78
|
"homepage": "https://github.com/hpcc-systems/Visualization",
|
|
79
|
-
"gitHead": "
|
|
79
|
+
"gitHead": "f6408d3f4eb56d5c8360923ec8ecc0391351a3ac"
|
|
80
80
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
.graph_GraphvizWidget {
|
|
2
|
+
--gv-bg: light-dark(#ffffff, #1a1a2e);
|
|
3
|
+
--gv-shadow: light-dark(#e6e6e6, #0d0d18);
|
|
4
|
+
--gv-fg: light-dark(#242424, #e0e0e0);
|
|
5
|
+
--gv-select-stroke: light-dark(#0078d4, #4cc2ff);
|
|
6
|
+
--gv-select-fill: light-dark(#deecf9, #1b3a5c);
|
|
7
|
+
|
|
8
|
+
--gv-unknown-stroke: light-dark(#616161, #9e9e9e);
|
|
9
|
+
--gv-unknown-fill: light-dark(#e0e0e0, #2a2a3e);
|
|
10
|
+
--gv-complete-stroke: light-dark(#2e7d32, #66bb6a);
|
|
11
|
+
--gv-complete-fill: light-dark(#e8f5e9, #1b3a1e);
|
|
12
|
+
--gv-failed-stroke: light-dark(#c62828, #ef5350);
|
|
13
|
+
--gv-failed-fill: light-dark(#ffebee, #3a1b1b);
|
|
14
|
+
--gv-running-stroke: light-dark(#e65100, #ffa726);
|
|
15
|
+
--gv-running-fill: light-dark(#fff3e0, #3a2a1b);
|
|
16
|
+
|
|
17
|
+
background-color: var(--gv-shadow);
|
|
18
|
+
stroke: var(--gv-fg);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.graph_GraphvizWidget > g > rect.zoomBackground {
|
|
22
|
+
fill: var(--gv-shadow) !important;
|
|
23
|
+
stroke: none;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* Selection rules */
|
|
27
|
+
.graph_GraphvizWidget .node>path,
|
|
28
|
+
.graph_GraphvizWidget .node>polygon,
|
|
29
|
+
.graph_GraphvizWidget .node>ellipse,
|
|
30
|
+
.graph_GraphvizWidget .node>polyline,
|
|
31
|
+
.graph_GraphvizWidget .node>rect,
|
|
32
|
+
.graph_GraphvizWidget .cluster>path,
|
|
33
|
+
.graph_GraphvizWidget .cluster>polygon,
|
|
34
|
+
.graph_GraphvizWidget .cluster>ellipse,
|
|
35
|
+
.graph_GraphvizWidget .cluster>polyline,
|
|
36
|
+
.graph_GraphvizWidget .cluster>rect {
|
|
37
|
+
pointer-events: all;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.graph_GraphvizWidget .node.selected>path,
|
|
41
|
+
.graph_GraphvizWidget .node.selected>polygon,
|
|
42
|
+
.graph_GraphvizWidget .node.selected>ellipse,
|
|
43
|
+
.graph_GraphvizWidget .node.selected>polyline,
|
|
44
|
+
.graph_GraphvizWidget .node.selected>rect,
|
|
45
|
+
.graph_GraphvizWidget .cluster.selected>:is(path, polygon, ellipse, polyline, rect):first-of-type {
|
|
46
|
+
stroke: var(--gv-select-stroke);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.graph_GraphvizWidget .node>path:first-of-type,
|
|
50
|
+
.graph_GraphvizWidget .node>polygon:first-of-type,
|
|
51
|
+
.graph_GraphvizWidget .node>ellipse:first-of-type,
|
|
52
|
+
.graph_GraphvizWidget .node>polyline:first-of-type,
|
|
53
|
+
.graph_GraphvizWidget .cluster>path:first-of-type,
|
|
54
|
+
.graph_GraphvizWidget .cluster>polygon:first-of-type,
|
|
55
|
+
.graph_GraphvizWidget .cluster>ellipse:first-of-type,
|
|
56
|
+
.graph_GraphvizWidget .cluster>polyline:first-of-type {
|
|
57
|
+
fill: var(--gv-bg);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.graph_GraphvizWidget .node.selected>path:first-of-type,
|
|
61
|
+
.graph_GraphvizWidget .node.selected>polygon:first-of-type,
|
|
62
|
+
.graph_GraphvizWidget .node.selected>ellipse:first-of-type,
|
|
63
|
+
.graph_GraphvizWidget .node.selected>polyline:first-of-type,
|
|
64
|
+
.graph_GraphvizWidget .cluster.selected>path:first-of-type,
|
|
65
|
+
.graph_GraphvizWidget .cluster.selected>polygon:first-of-type,
|
|
66
|
+
.graph_GraphvizWidget .cluster.selected>ellipse:first-of-type,
|
|
67
|
+
.graph_GraphvizWidget .cluster.selected>polyline:first-of-type {
|
|
68
|
+
fill: var(--gv-select-fill);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.graph_GraphvizWidget .edge.selected path,
|
|
72
|
+
.graph_GraphvizWidget .edge.selected polygon,
|
|
73
|
+
.graph_GraphvizWidget .edge.selected ellipse,
|
|
74
|
+
.graph_GraphvizWidget .edge.selected polyline {
|
|
75
|
+
stroke: var(--gv-select-stroke);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.graph_GraphvizWidget .edge.selected polygon,
|
|
79
|
+
.graph_GraphvizWidget .edge.selected ellipse,
|
|
80
|
+
.graph_GraphvizWidget .edge.selected polyline {
|
|
81
|
+
fill: var(--gv-select-stroke);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.graph_GraphvizWidget text {
|
|
85
|
+
stroke: none;
|
|
86
|
+
fill: var(--gv-fg);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.graph_GraphvizWidget .selected text {
|
|
90
|
+
font-weight: bold;
|
|
91
|
+
stroke: none;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* Generic state rules */
|
|
95
|
+
.graph_GraphvizWidget :is(.unknown, .complete, .failed, .running)>:is(path, polygon, ellipse, polyline) {
|
|
96
|
+
stroke: var(--_stroke);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.graph_GraphvizWidget :is(.unknown, .complete, .failed, .running):is(.node, .cluster)>:is(path, polygon, ellipse, polyline):first-of-type {
|
|
100
|
+
fill: var(--_fill);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.graph_GraphvizWidget :is(.unknown, .complete, .failed, .running).edge>:is(polygon, ellipse, polyline) {
|
|
104
|
+
fill: var(--_stroke);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.graph_GraphvizWidget .unknown {
|
|
108
|
+
--_stroke: var(--gv-unknown-stroke);
|
|
109
|
+
--_fill: var(--gv-unknown-fill);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.graph_GraphvizWidget .complete {
|
|
113
|
+
--_stroke: var(--gv-complete-stroke);
|
|
114
|
+
--_fill: var(--gv-complete-fill);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.graph_GraphvizWidget .failed {
|
|
118
|
+
--_stroke: var(--gv-failed-stroke);
|
|
119
|
+
--_fill: var(--gv-failed-fill);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.graph_GraphvizWidget .running {
|
|
123
|
+
--_stroke: var(--gv-running-stroke);
|
|
124
|
+
--_fill: var(--gv-running-fill);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* Keep node and cluster outlines visible when zoomed out */
|
|
128
|
+
.graph_GraphvizWidget .node>path,
|
|
129
|
+
.graph_GraphvizWidget .node>polygon,
|
|
130
|
+
.graph_GraphvizWidget .node>ellipse,
|
|
131
|
+
.graph_GraphvizWidget .node>polyline,
|
|
132
|
+
.graph_GraphvizWidget .cluster>path,
|
|
133
|
+
.graph_GraphvizWidget .cluster>polygon,
|
|
134
|
+
.graph_GraphvizWidget .cluster>ellipse,
|
|
135
|
+
.graph_GraphvizWidget .cluster>polyline {
|
|
136
|
+
vector-effect: non-scaling-stroke;
|
|
137
|
+
stroke-width: 0.75px;
|
|
138
|
+
}
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import type { Graph } from "@hpcc-js/wasm-graphviz";
|
|
2
|
+
import { d3Event, SVGZoomWidget, type Widget as WidgetT } from "@hpcc-js/common";
|
|
3
|
+
|
|
4
|
+
import "./Widget.css";
|
|
5
|
+
|
|
6
|
+
export const CUSTOM_HTML = "htmlContent";
|
|
7
|
+
export const CUSTOM_SVG = "svgContent";
|
|
8
|
+
export interface CustomVertex {
|
|
9
|
+
id: string;
|
|
10
|
+
svg?: string;
|
|
11
|
+
html?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class Widget extends SVGZoomWidget {
|
|
15
|
+
|
|
16
|
+
protected static readonly _customVertexDPI = 72;
|
|
17
|
+
|
|
18
|
+
private static readonly _svgColorMap: Record<string, string> = {
|
|
19
|
+
'"black"': '"var(--gv-fg)"',
|
|
20
|
+
'"white"': '"var(--gv-bg)"'
|
|
21
|
+
};
|
|
22
|
+
private static readonly _svgColorRe = /"black"|"white"/g;
|
|
23
|
+
|
|
24
|
+
protected _selection: { [id: string]: boolean } = {};
|
|
25
|
+
protected _selectionPreferredNode: { [id: string]: SVGGElement } = {};
|
|
26
|
+
|
|
27
|
+
protected _customVertices: CustomVertex[] = [];
|
|
28
|
+
|
|
29
|
+
constructor() {
|
|
30
|
+
super();
|
|
31
|
+
this._drawStartPos = "origin";
|
|
32
|
+
this.zoomToFitLimit(1);
|
|
33
|
+
this.showToolbar(false);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
protected _data: Graph;
|
|
37
|
+
data(_: Graph): this;
|
|
38
|
+
data(): Graph;
|
|
39
|
+
data(_?: Graph): this | Graph {
|
|
40
|
+
if (!arguments.length) return this._data;
|
|
41
|
+
this._data = _;
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
clearSelection(broadcast: boolean = false) {
|
|
46
|
+
this._selection = {};
|
|
47
|
+
this._selectionPreferredNode = {};
|
|
48
|
+
this._selectionChanged(broadcast);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
toggleSelection(id: string, broadcast: boolean = false, preferredNode?: SVGGElement) {
|
|
52
|
+
if (this._selection[id]) {
|
|
53
|
+
delete this._selection[id];
|
|
54
|
+
delete this._selectionPreferredNode[id];
|
|
55
|
+
} else {
|
|
56
|
+
this._selection[id] = true;
|
|
57
|
+
if (preferredNode) {
|
|
58
|
+
this._selectionPreferredNode[id] = preferredNode;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
this._selectionChanged(broadcast);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
selectionCompare(_: string[]): boolean {
|
|
65
|
+
const currSelection = this.selection();
|
|
66
|
+
return currSelection.length !== _.length || _.some(id => currSelection.indexOf(id) < 0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
selection(): string[];
|
|
70
|
+
selection(_: string[]): this;
|
|
71
|
+
selection(_: string[], broadcast: boolean): this;
|
|
72
|
+
selection(_?: string[], broadcast: boolean = false): string[] | this {
|
|
73
|
+
if (!arguments.length) return Object.keys(this._selection);
|
|
74
|
+
if (this.selectionCompare(_)) {
|
|
75
|
+
this.clearSelection();
|
|
76
|
+
_.forEach(id => this._selection[id] = true);
|
|
77
|
+
this._selectionPreferredNode = {};
|
|
78
|
+
this._selectionChanged(broadcast);
|
|
79
|
+
}
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
setClass(className: string, ids?: string[]): this {
|
|
84
|
+
if (ids) {
|
|
85
|
+
for (const id of ids) {
|
|
86
|
+
const elem = this._renderElement.select(`#${id}`);
|
|
87
|
+
if (!elem.empty()) {
|
|
88
|
+
elem.classed(className, true);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
this._renderElement
|
|
93
|
+
.selectAll(".node,.edge,.cluster")
|
|
94
|
+
.classed(className, true)
|
|
95
|
+
;
|
|
96
|
+
}
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
clearClass(className: string, ids?: string[]): this {
|
|
101
|
+
if (ids) {
|
|
102
|
+
for (const id of ids) {
|
|
103
|
+
const elem = this._renderElement.select(`#${id}`);
|
|
104
|
+
if (!elem.empty()) {
|
|
105
|
+
elem.classed(className, false);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
this._renderElement
|
|
110
|
+
.selectAll(".node,.edge,.cluster")
|
|
111
|
+
.classed(className, false)
|
|
112
|
+
;
|
|
113
|
+
}
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
hasClass(className: string, id: string): boolean {
|
|
118
|
+
const elem = this._renderElement.select(`#${id}`);
|
|
119
|
+
return !elem.empty() && elem.classed(className);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
protected _selectionChanged(broadcast = false) {
|
|
123
|
+
const hasClass = (node: any, className: string): boolean => {
|
|
124
|
+
return !!(node && node.classList && typeof node.classList.contains === "function" && node.classList.contains(className));
|
|
125
|
+
};
|
|
126
|
+
const addSelectedClass = (node: any): void => {
|
|
127
|
+
if (node && node.classList && typeof node.classList.add === "function") {
|
|
128
|
+
node.classList.add("selected");
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
this._renderElement.selectAll(".node,.edge,.cluster")
|
|
133
|
+
.classed("selected", false);
|
|
134
|
+
|
|
135
|
+
for (const id of Object.keys(this._selection)) {
|
|
136
|
+
const matches = this._renderElement.selectAll(`.node[id='${id}'],.edge[id='${id}'],.cluster[id='${id}']`) as any;
|
|
137
|
+
const nodes: SVGGElement[] = typeof matches.nodes === "function"
|
|
138
|
+
? matches.nodes()
|
|
139
|
+
: (() => {
|
|
140
|
+
const retVal: SVGGElement[] = [];
|
|
141
|
+
if (typeof matches.each === "function") {
|
|
142
|
+
matches.each(function (this: SVGGElement) {
|
|
143
|
+
retVal.push(this);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return retVal;
|
|
147
|
+
})();
|
|
148
|
+
if (!nodes.length) continue;
|
|
149
|
+
|
|
150
|
+
// Some graphs emit duplicate cluster IDs; prefer the exact clicked cluster when available.
|
|
151
|
+
const clusterNodes = nodes.filter(n => hasClass(n, "cluster"));
|
|
152
|
+
if (clusterNodes.length > 1) {
|
|
153
|
+
const preferred = this._selectionPreferredNode[id];
|
|
154
|
+
if (preferred && clusterNodes.indexOf(preferred) >= 0) {
|
|
155
|
+
addSelectedClass(preferred);
|
|
156
|
+
} else {
|
|
157
|
+
let largest = clusterNodes[0] as SVGGraphicsElement;
|
|
158
|
+
let largestArea = 0;
|
|
159
|
+
for (const node of clusterNodes) {
|
|
160
|
+
const bbox = (node as SVGGraphicsElement).getBBox();
|
|
161
|
+
const area = bbox.width * bbox.height;
|
|
162
|
+
if (area > largestArea) {
|
|
163
|
+
largestArea = area;
|
|
164
|
+
largest = node as SVGGraphicsElement;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
addSelectedClass(largest);
|
|
168
|
+
}
|
|
169
|
+
for (const node of nodes) {
|
|
170
|
+
if (!hasClass(node, "cluster")) {
|
|
171
|
+
addSelectedClass(node);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
for (const node of nodes) {
|
|
176
|
+
addSelectedClass(node);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (broadcast) {
|
|
182
|
+
this.selectionChanged();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
enter(domNode: HTMLElement | SVGElement, element?: SVGGElement): this {
|
|
187
|
+
super.enter(domNode, element);
|
|
188
|
+
const context = this;
|
|
189
|
+
this._renderElement
|
|
190
|
+
.on("click", function () {
|
|
191
|
+
const event = d3Event();
|
|
192
|
+
let target = event.target as SVGElement;
|
|
193
|
+
while (target && target !== event.currentTarget) {
|
|
194
|
+
const action = (target as Element).getAttribute?.("data-action");
|
|
195
|
+
if (action) {
|
|
196
|
+
let nodeEl = target.parentElement as unknown as SVGElement;
|
|
197
|
+
while (nodeEl && nodeEl !== event.currentTarget) {
|
|
198
|
+
if (nodeEl.classList?.contains("node")) {
|
|
199
|
+
context.vertexButtonClicked(nodeEl.id, action);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
nodeEl = nodeEl.parentElement as unknown as SVGElement;
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (target.classList.contains("node") || target.classList.contains("edge") || target.classList.contains("cluster")) {
|
|
207
|
+
if (!event.ctrlKey && !event.metaKey) {
|
|
208
|
+
context.clearSelection();
|
|
209
|
+
}
|
|
210
|
+
context.toggleSelection(target.id, true, target as SVGGElement);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
target = target.parentElement as unknown as SVGElement;
|
|
214
|
+
}
|
|
215
|
+
context.clearSelection(true);
|
|
216
|
+
})
|
|
217
|
+
.on("dblclick", function () {
|
|
218
|
+
const event = d3Event();
|
|
219
|
+
event.stopPropagation();
|
|
220
|
+
event.preventDefault();
|
|
221
|
+
let target = event.target as SVGElement;
|
|
222
|
+
while (target && target !== event.currentTarget) {
|
|
223
|
+
if (target.classList.contains("node") || target.classList.contains("edge") || target.classList.contains("cluster")) {
|
|
224
|
+
context.zoomToItem(target as SVGGraphicsElement);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
target = target.parentElement as unknown as SVGElement;
|
|
228
|
+
}
|
|
229
|
+
context.zoomToFit();
|
|
230
|
+
})
|
|
231
|
+
;
|
|
232
|
+
|
|
233
|
+
this._zoomGrab
|
|
234
|
+
.on("dblclick", function () {
|
|
235
|
+
const event = d3Event();
|
|
236
|
+
event.stopPropagation();
|
|
237
|
+
event.preventDefault();
|
|
238
|
+
context.zoomToFit();
|
|
239
|
+
})
|
|
240
|
+
;
|
|
241
|
+
|
|
242
|
+
this.on("startMarqueeSelection", () => {
|
|
243
|
+
context.clearSelection();
|
|
244
|
+
}).on("updateMarqueeSelection", () => {
|
|
245
|
+
const selectedIds: string[] = [];
|
|
246
|
+
|
|
247
|
+
// Use screen coordinates to avoid coordinate system complexity
|
|
248
|
+
// (graphviz root g has a Y-flip transform that getBBox() does not account for)
|
|
249
|
+
const marqueeNode = context._marqueeSelection?.node() as SVGRectElement | undefined;
|
|
250
|
+
if (!marqueeNode) return;
|
|
251
|
+
const marqueeRect = marqueeNode.getBoundingClientRect();
|
|
252
|
+
|
|
253
|
+
context._renderElement.selectAll(".node,.edge,.cluster")
|
|
254
|
+
.each(function (this: SVGGraphicsElement) {
|
|
255
|
+
if (!this.id) return;
|
|
256
|
+
const elemRect = this.getBoundingClientRect();
|
|
257
|
+
if (!elemRect) return;
|
|
258
|
+
|
|
259
|
+
if (marqueeRect.left <= elemRect.left && marqueeRect.right >= elemRect.right &&
|
|
260
|
+
marqueeRect.top <= elemRect.top && marqueeRect.bottom >= elemRect.bottom) {
|
|
261
|
+
selectedIds.push(this.id);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
context.selection(selectedIds);
|
|
266
|
+
}).on("endMarqueeSelection", () => {
|
|
267
|
+
context.selectionChanged();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
return this;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
protected collectCustomVertices(): CustomVertex[] {
|
|
274
|
+
const customVertices: CustomVertex[] = [];
|
|
275
|
+
for (const nodeName of this._data.nodeNames()) {
|
|
276
|
+
const svg = this._data.getNodeAttr(nodeName, CUSTOM_SVG);
|
|
277
|
+
const html = this._data.getNodeAttr(nodeName, CUSTOM_HTML);
|
|
278
|
+
if (!svg && !html) continue;
|
|
279
|
+
|
|
280
|
+
const svgWidth = parseFloat(this._data.getNodeAttr(nodeName, "svgWidth"));
|
|
281
|
+
const svgHeight = parseFloat(this._data.getNodeAttr(nodeName, "svgHeight"));
|
|
282
|
+
if (!isFinite(svgWidth) || !isFinite(svgHeight) || svgWidth <= 0 || svgHeight <= 0) continue;
|
|
283
|
+
|
|
284
|
+
customVertices.push({ id: nodeName, svg: svg || undefined, html: html || undefined });
|
|
285
|
+
|
|
286
|
+
this._data
|
|
287
|
+
.setNodeAttr(nodeName, "label", "")
|
|
288
|
+
.setNodeAttr(nodeName, "shape", this._data.getNodeAttr(nodeName, "shape") || "rectangle")
|
|
289
|
+
.setNodeAttr(nodeName, "fixedsize", true)
|
|
290
|
+
.setNodeAttr(nodeName, "width", svgWidth / Widget._customVertexDPI)
|
|
291
|
+
.setNodeAttr(nodeName, "height", svgHeight / Widget._customVertexDPI);
|
|
292
|
+
}
|
|
293
|
+
return customVertices;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
protected postrenderCustomVertices(customVertices: CustomVertex[]) {
|
|
297
|
+
for (const v of customVertices) {
|
|
298
|
+
const nodeGroup = this._renderElement.select(`#${v.id}`);
|
|
299
|
+
if (nodeGroup.empty()) continue;
|
|
300
|
+
|
|
301
|
+
const bbox = (nodeGroup.node() as SVGGraphicsElement).getBBox();
|
|
302
|
+
const cx = bbox.x + bbox.width / 2;
|
|
303
|
+
const cy = bbox.y + bbox.height / 2;
|
|
304
|
+
|
|
305
|
+
nodeGroup.select("polygon,ellipse,path")
|
|
306
|
+
.attr("stroke", "transparent")
|
|
307
|
+
.attr("fill", "transparent");
|
|
308
|
+
|
|
309
|
+
if (v.html) {
|
|
310
|
+
const nodeId = v.id;
|
|
311
|
+
const fo = nodeGroup.append("foreignObject")
|
|
312
|
+
.attr("class", CUSTOM_HTML)
|
|
313
|
+
.attr("width", bbox.width)
|
|
314
|
+
.attr("height", bbox.height)
|
|
315
|
+
.attr("x", bbox.x)
|
|
316
|
+
.attr("y", bbox.y);
|
|
317
|
+
|
|
318
|
+
const div = fo.append("xhtml:div")
|
|
319
|
+
.attr("xmlns", "http://www.w3.org/1999/xhtml")
|
|
320
|
+
.style("width", `${bbox.width}px`)
|
|
321
|
+
.style("height", `${bbox.height}px`)
|
|
322
|
+
.style("overflow", "hidden")
|
|
323
|
+
.html(v.html);
|
|
324
|
+
|
|
325
|
+
(div.node() as HTMLElement).querySelectorAll<HTMLElement>("[data-action]").forEach(btn => {
|
|
326
|
+
btn.addEventListener("click", (e) => {
|
|
327
|
+
e.stopPropagation();
|
|
328
|
+
this.vertexButtonClicked(nodeId, btn.dataset.action!);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
} else if (v.svg) {
|
|
332
|
+
const g = nodeGroup.append("g")
|
|
333
|
+
.attr("class", CUSTOM_SVG);
|
|
334
|
+
g.html(v.svg);
|
|
335
|
+
|
|
336
|
+
const contentBBox = (g.node() as SVGGraphicsElement).getBBox();
|
|
337
|
+
const dx = cx - (contentBBox.x + contentBBox.width / 2);
|
|
338
|
+
const dy = cy - (contentBBox.y + contentBBox.height / 2);
|
|
339
|
+
g.attr("transform", `translate(${dx},${dy})`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
protected _prevDot: string | undefined;
|
|
345
|
+
|
|
346
|
+
itemBBox(scopeID: string): ReturnType<Widget["getRenderElementBBox"]>;
|
|
347
|
+
itemBBox(node: SVGGraphicsElement): ReturnType<Widget["getRenderElementBBox"]>;
|
|
348
|
+
itemBBox(scopeIDOrNode: string | SVGGraphicsElement) {
|
|
349
|
+
const node = typeof scopeIDOrNode === "string"
|
|
350
|
+
? this._renderElement.select(`#${scopeIDOrNode}`).node() as SVGGraphicsElement
|
|
351
|
+
: scopeIDOrNode;
|
|
352
|
+
const renderNode = this._renderElement.node() as SVGGraphicsElement;
|
|
353
|
+
if (node && renderNode) {
|
|
354
|
+
const clientRect = node.getBoundingClientRect();
|
|
355
|
+
const inverseScreenCTM = renderNode.getScreenCTM()?.inverse();
|
|
356
|
+
if (inverseScreenCTM && clientRect.width && clientRect.height) {
|
|
357
|
+
const topLeft = {
|
|
358
|
+
x: inverseScreenCTM.a * clientRect.left + inverseScreenCTM.c * clientRect.top + inverseScreenCTM.e,
|
|
359
|
+
y: inverseScreenCTM.b * clientRect.left + inverseScreenCTM.d * clientRect.top + inverseScreenCTM.f
|
|
360
|
+
};
|
|
361
|
+
const bottomRight = {
|
|
362
|
+
x: inverseScreenCTM.a * clientRect.right + inverseScreenCTM.c * clientRect.bottom + inverseScreenCTM.e,
|
|
363
|
+
y: inverseScreenCTM.b * clientRect.right + inverseScreenCTM.d * clientRect.bottom + inverseScreenCTM.f
|
|
364
|
+
};
|
|
365
|
+
return {
|
|
366
|
+
x: Math.min(topLeft.x, bottomRight.x),
|
|
367
|
+
y: Math.min(topLeft.y, bottomRight.y),
|
|
368
|
+
width: Math.abs(bottomRight.x - topLeft.x),
|
|
369
|
+
height: Math.abs(bottomRight.y - topLeft.y)
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
return node.getBBox();
|
|
373
|
+
}
|
|
374
|
+
return this.getRenderElementBBox();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
zoomToItem(scopeID: string): this;
|
|
378
|
+
zoomToItem(node: SVGGraphicsElement): this;
|
|
379
|
+
zoomToItem(scopeIDOrNode: string | SVGGraphicsElement) {
|
|
380
|
+
const itemBBox = typeof scopeIDOrNode === "string"
|
|
381
|
+
? this.itemBBox(scopeIDOrNode)
|
|
382
|
+
: this.itemBBox(scopeIDOrNode);
|
|
383
|
+
this.zoomToBBox(itemBBox);
|
|
384
|
+
return this;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
update(domNode: HTMLElement | SVGElement, element: SVGGElement): this {
|
|
388
|
+
super.update(domNode, element);
|
|
389
|
+
|
|
390
|
+
this._customVertices = this.collectCustomVertices();
|
|
391
|
+
const dot = this._data.toDot();
|
|
392
|
+
if (this._prevDot !== dot) {
|
|
393
|
+
this._prevDot = dot;
|
|
394
|
+
const svg = this._data.layout("svg", "dot").replace(Widget._svgColorRe, m => Widget._svgColorMap[m]);
|
|
395
|
+
const svgDoc = new DOMParser().parseFromString(svg, "image/svg+xml");
|
|
396
|
+
const renderNode = this._renderElement.node();
|
|
397
|
+
renderNode.replaceChildren(...svgDoc.documentElement.childNodes);
|
|
398
|
+
this.postrenderCustomVertices(this._customVertices);
|
|
399
|
+
this._selectionChanged();
|
|
400
|
+
}
|
|
401
|
+
return this;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
exit(domNode: HTMLElement | SVGElement, element?: SVGGElement): this {
|
|
405
|
+
super.exit(domNode, element);
|
|
406
|
+
return this;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
render(callback?: (w: WidgetT) => void): this {
|
|
410
|
+
super.render((w: WidgetT) => {
|
|
411
|
+
|
|
412
|
+
if (callback) {
|
|
413
|
+
callback(w);
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
return this;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Events ---
|
|
420
|
+
selectionChanged() {
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
vertexButtonClicked(id: string, action: string) {
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
Widget.prototype._class += " graph_GraphvizWidget";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Widget.ts";
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Graph } from "@hpcc-js/wasm-graphviz";
|
|
2
|
+
import { SVGZoomWidget, type Widget as WidgetT } from "@hpcc-js/common";
|
|
3
|
+
import "./Widget.css";
|
|
4
|
+
export declare const CUSTOM_HTML = "htmlContent";
|
|
5
|
+
export declare const CUSTOM_SVG = "svgContent";
|
|
6
|
+
export interface CustomVertex {
|
|
7
|
+
id: string;
|
|
8
|
+
svg?: string;
|
|
9
|
+
html?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class Widget extends SVGZoomWidget {
|
|
12
|
+
protected static readonly _customVertexDPI = 72;
|
|
13
|
+
private static readonly _svgColorMap;
|
|
14
|
+
private static readonly _svgColorRe;
|
|
15
|
+
protected _selection: {
|
|
16
|
+
[id: string]: boolean;
|
|
17
|
+
};
|
|
18
|
+
protected _selectionPreferredNode: {
|
|
19
|
+
[id: string]: SVGGElement;
|
|
20
|
+
};
|
|
21
|
+
protected _customVertices: CustomVertex[];
|
|
22
|
+
constructor();
|
|
23
|
+
protected _data: Graph;
|
|
24
|
+
data(_: Graph): this;
|
|
25
|
+
data(): Graph;
|
|
26
|
+
clearSelection(broadcast?: boolean): void;
|
|
27
|
+
toggleSelection(id: string, broadcast?: boolean, preferredNode?: SVGGElement): void;
|
|
28
|
+
selectionCompare(_: string[]): boolean;
|
|
29
|
+
selection(): string[];
|
|
30
|
+
selection(_: string[]): this;
|
|
31
|
+
selection(_: string[], broadcast: boolean): this;
|
|
32
|
+
setClass(className: string, ids?: string[]): this;
|
|
33
|
+
clearClass(className: string, ids?: string[]): this;
|
|
34
|
+
hasClass(className: string, id: string): boolean;
|
|
35
|
+
protected _selectionChanged(broadcast?: boolean): void;
|
|
36
|
+
enter(domNode: HTMLElement | SVGElement, element?: SVGGElement): this;
|
|
37
|
+
protected collectCustomVertices(): CustomVertex[];
|
|
38
|
+
protected postrenderCustomVertices(customVertices: CustomVertex[]): void;
|
|
39
|
+
protected _prevDot: string | undefined;
|
|
40
|
+
itemBBox(scopeID: string): ReturnType<Widget["getRenderElementBBox"]>;
|
|
41
|
+
itemBBox(node: SVGGraphicsElement): ReturnType<Widget["getRenderElementBBox"]>;
|
|
42
|
+
zoomToItem(scopeID: string): this;
|
|
43
|
+
zoomToItem(node: SVGGraphicsElement): this;
|
|
44
|
+
update(domNode: HTMLElement | SVGElement, element: SVGGElement): this;
|
|
45
|
+
exit(domNode: HTMLElement | SVGElement, element?: SVGGElement): this;
|
|
46
|
+
render(callback?: (w: WidgetT) => void): this;
|
|
47
|
+
selectionChanged(): void;
|
|
48
|
+
vertexButtonClicked(id: string, action: string): void;
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Widget.ts";
|
package/types/index.d.ts
CHANGED