diff --git a/src/diagrams/er/erMarkers.js b/src/diagrams/er/erMarkers.js index 96933693b..3107671fd 100644 --- a/src/diagrams/er/erMarkers.js +++ b/src/diagrams/er/erMarkers.js @@ -1,21 +1,16 @@ -//import * as d3 from 'd3'; - const ERMarkers = { ONLY_ONE_START: 'ONLY_ONE_START', ONLY_ONE_END: 'ONLY_ONE_END', - ZERO_OR_ONE_START: 'ZERO_OR_ONE_START', ZERO_OR_ONE_END: 'ZERO_OR_ONE_END', - ONE_OR_MORE_START: 'ONE_OR_MORE_START', ONE_OR_MORE_END: 'ONE_OR_MORE_END', - ZERO_OR_MORE_START: 'ZERO_OR_MORE_START', ZERO_OR_MORE_END: 'ZERO_OR_MORE_END' }; /** - * Put the markers into the svg DOM for use in paths + * Put the markers into the svg DOM for later use with edge paths */ const insertMarkers = function(elem, conf) { let marker; @@ -96,73 +91,73 @@ const insertMarkers = function(elem, conf) { .append('defs') .append('marker') .attr('id', ERMarkers.ONE_OR_MORE_START) - .attr('refX', 0) - .attr('refY', 9) - .attr('markerWidth', 18) - .attr('markerHeight', 18) + .attr('refX', 18) + .attr('refY', 18) + .attr('markerWidth', 45) + .attr('markerHeight', 36) .attr('orient', 'auto') .append('path') .attr('stroke', conf.stroke) .attr('fill', 'none') - .attr('d', 'M0,0 L9,9 L0,18 M15,0 L15,18'); + .attr('d', 'M0,18 Q 18,0 36,18 Q 18,36 0,18 M42,9 L42,27'); elem .append('defs') .append('marker') .attr('id', ERMarkers.ONE_OR_MORE_END) - .attr('refX', 18) - .attr('refY', 9) - .attr('markerWidth', 21) - .attr('markerHeight', 18) + .attr('refX', 27) + .attr('refY', 18) + .attr('markerWidth', 45) + .attr('markerHeight', 36) .attr('orient', 'auto') .append('path') .attr('stroke', conf.stroke) .attr('fill', 'none') - .attr('d', 'M3,0 L3,18 M18,0 L9,9 L18,18'); + .attr('d', 'M3,9 L3,27 M9,18 Q27,0 45,18 Q27,36 9,18'); marker = elem .append('defs') .append('marker') .attr('id', ERMarkers.ZERO_OR_MORE_START) - .attr('refX', 0) - .attr('refY', 9) - .attr('markerWidth', 30) - .attr('markerHeight', 18) + .attr('refX', 18) + .attr('refY', 18) + .attr('markerWidth', 57) + .attr('markerHeight', 36) .attr('orient', 'auto'); marker .append('circle') .attr('stroke', conf.stroke) .attr('fill', 'white') - .attr('cx', 21) - .attr('cy', 9) + .attr('cx', 48) + .attr('cy', 18) .attr('r', 6); marker .append('path') .attr('stroke', conf.stroke) .attr('fill', 'none') - .attr('d', 'M0,0 L9,9 L0,18'); + .attr('d', 'M0,18 Q18,0 36,18 Q18,36 0,18'); marker = elem .append('defs') .append('marker') .attr('id', ERMarkers.ZERO_OR_MORE_END) - .attr('refX', 30) - .attr('refY', 9) - .attr('markerWidth', 30) - .attr('markerHeight', 18) + .attr('refX', 39) + .attr('refY', 18) + .attr('markerWidth', 57) + .attr('markerHeight', 36) .attr('orient', 'auto'); marker .append('circle') .attr('stroke', conf.stroke) .attr('fill', 'white') .attr('cx', 9) - .attr('cy', 9) + .attr('cy', 18) .attr('r', 6); marker .append('path') .attr('stroke', conf.stroke) .attr('fill', 'none') - .attr('d', 'M30,0 L21,9 L30,18'); + .attr('d', 'M21,18 Q39,0 57,18 Q39,36 21,18'); return; }; diff --git a/src/diagrams/er/erRenderer.js b/src/diagrams/er/erRenderer.js index 858f6b9cf..1933ffb95 100644 --- a/src/diagrams/er/erRenderer.js +++ b/src/diagrams/er/erRenderer.js @@ -16,9 +16,10 @@ export const setConf = function(cnf) { }; /** - * Function that adds the entities as vertices + * Function that adds the entities as vertices in the graph prior to laying out * @param entities The entities to be added to the graph * @param g The graph that is to be drawn + * @returns {Object} The object containing all the entities as properties */ const addEntities = function(entities, g) { const keys = Object.keys(entities); @@ -36,6 +37,7 @@ const addEntities = function(entities, g) { id: entity }); }); + return entities; }; /** @@ -92,26 +94,38 @@ const drawEntities = function(diagram, entities, g, svgId) { }); }; // drawEntities +/** + * Add each relationship to the graph + * @param relationships the relationships to be added + * @param g the graph + * @return {Array} The array of relationships + */ const addRelationships = function(relationships, g) { relationships.forEach(function(r) { g.setEdge(r.entityA, r.entityB, { relationship: r }); }); + return relationships; }; // addRelationships +/** + * + */ const drawRelationships = function(diagram, relationships, g) { relationships.forEach(function(rel) { - //drawRelationship(diagram, rel, g); drawRelationshipFromLayout(diagram, rel, g); }); }; // drawRelationships +/** + * Draw a relationship using edge information from the graph + * @param diagram the svg node + * @param rel the relationship to draw in the svg + * @param g the graph containing the edge information + */ const drawRelationshipFromLayout = function(diagram, rel, g) { // Find the edge relating to this relationship const edge = g.edge({ v: rel.entityA, w: rel.entityB }); - // Using it's points, generate a line function - edge.points = edge.points.filter(p => !Number.isNaN(p.y)); // TODO: why is necessary? - // Get a function that will generate the line path const lineFunction = d3 .line() @@ -130,7 +144,7 @@ const drawRelationshipFromLayout = function(diagram, rel, g) { .attr('stroke', conf.stroke) .attr('fill', 'none'); - // TODO: Understand this + // TODO: Understand this better let url = ''; if (conf.arrowMarkerAbsolute) { url = @@ -143,8 +157,8 @@ const drawRelationshipFromLayout = function(diagram, rel, g) { url = url.replace(/\)/g, '\\)'); } - // TODO: change the way enums are imported - // Decide which start and end markers it needs + // Decide which start and end markers it needs. It may be possible to be more concise here + // by reversing a start marker to make an end marker...but this will do for now switch (rel.cardinality) { case erDb.Cardinality.ONLY_ONE_TO_ONE_OR_MORE: svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_START + ')'); @@ -249,285 +263,10 @@ const drawRelationshipFromLayout = function(diagram, rel, g) { } }; -/* -const drawRelationship = function(diagram, relationship, g) { - // Set the from and to co-ordinates using the graph vertices - - let from = { - x: g.node(relationship.entityA).x, - y: g.node(relationship.entityA).y - }; - - let to = { - x: g.node(relationship.entityB).x, - y: g.node(relationship.entityB).y - }; - - diagram - .append('line') - .attr('x1', from.x) - .attr('y1', from.y) - .attr('x2', to.x) - .attr('y2', to.y) - .attr('stroke', conf.stroke); -}; // drawRelationship -*/ - -/* -const drawFeet = function(diagram, relationships, g) { - relationships.forEach(function(rel) { - // Get the points of intersection with the entities - const nodeA = g.node(rel.entityA); - const nodeB = g.node(rel.entityB); - - const fromIntersect = getIntersection( - nodeB.x - nodeA.x, - nodeB.y - nodeA.y, - nodeA.x, - nodeA.y, - nodeA.width / 2, - nodeA.height / 2 - ); - - dot(diagram, fromIntersect, conf.intersectColor); - - const toIntersect = getIntersection( - nodeA.x - nodeB.x, - nodeA.y - nodeB.y, - nodeB.x, - nodeB.y, - nodeB.width / 1, - nodeB.height / 2 - ); - - dot(diagram, toIntersect, conf.intersectColor); - - // Get the ankle and heel points - const anklePoints = getJoints(rel, fromIntersect, toIntersect, conf.ankleDistance); - - dot(diagram, { x: anklePoints.from.x, y: anklePoints.from.y }, conf.ankleColor); - dot(diagram, { x: anklePoints.to.x, y: anklePoints.to.y }, conf.ankleColor); - - const heelPoints = getJoints(rel, fromIntersect, toIntersect, conf.heelDistance); - - dot(diagram, { x: heelPoints.from.x, y: heelPoints.from.y }, conf.heelColor); - dot(diagram, { x: heelPoints.to.x, y: heelPoints.to.y }, conf.heelColor); - - // Get the toe points - const toePoints = getToes(rel, fromIntersect, toIntersect, conf.toeDistance); - - if (toePoints) { - dot(diagram, { x: toePoints.from.top.x, y: toePoints.from.top.y }, conf.toeColor); - dot(diagram, { x: toePoints.from.bottom.x, y: toePoints.from.bottom.y }, conf.toeColor); - dot(diagram, { x: toePoints.to.top.x, y: toePoints.to.top.y }, conf.toeColor); - dot(diagram, { x: toePoints.to.bottom.x, y: toePoints.to.bottom.y }, conf.toeColor); - - let paths = []; - paths.push(getToePath(heelPoints.from, toePoints.from.top, nodeA)); - paths.push(getToePath(heelPoints.from, toePoints.from.bottom, nodeA)); - paths.push(getToePath(heelPoints.to, toePoints.to.top, nodeB)); - paths.push(getToePath(heelPoints.to, toePoints.to.bottom, nodeB)); - - for (const path of paths) { - diagram - .append('path') - .attr('d', path) - .attr('stroke', conf.stroke) - .attr('fill', 'none'); - } - } - }); -}; // drawFeet - -const getToePath = function(heel, toe, tip) { - if (conf.toeStyle === 'straight') { - return `M ${heel.x} ${heel.y} L ${toe.x} ${toe.y} L ${tip.x} ${tip.y}`; - } else { - return `M ${heel.x} ${heel.y} Q ${toe.x} ${toe.y} ${tip.x} ${tip.y}`; - } -}; -*/ -/* -const getToes = function(relationship, fromPoint, toPoint, distance) { - if (conf.toeStyle === 'curved') { - distance *= 2; - } - - const gradient = (fromPoint.y - toPoint.y) / (fromPoint.x - toPoint.x); - const toeYDelta = getXDelta(distance, gradient); - const toeXDelta = toeYDelta * Math.abs(gradient); - - if (gradient > 0) { - const topToe = function(point) { - return { - x: point.x + toeXDelta, - y: point.y - toeYDelta - }; - }; - - const bottomToe = function(point) { - return { - x: point.x - toeXDelta, - y: point.y + toeYDelta - }; - }; - - const lower = { - top: fromPoint.x < toPoint.x ? topToe(toPoint) : topToe(fromPoint), - bottom: fromPoint.x < toPoint.x ? bottomToe(toPoint) : bottomToe(fromPoint) - }; - - const upper = { - top: fromPoint.x < toPoint.x ? topToe(fromPoint) : topToe(toPoint), - bottom: fromPoint.x < toPoint.x ? bottomToe(fromPoint) : bottomToe(toPoint) - }; - - return { - to: fromPoint.x < toPoint.x ? lower : upper, - from: fromPoint.x < toPoint.x ? upper : lower - }; - } -*/ -/* - if (fromPoint.x < toPoint.x) { - // Scenario A - - return { - to: { - top: { - x: toPoint.x + toeXDelta, - y: toPoint.y - toeYDelta - }, - bottom: { - x: toPoint.x - toeXDelta, - y: toPoint.y + toeYDelta - } - }, - from: { - top: { - x: fromPoint.x + toeXDelta, - y: fromPoint.y - toeYDelta - }, - bottom: { - x: fromPoint.x - toeXDelta, - y: fromPoint.y + toeYDelta - } - } - }; - } else { - // Scenario E - } -*/ -/* -}; // getToes -*/ -/* -const getJoints = function(relationship, fromPoint, toPoint, distance) { - const gradient = (fromPoint.y - toPoint.y) / (fromPoint.x - toPoint.x); - let jointXDelta = getXDelta(distance, gradient); - let jointYDelta = jointXDelta * Math.abs(gradient); - - let toX, toY; - let fromX, fromY; - - if (gradient > 0) { - if (fromPoint.x < toPoint.x) { - // Scenario A - } else { - // Scenario E - jointXDelta *= -1; - jointYDelta *= -1; - } - - toX = toPoint.x - jointXDelta; - toY = toPoint.y - jointYDelta; - fromX = fromPoint.x + jointXDelta; - fromY = fromPoint.y + jointYDelta; - } - - if (gradient < 0) { - if (fromPoint.x < toPoint.x) { - // Scenario C - jointXDelta *= -1; - jointYDelta *= -1; - } else { - // Scenario G - } - - toX = toPoint.x + jointXDelta; - toY = toPoint.y - jointYDelta; - fromX = fromPoint.x - jointXDelta; - fromY = fromPoint.y + jointYDelta; - } - - if (!isFinite(gradient)) { - if (fromPoint.y < toPoint.y) { - // Scenario B - } else { - // Scenario F - jointXDelta *= -1; - jointYDelta *= -1; - } - - toX = toPoint.x; - toY = toPoint.y - distance; - fromX = fromPoint.x; - fromY = fromPoint.y + distance; - } - - if (gradient === 0) { - if (fromPoint.x < toPoint.x) { - // Scenario D - } else { - // Scenario H - jointXDelta *= -1; - jointYDelta *= -1; - } - - toX = toPoint.x - distance; - toY = toPoint.y; - fromX = fromPoint.x + distance; - fromY = fromPoint.y; - } - - return { - from: { x: fromX, y: fromY }, - to: { x: toX, y: toY } - }; -}; -*/ - -/* -const getXDelta = function(hypotenuse, gradient) { - return Math.sqrt((hypotenuse * hypotenuse) / (Math.abs(gradient) + 1)); -}; - -const getIntersection = function(dx, dy, cx, cy, w, h) { - if (Math.abs(dy / dx) < h / w) { - // Hit vertical edge of box - return { x: cx + (dx > 0 ? w : -w), y: cy + (dy * w) / Math.abs(dx) }; - } else { - // Hit horizontal edge of box - return { x: cx + (dx * h) / Math.abs(dy), y: cy + (dy > 0 ? h : -h) }; - } -}; // getIntersection - -const dot = function(diagram, p, color) { - // stick a small circle at point p - if (conf.dots) { - diagram - .append('circle') - .attr('cx', p.x) - .attr('cy', p.y) - .attr('r', conf.dotRadius) - .attr('fill', color); - } -}; // dot -*/ /** - * Draw en E-R diagram in the tag with id: id based on the text definition of the graph - * @param text - * @param id + * Draw en E-R diagram in the tag with id: id based on the text definition of the diagram + * @param text the text of the diagram + * @param id the unique id of the DOM node that contains the diagram */ export const draw = function(text, id) { logger.info('Drawing ER diagram'); @@ -543,25 +282,26 @@ export const draw = function(text, id) { } // Get a reference to the diagram node - const diagram = d3.select(`[id='${id}']`); + const svg = d3.select(`[id='${id}']`); - // Add cardinality 'marker' definitions to the svg - erMarkers.insertMarkers(diagram, conf); + // Add cardinality marker definitions to the svg + erMarkers.insertMarkers(svg, conf); // Create the graph let g; // TODO: Explore directed vs undirected graphs, and how the layout is affected // An E-R diagram could be said to be undirected, but there is merit in setting - // the direction from parent to child (1 to many) as this influences graphlib to - // put the parent above the child, which is intuitive + // the direction from parent to child in a one-to-many as this influences graphlib to + // put the parent above the child (does it?), which is intuitive. Most relationships + // in ER diagrams are one-to-many. g = new graphlib.Graph({ multigraph: true, directed: true, compound: false }) .setGraph({ - rankdir: 'TB', + rankdir: 'LR', marginx: 20, marginy: 20, nodesep: 100, @@ -571,31 +311,18 @@ export const draw = function(text, id) { return {}; }); - // Fetch the entities (which will become vertices) - const entities = erDb.getEntities(); - - // Add all the entities to the graph - addEntities(entities, g); - - const relationships = erDb.getRelationships(); - // Add all the relationships as edges on the graph - addRelationships(relationships, g); - - // Set up an SVG group so that we can translate the final graph. - // TODO: This is redundant -just use diagram from above - const svg = d3.select(`[id="${id}"]`); + // Add the entities and relationships to the graph + const entities = addEntities(erDb.getEntities(), g); + const relationships = addRelationships(erDb.getRelationships(), g); dagre.layout(g); // Node and edge positions will be updated - // Run the renderer. This is what draws the final graph. - //const element = d3.select('#' + id + ' g'); - //render(element, g); + // Draw the relationships first because their markers need to be + // clipped by the entity boxes + drawRelationships(svg, relationships, g); + drawEntities(svg, entities, g, id); - //drawFeet(diagram, relationships, g); - drawRelationships(diagram, relationships, g); - drawEntities(diagram, entities, g, id); - - const padding = 8; + const padding = 8; // TODO: move this to config const svgBounds = svg.node().getBBox(); const width = svgBounds.width + padding * 4; diff --git a/src/mermaidAPI.js b/src/mermaidAPI.js index 95dc56e70..fa9e7ece5 100644 --- a/src/mermaidAPI.js +++ b/src/mermaidAPI.js @@ -369,7 +369,7 @@ const config = { /** * Stroke color of box edges and lines */ - stroke: 'purple', + stroke: 'gray', /** * Fill color of entity boxes @@ -377,52 +377,11 @@ const config = { fill: 'honeydew', /** - * Distance of the 'ankle' from the intersection point + * Opacity of entity boxes - if you want to see how the crows feet + * retain their elegant joins to the boxes regardless of the angle of incidence + * then override this to something less than 100% */ - ankleDistance: 35, - - /** - * Distance of the 'heel' from the intersection point - */ - heelDistance: 20, - - /** - * Distance of the side 'toes' perpendicular to the intersection point - */ - toeDistance: 12, - - /** - * The style of the toes on the crow's foot: either 'curved' or 'straight' - */ - toeStyle: 'curved', - - /** - * THE REMAINING CONFIG OPTIONS FOR 'er' DIAGRAMS ARE EXPERIMENTAL AND ARE USEFUL - * DURING DEVELOPMENT BUT WILL PROBABLY BE REMOVED BEFORE E-R DIAGRAMS ARE PRODUCTIONIZED. - * THEY ARE HELPFUL IN DIAGNOSING POSITIONAL AND LAYOUT-RELATED ISSUES; THEY WOULDN'T - * LOOK GOOD ON REAL DIAGRAMS - */ - - // Opacity of entity boxes - helpful when < 100% to see lines 'behind' the box - fillOpacity: '100%', - - // Whether to show dots at important points in the diagram geometry - dots: false, - - // Radius of dots - dotRadius: 1.5, - - // Color of intersection point dots - intersectColor: 'green', - - // Color of 'ankle' dots - ankleColor: 'red', - - // Color of 'heel' dots - heelColor: 'blue', - - // Color of 'toe' dots - toeColor: 'darkorchid' + fillOpacity: '100%' } };