885 lines
29 KiB
JavaScript
885 lines
29 KiB
JavaScript
// this file is from https://github.com/lutzroeder/netron/blob/main/source/grapher.js
|
|
var grapher = grapher || {};
|
|
var dagre = window.dagre;
|
|
|
|
grapher.Graph = class {
|
|
|
|
constructor(compound, layout) {
|
|
this._layout = layout;
|
|
this._isCompound = compound;
|
|
this._nodes = new Map();
|
|
this._edges = new Map();
|
|
this._children = {};
|
|
this._children['\x00'] = {};
|
|
this._parent = {};
|
|
}
|
|
|
|
setNode(node) {
|
|
const key = node.name;
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
setEdge(edge) {
|
|
if (!this._nodes.has(edge.v)) {
|
|
throw new grapher.Error("Invalid edge '" + JSON.stringify(edge.v) + "'.");
|
|
}
|
|
if (!this._nodes.has(edge.w)) {
|
|
throw new grapher.Error("Invalid edge '" + JSON.stringify(edge.w) + "'.");
|
|
}
|
|
const key = edge.v + ':' + edge.w;
|
|
if (!this._edges.has(key)) {
|
|
this._edges.set(key, { v: edge.v, w: edge.w, label: edge });
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
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 [];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
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"));
|
|
edgePathGroupDefs.appendChild(marker("arrowhead-select"));
|
|
edgePathGroupDefs.appendChild(marker("arrowhead-hover"));
|
|
for (const nodeId of this.nodes.keys()) {
|
|
const entry = this.node(nodeId);
|
|
const node = entry.label;
|
|
if (this.children(nodeId).length == 0) {
|
|
node.build(document, nodeGroup);
|
|
} else {
|
|
// cluster
|
|
node.rectangle = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
if (node.rx) {
|
|
node.rectangle.setAttribute('rx', entry.rx);
|
|
}
|
|
if (node.ry) {
|
|
node.rectangle.setAttribute('ry', entry.ry);
|
|
}
|
|
node.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
node.element.setAttribute('class', 'cluster');
|
|
node.element.appendChild(node.rectangle);
|
|
clusterGroup.appendChild(node.element);
|
|
}
|
|
}
|
|
|
|
for (const edge of this.edges.values()) {
|
|
edge.label.build(document, edgePathGroup, edgeLabelGroup);
|
|
}
|
|
}
|
|
|
|
measure() {
|
|
for (const key of this.nodes.keys()) {
|
|
const entry = this.node(key);
|
|
if (this.children(key).length == 0) {
|
|
const node = entry.label;
|
|
node.measure();
|
|
}
|
|
}
|
|
}
|
|
|
|
layout() {
|
|
dagre.layout(this, this._layout);
|
|
for (const key of this.nodes.keys()) {
|
|
const entry = this.node(key);
|
|
if (this.children(key).length == 0) {
|
|
const node = entry.label;
|
|
node.layout();
|
|
}
|
|
}
|
|
}
|
|
|
|
update() {
|
|
for (const nodeId of this.nodes.keys()) {
|
|
if (this.children(nodeId).length == 0) {
|
|
// node
|
|
const entry = this.node(nodeId);
|
|
const node = entry.label;
|
|
node.update();
|
|
} else {
|
|
// cluster
|
|
const entry = this.node(nodeId);
|
|
const node = entry.label;
|
|
node.element.setAttribute('transform', 'translate(' + node.x + ',' + node.y + ')');
|
|
node.rectangle.setAttribute('x', - node.width / 2);
|
|
node.rectangle.setAttribute('y', - node.height / 2);
|
|
node.rectangle.setAttribute('width', node.width);
|
|
node.rectangle.setAttribute('height', node.height);
|
|
}
|
|
}
|
|
for (const edge of this.edges.values()) {
|
|
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 node-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.element.appendChild(this.border);
|
|
}
|
|
|
|
measure() {
|
|
this.height = 0;
|
|
for (const block of this._blocks) {
|
|
block.measure();
|
|
this.height = this.height + block.height;
|
|
}
|
|
this.width = Math.max(...this._blocks.map((block) => block.width));
|
|
for (const block of this._blocks) {
|
|
block.width = this.width;
|
|
}
|
|
}
|
|
|
|
layout() {
|
|
let y = 0;
|
|
for (const block of this._blocks) {
|
|
block.x = 0;
|
|
block.y = y;
|
|
block.width = this.width;
|
|
block.layout();
|
|
y += block.height;
|
|
}
|
|
}
|
|
|
|
update() {
|
|
for (const block of this._blocks) {
|
|
block.update();
|
|
}
|
|
this.border.setAttribute('d', grapher.Node.roundedRect(0, 0, this.width, this.height, true, true, true, true));
|
|
this.element.setAttribute('transform', 'translate(' + (this.x - (this.width / 2)) + ',' + (this.y - (this.height / 2)) + ')');
|
|
this.element.style.removeProperty('opacity');
|
|
}
|
|
|
|
select() {
|
|
if (this.element) {
|
|
this.element.classList.add('select');
|
|
return [ this.element ];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
deselect() {
|
|
if (this.element) {
|
|
this.element.classList.remove('select');
|
|
}
|
|
}
|
|
|
|
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;
|
|
for (const entry of this._entries) {
|
|
entry.build(document, parent);
|
|
}
|
|
if (!this.first) {
|
|
this.line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
parent.appendChild(this.line);
|
|
}
|
|
for (let i = 0; i < this._entries.length; i++) {
|
|
const entry = this._entries[i];
|
|
if (i != 0) {
|
|
entry.line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
parent.appendChild(entry.line);
|
|
}
|
|
}
|
|
}
|
|
|
|
measure() {
|
|
this.width = 0;
|
|
this.height = 0;
|
|
for (const entry of this._entries) {
|
|
entry.measure();
|
|
this.height = Math.max(this.height, entry.height);
|
|
this.width += entry.width;
|
|
}
|
|
}
|
|
|
|
layout() {
|
|
let x = this.width;
|
|
for (let i = this._entries.length - 1; i >= 0; i--) {
|
|
const entry = this._entries[i];
|
|
if (i > 0) {
|
|
x -= entry.width;
|
|
entry.x = x;
|
|
} else {
|
|
entry.x = 0;
|
|
entry.width = x;
|
|
}
|
|
}
|
|
}
|
|
|
|
update() {
|
|
for (let i = 0; i < this._entries.length; i++) {
|
|
const entry = this._entries[i];
|
|
entry.element.setAttribute('transform', 'translate(' + entry.x + ',' + this.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 = 1; i < this._entries.length; i++) {
|
|
const entry = this._entries[i];
|
|
const line = entry.line;
|
|
line.setAttribute('class', 'node');
|
|
line.setAttribute('x1', entry.x);
|
|
line.setAttribute('x2', entry.x);
|
|
line.setAttribute('y1', this.y);
|
|
line.setAttribute('y2', this.y + this.height);
|
|
}
|
|
if (this.line) {
|
|
this.line.setAttribute('class', 'node');
|
|
this.line.setAttribute('x1', 0);
|
|
this.line.setAttribute('x2', this.width);
|
|
this.line.setAttribute('y1', this.y);
|
|
this.line.setAttribute('y2', this.y);
|
|
}
|
|
}
|
|
};
|
|
|
|
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);
|
|
}
|
|
|
|
emit(event, data) {
|
|
if (this._events && this._events[event]) {
|
|
for (const callback of this._events[event]) {
|
|
callback(this, data);
|
|
}
|
|
}
|
|
}
|
|
|
|
build(document, parent) {
|
|
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', (e) => {
|
|
e.stopPropagation();
|
|
this.emit('click');
|
|
});
|
|
}
|
|
if (this.tooltip) {
|
|
const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
|
|
title.textContent = this.tooltip;
|
|
this.element.appendChild(title);
|
|
}
|
|
this.text.textContent = this.content || '\u00A0';
|
|
}
|
|
|
|
measure() {
|
|
const yPadding = 4;
|
|
const xPadding = 7;
|
|
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;
|
|
}
|
|
|
|
layout() {
|
|
}
|
|
};
|
|
|
|
grapher.Node.List = class {
|
|
|
|
constructor() {
|
|
this._items = [];
|
|
this._events = {};
|
|
}
|
|
|
|
add(name, value, tooltip, separator) {
|
|
const item = new grapher.Node.List.Item(name, value, tooltip, separator);
|
|
this._items.push(item);
|
|
return item;
|
|
}
|
|
|
|
on(event, callback) {
|
|
this._events[event] = this._events[event] || [];
|
|
this._events[event].push(callback);
|
|
}
|
|
|
|
emit(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.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
this.element.setAttribute('class', 'node-attribute-list');
|
|
if (this._events.click) {
|
|
this.element.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
this.emit('click');
|
|
});
|
|
}
|
|
this.background = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
this.element.appendChild(this.background);
|
|
parent.appendChild(this.element);
|
|
for (const item of this._items) {
|
|
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
group.setAttribute('class', 'node-attribute');
|
|
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
text.setAttribute('xml:space', 'preserve');
|
|
if (item.tooltip) {
|
|
const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
|
|
title.textContent = item.tooltip;
|
|
text.appendChild(title);
|
|
}
|
|
const colon = item.type === 'node' || item.type === 'node[]';
|
|
const name = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
|
name.textContent = colon ? item.name + ':' : item.name;
|
|
if (item.separator.trim() !== '=' && !colon) {
|
|
name.style.fontWeight = 'bold';
|
|
}
|
|
text.appendChild(name);
|
|
group.appendChild(text);
|
|
this.element.appendChild(group);
|
|
item.group = group;
|
|
item.text = text;
|
|
if (item.type === 'node') {
|
|
const node = item.value;
|
|
node.build(document, item.group);
|
|
} else if (item.type === 'node[]') {
|
|
for (const node of item.value) {
|
|
node.build(document, item.group);
|
|
}
|
|
} else {
|
|
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
|
tspan.textContent = item.separator + item.value;
|
|
item.text.appendChild(tspan);
|
|
}
|
|
}
|
|
if (!this.first) {
|
|
this.line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
this.line.setAttribute('class', 'node');
|
|
this.element.appendChild(this.line);
|
|
}
|
|
}
|
|
|
|
measure() {
|
|
this.width = 75;
|
|
this.height = 3;
|
|
const yPadding = 1;
|
|
const xPadding = 6;
|
|
for (let i = 0; i < this._items.length; i++) {
|
|
const item = this._items[i];
|
|
const size = item.text.getBBox();
|
|
item.width = xPadding + size.width + xPadding;
|
|
item.height = yPadding + size.height + yPadding;
|
|
item.offset = size.y;
|
|
this.height += item.height;
|
|
if (item.type === 'node') {
|
|
const node = item.value;
|
|
node.measure();
|
|
this.width = Math.max(150, this.width, node.width + (2 * xPadding));
|
|
this.height += node.height + yPadding + yPadding + yPadding + yPadding;
|
|
if (i === this._items.length - 1) {
|
|
this.height += 3;
|
|
}
|
|
} else if (item.type === 'node[]') {
|
|
for (const node of item.value) {
|
|
node.measure();
|
|
this.width = Math.max(150, this.width, node.width + (2 * xPadding));
|
|
this.height += node.height + yPadding + yPadding + yPadding + yPadding;
|
|
}
|
|
if (i === this._items.length - 1) {
|
|
this.height += 3;
|
|
}
|
|
}
|
|
this.width = Math.max(this.width, item.width);
|
|
}
|
|
this.height += 3;
|
|
}
|
|
|
|
layout() {
|
|
const yPadding = 1;
|
|
const xPadding = 6;
|
|
let y = 3;
|
|
for (const item of this._items) {
|
|
item.x = this.x + xPadding;
|
|
item.y = y + yPadding - item.offset;
|
|
y += item.height;
|
|
if (item.type === 'node') {
|
|
const node = item.value;
|
|
node.width = this.width - xPadding - xPadding;
|
|
node.layout();
|
|
node.x = this.x + xPadding + (node.width / 2);
|
|
node.y = y + (node.height / 2) + yPadding + yPadding;
|
|
y += node.height + yPadding + yPadding + yPadding + yPadding;
|
|
} else if (item.type === 'node[]') {
|
|
for (const node of item.value) {
|
|
node.width = this.width - xPadding - xPadding;
|
|
node.layout();
|
|
node.x = this.x + xPadding + (node.width / 2);
|
|
node.y = y + (node.height / 2) + yPadding + yPadding;
|
|
y += node.height + yPadding + yPadding + yPadding + yPadding;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
update() {
|
|
this.element.setAttribute('transform', 'translate(' + this.x + ',' + this.y + ')');
|
|
this.background.setAttribute('d', grapher.Node.roundedRect(0, 0, this.width, this.height, this.first, this.first, this.last, this.last));
|
|
for (const item of this._items) {
|
|
const text = item.text;
|
|
text.setAttribute('x', item.x);
|
|
text.setAttribute('y', item.y);
|
|
if (item.type === 'node') {
|
|
const node = item.value;
|
|
node.update();
|
|
} else if (item.type === 'node[]') {
|
|
for (const node of item.value) {
|
|
node.update();
|
|
}
|
|
}
|
|
}
|
|
if (this.line) {
|
|
this.line.setAttribute('x1', 0);
|
|
this.line.setAttribute('x2', this.width);
|
|
this.line.setAttribute('y1', 0);
|
|
this.line.setAttribute('y2', 0);
|
|
}
|
|
for (const item of this._items) {
|
|
if (item.value instanceof grapher.Node) {
|
|
const node = item.value;
|
|
node.update();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
grapher.Node.List.Item = class {
|
|
|
|
constructor(name, value, tooltip, separator) {
|
|
this.name = name;
|
|
this.value = value;
|
|
this.tooltip = tooltip;
|
|
this.separator = separator;
|
|
if (value instanceof grapher.Node) {
|
|
this.type = 'node';
|
|
} else if (Array.isArray(value) && value.every((value) => value instanceof grapher.Node)) {
|
|
this.type = 'node[]';
|
|
}
|
|
}
|
|
};
|
|
|
|
grapher.Node.Canvas = class {
|
|
|
|
constructor() {
|
|
this.width = 0;
|
|
this.height = 80;
|
|
}
|
|
|
|
build(/* document, parent */) {
|
|
}
|
|
|
|
update(/* parent, top, width , first, last */) {
|
|
}
|
|
};
|
|
|
|
grapher.Edge = class {
|
|
|
|
constructor(from, to) {
|
|
this.from = from;
|
|
this.to = to;
|
|
}
|
|
|
|
build(document, edgePathGroupElement, edgeLabelGroupElement) {
|
|
const createElement = (name) => {
|
|
return document.createElementNS('http://www.w3.org/2000/svg', name);
|
|
};
|
|
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);
|
|
this.hitTest = createElement('path');
|
|
this.hitTest.setAttribute('class', 'edge-path-hit-test');
|
|
this.hitTest.addEventListener('pointerover', () => this.emit('pointerover'));
|
|
this.hitTest.addEventListener('pointerleave', () => this.emit('pointerleave'));
|
|
this.hitTest.addEventListener('click', () => this.emit('click'));
|
|
edgePathGroupElement.appendChild(this.hitTest);
|
|
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 intersectRect = (node, point) => {
|
|
const x = node.x;
|
|
const y = node.y;
|
|
const dx = point.x - x;
|
|
const dy = point.y - y;
|
|
let h = node.height / 2;
|
|
let w = node.width / 2;
|
|
if (Math.abs(dy) * w > Math.abs(dx) * h) {
|
|
if (dy < 0) {
|
|
h = -h;
|
|
}
|
|
return { x: x + (dy === 0 ? 0 : h * dx / dy), y: y + h };
|
|
}
|
|
if (dx < 0) {
|
|
w = -w;
|
|
}
|
|
return { x: x + w, y: y + (dx === 0 ? 0 : w * dy / dx) };
|
|
};
|
|
const curvePath = (edge, tail, head) => {
|
|
const points = edge.points.slice(1, edge.points.length - 1);
|
|
points.unshift(intersectRect(tail, points[0]));
|
|
points.push(intersectRect(head, points[points.length - 1]));
|
|
return new grapher.Edge.Curve(points).path.data;
|
|
};
|
|
const edgePath = curvePath(this, this.from, this.to);
|
|
this.element.setAttribute('d', edgePath);
|
|
this.hitTest.setAttribute('d', edgePath);
|
|
if (this.labelElement) {
|
|
this.labelElement.setAttribute('transform', 'translate(' + (this.x - (this.width / 2)) + ',' + (this.y - (this.height / 2)) + ')');
|
|
this.labelElement.style.opacity = 1;
|
|
}
|
|
}
|
|
|
|
select() {
|
|
if (this.element) {
|
|
if (!this.element.classList.contains('select')) {
|
|
const path = this.element;
|
|
path.classList.add('select');
|
|
this.element = path.cloneNode(true);
|
|
path.parentNode.replaceChild(this.element, path);
|
|
}
|
|
return [ this.element ];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
deselect() {
|
|
if (this.element && this.element.classList.contains('select')) {
|
|
const path = this.element;
|
|
path.classList.remove('select');
|
|
this.element = path.cloneNode(true);
|
|
path.parentNode.replaceChild(this.element, path);
|
|
}
|
|
}
|
|
};
|
|
|
|
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;
|
|
default:
|
|
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 window !== 'undefined' && typeof window === 'object') {
|
|
window.grapher = grapher;
|
|
} |