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 = {}; } graph() { return this._options; } setNode(node) { this._nodes.set(node.name, node); if (this._isCompound) { this._parent[node.name] = '\x00'; this._children[node.name] = {}; this._children['\x00'][node.name] = true; } return this; } 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, edge); } return this; } 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; } nodes() { return this._nodes; } hasNode(key) { return this._nodes.has(key); } node(key) { return this._nodes.get(key); } edges() { return Array.from(this._edges.values()); } edge(key) { return key; } 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, 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().keys()) { 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().keys()) { 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(); } } }; 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; } build(document, contextElement) { 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; const tops = []; for (const block of this._blocks) { tops.push(height); block.build(document, this.label); if (width < block.width) { width = block.width; } height = height + block.height; } for (let i = 0; i < this._blocks.length; i++) { const top = tops.shift(); this._blocks[i].update(this.label, top, width, i == 0, i == this._blocks.length - 1); } const borderElement = createElement('path'); borderElement.setAttribute('class', [ 'node', 'border' ].join(' ')); borderElement.setAttribute('d', grapher.Node.roundedRect(0, 0, width, height, true, true, true, true)); this.label.appendChild(borderElement); 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) { 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._items = []; } add(id, classList, content, tooltip, handler) { this._items.push({ id: id, classList: classList, content: content, tooltip: tooltip, handler: handler }); } build(document, parentElement) { this._document = document; this._width = 0; this._height = 0; this._elements = []; let x = 0; const y = 0; for (const item of this._items) { const yPadding = 4; const xPadding = 7; const element = this.createElement('g'); const classList = [ 'node-item' ]; parentElement.appendChild(element); const pathElement = this.createElement('path'); const textElement = this.createElement('text'); element.appendChild(pathElement); element.appendChild(textElement); if (item.classList) { classList.push(...item.classList); } element.setAttribute('class', classList.join(' ')); if (item.id) { element.setAttribute('id', item.id); } if (item.handler) { element.addEventListener('click', item.handler); } if (item.tooltip) { const titleElement = this.createElement('title'); titleElement.textContent = item.tooltip; element.appendChild(titleElement); } if (item.content) { textElement.textContent = item.content; } const boundingBox = textElement.getBBox(); const width = boundingBox.width + xPadding + xPadding; const height = boundingBox.height + yPadding + yPadding; this._elements.push({ 'group': element, 'text': textElement, 'path': pathElement, 'x': x, 'y': y, 'width': width, 'height': height, 'tx': xPadding, 'ty': yPadding - boundingBox.y, }); x += width; if (this._height < height) { this._height = height; } if (x > this._width) { this._width = x; } } } get width() { return this._width; } get height() { return this._height; } update(parentElement, top, width, first, last) { const dx = width - this._width; let i; let element; for (i = 0; i < this._elements.length; i++) { element = this._elements[i]; if (i == 0) { element.width = element.width + dx; } else { element.x = element.x + dx; element.tx = element.tx + dx; } element.y = element.y + top; } for (i = 0; i < this._elements.length; i++) { element = this._elements[i]; element.group.setAttribute('transform', 'translate(' + element.x + ',' + element.y + ')'); const r1 = i == 0 && first; const r2 = i == this._elements.length - 1 && first; const r3 = i == this._elements.length - 1 && last; const r4 = i == 0 && last; element.path.setAttribute('d', grapher.Node.roundedRect(0, 0, element.width, element.height, r1, r2, r3, r4)); element.text.setAttribute('x', 6); element.text.setAttribute('y', element.ty); } let lineElement; for (i = 0; i < this._elements.length; i++) { element = this._elements[i]; if (i != 0) { lineElement = this.createElement('line'); lineElement.setAttribute('class', 'node'); lineElement.setAttribute('x1', element.x); lineElement.setAttribute('x2', element.x); lineElement.setAttribute('y1', top); lineElement.setAttribute('y2', top + this._height); parentElement.appendChild(lineElement); } } if (!first) { lineElement = this.createElement('line'); lineElement.setAttribute('class', 'node'); lineElement.setAttribute('x1', 0); lineElement.setAttribute('x2', width); lineElement.setAttribute('y1', top); lineElement.setAttribute('y2', top); parentElement.appendChild(lineElement); } } createElement(name) { return this._document.createElementNS('http://www.w3.org/2000/svg', name); } }; grapher.Node.List = class { constructor() { this._items = []; } add(id, name, value, tooltip, separator) { this._items.push({ id: id, name: name, value: value, tooltip: tooltip, separator: separator }); } get handler() { return this._handler; } set handler(handler) { this._handler = handler; } build(document, parentElement) { this._document = document; this._width = 0; this._height = 0; const x = 0; const y = 0; this._element = this.createElement('g'); this._element.setAttribute('class', 'node-attribute'); parentElement.appendChild(this._element); if (this._handler) { this._element.addEventListener('click', this._handler); } this._backgroundElement = this.createElement('path'); this._element.appendChild(this._backgroundElement); this._element.setAttribute('transform', 'translate(' + x + ',' + y + ')'); this._height += 3; for (const item of this._items) { const yPadding = 1; const xPadding = 6; const textElement = this.createElement('text'); if (item.id) { textElement.setAttribute('id', item.id); } textElement.setAttribute('xml:space', 'preserve'); this._element.appendChild(textElement); if (item.tooltip) { const titleElement = this.createElement('title'); titleElement.textContent = item.tooltip; textElement.appendChild(titleElement); } const textNameElement = this.createElement('tspan'); textNameElement.textContent = item.name; if (item.separator.trim() != '=') { textNameElement.style.fontWeight = 'bold'; } textElement.appendChild(textNameElement); const textValueElement = this.createElement('tspan'); textValueElement.textContent = item.separator + item.value; textElement.appendChild(textValueElement); const size = textElement.getBBox(); const width = xPadding + size.width + xPadding; if (this._width < width) { this._width = width; } textElement.setAttribute('x', x + xPadding); textElement.setAttribute('y', this._height + yPadding - size.y); this._height += yPadding + size.height + yPadding; } this._height += 3; if (this._width < 75) { this._width = 75; } } get width() { return this._width; } get height() { return this._height; } update(parentElement, top, width , first, last) { this._element.setAttribute('transform', 'translate(0,' + top + ')'); const r1 = first; const r2 = first; const r3 = last; const r4 = last; this._backgroundElement.setAttribute('d', grapher.Node.roundedRect(0, 0, width, this._height, r1, r2, r3, r4)); if (!first) { const lineElement = this.createElement('line'); lineElement.setAttribute('class', 'node'); lineElement.setAttribute('x1', 0); lineElement.setAttribute('x2', width); lineElement.setAttribute('y1', 0); lineElement.setAttribute('y2', 0); this._element.appendChild(lineElement); } } createElement(name) { return this._document.createElementNS('http://www.w3.org/2000/svg', name); } }; class Path { 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; } } class Curve { constructor(context) { this._context = context; } lineStart() { this._x0 = NaN; this._x1 = NaN; this._y0 = NaN; this._y1 = NaN; this._point = 0; } lineEnd() { switch (this._point) { case 3: this.curve(this._x1, this._y1); this._context.lineTo(this._x1, this._y1); break; case 2: this._context.lineTo(this._x1, this._y1); break; } if (this._line || (this._line !== 0 && this._point === 1)) { this._context.closePath(); } this._line = 1 - this._line; } point(x, y) { x = +x; y = +y; switch (this._point) { case 0: this._point = 1; if (this._line) { this._context.lineTo(x, y); } else { this._context.moveTo(x, y); } break; case 1: this._point = 2; break; case 2: this._point = 3; this._context.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._context.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 = 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 }; } }; if (typeof module !== 'undefined' && typeof module.exports === 'object') { module.exports.Graph = grapher.Graph; module.exports.Node = grapher.Node; module.exports.Edge = grapher.Edge; }