You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

798 lines
26 KiB
JavaScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

var grapher = grapher || {};
var dagre = dagre || require('./dagre');
grapher.Graph = class {
constructor(compound, options) {
this._isCompound = compound;
this._options = options;
this._nodes = new Map();
this._edges = new Map();
this._children = {};
this._children['\x00'] = {};
this._parent = {};
// My code
this._modelNodeName2ViewNode = new Map();
this._modelNodeName2State = new Map();
this._namedEdges = new Map();
}
get options() {
return this._options;
}
setNode(node) {
const key = node.name; // node id
const value = this._nodes.get(key);
if (value) {
value.label = node;
}
else {
this._nodes.set(key, { v: key, label: node });
if (this._isCompound) {
this._parent[key] = '\x00';
this._children[key] = {};
this._children['\x00'][key] = true;
}
}
// My code
// var modelNodeName = node.value.name;
// if (modelNodeName == '') { // in case that model node has no name
// modelNodeName = node.value.type.name + node.name
// // console.log(node.value)
// // console.log(modelNodeName)
// }
const modelNodeName = node.modelNodeName
this._modelNodeName2ViewNode.set(modelNodeName, node);
this._modelNodeName2State.set(modelNodeName, 'Exist');
// console.log(modelNodeName)
// console.log(node.modelNodeName)
}
setEdge(edge) {
if (!this._nodes.has(edge.v)) {
throw new grapher.Error();
}
if (!this._nodes.has(edge.w)) {
throw new grapher.Error();
}
const key = edge.v + ':' + edge.w;
if (!this._edges.has(key)) {
this._edges.set(key, { v: edge.v, w: edge.w, label: edge });
}
// My code
// _namedEdges: from : to
// var from_node_name = edge.from.value.name
// var to_node_name = edge.to.value.name
var from_node_name = edge.from.modelNodeName
var to_node_name = edge.to.modelNodeName
if (!this._namedEdges.has(from_node_name)) {
this._namedEdges.set(from_node_name, []);
}
this._namedEdges.get(from_node_name).push(to_node_name);
}
setParent(node, parent) {
if (!this._isCompound) {
throw new Error("Cannot set parent in a non-compound graph");
}
parent += "";
for (let ancestor = parent; ancestor; ancestor = this.parent(ancestor)) {
if (ancestor === node) {
throw new Error("Setting " + parent + " as parent of " + node + " would create a cycle");
}
}
delete this._children[this._parent[node]][node];
this._parent[node] = parent;
this._children[parent][node] = true;
return this;
}
get nodes() {
return this._nodes;
}
hasNode(key) {
return this._nodes.has(key);
}
node(key) {
return this._nodes.get(key);
}
get edges() {
return this._edges;
}
parent(key) {
if (this._isCompound) {
const parent = this._parent[key];
if (parent !== '\x00') {
return parent;
}
}
}
children(key) {
key = key === undefined ? '\x00' : key;
if (this._isCompound) {
const children = this._children[key];
if (children) {
return Object.keys(children);
}
}
else if (key === '\x00') {
return this.nodes.keys();
}
else if (this.hasNode(key)) {
return [];
}
}
build(document, origin) {
const createGroup = (name) => {
const element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
element.setAttribute('id', name);
element.setAttribute('class', name);
origin.appendChild(element);
return element;
};
const clusterGroup = createGroup('clusters');
const edgePathGroup = createGroup('edge-paths');
const edgeLabelGroup = createGroup('edge-labels');
const nodeGroup = createGroup('nodes');
// ====> 显示 边上的箭头
const edgePathGroupDefs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
edgePathGroup.appendChild(edgePathGroupDefs);
const marker = (id) => {
const element = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
element.setAttribute('id', id);
element.setAttribute('viewBox', '0 0 10 10');
element.setAttribute('refX', 9);
element.setAttribute('refY', 5);
element.setAttribute('markerUnits', 'strokeWidth');
element.setAttribute('markerWidth', 8);
element.setAttribute('markerHeight', 6);
element.setAttribute('orient', 'auto');
const markerPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
markerPath.setAttribute('d', 'M 0 0 L 10 5 L 0 10 L 4 5 z');
markerPath.style.setProperty('stroke-width', 1);
element.appendChild(markerPath);
return element;
};
edgePathGroupDefs.appendChild(marker("arrowhead-vee"));
edgePathGroupDefs.appendChild(marker("arrowhead-vee-select"));
// <==== 显示 边上的箭头
// console.log(this.nodes)
for (const nodeId of this.nodes.keys()) {
const node = this.node(nodeId);
if (this.children(nodeId).length == 0) {
// node
// type(node.label) == view.Node
node.label.build(document, nodeGroup); // 如果注释TypeError Cannot read properties of undefined (reading 'setAttribute')
}
else {
// cluster
node.label.rectangle = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
if (node.label.rx) {
node.label.rectangle.setAttribute('rx', node.rx);
}
if (node.label.ry) {
node.label.rectangle.setAttribute('ry', node.ry);
}
node.label.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
node.label.element.setAttribute('class', 'cluster');
node.label.element.appendChild(node.label.rectangle);
clusterGroup.appendChild(node.label.element);
}
}
for (const edge of this.edges.values()) {
edge.label.build(document, edgePathGroup, edgeLabelGroup);
}
}
update() {
dagre.layout(this);
for (const nodeId of this.nodes.keys()) {
const node = this.node(nodeId);
if (this.children(nodeId).length == 0) {
// node
node.label.update(); // 让节点显示出来
}
// ===> 这段没有操作
else {
// cluster
const node = this.node(nodeId);
node.label.element.setAttribute('transform', 'translate(' + node.label.x + ',' + node.label.y + ')');
node.label.rectangle.setAttribute('x', - node.label.width / 2);
node.label.rectangle.setAttribute('y', - node.label.height / 2 );
node.label.rectangle.setAttribute('width', node.label.width);
node.label.rectangle.setAttribute('height', node.label.height);
}
// <=== 这段没有操作
}
// console.log(this.edges)
for (const edge of this.edges.values()) {
// console.log(edge.label)
edge.label.update(); // 让边显示出来
}
}
};
grapher.Node = class {
constructor() {
this._blocks = [];
}
header() {
const block = new grapher.Node.Header();
this._blocks.push(block);
return block;
}
list() {
const block = new grapher.Node.List();
this._blocks.push(block);
return block;
}
canvas() {
const block = new grapher.Node.Canvas();
this._blocks.push(block);
return block;
}
build(document, parent) {
this.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
if (this.id) {
this.element.setAttribute('id', this.id);
}
this.element.setAttribute('class', this.class ? 'node ' + this.class : 'node');
this.element.style.opacity = 0;
parent.appendChild(this.element);
// ===> 配置每个节点的框边界
this.border = document.createElementNS('http://www.w3.org/2000/svg', 'path');
this.border.setAttribute('class', [ 'node', 'border' ].join(' '));
this.element.appendChild(this.border);
// <=== 配置每个节点的框边界
for (let i = 0; i < this._blocks.length; i++) {
const block = this._blocks[i];
block.first = i === 0;
block.last = i === this._blocks.length - 1;
block.build(document, this.element);
}
this.layout(); // 这一行注释后,边和节点全部乱成一团,报错一堆
}
layout() {
const width = Math.max(...this._blocks.map((block) => block.width));
let height = 0;
for (let i = 0; i < this._blocks.length; i++) {
const block = this._blocks[i];
block.y = height;
block.update(this.element, height, width, i == 0, i == this._blocks.length - 1);
height = height + block.height;
}
// 这一行画每个节点的框边界
this.border.setAttribute('d', grapher.Node.roundedRect(0, 0, width, height, true, true, true, true));
const nodeBox = this.element.getBBox();
this.width = nodeBox.width;
this.height = nodeBox.height;
}
update() {
// console.log(this)
// 没有这一行,所有节点都左对齐到左上角
// 这一行对所有节点框进行平移
this.element.setAttribute('transform', 'translate(' + (this.x - (this.width / 2)) + ',' + (this.y - (this.height / 2)) + ')');
// 设定不透明度
this.element.style.opacity = 1;
}
static roundedRect(x, y, width, height, r1, r2, r3, r4) {
const radius = 5;
r1 = r1 ? radius : 0;
r2 = r2 ? radius : 0;
r3 = r3 ? radius : 0;
r4 = r4 ? radius : 0;
return "M" + (x + r1) + "," + y +
"h" + (width - r1 - r2) +
"a" + r2 + "," + r2 + " 0 0 1 " + r2 + "," + r2 +
"v" + (height - r2 - r3) +
"a" + r3 + "," + r3 + " 0 0 1 " + -r3 + "," + r3 +
"h" + (r3 + r4 - width) +
"a" + r4 + "," + r4 + " 0 0 1 " + -r4 + "," + -r4 +
'v' + (-height + r4 + r1) +
"a" + r1 + "," + r1 + " 0 0 1 " + r1 + "," + -r1 +
"z";
}
};
grapher.Node.Header = class {
constructor() {
this._entries = [];
}
add(id, classList, content, tooltip, handler) {
const entry = new grapher.Node.Header.Entry(id, classList, content, tooltip, handler);
this._entries.push(entry);
return entry;
}
build(document, parent) {
this._document = document;
this.width = 0;
this.height = 0;
let x = 0;
const y = 0;
for (const entry of this._entries) {
entry.x = x;
entry.y = y;
entry.build(document, parent);
x += entry.width;
this.height = Math.max(entry.height, this.height);
this.width = Math.max(x, this.width);
}
if (!this.first) {
this.line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
parent.appendChild(this.line);
}
}
update(parent, top, width) {
const document = this._document;
const dx = width - this.width;
for (let i = 0; i < this._entries.length; i++) {
const entry = this._entries[i];
if (i == 0) {
entry.width = entry.width + dx;
}
else {
entry.x = entry.x + dx;
entry.tx = entry.tx + dx;
}
entry.y = entry.y + top;
}
for (let i = 0; i < this._entries.length; i++) {
const entry = this._entries[i];
entry.element.setAttribute('transform', 'translate(' + entry.x + ',' + entry.y + ')');
const r1 = i == 0 && this.first;
const r2 = i == this._entries.length - 1 && this.first;
const r3 = i == this._entries.length - 1 && this.last;
const r4 = i == 0 && this.last;
entry.path.setAttribute('d', grapher.Node.roundedRect(0, 0, entry.width, entry.height, r1, r2, r3, r4));
entry.text.setAttribute('x', 6);
entry.text.setAttribute('y', entry.ty);
}
for (let i = 0; i < this._entries.length; i++) {
const entry = this._entries[i];
if (i != 0) {
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('class', 'node');
line.setAttribute('x1', entry.x);
line.setAttribute('x2', entry.x);
line.setAttribute('y1', top);
line.setAttribute('y2', top + this.height);
parent.appendChild(line);
}
}
if (this.line) {
this.line.setAttribute('class', 'node');
this.line.setAttribute('x1', 0);
this.line.setAttribute('x2', width);
this.line.setAttribute('y1', top);
this.line.setAttribute('y2', top);
}
}
};
grapher.Node.Header.Entry = class {
constructor(id, classList, content, tooltip, handler) {
this.id = id;
this.classList = classList;
this.content = content;
this.tooltip = tooltip;
this.handler = handler;
this.events = {};
}
on(event, callback) {
this.events[event] = this.events[event] || [];
this.events[event].push(callback);
}
raise(event, data) {
if (this.events && this.events[event]) {
for (const callback of this.events[event]) {
callback(this, data);
}
}
}
build(document, parent) {
const yPadding = 4;
const xPadding = 7;
this.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
parent.appendChild(this.element);
this.path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
this.text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
this.element.appendChild(this.path);
this.element.appendChild(this.text);
const classList = [ 'node-item' ];
if (this.classList) {
classList.push(...this.classList);
}
this.element.setAttribute('class', classList.join(' '));
if (this.id) {
this.element.setAttribute('id', this.id);
}
if (this.events.click) {
this.element.addEventListener('click', () => this.raise('click'));
}
if (this.tooltip) {
const titleElement = document.createElementNS('http://www.w3.org/2000/svg', 'title');
titleElement.textContent = this.tooltip;
this.element.appendChild(titleElement);
}
if (this.content) {
this.text.textContent = this.content;
}
const boundingBox = this.text.getBBox();
this.width = boundingBox.width + xPadding + xPadding;
this.height = boundingBox.height + yPadding + yPadding;
this.tx = xPadding;
this.ty = yPadding - boundingBox.y;
}
};
grapher.Node.List = class {
constructor() {
this._items = [];
this.events = {};
}
add(id, name, value, tooltip, separator) {
const item = new grapher.Node.List.Item(id, name, value, tooltip, separator);
this._items.push(item);
return item;
}
on(event, callback) {
this.events[event] = this.events[event] || [];
this.events[event].push(callback);
}
raise(event, data) {
if (this.events && this.events[event]) {
for (const callback of this.events[event]) {
callback(this, data);
}
}
}
build(document, parent) {
this._document = document;
this.width = 0;
this.height = 0;
const x = 0;
const y = 0;
this.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
this.element.setAttribute('class', 'node-attribute');
if (this.events.click) {
this.element.addEventListener('click', () => this.raise('click'));
}
this.element.setAttribute('transform', 'translate(' + x + ',' + y + ')');
this.background = document.createElementNS('http://www.w3.org/2000/svg', 'path');
this.element.appendChild(this.background);
parent.appendChild(this.element);
this.height += 3;
for (const item of this._items) {
const yPadding = 1;
const xPadding = 6;
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
if (item.id) {
text.setAttribute('id', item.id);
}
text.setAttribute('xml:space', 'preserve');
this.element.appendChild(text);
if (item.tooltip) {
const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
title.textContent = item.tooltip;
text.appendChild(title);
}
const name = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
name.textContent = item.name;
if (item.separator.trim() != '=') {
name.style.fontWeight = 'bold';
}
text.appendChild(name);
const textValueElement = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
textValueElement.textContent = item.separator + item.value;
text.appendChild(textValueElement);
const size = text.getBBox();
const width = xPadding + size.width + xPadding;
this.width = Math.max(width, this.width);
text.setAttribute('x', x + xPadding);
text.setAttribute('y', this.height + yPadding - size.y);
this.height += yPadding + size.height + yPadding;
}
this.height += 3;
this.width = Math.max(75, this.width);
if (!this.first) {
this.line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
this.line.setAttribute('class', 'node');
this.element.appendChild(this.line);
}
}
update(parent, top, width) {
this.element.setAttribute('transform', 'translate(0,' + this.y + ')');
this.background.setAttribute('d', grapher.Node.roundedRect(0, 0, width, this.height, this.first, this.first, this.last, this.last));
if (this.line) {
this.line.setAttribute('x1', 0);
this.line.setAttribute('x2', width);
this.line.setAttribute('y1', 0);
this.line.setAttribute('y2', 0);
}
}
};
grapher.Node.List.Item = class {
constructor(id, name, value, tooltip, separator) {
this.id = id;
this.name = name;
this.value = value;
this.tooltip = tooltip;
this.separator = separator;
}
};
grapher.Node.Canvas = class {
constructor() {
this.width = 0;
this.height = 0;
}
build(/* document, parent */) {
}
update(/* parent, top, width , first, last */) {
}
};
grapher.Edge = class {
constructor(from, to) {
this.from = from;
this.to = to;
}
get arrowhead() {
return 'vee';
}
build(document, edgePathGroupElement, edgeLabelGroupElement) {
const createElement = (name) => {
return document.createElementNS('http://www.w3.org/2000/svg', name);
};
// 生成path对应的element
this.element = createElement('path');
if (this.id) {
this.element.setAttribute('id', this.id);
}
this.element.setAttribute('class', this.class ? 'edge-path ' + this.class : 'edge-path');
edgePathGroupElement.appendChild(this.element);
// 生成label对应的element
if (this.label) {
const tspan = createElement('tspan');
tspan.setAttribute('xml:space', 'preserve');
tspan.setAttribute('dy', '1em');
tspan.setAttribute('x', '1');
tspan.appendChild(document.createTextNode(this.label));
this.labelElement = createElement('text');
this.labelElement.appendChild(tspan);
this.labelElement.style.opacity = 0;
this.labelElement.setAttribute('class', 'edge-label');
if (this.id) {
this.labelElement.setAttribute('id', 'edge-label-' + this.id);
}
edgeLabelGroupElement.appendChild(this.labelElement);
const edgeBox = this.labelElement.getBBox();
this.width = edgeBox.width;
this.height = edgeBox.height;
}
}
update() {
const edgePath = grapher.Edge._computeCurvePath(this, this.from, this.to);
this.element.setAttribute('d', edgePath); // ===> 把边画出来
// ===> 让label显示出来
if (this.labelElement) {
this.labelElement.setAttribute('transform', 'translate(' + (this.x - (this.width / 2)) + ',' + (this.y - (this.height / 2)) + ')');
this.labelElement.style.opacity = 1;
}
// <=== 让label显示出来
}
static _computeCurvePath(edge, tail, head) {
const points = edge.points.slice(1, edge.points.length - 1);
points.unshift(grapher.Edge._intersectRect(tail, points[0]));
points.push(grapher.Edge._intersectRect(head, points[points.length - 1]));
const curve = new grapher.Edge.Curve(points);
return curve.path.data;
}
static _intersectRect(node, point) {
const x = node.x;
const y = node.y;
const dx = point.x - x;
const dy = point.y - y;
let w = node.width / 2;
let h = node.height / 2;
let sx;
let sy;
if (Math.abs(dy) * w > Math.abs(dx) * h) {
if (dy < 0) {
h = -h;
}
sx = dy === 0 ? 0 : h * dx / dy;
sy = h;
}
else {
if (dx < 0) {
w = -w;
}
sx = w;
sy = dx === 0 ? 0 : w * dy / dx;
}
return {
x: x + sx,
y: y + sy
};
}
};
grapher.Edge.Curve = class {
constructor(points) {
this._path = new grapher.Edge.Path();
this._x0 = NaN;
this._x1 = NaN;
this._y0 = NaN;
this._y1 = NaN;
this._state = 0;
for (let i = 0; i < points.length; i++) {
const point = points[i];
this.point(point.x, point.y);
if (i === points.length - 1) {
switch (this._state) {
case 3:
this.curve(this._x1, this._y1);
this._path.lineTo(this._x1, this._y1);
break;
case 2:
this._path.lineTo(this._x1, this._y1);
break;
}
if (this._line || (this._line !== 0 && this._point === 1)) {
this._path.closePath();
}
this._line = 1 - this._line;
}
}
}
get path() {
return this._path;
}
point(x, y) {
x = +x;
y = +y;
switch (this._state) {
case 0:
this._state = 1;
if (this._line) {
this._path.lineTo(x, y);
}
else {
this._path.moveTo(x, y);
}
break;
case 1:
this._state = 2;
break;
case 2:
this._state = 3;
this._path.lineTo((5 * this._x0 + this._x1) / 6, (5 * this._y0 + this._y1) / 6);
this.curve(x, y);
break;
default:
this.curve(x, y);
break;
}
this._x0 = this._x1;
this._x1 = x;
this._y0 = this._y1;
this._y1 = y;
}
curve(x, y) {
this._path.bezierCurveTo(
(2 * this._x0 + this._x1) / 3,
(2 * this._y0 + this._y1) / 3,
(this._x0 + 2 * this._x1) / 3,
(this._y0 + 2 * this._y1) / 3,
(this._x0 + 4 * this._x1 + x) / 6,
(this._y0 + 4 * this._y1 + y) / 6
);
}
};
grapher.Edge.Path = class {
constructor() {
this._x0 = null;
this._y0 = null;
this._x1 = null;
this._y1 = null;
this._data = '';
}
moveTo(x, y) {
this._data += "M" + (this._x0 = this._x1 = +x) + "," + (this._y0 = this._y1 = +y);
}
lineTo(x, y) {
this._data += "L" + (this._x1 = +x) + "," + (this._y1 = +y);
}
bezierCurveTo(x1, y1, x2, y2, x, y) {
this._data += "C" + (+x1) + "," + (+y1) + "," + (+x2) + "," + (+y2) + "," + (this._x1 = +x) + "," + (this._y1 = +y);
}
closePath() {
if (this._x1 !== null) {
this._x1 = this._x0;
this._y1 = this._y0;
this._data += "Z";
}
}
get data() {
return this._data;
}
};
if (typeof module !== 'undefined' && typeof module.exports === 'object') {
module.exports.Graph = grapher.Graph;
module.exports.Node = grapher.Node;
module.exports.Edge = grapher.Edge;
}