fixed class diagram 2nd pr

This commit is contained in:
Dan Shai
2019-06-14 01:22:46 +03:00
parent ec298185a3
commit 5b6e9747b7

View File

@@ -1,456 +1,444 @@
import dagre from "dagre-layout"; import * as d3 from 'd3'
import graphlib from "graphlibrary"; import dagre from 'dagre-layout'
import * as d3 from "d3"; import graphlib from 'graphlibrary'
import { logger } from '../../logger'
import classDb from './classDb'
import { parser } from './parser/classDiagram'
import classDb from "./classDb"; parser.yy = classDb
import { logger } from "../../logger";
import { parser } from "./parser/classDiagram";
Array.prototype.removeIf = function(callback) { const idCache = {}
var i = this.length;
while (i--) {
if (callback(this[i], i)) {
this.splice(i, 1);
}
}
};
parser.yy = classDb; let classCnt = 0
const idCache = {};
let classCnt = 0;
const conf = { const conf = {
dividerMargin: 10, dividerMargin: 10,
padding: 5, padding: 5,
textHeight: 10 textHeight: 10
}; }
// Todo optimize // Todo optimize
const getGraphId = function (label) { const getGraphId = function (label) {
const keys = Object.keys(idCache); const keys = Object.keys(idCache)
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {
if (idCache[keys[i]].label === label) { if (idCache[keys[i]].label === label) {
return keys[i]; return keys[i]
} }
} }
return undefined; return undefined
}; }
/** /**
* Setup arrow head and define the marker. The result is appended to the svg. * Setup arrow head and define the marker. The result is appended to the svg.
*/ */
const insertMarkers = function (elem) { const insertMarkers = function (elem) {
elem elem
.append("defs") .append('defs')
.append("marker") .append('marker')
.attr("id", "extensionStart") .attr('id', 'extensionStart')
.attr("class", "extension") .attr('class', 'extension')
.attr("refX", 0) .attr('refX', 0)
.attr("refY", 7) .attr('refY', 7)
.attr("markerWidth", 190) .attr('markerWidth', 190)
.attr("markerHeight", 240) .attr('markerHeight', 240)
.attr("orient", "auto") .attr('orient', 'auto')
.append("path") .append('path')
.attr("d", "M 1,7 L18,13 V 1 Z"); .attr('d', 'M 1,7 L18,13 V 1 Z')
elem elem
.append("defs") .append('defs')
.append("marker") .append('marker')
.attr("id", "extensionEnd") .attr('id', 'extensionEnd')
.attr("refX", 19) .attr('refX', 19)
.attr("refY", 7) .attr('refY', 7)
.attr("markerWidth", 20) .attr('markerWidth', 20)
.attr("markerHeight", 28) .attr('markerHeight', 28)
.attr("orient", "auto") .attr('orient', 'auto')
.append("path") .append('path')
.attr("d", "M 1,1 V 13 L18,7 Z"); // this is actual shape for arrowhead .attr('d', 'M 1,1 V 13 L18,7 Z') // this is actual shape for arrowhead
elem elem
.append("defs") .append('defs')
.append("marker") .append('marker')
.attr("id", "compositionStart") .attr('id', 'compositionStart')
.attr("class", "extension") .attr('class', 'extension')
.attr("refX", 0) .attr('refX', 0)
.attr("refY", 7) .attr('refY', 7)
.attr("markerWidth", 190) .attr('markerWidth', 190)
.attr("markerHeight", 240) .attr('markerHeight', 240)
.attr("orient", "auto") .attr('orient', 'auto')
.append("path") .append('path')
.attr("d", "M 18,7 L9,13 L1,7 L9,1 Z"); .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z')
elem elem
.append("defs") .append('defs')
.append("marker") .append('marker')
.attr("id", "compositionEnd") .attr('id', 'compositionEnd')
.attr("refX", 19) .attr('refX', 19)
.attr("refY", 7) .attr('refY', 7)
.attr("markerWidth", 20) .attr('markerWidth', 20)
.attr("markerHeight", 28) .attr('markerHeight', 28)
.attr("orient", "auto") .attr('orient', 'auto')
.append("path") .append('path')
.attr("d", "M 18,7 L9,13 L1,7 L9,1 Z"); .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z')
elem elem
.append("defs") .append('defs')
.append("marker") .append('marker')
.attr("id", "aggregationStart") .attr('id', 'aggregationStart')
.attr("class", "extension") .attr('class', 'extension')
.attr("refX", 0) .attr('refX', 0)
.attr("refY", 7) .attr('refY', 7)
.attr("markerWidth", 190) .attr('markerWidth', 190)
.attr("markerHeight", 240) .attr('markerHeight', 240)
.attr("orient", "auto") .attr('orient', 'auto')
.append("path") .append('path')
.attr("d", "M 18,7 L9,13 L1,7 L9,1 Z"); .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z')
elem elem
.append("defs") .append('defs')
.append("marker") .append('marker')
.attr("id", "aggregationEnd") .attr('id', 'aggregationEnd')
.attr("refX", 19) .attr('refX', 19)
.attr("refY", 7) .attr('refY', 7)
.attr("markerWidth", 20) .attr('markerWidth', 20)
.attr("markerHeight", 28) .attr('markerHeight', 28)
.attr("orient", "auto") .attr('orient', 'auto')
.append("path") .append('path')
.attr("d", "M 18,7 L9,13 L1,7 L9,1 Z"); .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z')
elem elem
.append("defs") .append('defs')
.append("marker") .append('marker')
.attr("id", "dependencyStart") .attr('id', 'dependencyStart')
.attr("class", "extension") .attr('class', 'extension')
.attr("refX", 0) .attr('refX', 0)
.attr("refY", 7) .attr('refY', 7)
.attr("markerWidth", 190) .attr('markerWidth', 190)
.attr("markerHeight", 240) .attr('markerHeight', 240)
.attr("orient", "auto") .attr('orient', 'auto')
.append("path") .append('path')
.attr("d", "M 5,7 L9,13 L1,7 L9,1 Z"); .attr('d', 'M 5,7 L9,13 L1,7 L9,1 Z')
elem elem
.append("defs") .append('defs')
.append("marker") .append('marker')
.attr("id", "dependencyEnd") .attr('id', 'dependencyEnd')
.attr("refX", 19) .attr('refX', 19)
.attr("refY", 7) .attr('refY', 7)
.attr("markerWidth", 20) .attr('markerWidth', 20)
.attr("markerHeight", 28) .attr('markerHeight', 28)
.attr("orient", "auto") .attr('orient', 'auto')
.append("path") .append('path')
.attr("d", "M 18,7 L9,13 L14,7 L9,1 Z"); .attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z')
}; }
let edgeCount = 0; let edgeCount = 0
let total = 0; let total = 0
const drawEdge = function (elem, path, relation) { const drawEdge = function (elem, path, relation) {
const getRelationType = function (type) { const getRelationType = function (type) {
switch (type) { switch (type) {
case classDb.relationType.AGGREGATION: case classDb.relationType.AGGREGATION:
return "aggregation"; return 'aggregation'
case classDb.relationType.EXTENSION: case classDb.relationType.EXTENSION:
return "extension"; return 'extension'
case classDb.relationType.COMPOSITION: case classDb.relationType.COMPOSITION:
return "composition"; return 'composition'
case classDb.relationType.DEPENDENCY: case classDb.relationType.DEPENDENCY:
return "dependency"; return 'dependency'
}
} }
};
path.points.removeIf(function(p, idx) { path.points = path.points.filter(p => !Number.isNaN(p))
return isNaN(p.y);
});
// The data for our line // The data for our line
const lineData = path.points; const lineData = path.points
// This is the accessor function we talked about above // This is the accessor function we talked about above
const lineFunction = d3 const lineFunction = d3
.line() .line()
.x(function (d) { .x(function (d) {
return d.x; return d.x
}) })
.y(function (d) { .y(function (d) {
return d.y; return d.y
}) })
.curve(d3.curveBasis); .curve(d3.curveBasis)
const svgPath = elem const svgPath = elem
.append("path") .append('path')
.attr("d", lineFunction(lineData)) .attr('d', lineFunction(lineData))
.attr("id", "edge" + edgeCount) .attr('id', 'edge' + edgeCount)
.attr("class", "relation"); .attr('class', 'relation')
let url = ""; let url = ''
if (conf.arrowMarkerAbsolute) { if (conf.arrowMarkerAbsolute) {
url = url =
window.location.protocol + window.location.protocol +
"//" + '//' +
window.location.host + window.location.host +
window.location.pathname + window.location.pathname +
window.location.search; window.location.search
url = url.replace(/\(/g, "\\("); url = url.replace(/\(/g, '\\(')
url = url.replace(/\)/g, "\\)"); url = url.replace(/\)/g, '\\)')
} }
if (relation.relation.type1 !== "none") { if (relation.relation.type1 !== 'none') {
svgPath.attr( svgPath.attr(
"marker-start", 'marker-start',
"url(" + 'url(' +
url + url +
"#" + '#' +
getRelationType(relation.relation.type1) + getRelationType(relation.relation.type1) +
"Start" + 'Start' +
")" ')'
); )
} }
if (relation.relation.type2 !== "none") { if (relation.relation.type2 !== 'none') {
svgPath.attr( svgPath.attr(
"marker-end", 'marker-end',
"url(" + 'url(' +
url + url +
"#" + '#' +
getRelationType(relation.relation.type2) + getRelationType(relation.relation.type2) +
"End" + 'End' +
")" ')'
); )
} }
let x, y; let x, y
const l = path.points.length; const l = path.points.length
if (l % 2 !== 0 && l > 1) { if (l % 2 !== 0 && l > 1) {
const p1 = path.points[Math.floor(l / 2)]; const p1 = path.points[Math.floor(l / 2)]
const p2 = path.points[Math.ceil(l / 2)]; const p2 = path.points[Math.ceil(l / 2)]
x = (p1.x + p2.x) / 2; x = (p1.x + p2.x) / 2
y = (p1.y + p2.y) / 2; y = (p1.y + p2.y) / 2
} else { } else {
const p = path.points[Math.floor(l / 2)]; const p = path.points[Math.floor(l / 2)]
x = p.x; x = p.x
y = p.y; y = p.y
} }
if (typeof relation.title !== "undefined") { if (typeof relation.title !== 'undefined') {
const g = elem.append("g").attr("class", "classLabel"); const g = elem.append('g').attr('class', 'classLabel')
const label = g const label = g
.append("text") .append('text')
.attr("class", "label") .attr('class', 'label')
.attr("x", x) .attr('x', x)
.attr("y", y) .attr('y', y)
.attr("fill", "red") .attr('fill', 'red')
.attr("text-anchor", "middle") .attr('text-anchor', 'middle')
.text(relation.title); .text(relation.title)
window.label = label; window.label = label
const bounds = label.node().getBBox(); const bounds = label.node().getBBox()
g.insert("rect", ":first-child") g.insert('rect', ':first-child')
.attr("class", "box") .attr('class', 'box')
.attr("x", bounds.x - conf.padding / 2) .attr('x', bounds.x - conf.padding / 2)
.attr("y", bounds.y - conf.padding / 2) .attr('y', bounds.y - conf.padding / 2)
.attr("width", bounds.width + conf.padding) .attr('width', bounds.width + conf.padding)
.attr("height", bounds.height + conf.padding); .attr('height', bounds.height + conf.padding)
} }
edgeCount++; edgeCount++
}; }
const drawClass = function (elem, classDef) { const drawClass = function (elem, classDef) {
logger.info("Rendering class " + classDef); logger.info('Rendering class ' + classDef)
const addTspan = function (textEl, txt, isFirst) { const addTspan = function (textEl, txt, isFirst) {
const tSpan = textEl const tSpan = textEl
.append("tspan") .append('tspan')
.attr("x", conf.padding) .attr('x', conf.padding)
.text(txt); .text(txt)
if (!isFirst) { if (!isFirst) {
tSpan.attr("dy", conf.textHeight); tSpan.attr('dy', conf.textHeight)
}
} }
};
const id = "classId" + (classCnt % total); const id = 'classId' + (classCnt % total)
const classInfo = { const classInfo = {
id: id, id: id,
label: classDef.id, label: classDef.id,
width: 0, width: 0,
height: 0 height: 0
}; }
const g = elem const g = elem
.append("g") .append('g')
.attr("id", id) .attr('id', id)
.attr("class", "classGroup"); .attr('class', 'classGroup')
const title = g const title = g
.append("text") .append('text')
.attr("x", conf.padding) .attr('x', conf.padding)
.attr("y", conf.textHeight + conf.padding) .attr('y', conf.textHeight + conf.padding)
.text(classDef.id); .text(classDef.id)
const titleHeight = title.node().getBBox().height; const titleHeight = title.node().getBBox().height
const membersLine = g const membersLine = g
.append("line") // text label for the x axis .append('line') // text label for the x axis
.attr("x1", 0) .attr('x1', 0)
.attr("y1", conf.padding + titleHeight + conf.dividerMargin / 2) .attr('y1', conf.padding + titleHeight + conf.dividerMargin / 2)
.attr("y2", conf.padding + titleHeight + conf.dividerMargin / 2); .attr('y2', conf.padding + titleHeight + conf.dividerMargin / 2)
const members = g const members = g
.append("text") // text label for the x axis .append('text') // text label for the x axis
.attr("x", conf.padding) .attr('x', conf.padding)
.attr("y", titleHeight + conf.dividerMargin + conf.textHeight) .attr('y', titleHeight + conf.dividerMargin + conf.textHeight)
.attr("fill", "white") .attr('fill', 'white')
.attr("class", "classText"); .attr('class', 'classText')
let isFirst = true; let isFirst = true
classDef.members.forEach(function (member) { classDef.members.forEach(function (member) {
addTspan(members, member, isFirst); addTspan(members, member, isFirst)
isFirst = false; isFirst = false
}); })
const membersBox = members.node().getBBox(); const membersBox = members.node().getBBox()
const methodsLine = g const methodsLine = g
.append("line") // text label for the x axis .append('line') // text label for the x axis
.attr("x1", 0) .attr('x1', 0)
.attr( .attr(
"y1", 'y1',
conf.padding + titleHeight + conf.dividerMargin + membersBox.height conf.padding + titleHeight + conf.dividerMargin + membersBox.height
) )
.attr( .attr(
"y2", 'y2',
conf.padding + titleHeight + conf.dividerMargin + membersBox.height conf.padding + titleHeight + conf.dividerMargin + membersBox.height
); )
const methods = g const methods = g
.append("text") // text label for the x axis .append('text') // text label for the x axis
.attr("x", conf.padding) .attr('x', conf.padding)
.attr( .attr(
"y", 'y',
titleHeight + 2 * conf.dividerMargin + membersBox.height + conf.textHeight titleHeight + 2 * conf.dividerMargin + membersBox.height + conf.textHeight
) )
.attr("fill", "white") .attr('fill', 'white')
.attr("class", "classText"); .attr('class', 'classText')
isFirst = true; isFirst = true
classDef.methods.forEach(function (method) { classDef.methods.forEach(function (method) {
addTspan(methods, method, isFirst); addTspan(methods, method, isFirst)
isFirst = false; isFirst = false
}); })
const classBox = g.node().getBBox(); const classBox = g.node().getBBox()
g.insert("rect", ":first-child") g.insert('rect', ':first-child')
.attr("x", 0) .attr('x', 0)
.attr("y", 0) .attr('y', 0)
.attr("width", classBox.width + 2 * conf.padding) .attr('width', classBox.width + 2 * conf.padding)
.attr("height", classBox.height + conf.padding + 0.5 * conf.dividerMargin); .attr('height', classBox.height + conf.padding + 0.5 * conf.dividerMargin)
membersLine.attr("x2", classBox.width + 2 * conf.padding); membersLine.attr('x2', classBox.width + 2 * conf.padding)
methodsLine.attr("x2", classBox.width + 2 * conf.padding); methodsLine.attr('x2', classBox.width + 2 * conf.padding)
classInfo.width = classBox.width + 2 * conf.padding; classInfo.width = classBox.width + 2 * conf.padding
classInfo.height = classBox.height + conf.padding + 0.5 * conf.dividerMargin; classInfo.height = classBox.height + conf.padding + 0.5 * conf.dividerMargin
idCache[id] = classInfo; idCache[id] = classInfo
classCnt++; classCnt++
return classInfo; return classInfo
}; }
export const setConf = function (cnf) { export const setConf = function (cnf) {
const keys = Object.keys(cnf); const keys = Object.keys(cnf)
keys.forEach(function (key) { keys.forEach(function (key) {
conf[key] = cnf[key]; conf[key] = cnf[key]
}); })
}; }
/** /**
* Draws a flowchart in the tag with id: id based on the graph definition in text. * Draws a flowchart in the tag with id: id based on the graph definition in text.
* @param text * @param text
* @param id * @param id
*/ */
export const draw = function (text, id) { export const draw = function (text, id) {
parser.yy.clear(); parser.yy.clear()
parser.parse(text); parser.parse(text)
logger.info("Rendering diagram " + text); logger.info('Rendering diagram ' + text)
/// / Fetch the default direction, use TD if none was found /// / Fetch the default direction, use TD if none was found
const diagram = d3.select(`[id="${id}"]`); const diagram = d3.select(`[id='${id}']`)
insertMarkers(diagram); insertMarkers(diagram)
// Layout graph, Create a new directed graph // Layout graph, Create a new directed graph
const g = new graphlib.Graph({ const g = new graphlib.Graph({
multigraph: true multigraph: true
}); })
// Set an object for the graph label // Set an object for the graph label
g.setGraph({ g.setGraph({
isMultiGraph: true isMultiGraph: true
}); })
// Default to assigning a new object as a label for each new edge. // Default to assigning a new object as a label for each new edge.
g.setDefaultEdgeLabel(function () { g.setDefaultEdgeLabel(function () {
return {}; return {}
}); })
const classes = classDb.getClasses(); const classes = classDb.getClasses()
const keys = Object.keys(classes); const keys = Object.keys(classes)
total = keys.length; total = keys.length
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {
const classDef = classes[keys[i]]; const classDef = classes[keys[i]]
const node = drawClass(diagram, classDef); const node = drawClass(diagram, classDef)
// Add nodes to the graph. The first argument is the node id. The second is // Add nodes to the graph. The first argument is the node id. The second is
// metadata about the node. In this case we're going to add labels to each of // metadata about the node. In this case we're going to add labels to each of
// our nodes. // our nodes.
g.setNode(node.id, node); g.setNode(node.id, node)
logger.info("Org height: " + node.height); logger.info('Org height: ' + node.height)
} }
const relations = classDb.getRelations(); const relations = classDb.getRelations()
relations.forEach(function (relation) { relations.forEach(function (relation) {
logger.info( logger.info(
"tjoho" + 'tjoho' +
getGraphId(relation.id1) + getGraphId(relation.id1) +
getGraphId(relation.id2) + getGraphId(relation.id2) +
JSON.stringify(relation) JSON.stringify(relation)
); )
g.setEdge(getGraphId(relation.id1), getGraphId(relation.id2), { g.setEdge(getGraphId(relation.id1), getGraphId(relation.id2), {
relation: relation relation: relation
}); })
}); })
dagre.layout(g); dagre.layout(g)
g.nodes().forEach(function (v) { g.nodes().forEach(function (v) {
if (typeof v !== "undefined" && typeof g.node(v) !== "undefined") { if (typeof v !== 'undefined' && typeof g.node(v) !== 'undefined') {
logger.debug("Node " + v + ": " + JSON.stringify(g.node(v))); logger.debug('Node ' + v + ': ' + JSON.stringify(g.node(v)))
d3.select("#" + v).attr( d3.select('#' + v).attr(
"transform", 'transform',
"translate(" + 'translate(' +
(g.node(v).x - g.node(v).width / 2) + (g.node(v).x - g.node(v).width / 2) +
"," + ',' +
(g.node(v).y - g.node(v).height / 2) + (g.node(v).y - g.node(v).height / 2) +
" )" ' )'
); )
} }
}); })
g.edges().forEach(function (e) { g.edges().forEach(function (e) {
if (typeof e !== "undefined" && typeof g.edge(e) !== "undefined") { if (typeof e !== 'undefined' && typeof g.edge(e) !== 'undefined') {
logger.debug( logger.debug(
"Edge " + e.v + " -> " + e.w + ": " + JSON.stringify(g.edge(e)) 'Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(g.edge(e))
); )
drawEdge(diagram, g.edge(e), g.edge(e).relation); drawEdge(diagram, g.edge(e), g.edge(e).relation)
} }
}); })
diagram.attr("height", "100%"); diagram.attr('height', '100%')
diagram.attr("width", "100%"); diagram.attr('width', '100%')
diagram.attr( diagram.attr(
"viewBox", 'viewBox',
"0 0 " + (g.graph().width + 20) + " " + (g.graph().height + 20) '0 0 ' + (g.graph().width + 20) + ' ' + (g.graph().height + 20)
); )
}; }
export default { export default {
setConf, setConf,
draw draw
}; }