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.

813 lines
26 KiB
JavaScript

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._modelNodeName2ModelNode = new Map();
this._modelNodeName2State = new Map();
this._namedEdges = new Map();
this._pathArgumentNames = new Set(); // the name of arguments which occurs in both sides of an edge
this._renameMap = new Map();
this._addedNode = 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;
}
}
const modelNodeName = node.modelNodeName
this._modelNodeName2ViewNode.set(modelNodeName, node);
this._modelNodeName2ModelNode.set(modelNodeName, node.value)
// _modelNodeName2State save our modifications, and wil be initilized at the first graph construction only
// otherwise the modfications will lost
if (!this._modelNodeName2State.get(modelNodeName)) {
this._modelNodeName2State.set(modelNodeName, 'Exist');
}
}
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.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"));
// <==== 显示 边上的箭头
for (const nodeId of this.nodes.keys()) {
const node = this.node(nodeId);
if (this.children(nodeId).length == 0) {
if (this._modelNodeName2State.get(node.label.modelNodeName) == 'Exist') {
node.label.build(document, nodeGroup);
}
}
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()) {
var node_from = this._nodes.get(edge.v).label;
var node_to = this._nodes.get(edge.w).label;
if (
this._modelNodeName2State.get(node_from.modelNodeName) == 'Exist' &&
this._modelNodeName2State.get(node_to.modelNodeName) == 'Exist'
)
{
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
if (this._modelNodeName2State.get(node.label.modelNodeName) == 'Exist') {
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);
}
}
for (const edge of this.edges.values()) {
var node_from = this._nodes.get(edge.v).label;
var node_to = this._nodes.get(edge.w).label;
if (
this._modelNodeName2State.get(node_from.modelNodeName) == 'Exist' &&
this._modelNodeName2State.get(node_to.modelNodeName) == 'Exist'
)
{
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;
}