|
|
@@ -108,9 +108,98 @@ grapher.Graph = class {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- render(document, originElement) {
|
|
|
- const renderer = new grapher.Renderer(document, originElement);
|
|
|
- renderer.render(this);
|
|
|
+ build(document, originElement) {
|
|
|
+
|
|
|
+ const createElement = (name) => {
|
|
|
+ return document.createElementNS('http://www.w3.org/2000/svg', name);
|
|
|
+ };
|
|
|
+ const createGroup = (name) => {
|
|
|
+ const element = createElement('g');
|
|
|
+ element.setAttribute('id', name);
|
|
|
+ element.setAttribute('class', name);
|
|
|
+ originElement.appendChild(element);
|
|
|
+ return element;
|
|
|
+ };
|
|
|
+
|
|
|
+ const clusterGroup = createGroup('clusters');
|
|
|
+ const edgePathGroup = createGroup('edge-paths');
|
|
|
+ const edgeLabelGroup = createGroup('edge-labels');
|
|
|
+ const nodeGroup = createGroup('nodes');
|
|
|
+
|
|
|
+ const edgePathGroupDefs = createElement('defs');
|
|
|
+ edgePathGroup.appendChild(edgePathGroupDefs);
|
|
|
+ const marker = (id) => {
|
|
|
+ const element = createElement('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 = createElement('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()) {
|
|
|
+ const node = this.node(nodeId);
|
|
|
+ if (this.children(nodeId).length == 0) {
|
|
|
+ // node
|
|
|
+ node.build(document, nodeGroup);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ // cluster
|
|
|
+ node.rectangle = createElement('rect');
|
|
|
+ if (node.rx) {
|
|
|
+ node.rectangle.setAttribute('rx', node.rx);
|
|
|
+ }
|
|
|
+ if (node.ry) {
|
|
|
+ node.rectangle.setAttribute('ry', node.ry);
|
|
|
+ }
|
|
|
+ node.element = createElement('g');
|
|
|
+ node.element.setAttribute('class', 'cluster');
|
|
|
+ node.element.appendChild(node.rectangle);
|
|
|
+ clusterGroup.appendChild(node.element);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const edgeId of this.edges()) {
|
|
|
+ const edge = this.edge(edgeId);
|
|
|
+ edge.build(document, edgePathGroup, edgeLabelGroup);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ layout() {
|
|
|
+
|
|
|
+ dagre.layout(this);
|
|
|
+
|
|
|
+ for (const nodeId of this.nodes()) {
|
|
|
+ const node = this.node(nodeId);
|
|
|
+ if (this.children(nodeId).length == 0) {
|
|
|
+ // node
|
|
|
+ node.layout();
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ // cluster
|
|
|
+ const node = this.node(nodeId);
|
|
|
+ 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 edgeId of this.edges()) {
|
|
|
+ const edge = this.edge(edgeId);
|
|
|
+ edge.layout();
|
|
|
+ }
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -133,9 +222,22 @@ grapher.Node = class {
|
|
|
}
|
|
|
|
|
|
build(document, contextElement) {
|
|
|
- this._document = document;
|
|
|
- const rootElement = this.createElement('g');
|
|
|
- contextElement.appendChild(rootElement);
|
|
|
+ const createElement = (name) => {
|
|
|
+ return document.createElementNS('http://www.w3.org/2000/svg', name);
|
|
|
+ };
|
|
|
+ this.label = createElement('g');
|
|
|
+
|
|
|
+ this.element = createElement('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;
|
|
|
+ this.element.appendChild(this.label);
|
|
|
+
|
|
|
+ contextElement.appendChild(this.element);
|
|
|
+
|
|
|
+ // contextElement.appendChild(this.label);
|
|
|
|
|
|
let width = 0;
|
|
|
let height = 0;
|
|
|
@@ -143,7 +245,7 @@ grapher.Node = class {
|
|
|
|
|
|
for (const block of this._blocks) {
|
|
|
tops.push(height);
|
|
|
- block.build(document, rootElement);
|
|
|
+ block.build(document, this.label);
|
|
|
if (width < block.width) {
|
|
|
width = block.width;
|
|
|
}
|
|
|
@@ -152,16 +254,25 @@ grapher.Node = class {
|
|
|
|
|
|
for (let i = 0; i < this._blocks.length; i++) {
|
|
|
const top = tops.shift();
|
|
|
- this._blocks[i].update(rootElement, top, width, i == 0, i == this._blocks.length - 1);
|
|
|
+ this._blocks[i].update(this.label, top, width, i == 0, i == this._blocks.length - 1);
|
|
|
}
|
|
|
|
|
|
- const borderElement = this.createElement('path');
|
|
|
+ const borderElement = createElement('path');
|
|
|
borderElement.setAttribute('class', [ 'node', 'border' ].join(' '));
|
|
|
borderElement.setAttribute('d', grapher.Node.roundedRect(0, 0, width, height, true, true, true, true));
|
|
|
- rootElement.appendChild(borderElement);
|
|
|
+ this.label.appendChild(borderElement);
|
|
|
|
|
|
- contextElement.innerHTML = '';
|
|
|
- return rootElement;
|
|
|
+ const nodeBox = this.label.getBBox();
|
|
|
+ const nodeX = - nodeBox.width / 2;
|
|
|
+ const nodeY = - nodeBox.height / 2;
|
|
|
+ this.width = nodeBox.width;
|
|
|
+ this.height = nodeBox.height;
|
|
|
+ this.label.setAttribute('transform', 'translate(' + nodeX + ',' + nodeY + ')');
|
|
|
+ }
|
|
|
+
|
|
|
+ layout() {
|
|
|
+ this.element.setAttribute('transform', 'translate(' + this.x + ',' + this.y + ')');
|
|
|
+ this.element.style.opacity = 1;
|
|
|
}
|
|
|
|
|
|
static roundedRect(x, y, width, height, r1, r2, r3, r4) {
|
|
|
@@ -181,10 +292,6 @@ grapher.Node = class {
|
|
|
"a" + r1 + "," + r1 + " 0 0 1 " + r1 + "," + -r1 +
|
|
|
"z";
|
|
|
}
|
|
|
-
|
|
|
- createElement(name) {
|
|
|
- return this._document.createElementNS('http://www.w3.org/2000/svg', name);
|
|
|
- }
|
|
|
};
|
|
|
|
|
|
grapher.Node.Header = class {
|
|
|
@@ -434,216 +541,6 @@ grapher.Node.List = class {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-grapher.Renderer = class {
|
|
|
-
|
|
|
- constructor(document, svgElement) {
|
|
|
- this._document = document;
|
|
|
- this._svgElement = svgElement;
|
|
|
- }
|
|
|
-
|
|
|
- render(graph) {
|
|
|
-
|
|
|
- const svgClusterGroup = this.createElement('g');
|
|
|
- svgClusterGroup.setAttribute('id', 'clusters');
|
|
|
- svgClusterGroup.setAttribute('class', 'clusters');
|
|
|
- this._svgElement.appendChild(svgClusterGroup);
|
|
|
-
|
|
|
- const svgEdgePathGroup = this.createElement('g');
|
|
|
- svgEdgePathGroup.setAttribute('id', 'edge-paths');
|
|
|
- svgEdgePathGroup.setAttribute('class', 'edge-paths');
|
|
|
- this._svgElement.appendChild(svgEdgePathGroup);
|
|
|
-
|
|
|
- const svgEdgeLabelGroup = this.createElement('g');
|
|
|
- svgEdgeLabelGroup.setAttribute('id', 'edge-labels');
|
|
|
- svgEdgeLabelGroup.setAttribute('class', 'edge-labels');
|
|
|
- this._svgElement.appendChild(svgEdgeLabelGroup);
|
|
|
-
|
|
|
- const svgNodeGroup = this.createElement('g');
|
|
|
- svgNodeGroup.setAttribute('id', 'nodes');
|
|
|
- svgNodeGroup.setAttribute('class', 'nodes');
|
|
|
- this._svgElement.appendChild(svgNodeGroup);
|
|
|
-
|
|
|
- for (const nodeId of graph.nodes()) {
|
|
|
- if (graph.children(nodeId).length == 0) {
|
|
|
- const node = graph.node(nodeId);
|
|
|
- const element = this.createElement('g');
|
|
|
- if (node.id) {
|
|
|
- element.setAttribute('id', node.id);
|
|
|
- }
|
|
|
- element.setAttribute('class', node.class ? 'node ' + node.class : 'node');
|
|
|
- element.style.opacity = 0;
|
|
|
- const container = this.createElement('g');
|
|
|
- container.appendChild(node.label);
|
|
|
- element.appendChild(container);
|
|
|
- svgNodeGroup.appendChild(element);
|
|
|
- const nodeBox = node.label.getBBox();
|
|
|
- const nodeX = - nodeBox.width / 2;
|
|
|
- const nodeY = - nodeBox.height / 2;
|
|
|
- container.setAttribute('transform', 'translate(' + nodeX + ',' + nodeY + ')');
|
|
|
- node.width = nodeBox.width;
|
|
|
- node.height = nodeBox.height;
|
|
|
- node.element = element;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- for (const edgeId of graph.edges()) {
|
|
|
- const edge = graph.edge(edgeId);
|
|
|
- if (edge.label) {
|
|
|
- const tspan = this.createElement('tspan');
|
|
|
- tspan.setAttribute('xml:space', 'preserve');
|
|
|
- tspan.setAttribute('dy', '1em');
|
|
|
- tspan.setAttribute('x', '1');
|
|
|
- tspan.appendChild(this._document.createTextNode(edge.label));
|
|
|
- const text = this.createElement('text');
|
|
|
- text.appendChild(tspan);
|
|
|
- const textContainer = this.createElement('g');
|
|
|
- textContainer.appendChild(text);
|
|
|
- const labelElement = this.createElement('g');
|
|
|
- labelElement.style.opacity = 0;
|
|
|
- labelElement.setAttribute('class', 'edge-label');
|
|
|
- if (edge.id) {
|
|
|
- labelElement.setAttribute('id', 'edge-label-' + edge.id);
|
|
|
- }
|
|
|
- labelElement.appendChild(textContainer);
|
|
|
- svgEdgeLabelGroup.appendChild(labelElement);
|
|
|
- const edgeBox = textContainer.getBBox();
|
|
|
- const edgeX = - edgeBox.width / 2;
|
|
|
- const edgeY = - edgeBox.height / 2;
|
|
|
- textContainer.setAttribute('transform', 'translate(' + edgeX + ',' + edgeY + ')');
|
|
|
- edge.width = edgeBox.width;
|
|
|
- edge.height = edgeBox.height;
|
|
|
- edge.labelElement = labelElement;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- dagre.layout(graph);
|
|
|
-
|
|
|
- for (const nodeId of graph.nodes()) {
|
|
|
- if (graph.children(nodeId).length == 0) {
|
|
|
- const node = graph.node(nodeId);
|
|
|
- node.element.setAttribute('transform', 'translate(' + node.x + ',' + node.y + ')');
|
|
|
- node.element.style.opacity = 1;
|
|
|
- delete node.element;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- for (const edgeId of graph.edges()) {
|
|
|
- const edge = graph.edge(edgeId);
|
|
|
- if (edge.labelElement) {
|
|
|
- edge.labelElement.setAttribute('transform', 'translate(' + edge.x + ',' + edge.y + ')');
|
|
|
- edge.labelElement.style.opacity = 1;
|
|
|
- delete edge.labelElement;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- const edgePathGroupDefs = this.createElement('defs');
|
|
|
- svgEdgePathGroup.appendChild(edgePathGroupDefs);
|
|
|
- const marker = (id) => {
|
|
|
- const element = this.createElement('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 = this.createElement('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 edgeId of graph.edges()) {
|
|
|
- const edge = graph.edge(edgeId);
|
|
|
- const edgePath = grapher.Renderer._computeCurvePath(edge, graph.node(edgeId.v), graph.node(edgeId.w));
|
|
|
- const edgeElement = this.createElement('path');
|
|
|
- edgeElement.setAttribute('class', edge.class ? 'edge-path ' + edge.class : 'edge-path');
|
|
|
- edgeElement.setAttribute('d', edgePath);
|
|
|
- if (edge.id) {
|
|
|
- edgeElement.setAttribute('id', edge.id);
|
|
|
- }
|
|
|
- svgEdgePathGroup.appendChild(edgeElement);
|
|
|
- }
|
|
|
-
|
|
|
- for (const nodeId of graph.nodes()) {
|
|
|
- if (graph.children(nodeId).length > 0) {
|
|
|
- const node = graph.node(nodeId);
|
|
|
- const nodeElement = this.createElement('g');
|
|
|
- nodeElement.setAttribute('class', 'cluster');
|
|
|
- nodeElement.setAttribute('transform', 'translate(' + node.x + ',' + node.y + ')');
|
|
|
- const rect = this.createElement('rect');
|
|
|
- rect.setAttribute('x', - node.width / 2);
|
|
|
- rect.setAttribute('y', - node.height / 2 );
|
|
|
- rect.setAttribute('width', node.width);
|
|
|
- rect.setAttribute('height', node.height);
|
|
|
- if (node.rx) {
|
|
|
- rect.setAttribute('rx', node.rx);
|
|
|
- }
|
|
|
- if (node.ry) {
|
|
|
- rect.setAttribute('ry', node.ry);
|
|
|
- }
|
|
|
- nodeElement.appendChild(rect);
|
|
|
- svgClusterGroup.appendChild(nodeElement);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- createElement(name) {
|
|
|
- return this._document.createElementNS('http://www.w3.org/2000/svg', name);
|
|
|
- }
|
|
|
-
|
|
|
- static _computeCurvePath(edge, tail, head) {
|
|
|
- const points = edge.points.slice(1, edge.points.length - 1);
|
|
|
- points.unshift(grapher.Renderer.intersectRect(tail, points[0]));
|
|
|
- points.push(grapher.Renderer.intersectRect(head, points[points.length - 1]));
|
|
|
-
|
|
|
- const path = new Path();
|
|
|
- const curve = new Curve(path);
|
|
|
- for (let i = 0; i < points.length; i++) {
|
|
|
- const point = points[i];
|
|
|
- if (i == 0) {
|
|
|
- curve.lineStart();
|
|
|
- }
|
|
|
- curve.point(point.x, point.y);
|
|
|
- if (i == points.length - 1) {
|
|
|
- curve.lineEnd();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return 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};
|
|
|
- }
|
|
|
-};
|
|
|
-
|
|
|
class Path {
|
|
|
|
|
|
constructor() {
|
|
|
@@ -754,9 +651,109 @@ class Curve {
|
|
|
|
|
|
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);
|
|
|
+ };
|
|
|
+ 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);
|
|
|
+
|
|
|
+ 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));
|
|
|
+ const text = createElement('text');
|
|
|
+ text.appendChild(tspan);
|
|
|
+ const textContainer = createElement('g');
|
|
|
+ textContainer.appendChild(text);
|
|
|
+ this.labelElement = createElement('g');
|
|
|
+ this.labelElement.style.opacity = 0;
|
|
|
+ this.labelElement.setAttribute('class', 'edge-label');
|
|
|
+ if (this.id) {
|
|
|
+ this.labelElement.setAttribute('id', 'edge-label-' + this.id);
|
|
|
+ }
|
|
|
+ this.labelElement.appendChild(textContainer);
|
|
|
+ edgeLabelGroupElement.appendChild(this.labelElement);
|
|
|
+ const edgeBox = textContainer.getBBox();
|
|
|
+ const edgeX = - edgeBox.width / 2;
|
|
|
+ const edgeY = - edgeBox.height / 2;
|
|
|
+ textContainer.setAttribute('transform', 'translate(' + edgeX + ',' + edgeY + ')');
|
|
|
+ this.width = edgeBox.width;
|
|
|
+ this.height = edgeBox.height;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ layout() {
|
|
|
+ const edgePath = grapher.Edge._computeCurvePath(this, this.from, this.to);
|
|
|
+ this.element.setAttribute('d', edgePath);
|
|
|
+
|
|
|
+ if (this.labelElement) {
|
|
|
+ this.labelElement.setAttribute('transform', 'translate(' + this.x + ',' + this.y + ')');
|
|
|
+ this.labelElement.style.opacity = 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 path = new Path();
|
|
|
+ const curve = new Curve(path);
|
|
|
+ for (let i = 0; i < points.length; i++) {
|
|
|
+ const point = points[i];
|
|
|
+ if (i == 0) {
|
|
|
+ curve.lineStart();
|
|
|
+ }
|
|
|
+ curve.point(point.x, point.y);
|
|
|
+ if (i == points.length - 1) {
|
|
|
+ curve.lineEnd();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return 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};
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
|