UNPKG

inheritance-diagram

Version:

Build an inheritance diagram for a class

481 lines (395 loc) 13.2 kB
<!DOCTYPE html> <html> <head> <style>html,body {height:100%; margin:0}</style> </head> <body> <script> /* * Copyright (c) 2016-2020 Valerii Zinchenko * Licensed under MIT (https://gitlab.com/valerii-zinchenko/inheritance-diagram/blob/master/LICENSE.txt) * All source files are available at: https://gitlab.com/valerii-zinchenko/inheritance-diagram */ const tmp = document.createElement('div'); function escape(text) { tmp.innerText = text; return tmp.innerHTML; } function createSVGElement(nodeName, attributes) { const el = document.createElementNS('http://www.w3.org/2000/svg', nodeName); setAttributes(el, attributes); return el; } function setAttributes(el, attributes) { for (let key in attributes) { el.setAttribute(key, attributes[key]); } } function createNodeElement(g, node, renderingProperties, align = 'center') { let domGroup = node.element; if (domGroup) { // reposition g.append(domGroup); return node.element; } const {padding} = renderingProperties.node; let nodeClass = node.type; // App link element if possible and make that element as the main container of rectangle and text // -------------------------------------------------- if (node.type !== 'noi') { if (node.data.link) { domGroup = createSVGElement('a', { 'xlink:href': escape(node.data.link) }); } else { nodeClass += ' no-ref'; } } if (!domGroup) { domGroup = createSVGElement('g'); } g.append(domGroup); setAttributes(domGroup, { 'class': nodeClass }); // -------------------------------------------------- // Measure the text height // -------------------------------------------------- const tmpText = createSVGElement('text'); // This character set is used just to calculate the height of a node to keep all rectangles with the same height tmpText.innerHTML = '0123456789QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm-=!@#$%^&*(),./<>;\':"[]\\{}|`~'; domGroup.append(tmpText); const textHeight = tmpText.getBoundingClientRect().height; const boxHeight = textHeight + 2*padding[1]; tmpText.remove(); // -------------------------------------------------- // Add text (node name) into the main container const text = createSVGElement('text', { y: boxHeight / 2 + textHeight / 3, 'text-anchor': 'middle' }); text.innerHTML = escape(node.name); // adding text into the DOM to render and calculate the width domGroup.append(text); const textCR = text.getBoundingClientRect(); const width = 2*padding[0] + textCR.width; setAttributes(text, { x: width/2 }); // Create rectangle element in the main container const domBorder = createSVGElement('rect', { height: boxHeight, width }); domGroup.append(domBorder); // move text on top of the rectangle domGroup.append(text); switch (align) { case 'right': domGroup.setAttribute('transform', `translate(${-width}, 0)`); break; case 'center': domGroup.setAttribute('transform', `translate(${-width/2}, 0)`); break; // No processing for left aligned is added, because if will have "translate(0, 0)" } node.element = domGroup; return domGroup; } function createNodeCluster(g, node, renderingProperties, extraYOffset = 0) { const yMargin = renderingProperties.node.margin[0]; createNodeElement(g, node, renderingProperties); const nodeHeight = node.element.getBoundingClientRect().height; if (node.children.length > 0) { const container = createSVGElement('g'); g.append(container); createChildrenNodes(container, node.children, renderingProperties); const firstRect = node.children[0].element.getBoundingClientRect(); const lastRect = node.children[node.children.length-1].element.getBoundingClientRect(); const start = firstRect.x + firstRect.width/2; const end = (lastRect.x + lastRect.width/2 - start) / 2; const yOffset = nodeHeight + yMargin + extraYOffset; setAttributes(container, { transform: `translate(${-(start + end)}, ${yOffset})` }); g.prepend(createSVGElement('polyline', { 'class': 'inherit', 'marker-end': 'url(#inheritance)', points: `0,${yOffset - yMargin / 3} 0,${node.element.getBoundingClientRect().height}` })); } // reposition createNodeElement(g, node, renderingProperties); return g; } function createChildrenNodes(container, children, renderingProperties) { children.forEach(child => { const g = createSVGElement('g'); container.append(g); createNodeCluster(g, child, renderingProperties); }); const lineContainer = createSVGElement('g'); let child = container.firstChild; const type = 'inherit'; const {margin} = renderingProperties.node; let x = child.getBoundingClientRect().x; const xPositions = []; while (child) { const cr = child.getBoundingClientRect(); const xPos = x - cr.x; xPositions.push(xPos); setAttributes(child, { transform: `translate(${xPos}, 0)` }); x += cr.width + margin[1]; child = child.nextSibling; } // Remove the first X position because it is always 0. xPositions.shift(); const lineBusOffset = margin[0] / 3; if (xPositions.length === 0) { lineContainer.append(createSVGElement('polyline', { 'class': type, points: `0,0 0,${-lineBusOffset}` })); } else { // Remove the last X position to build a common connection line between the first and the last children and use the remaining children X positions to draw a simple connection to the common line const endX = xPositions.pop(); lineContainer.append(createSVGElement('polyline', { 'class': type, points: `0,0 0,${-lineBusOffset} ${endX},${-lineBusOffset} ${endX},0` })); xPositions.forEach(x => { lineContainer.append(createSVGElement('polyline', { 'class': type, points: `0,0 0,${-lineBusOffset}`, transform: `translate(${x}, 0)` })); }); } container.append(lineContainer); } function createParentCluster(container, noi, renderingProperties, xOffset) { const {margin} = renderingProperties.node; noi.parentStack.forEach((node, index) => { const g = createSVGElement('g'); container.prepend(g); createNodeElement(g, node, renderingProperties); if (node.implements.length > 0) { createInterfaces(g, node, renderingProperties, xOffset); } if (node.mixes.length > 0) { createMixins(g, node, renderingProperties, xOffset); } const nodeHeight = g.getBoundingClientRect().height; const offset = nodeHeight + margin[0]; g.prepend(createSVGElement('polyline', { 'class': 'inherit', 'marker-end': 'url(#inheritance)', points: `0,${offset} 0,${node.element.getBoundingClientRect().height}` })); setAttributes(g, { transform: `translate(0, ${-offset})` }); container = g; }); } function createInterfaces(g, node, renderingProperties, xOffset) { const type = 'interface'; const {margin} = renderingProperties.node; const nodeHeight = node.element.getBoundingClientRect().height; const nodeYSpacing = margin[0]/2; const dy = nodeHeight + nodeYSpacing; const container = createSVGElement('g'); g.prepend(container); const ys = []; node.implements.forEach((item, index) => { const g = createSVGElement('g'); container.append(g); createNodeElement(g, item, renderingProperties, 'left'); const y = index * dy; ys.push(y); setAttributes(g, { transform: `translate(0, ${y})` }); }); const lineContainer = createSVGElement('g'); const nodeRect = node.element.getBoundingClientRect(); const lineBusOffset = 2*margin[1]; if (ys.length > 1) { ys.shift(); const endY = ys.pop() + nodeHeight/2; lineContainer.append(createSVGElement('polyline', { 'class': type, points: `0,${nodeHeight/2} -${lineBusOffset},${nodeHeight/2} -${lineBusOffset},${endY} 0,${endY}`, 'marker-end': 'url(#inheritance)', 'marker-start': 'url(#inheritance)' })); } ys.forEach(y => { lineContainer.append(createSVGElement('polyline', { 'class': type, points: `0,0 -${lineBusOffset},0`, transform: `translate(0, ${y + nodeHeight/2})`, 'marker-start': 'url(#inheritance)' })); }); const y = nodeRect.height/2; container.prepend(createSVGElement('polyline', { 'class': 'implements', points: `-${lineBusOffset},${y} -${xOffset - nodeRect.width/2},${y}` })); container.prepend(lineContainer); setAttributes(container, { transform: `translate(${xOffset}, 0)` }); } function createMixins(g, node, renderingProperties, xOffset) { const type = 'mixes'; const {margin} = renderingProperties.node; const nodeHeight = node.element.getBoundingClientRect().height; const nodeYSpacing = margin[0]/2; const dy = nodeHeight + nodeYSpacing; const container = createSVGElement('g'); g.prepend(container); const ys = []; node.mixes.forEach((item, index) => { const g = createSVGElement('g'); container.append(g); createNodeElement(g, item, renderingProperties, 'right'); const y = index * dy; ys.push(y); setAttributes(g, { transform: `translate(0, ${y})` }); }); const lineContainer = createSVGElement('g'); const lineBusOffset = 2*margin[1]; if (ys.length > 1) { ys.shift(); const endY = ys.pop() + nodeHeight/2; lineContainer.append(createSVGElement('polyline', { 'class': type, points: `0,${nodeHeight/2} ${lineBusOffset},${nodeHeight/2} ${lineBusOffset},${endY} 0,${endY}`, 'marker-end': 'url(#inheritance)', 'marker-start': 'url(#inheritance)' })); } ys.forEach(y => { lineContainer.append(createSVGElement('polyline', { 'class': type, points: `0,0 ${lineBusOffset},0`, transform: `translate(0, ${y + nodeHeight/2})`, 'marker-start': 'url(#inheritance)' })); }); const nodeRect = node.element.getBoundingClientRect(); const y = nodeRect.height/2; container.prepend(createSVGElement('polyline', { 'class': 'mixes', points: `${lineBusOffset},${y} ${xOffset - nodeRect.width/2},${y}` })); container.prepend(lineContainer); setAttributes(container, { transform: `translate(${-xOffset}, 0)` }); } function render(noi, renderingProperties) { const endMarker = { width: 6, height: 5 }; const css = ` text { font-size: initial; } rect { stroke-width: 2; stroke: black; fill: white; } marker { overflow: visible; } marker rect { stroke: none; stroke-width: 0 } marker path { fill: white; stroke: black; } a { cursor: pointer; } a text { fill: blue; text-decoration: underline; } polyline, line { stroke-width: 2; stroke: black; fill: none; } .child rect, .parent rect { stroke: blue; } .interface rect { stroke: blueviolet; } .mixin rect { stroke: green; } .no-ref rect { fill: lightgray; } ${renderingProperties.css || ''} `.replace(/\n/g, ' '); document.body.innerHTML = ` <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"> <defs> <style type="text/css">${escape(css)}</style> <marker id="inheritance" refX="${endMarker.height}" refY="${endMarker.width/2}" orient="auto-start-reverse"> <rect width="${endMarker.width}" height="${endMarker.height}"></rect> <path d="M 0 0 L ${endMarker.height} ${endMarker.width/2} L 0 ${endMarker.width} z"></path> </marker> </defs> <g></g> </svg>`.replace(/\n/g, '').replace(/\s{2,}/g, ' '); const svg = document.body.querySelector('svg'); const diagramContainer = svg.querySelector('g'); const {margin}= renderingProperties.node; createNodeElement(diagramContainer, noi, renderingProperties); let xOffset = noi.element.getBoundingClientRect().width / 2; noi.parentStack.forEach(parent => { createNodeElement(diagramContainer, parent, renderingProperties); xOffset = Math.max(xOffset, parent.element.getBoundingClientRect().width / 2); }); xOffset += 4*margin[1]; let diagramClientRect = diagramContainer.getBoundingClientRect(); let lastInterfaceY = 0 if (noi.implements.length > 0) { createInterfaces(diagramContainer, noi, renderingProperties, xOffset); const lastRect = noi.implements[noi.implements.length-1].element.getBoundingClientRect(); lastInterfaceY = lastRect.y; } let lastMixinY = 0; if (noi.mixes.length > 0) { createMixins(diagramContainer, noi, renderingProperties, xOffset); const lastRect = noi.mixes[noi.mixes.length-1].element.getBoundingClientRect(); lastMixinY = lastRect.y; } createParentCluster(diagramContainer, noi, renderingProperties, xOffset); createNodeCluster(diagramContainer, noi, renderingProperties, Math.max(lastInterfaceY, lastMixinY)); diagramClientRect = diagramContainer.getBoundingClientRect(); diagramContainer.setAttribute("transform", `translate(${-diagramClientRect.x + margin[1]/2}, ${-diagramClientRect.y + margin[0]/2})`); setAttributes(svg, { width: diagramClientRect.width + margin[1], height: diagramClientRect.height + margin[0] }); return document.body.innerHTML; } </script> </body> </html>