From 0555fca5d8aba2ff42fcc37fbf110cadd68dfdb9 Mon Sep 17 00:00:00 2001 From: Adrian Hall Date: Mon, 2 Mar 2020 10:03:55 +0000 Subject: [PATCH 01/14] Initial parsing logic for ER diagrams --- src/diagrams/er/erDb.js | 77 ++++++++ src/diagrams/er/parser/erDiagram.jison | 84 +++++++++ src/diagrams/er/parser/erDiagram.spec.js | 226 +++++++++++++++++++++++ 3 files changed, 387 insertions(+) create mode 100644 src/diagrams/er/erDb.js create mode 100644 src/diagrams/er/parser/erDiagram.jison create mode 100644 src/diagrams/er/parser/erDiagram.spec.js diff --git a/src/diagrams/er/erDb.js b/src/diagrams/er/erDb.js new file mode 100644 index 000000000..1ab913287 --- /dev/null +++ b/src/diagrams/er/erDb.js @@ -0,0 +1,77 @@ +/** + * + */ +import { logger } from '../../logger'; + +let entities = {}; +let relationships = []; +let title = ''; + +const Cardinality = { + ONLY_ONE_TO_ONE_OR_MORE : 'ONLY_ONE_TO_ONE_OR_MORE', + ONLY_ONE_TO_ZERO_OR_MORE : 'ONLY_ONE_TO_ZERO_OR_MORE', + ZERO_OR_ONE_TO_ZERO_OR_MORE : 'ZERO_OR_ONE_TO_ZERO_OR_MORE', + ZERO_OR_ONE_TO_ONE_OR_MORE : 'ZERO_OR_ONE_TO_ONE_OR_MORE', + ONE_OR_MORE_TO_ONLY_ONE : 'ONE_OR_MORE_TO_ONLY_ONE', + ZERO_OR_MORE_TO_ONLY_ONE : 'ZERO_OR_MORE_TO_ONLY_ONE', + ZERO_OR_MORE_TO_ZERO_OR_ONE : 'ZERO_OR_MORE_TO_ZERO_OR_ONE', + ONE_OR_MORE_TO_ZERO_OR_ONE : 'ONE_OR_MORE_TO_ZERO_OR_ONE', + ZERO_OR_ONE_TO_ONLY_ONE : 'ZERO_OR_ONE_TO_ONLY_ONE', + ONLY_ONE_TO_ONLY_ONE : 'ONLY_ONE_TO_ONLY_ONE', + ONLY_ONE_TO_ZERO_OR_ONE : 'ONLY_ONE_TO_ZERO_OR_ONE', + ZERO_OR_ONE_TO_ZERO_OR_ONE : 'ZERO_OR_ONE_TO_ZERO_OR_ONE', + ZERO_OR_MORE_TO_ZERO_OR_MORE: 'ZERO_OR_MORE_TO_ZERO_OR_MORE', + ZERO_OR_MORE_TO_ONE_OR_MORE : 'ZERO_OR_MORE_TO_ONE_OR_MORE', + ONE_OR_MORE_TO_ZERO_OR_MORE : 'ONE_OR_MORE_TO_ZERO_OR_MORE', + ONE_OR_MORE_TO_ONE_OR_MORE : 'ONE_OR_MORE_TO_ONE_OR_MORE' +}; + +const addEntity = function(name) { + if (typeof entities[name] === 'undefined') { + entities[name] = name; + logger.debug('Added new entity :', name); + } +}; + +const getEntities = () => entities; + +const addRelationship = function(entA, rolA, entB, rolB, card) { + let rel = { + entityA : entA, + roleA : rolA, + entityB : entB, + roleB : rolB, + cardinality : card + }; + + relationships.push(rel); + logger.debug('Added new relationship :', rel); +} + +const getRelationships = () => relationships; + +// Keep this - TODO: revisit...allow the diagram to have a title +const setTitle = function(txt) { + title = txt; +}; + +const getTitle = function() { + return title; +}; + +const clear = function() { + entities = {}; + relationships = []; + title = ''; +}; + +export default { + Cardinality, + addEntity, + getEntities, + addRelationship, + getRelationships, + clear, + setTitle, + getTitle +}; diff --git a/src/diagrams/er/parser/erDiagram.jison b/src/diagrams/er/parser/erDiagram.jison new file mode 100644 index 000000000..c254c9099 --- /dev/null +++ b/src/diagrams/er/parser/erDiagram.jison @@ -0,0 +1,84 @@ +%lex + +%x string +%options case-insensitive + +%% +\s+ /* skip whitespace */ +[\s]+ return 'SPACE'; +["] { this.begin("string");} +["] { this.popState(); } +[^"]* { return 'STR'; } +"erDiagram" return 'ER_DIAGRAM'; +[A-Za-z][A-Za-z0-9]* return 'ALPHANUM'; +\>\?\-\?\< return 'ZERO_OR_MORE_TO_ZERO_OR_MORE'; +\>\?\-\!\< return 'ZERO_OR_MORE_TO_ONE_OR_MORE'; +\>\!\-\!\< return 'ONE_OR_MORE_TO_ONE_OR_MORE'; +\>\!\-\?\< return 'ONE_OR_MORE_TO_ZERO_OR_MORE'; +\!\-\!\< return 'ONLY_ONE_TO_ONE_OR_MORE'; +\!\-\?\< return 'ONLY_ONE_TO_ZERO_OR_MORE'; +\?\-\?\< return 'ZERO_OR_ONE_TO_ZERO_OR_MORE'; +\?\-\!\< return 'ZERO_OR_ONE_TO_ONE_OR_MORE'; +\>\!\-\! return 'ONE_OR_MORE_TO_ONLY_ONE'; +\>\?\-\! return 'ZERO_OR_MORE_TO_ONLY_ONE'; +\>\?\-\? return 'ZERO_OR_MORE_TO_ZERO_OR_ONE'; +\>\!\-\? return 'ONE_OR_MORE_TO_ZERO_OR_ONE'; +\?\-\! return 'ZERO_OR_ONE_TO_ONLY_ONE'; +\!\-\! return 'ONLY_ONE_TO_ONLY_ONE'; +\!\-\? return 'ONLY_ONE_TO_ZERO_OR_ONE'; +\?\-\? return 'ZERO_OR_ONE_TO_ZERO_OR_ONE'; +. return yytext[0]; +<> return 'EOF'; + + +/lex + +%start start +%% /* language grammar */ + +start + : 'ER_DIAGRAM' document 'EOF' { /*console.log('finished parsing');*/ } + ; + +document + : /* empty */ + | document statement + ; + +statement + : entityName relationship entityName ':' role ',' role + { + yy.addEntity($1); + yy.addEntity($3); + yy.addRelationship($1, $5, $3, $7, $2); + /*console.log($1 + $2 + $3 + ':' + $5 + ',' + $7);*/ + }; + +entityName + : 'ALPHANUM' { $$ = $1; } + ; + +relationship + : 'ONLY_ONE_TO_ONE_OR_MORE' { $$ = yy.Cardinality.ONLY_ONE_TO_ONE_OR_MORE; } + | 'ONLY_ONE_TO_ZERO_OR_MORE' { $$ = yy.Cardinality.ONLY_ONE_TO_ZERO_OR_MORE; } + | 'ZERO_OR_ONE_TO_ZERO_OR_MORE' { $$ = yy.Cardinality.ZERO_OR_ONE_TO_ZERO_OR_MORE; } + | 'ZERO_OR_ONE_TO_ONE_OR_MORE' { $$ = yy.Cardinality.ZERO_OR_ONE_TO_ONE_OR_MORE; } + | 'ONE_OR_MORE_TO_ONLY_ONE' { $$ = yy.Cardinality.ONE_OR_MORE_TO_ONLY_ONE; } + | 'ZERO_OR_MORE_TO_ONLY_ONE' { $$ = yy.Cardinality.ZERO_OR_MORE_TO_ONLY_ONE; } + | 'ZERO_OR_MORE_TO_ZERO_OR_ONE' { $$ = yy.Cardinality.ZERO_OR_MORE_TO_ZERO_OR_ONE; } + | 'ONE_OR_MORE_TO_ZERO_OR_ONE' { $$ = yy.Cardinality.ONE_OR_MORE_TO_ZERO_OR_ONE; } + | 'ZERO_OR_ONE_TO_ONLY_ONE' { $$ = yy.Cardinality.ZERO_OR_ONE_TO_ONLY_ONE; } + | 'ONLY_ONE_TO_ONLY_ONE' { $$ = yy.Cardinality.ONLY_ONE_TO_ONLY_ONE; } + | 'ONLY_ONE_TO_ZERO_OR_ONE' { $$ = yy.Cardinality.ONLY_ONE_TO_ZERO_OR_ONE; } + | 'ZERO_OR_ONE_TO_ZERO_OR_ONE' { $$ = yy.Cardinality.ZERO_OR_ONE_TO_ZERO_OR_ONE; } + | 'ZERO_OR_MORE_TO_ZERO_OR_MORE' { $$ = yy.Cardinality.ZERO_OR_MORE_TO_ZERO_OR_MORE; } + | 'ZERO_OR_MORE_TO_ONE_OR_MORE' { $$ = yy.Cardinality.ZERO_OR_MORE_TO_ONE_OR_MORE; } + | 'ONE_OR_MORE_TO_ONE_OR_MORE' { $$ = yy.Cardinality.ONE_OR_MORE_TO_ONE_OR_MORE; } + | 'ONE_OR_MORE_TO_ZERO_OR_MORE' { $$ = yy.Cardinality.ONE_OR_MORE_TO_ZERO_OR_MORE; } + ; + +role + : 'STR' { $$ = $1; } + | 'ALPHANUM' { $$ = $1; } + ; +%% diff --git a/src/diagrams/er/parser/erDiagram.spec.js b/src/diagrams/er/parser/erDiagram.spec.js new file mode 100644 index 000000000..e04c7130d --- /dev/null +++ b/src/diagrams/er/parser/erDiagram.spec.js @@ -0,0 +1,226 @@ +import erDb from '../erDb'; +import erDiagram from './erDiagram'; +import { setConfig } from '../../../config'; +import logger from '../../../logger'; + +setConfig({ + securityLevel: 'strict' +}); + +describe('when parsing ER diagram it...', function() { + + beforeEach(function() { + erDiagram.parser.yy = erDb; + erDiagram.parser.yy.clear(); + }); + + it('should associate two entities correctly', function() { + erDiagram.parser.parse('erDiagram\nCAR !-?< DRIVER : "insured for", "can drive"'); + const entities = erDb.getEntities(); + const relationships = erDb.getRelationships(); + const carEntity = entities.CAR; + const driverEntity = entities.DRIVER; + + expect(carEntity).toBe('CAR'); + expect(driverEntity).toBe('DRIVER'); + expect(relationships.length).toBe(1); + expect(relationships[0].cardinality).toBe(erDb.Cardinality.ONLY_ONE_TO_ZERO_OR_MORE); + }); + + it('should not create duplicate entities', function() { + const line1 = 'CAR !-?< DRIVER : "insured for", "can drive"'; + const line2 = 'DRIVER !-! LICENSE : has, "belongs to"'; + erDiagram.parser.parse(`erDiagram\n${line1}\n${line2}`); + const entities = erDb.getEntities(); + + expect(Object.keys(entities).length).toBe(3); + }); + + it('should create the roles specified', function() { + const teacherRole = 'is teacher of'; + const studentRole = 'is student of'; + const line1 = `TEACHER >?-?< STUDENT : "${teacherRole}", "${studentRole}"`; + erDiagram.parser.parse(`erDiagram\n${line1}`); + const rels = erDb.getRelationships(); + + expect(rels[0].roleA).toBe(`${teacherRole}`); + expect(rels[0].roleB).toBe(`${studentRole}`); + }); + + it('should allow recursive relationships', function() { + erDiagram.parser.parse('erDiagram\nNODE !-?< NODE : "leads to", "comes from"'); + expect(Object.keys(erDb.getEntities()).length).toBe(1); + }); + + it('should allow more than one relationship between the same two entities', function() { + const line1 = 'CAR !-?< PERSON : "insured for", "may drive"'; + const line2 = 'CAR >?-! PERSON : "owned by", "owns"'; + erDiagram.parser.parse(`erDiagram\n${line1}\n${line2}`); + const entities = erDb.getEntities(); + const rels = erDb.getRelationships(); + + expect(Object.keys(entities).length).toBe(2); + expect(rels.length).toBe(2); + }); + + it('should limit the number of relationships between the same two entities', function() { + /* TODO */ + }); + + it ('should not allow relationships between the same two entities unless the roles are different', function() { + /* TODO */ + }); + + it('should handle only-one-to-one-or-more relationships', function() { + erDiagram.parser.parse('erDiagram\nA !-!< B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ONLY_ONE_TO_ONE_OR_MORE); + }); + + it('should handle only-one-to-zero-or-more relationships', function() { + erDiagram.parser.parse('erDiagram\nA !-?< B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ONLY_ONE_TO_ZERO_OR_MORE); + + }); + + it('should handle zero-or-one-to-zero-or-more relationships', function() { + erDiagram.parser.parse('erDiagram\nA ?-?< B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ZERO_OR_ONE_TO_ZERO_OR_MORE); + }); + + it('should handle zero-or-one-to-one-or-more relationships', function() { + erDiagram.parser.parse('erDiagram\nA ?-!< B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ZERO_OR_ONE_TO_ONE_OR_MORE); + }); + + it('should handle one-or-more-to-only-one relationships', function() { + erDiagram.parser.parse('erDiagram\nA >!-! B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ONE_OR_MORE_TO_ONLY_ONE); + }); + + it('should handle zero-or-more-to-only-one relationships', function() { + erDiagram.parser.parse('erDiagram\nA >?-! B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ZERO_OR_MORE_TO_ONLY_ONE); + }); + + it('should handle zero-or-more-to-zero-or-one relationships', function() { + erDiagram.parser.parse('erDiagram\nA >?-? B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ZERO_OR_MORE_TO_ZERO_OR_ONE); + }); + + it('should handle one-or-more-to-zero-or-one relationships', function() { + erDiagram.parser.parse('erDiagram\nA >!-? B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ONE_OR_MORE_TO_ZERO_OR_ONE); + }); + + it('should handle zero-or-one-to-only-one relationships', function() { + erDiagram.parser.parse('erDiagram\nA ?-! B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ZERO_OR_ONE_TO_ONLY_ONE); + }); + + it('should handle only-one-to-only-one relationships', function() { + erDiagram.parser.parse('erDiagram\nA !-! B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ONLY_ONE_TO_ONLY_ONE); + }); + + it('should handle only-one-to-zero-or-one relationships', function() { + erDiagram.parser.parse('erDiagram\nA !-? B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ONLY_ONE_TO_ZERO_OR_ONE); + }); + + it('should handle zero-or-one-to-zero-or-one relationships', function() { + erDiagram.parser.parse('erDiagram\nA ?-? B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ZERO_OR_ONE_TO_ZERO_OR_ONE); + }); + + it('should handle zero-or-more-to-zero-or-more relationships', function() { + erDiagram.parser.parse('erDiagram\nA >?-?< B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ZERO_OR_MORE_TO_ZERO_OR_MORE); + }); + + it('should handle one-or-more-to-one-or-more relationships', function() { + erDiagram.parser.parse('erDiagram\nA >!-!< B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ONE_OR_MORE_TO_ONE_OR_MORE); + }); + + it('should handle zero-or-more-to-one-or-more relationships', function() { + erDiagram.parser.parse('erDiagram\nA >?-!< B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ZERO_OR_MORE_TO_ONE_OR_MORE); + }); + + it('should handle one-or-more-to-zero-or-more relationships', function() { + erDiagram.parser.parse('erDiagram\nA >!-?< B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ONE_OR_MORE_TO_ZERO_OR_MORE); + }); + + it('should not accept a syntax error', function() { + const doc = 'erDiagram\nA xxx B : has, has'; + expect(() => { + erDiagram.parser.parse(doc); + }).toThrowError(); + }); + +}); From 6985391437d76138fad1c2436085982b4ba77ddc Mon Sep 17 00:00:00 2001 From: Adrian Hall Date: Mon, 2 Mar 2020 11:46:07 +0000 Subject: [PATCH 02/14] Minor clean up for eslint --- src/diagrams/er/erDb.js | 42 ++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/diagrams/er/erDb.js b/src/diagrams/er/erDb.js index 1ab913287..d43442ad7 100644 --- a/src/diagrams/er/erDb.js +++ b/src/diagrams/er/erDb.js @@ -8,22 +8,22 @@ let relationships = []; let title = ''; const Cardinality = { - ONLY_ONE_TO_ONE_OR_MORE : 'ONLY_ONE_TO_ONE_OR_MORE', - ONLY_ONE_TO_ZERO_OR_MORE : 'ONLY_ONE_TO_ZERO_OR_MORE', - ZERO_OR_ONE_TO_ZERO_OR_MORE : 'ZERO_OR_ONE_TO_ZERO_OR_MORE', - ZERO_OR_ONE_TO_ONE_OR_MORE : 'ZERO_OR_ONE_TO_ONE_OR_MORE', - ONE_OR_MORE_TO_ONLY_ONE : 'ONE_OR_MORE_TO_ONLY_ONE', - ZERO_OR_MORE_TO_ONLY_ONE : 'ZERO_OR_MORE_TO_ONLY_ONE', - ZERO_OR_MORE_TO_ZERO_OR_ONE : 'ZERO_OR_MORE_TO_ZERO_OR_ONE', - ONE_OR_MORE_TO_ZERO_OR_ONE : 'ONE_OR_MORE_TO_ZERO_OR_ONE', - ZERO_OR_ONE_TO_ONLY_ONE : 'ZERO_OR_ONE_TO_ONLY_ONE', - ONLY_ONE_TO_ONLY_ONE : 'ONLY_ONE_TO_ONLY_ONE', - ONLY_ONE_TO_ZERO_OR_ONE : 'ONLY_ONE_TO_ZERO_OR_ONE', - ZERO_OR_ONE_TO_ZERO_OR_ONE : 'ZERO_OR_ONE_TO_ZERO_OR_ONE', + ONLY_ONE_TO_ONE_OR_MORE: 'ONLY_ONE_TO_ONE_OR_MORE', + ONLY_ONE_TO_ZERO_OR_MORE: 'ONLY_ONE_TO_ZERO_OR_MORE', + ZERO_OR_ONE_TO_ZERO_OR_MORE: 'ZERO_OR_ONE_TO_ZERO_OR_MORE', + ZERO_OR_ONE_TO_ONE_OR_MORE: 'ZERO_OR_ONE_TO_ONE_OR_MORE', + ONE_OR_MORE_TO_ONLY_ONE: 'ONE_OR_MORE_TO_ONLY_ONE', + ZERO_OR_MORE_TO_ONLY_ONE: 'ZERO_OR_MORE_TO_ONLY_ONE', + ZERO_OR_MORE_TO_ZERO_OR_ONE: 'ZERO_OR_MORE_TO_ZERO_OR_ONE', + ONE_OR_MORE_TO_ZERO_OR_ONE: 'ONE_OR_MORE_TO_ZERO_OR_ONE', + ZERO_OR_ONE_TO_ONLY_ONE: 'ZERO_OR_ONE_TO_ONLY_ONE', + ONLY_ONE_TO_ONLY_ONE: 'ONLY_ONE_TO_ONLY_ONE', + ONLY_ONE_TO_ZERO_OR_ONE: 'ONLY_ONE_TO_ZERO_OR_ONE', + ZERO_OR_ONE_TO_ZERO_OR_ONE: 'ZERO_OR_ONE_TO_ZERO_OR_ONE', ZERO_OR_MORE_TO_ZERO_OR_MORE: 'ZERO_OR_MORE_TO_ZERO_OR_MORE', - ZERO_OR_MORE_TO_ONE_OR_MORE : 'ZERO_OR_MORE_TO_ONE_OR_MORE', - ONE_OR_MORE_TO_ZERO_OR_MORE : 'ONE_OR_MORE_TO_ZERO_OR_MORE', - ONE_OR_MORE_TO_ONE_OR_MORE : 'ONE_OR_MORE_TO_ONE_OR_MORE' + ZERO_OR_MORE_TO_ONE_OR_MORE: 'ZERO_OR_MORE_TO_ONE_OR_MORE', + ONE_OR_MORE_TO_ZERO_OR_MORE: 'ONE_OR_MORE_TO_ZERO_OR_MORE', + ONE_OR_MORE_TO_ONE_OR_MORE: 'ONE_OR_MORE_TO_ONE_OR_MORE' }; const addEntity = function(name) { @@ -37,16 +37,16 @@ const getEntities = () => entities; const addRelationship = function(entA, rolA, entB, rolB, card) { let rel = { - entityA : entA, - roleA : rolA, - entityB : entB, - roleB : rolB, - cardinality : card + entityA: entA, + roleA: rolA, + entityB: entB, + roleB: rolB, + cardinality: card }; relationships.push(rel); logger.debug('Added new relationship :', rel); -} +}; const getRelationships = () => relationships; From 1e2d014ac9991b81c1ee9f3e0ac939851eba27f1 Mon Sep 17 00:00:00 2001 From: Adrian Hall Date: Tue, 3 Mar 2020 21:44:18 +0000 Subject: [PATCH 03/14] Basic rendering for ER diagrams --- src/diagrams/er/erRenderer.js | 478 ++++++++++++++++++++++++++++++++++ src/mermaidAPI.js | 90 +++++++ src/utils.js | 4 + 3 files changed, 572 insertions(+) create mode 100644 src/diagrams/er/erRenderer.js diff --git a/src/diagrams/er/erRenderer.js b/src/diagrams/er/erRenderer.js new file mode 100644 index 000000000..1a6368e1d --- /dev/null +++ b/src/diagrams/er/erRenderer.js @@ -0,0 +1,478 @@ +import graphlib from 'graphlib'; +import * as d3 from 'd3'; +import erDb from './erDb'; +import erParser from './parser/erDiagram'; +import dagre from 'dagre'; +import { getConfig } from '../../config'; +import { logger } from '../../logger'; + +const conf = {}; +export const setConf = function(cnf) { + const keys = Object.keys(cnf); + for (let i = 0; i < keys.length; i++) { + conf[keys[i]] = cnf[keys[i]]; + } +}; + +/** + * Function that adds the entities as vertices + * @param entities The entities to be added to the graph + * @param g The graph that is to be drawn + */ +const addEntities = function(entities, g) { + const keys = Object.keys(entities); + //const fontFamily = getConfig().fontFamily; + + keys.forEach(function(id) { + const entity = entities[id]; + + const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + + // Add the text content (the entity id) + /* + const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan'); + tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve'); + tspan.setAttribute('dy', '1em'); + tspan.setAttribute('x', '1'); + tspan.textContent = id; + tspan.setAttribute('style', 'font-family: monospace'); + svgLabel.appendChild(tspan); + */ + g.setNode(entity, { + labelType: 'svg', + width: 100, + height: 75, + //labelStyle: labelStyle, + shape: 'rect', + label: svgLabel, + id: entity + }); + }); +}; + +/** + * Use D3 to construct the svg elements for the entities + * @param diagram the svg node that contains the diagram + * @param entities the entities to be drawn + * @param g the dagre graph that contains the vertex and edge definitions post-layout + */ +const drawEntities = function(diagram, entities, g) { + // For each vertex in the graph: + // - append the text label centred in the right place + // - get it's bounding box and calculate the size of the enclosing rectangle + // - insert the enclosing rectangle + + g.nodes().forEach(function(v) { + console.debug('Handling node ', v); + + // Get the centre co-ordinate of the node so that we can centre the entity name + let centre = { x: g.node(v).x, y: g.node(v).y }; + + // Label the entity - this is done first so that we can get the bounding box + // which then determines the size of the rectangle + let textNode = diagram + .append('text') + .attr('x', centre.x) + .attr('y', centre.y) + .attr('dominant-baseline', 'middle') + .attr('text-anchor', 'middle') + .attr('style', 'font-family: ' + getConfig().fontFamily) + .text(v); + + let textBBox = textNode.node().getBBox(); + let entityWidth = Math.max(conf.minEntityWidth, textBBox.width + conf.entityPadding * 2); + let entityHeight = Math.max(conf.minEntityHeight, textBBox.height + conf.entityPadding * 2); + + // Add info to the node so that we can retrieve it later when drawing relationships + g.node(v).width = entityWidth; + g.node(v).height = entityHeight; + + // Draw the rectangle + let rectX = centre.x - entityWidth / 2; + let rectY = centre.y - entityHeight / 2; + diagram + .insert('rect') + .attr('fill', conf.fill) + .attr('fill-opacity', conf.fillOpacity) + .attr('stroke', conf.stroke) + .attr('x', rectX) + .attr('y', rectY) + .attr('width', entityWidth) + .attr('height', entityHeight); + + // TODO: Revisit + // Bit of a hack - we're adding the text AGAIN because + // the rectangle is filled to obscure the lines that go to the centre! + diagram + .append('text') + .attr('x', centre.x) + .attr('y', centre.y) + .attr('dominant-baseline', 'middle') + .attr('text-anchor', 'middle') + .attr('style', 'font-family: ' + getConfig().fontFamily) + .text(v); + }); +}; // drawEntities + +const addRelationships = function(relationships, g) { + relationships.forEach(function(r) { + g.setEdge(r.entityA, r.entityB, { relationship: r }); + }); +}; + +const drawRelationships = function(diagram, relationships, g) { + relationships.forEach(function(rel) { + drawRelationship(diagram, rel, g); + }); +}; // drawRelationships + +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) { + 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 } + }; +}; + +// Calculate point pXDelta w.r.t. an intersect point + +// Calcualate point pYDelta w.r.t. an intersect point + +// Calculate point qXDelta w.r.t. an intersect point + +// Calculate point qYDelta w.r.t. an intersect point + +// Now draw from the heel to point P then to the centre of the target entity + +// Now do the same again using point Q instead of P + +// Now draw the ankle + +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 + */ +export const draw = function(text, id) { + logger.info('Drawing ER diagram'); + erDb.clear(); + const parser = erParser.parser; + parser.yy = erDb; + + // Parse the text to populate erDb + try { + parser.parse(text); + } catch (err) { + logger.debug('Parsing failed'); + } + + // Get a reference to the diagram node + const diagram = d3.select(`[id='${id}']`); + + // Add cardinality 'marker' definitions to the svg + //insertMarkers(diagram); + + // 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 + g = new graphlib.Graph({ + multigraph: true, + directed: true, + compound: false + }) + .setGraph({ + rankdir: 'TB', + marginx: 20, + marginy: 20, + nodesep: 100, + ranksep: 100 + }) + .setDefaultEdgeLabel(function() { + 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}"]`); + + 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); + + drawRelationships(diagram, relationships, g); + drawFeet(diagram, relationships, g); + drawEntities(diagram, entities, g); + + const padding = 8; + + const svgBounds = svg.node().getBBox(); + const width = svgBounds.width + padding * 4; + const height = svgBounds.height + padding * 4; + logger.debug( + `new ViewBox 0 0 ${width} ${height}`, + `translate(${padding - g._label.marginx}, ${padding - g._label.marginy})` + ); + + if (conf.useMaxWidth) { + svg.attr('width', '100%'); + svg.attr('style', `max-width: ${width}px;`); + } else { + svg.attr('height', height); + svg.attr('width', width); + } + + svg.attr('viewBox', `0 0 ${width} ${height}`); + svg + .select('g') + .attr('transform', `translate(${padding - g._label.marginx}, ${padding - svgBounds.y})`); +}; // draw + +export default { + setConf, + //addEntities, + draw +}; diff --git a/src/mermaidAPI.js b/src/mermaidAPI.js index 205254540..95dc56e70 100644 --- a/src/mermaidAPI.js +++ b/src/mermaidAPI.js @@ -40,6 +40,9 @@ import infoDb from './diagrams/info/infoDb'; import pieRenderer from './diagrams/pie/pieRenderer'; import pieParser from './diagrams/pie/parser/pie'; import pieDb from './diagrams/pie/pieDb'; +import erDb from './diagrams/er/erDb'; +import erParser from './diagrams/er/parser/erDiagram'; +import erRenderer from './diagrams/er/erRenderer'; const themes = {}; for (const themeName of ['default', 'forest', 'dark', 'neutral']) { @@ -342,6 +345,84 @@ const config = { edgeLengthFactor: '20', compositTitleSize: 35, radius: 5 + }, + + /** + * The object containing configurations specific for entity relationship diagrams + */ + er: { + /** + * The mimimum width of an entity box + */ + minEntityWidth: 100, + + /** + * The minimum height of an entity box + */ + minEntityHeight: 75, + + /** + * The minimum internal padding between the text in an entity box and the enclosing box borders + */ + entityPadding: 15, + + /** + * Stroke color of box edges and lines + */ + stroke: 'purple', + + /** + * Fill color of entity boxes + */ + fill: 'honeydew', + + /** + * Distance of the 'ankle' from the intersection point + */ + 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' } }; @@ -389,6 +470,11 @@ function parse(text) { parser = pieParser; parser.parser.yy = pieDb; break; + case 'er': + logger.debug('er'); + parser = erParser; + parser.parser.yy = erDb; + break; } parser.parser.yy.parseError = (str, hash) => { @@ -606,6 +692,10 @@ const render = function(id, _txt, cb, container) { pieRenderer.setConf(config.class); pieRenderer.draw(txt, id, pkg.version); break; + case 'er': + erRenderer.setConf(config.er); + erRenderer.draw(txt, id, pkg.version); + break; } d3.select(`[id="${id}"]`) diff --git a/src/utils.js b/src/utils.js index 1aec62d4f..055bb867a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -49,6 +49,10 @@ export const detectType = function(text) { return 'pie'; } + if (text.match(/^\s*erDiagram/)) { + return 'er'; + } + return 'flowchart'; }; From 061045016fc4eed8da1dd9c95369b8c0adbd76bc Mon Sep 17 00:00:00 2001 From: Adrian Hall Date: Sat, 7 Mar 2020 11:05:56 +0000 Subject: [PATCH 04/14] Minor tidy up of prototype code --- src/diagrams/er/erRenderer.js | 135 ++++++++++++++++++---------------- 1 file changed, 71 insertions(+), 64 deletions(-) diff --git a/src/diagrams/er/erRenderer.js b/src/diagrams/er/erRenderer.js index 1a6368e1d..15eeb6499 100644 --- a/src/diagrams/er/erRenderer.js +++ b/src/diagrams/er/erRenderer.js @@ -21,28 +21,15 @@ export const setConf = function(cnf) { */ const addEntities = function(entities, g) { const keys = Object.keys(entities); - //const fontFamily = getConfig().fontFamily; keys.forEach(function(id) { const entity = entities[id]; - const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); - // Add the text content (the entity id) - /* - const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan'); - tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve'); - tspan.setAttribute('dy', '1em'); - tspan.setAttribute('x', '1'); - tspan.textContent = id; - tspan.setAttribute('style', 'font-family: monospace'); - svgLabel.appendChild(tspan); - */ g.setNode(entity, { labelType: 'svg', width: 100, height: 75, - //labelStyle: labelStyle, shape: 'rect', label: svgLabel, id: entity @@ -56,7 +43,7 @@ const addEntities = function(entities, g) { * @param entities the entities to be drawn * @param g the dagre graph that contains the vertex and edge definitions post-layout */ -const drawEntities = function(diagram, entities, g) { +const drawEntities = function(diagram, entities, g, svgId) { // For each vertex in the graph: // - append the text label centred in the right place // - get it's bounding box and calculate the size of the enclosing rectangle @@ -66,12 +53,14 @@ const drawEntities = function(diagram, entities, g) { console.debug('Handling node ', v); // Get the centre co-ordinate of the node so that we can centre the entity name - let centre = { x: g.node(v).x, y: g.node(v).y }; + const centre = { x: g.node(v).x, y: g.node(v).y }; // Label the entity - this is done first so that we can get the bounding box // which then determines the size of the rectangle - let textNode = diagram + const textId = 'entity-' + v + '-' + svgId; + const textNode = diagram .append('text') + .attr('id', textId) .attr('x', centre.x) .attr('y', centre.y) .attr('dominant-baseline', 'middle') @@ -79,19 +68,19 @@ const drawEntities = function(diagram, entities, g) { .attr('style', 'font-family: ' + getConfig().fontFamily) .text(v); - let textBBox = textNode.node().getBBox(); - let entityWidth = Math.max(conf.minEntityWidth, textBBox.width + conf.entityPadding * 2); - let entityHeight = Math.max(conf.minEntityHeight, textBBox.height + conf.entityPadding * 2); + const textBBox = textNode.node().getBBox(); + const entityWidth = Math.max(conf.minEntityWidth, textBBox.width + conf.entityPadding * 2); + const entityHeight = Math.max(conf.minEntityHeight, textBBox.height + conf.entityPadding * 2); // Add info to the node so that we can retrieve it later when drawing relationships g.node(v).width = entityWidth; g.node(v).height = entityHeight; - // Draw the rectangle - let rectX = centre.x - entityWidth / 2; - let rectY = centre.y - entityHeight / 2; + // Draw the rectangle - insert it before the text so that the text is not obscured + const rectX = centre.x - entityWidth / 2; + const rectY = centre.y - entityHeight / 2; diagram - .insert('rect') + .insert('rect', '#' + textId) .attr('fill', conf.fill) .attr('fill-opacity', conf.fillOpacity) .attr('stroke', conf.stroke) @@ -99,18 +88,6 @@ const drawEntities = function(diagram, entities, g) { .attr('y', rectY) .attr('width', entityWidth) .attr('height', entityHeight); - - // TODO: Revisit - // Bit of a hack - we're adding the text AGAIN because - // the rectangle is filled to obscure the lines that go to the centre! - diagram - .append('text') - .attr('x', centre.x) - .attr('y', centre.y) - .attr('dominant-baseline', 'middle') - .attr('text-anchor', 'middle') - .attr('style', 'font-family: ' + getConfig().fontFamily) - .text(v); }); }; // drawEntities @@ -118,7 +95,7 @@ const addRelationships = function(relationships, g) { relationships.forEach(function(r) { g.setEdge(r.entityA, r.entityB, { relationship: r }); }); -}; +}; // addRelationships const drawRelationships = function(diagram, relationships, g) { relationships.forEach(function(rel) { @@ -231,35 +208,66 @@ const getToes = function(relationship, fromPoint, toPoint, distance) { const toeXDelta = toeYDelta * Math.abs(gradient); if (gradient > 0) { - if (fromPoint.x < toPoint.x) { - // Scenario A - + const topToe = function(point) { 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 - } - } + x: point.x + toeXDelta, + y: point.y - toeYDelta }; - } else { - // Scenario E - } + }; + + 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) { @@ -445,7 +453,7 @@ export const draw = function(text, id) { drawRelationships(diagram, relationships, g); drawFeet(diagram, relationships, g); - drawEntities(diagram, entities, g); + drawEntities(diagram, entities, g, id); const padding = 8; @@ -473,6 +481,5 @@ export const draw = function(text, id) { export default { setConf, - //addEntities, draw }; From 9aacc85a165a06df4319a981da0867d47aa7903a Mon Sep 17 00:00:00 2001 From: Adrian Hall Date: Sun, 8 Mar 2020 22:22:33 +0000 Subject: [PATCH 05/14] Working experimental version using markers --- src/diagrams/er/erMarkers.js | 174 ++++++++++++++++++++++++++++++++++ src/diagrams/er/erRenderer.js | 116 ++++++++++++++++++++++- 2 files changed, 287 insertions(+), 3 deletions(-) create mode 100644 src/diagrams/er/erMarkers.js diff --git a/src/diagrams/er/erMarkers.js b/src/diagrams/er/erMarkers.js new file mode 100644 index 000000000..af9edbc9c --- /dev/null +++ b/src/diagrams/er/erMarkers.js @@ -0,0 +1,174 @@ +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 + */ +const insertMarkers = function(elem, conf) { + let marker; + + const markerWidth = + elem + .append('defs') + .append('marker') + .attr('id', ERMarkers.ONLY_ONE_START) + .attr('refX', 0) + .attr('refY', 9) + .attr('markerWidth', 18) + .attr('markerHeight', 18) + .attr('orient', 'auto') + .append('path') + .attr('stroke', conf.stroke) + .attr('fill', 'none') + .attr('d', 'M9,0 L9,18 M15,0 L15,18'); + + elem + .append('defs') + .append('marker') + .attr('id', ERMarkers.ONLY_ONE_END) + .attr('refX', 18) + .attr('refY', 9) + .attr('markerWidth', 18) + .attr('markerHeight', 18) + .attr('orient', 'auto') + .append('path') + .attr('stroke', conf.stroke) + .attr('fill', 'none') + .attr('d', 'M3,0 L3,18 M9,0 L9,18'); + + marker = elem + .append('defs') + .append('marker') + .attr('id', ERMarkers.ZERO_OR_ONE_START) + .attr('refX', 0) + .attr('refY', 9) + .attr('markerWidth', 30) + .attr('markerHeight', 18) + .attr('orient', 'auto'); + marker + .append('circle') + .attr('stroke', conf.stroke) + .attr('fill', 'white') + .attr('cx', 21) + .attr('cy', 9) + .attr('r', 6); + marker + .append('path') + .attr('stroke', conf.stroke) + .attr('fill', 'none') + .attr('d', 'M9,0 L9,18'); + + marker = elem + .append('defs') + .append('marker') + .attr('id', ERMarkers.ZERO_OR_ONE_END) + .attr('refX', 30) + .attr('refY', 9) + .attr('markerWidth', 30) + .attr('markerHeight', 18) + .attr('orient', 'auto'); + marker + .append('circle') + .attr('stroke', conf.stroke) + .attr('fill', 'white') + .attr('cx', 9) + .attr('cy', 9) + .attr('r', 6); + marker + .append('path') + .attr('stroke', conf.stroke) + .attr('fill', 'none') + .attr('d', 'M21,0 L21,18'); + + elem + .append('defs') + .append('marker') + .attr('id', ERMarkers.ONE_OR_MORE_START) + .attr('refX', 0) + .attr('refY', 9) + .attr('markerWidth', 18) + .attr('markerHeight', 18) + .attr('orient', 'auto') + .append('path') + .attr('stroke', conf.stroke) + .attr('fill', 'none') + .attr('d', 'M0,0 L9,9 L0,18 M15,0 L15,18'); + + 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('orient', 'auto') + .append('path') + .attr('stroke', conf.stroke) + .attr('fill', 'none') + .attr('d', 'M3,0 L3,18 M18,0 L9,9 L18,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('orient', 'auto'); + marker + .append('circle') + .attr('stroke', conf.stroke) + .attr('fill', 'white') + .attr('cx', 21) + .attr('cy', 9) + .attr('r', 6); + marker + .append('path') + .attr('stroke', conf.stroke) + .attr('fill', 'none') + .attr('d', 'M0,0 L9,9 L0,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('orient', 'auto'); + marker + .append('circle') + .attr('stroke', conf.stroke) + .attr('fill', 'white') + .attr('cx', 9) + .attr('cy', 9) + .attr('r', 6); + marker + .append('path') + .attr('stroke', conf.stroke) + .attr('fill', 'none') + .attr('d', 'M30,0 L21,9 L30,18'); + + return; +}; + +export default { + ERMarkers, + insertMarkers +}; diff --git a/src/diagrams/er/erRenderer.js b/src/diagrams/er/erRenderer.js index 15eeb6499..ba010dab7 100644 --- a/src/diagrams/er/erRenderer.js +++ b/src/diagrams/er/erRenderer.js @@ -5,6 +5,7 @@ import erParser from './parser/erDiagram'; import dagre from 'dagre'; import { getConfig } from '../../config'; import { logger } from '../../logger'; +import erMarkers from './erMarkers'; const conf = {}; export const setConf = function(cnf) { @@ -99,10 +100,119 @@ const addRelationships = function(relationships, g) { const drawRelationships = function(diagram, relationships, g) { relationships.forEach(function(rel) { - drawRelationship(diagram, rel, g); + //drawRelationship(diagram, rel, g); + drawRelationshipFromLayout(diagram, rel, g); }); }; // drawRelationships +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() + .x(function(d) { + return d.x; + }) + .y(function(d) { + return d.y; + }) + .curve(d3.curveBasis); + + // Append the line to the diagram node + const svgPath = diagram + .append('path') + .attr('d', lineFunction(edge.points)) + .attr('stroke', conf.stroke) + .attr('fill', 'none'); + + // TODO: Understand this + let url = ''; + if (conf.arrowMarkerAbsolute) { + url = + window.location.protocol + + '//' + + window.location.host + + window.location.pathname + + window.location.search; + url = url.replace(/\(/g, '\\('); + url = url.replace(/\)/g, '\\)'); + } + + // TODO: change the way enums are imported + // Decide which start and end markers it needs + switch (rel.cardinality) { + case erDb.Cardinality.ONLY_ONE_TO_ONE_OR_MORE: + svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_START + ')'); + svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_END + ')'); + break; + case erDb.Cardinality.ONLY_ONE_TO_ZERO_OR_MORE: + svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_START + ')'); + svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_END + ')'); + break; + case erDb.Cardinality.ZERO_OR_ONE_TO_ZERO_OR_MORE: + svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_START + ')'); + svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_END + ')'); + break; + case erDb.Cardinality.ZERO_OR_ONE_TO_ONE_OR_MORE: + svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_START + ')'); + svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_END + ')'); + break; + case erDb.Cardinality.ONE_OR_MORE_TO_ONLY_ONE: + svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_START + ')'); + svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_END + ')'); + break; + case erDb.Cardinality.ZERO_OR_MORE_TO_ONLY_ONE: + svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_START + ')'); + svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_END + ')'); + break; + case erDb.Cardinality.ZERO_OR_MORE_TO_ZERO_OR_ONE: + svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_START + ')'); + svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_END + ')'); + break; + case erDb.Cardinality.ONE_OR_MORE_TO_ZERO_OR_ONE: + svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_START + ')'); + svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_END + ')'); + break; + case erDb.Cardinality.ZERO_OR_ONE_TO_ONLY_ONE: + svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_START + ')'); + svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_END + ')'); + break; + case erDb.Cardinality.ONLY_ONE_TO_ONLY_ONE: + svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_START + ')'); + svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_END + ')'); + break; + case erDb.Cardinality.ONLY_ONE_TO_ZERO_OR_ONE: + svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_START + ')'); + svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_END + ')'); + break; + case erDb.Cardinality.ZERO_OR_ONE_TO_ZERO_OR_ONE: + svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_START + ')'); + svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_END + ')'); + break; + case erDb.Cardinality.ZERO_OR_MORE_TO_ZERO_OR_MORE: + svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_START + ')'); + svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_END + ')'); + break; + case erDb.Cardinality.ZERO_OR_MORE_TO_ONE_OR_MORE: + svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_START + ')'); + svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_END + ')'); + break; + case erDb.Cardinality.ONE_OR_MORE_TO_ZERO_OR_MORE: + svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_START + ')'); + svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_END + ')'); + break; + case erDb.Cardinality.ONE_OR_MORE_TO_ONE_OR_MORE: + svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_START + ')'); + svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_END + ')'); + break; + } +}; + const drawRelationship = function(diagram, relationship, g) { // Set the from and to co-ordinates using the graph vertices @@ -406,7 +516,7 @@ export const draw = function(text, id) { const diagram = d3.select(`[id='${id}']`); // Add cardinality 'marker' definitions to the svg - //insertMarkers(diagram); + erMarkers.insertMarkers(diagram, conf); // Create the graph let g; @@ -451,8 +561,8 @@ export const draw = function(text, id) { //const element = d3.select('#' + id + ' g'); //render(element, g); + //drawFeet(diagram, relationships, g); drawRelationships(diagram, relationships, g); - drawFeet(diagram, relationships, g); drawEntities(diagram, entities, g, id); const padding = 8; From 29b6e00071e492a6ac21838086f8641d3530d08e Mon Sep 17 00:00:00 2001 From: Adrian Hall Date: Mon, 9 Mar 2020 12:36:43 +0000 Subject: [PATCH 06/14] clean up for eslint --- src/diagrams/er/erMarkers.js | 3 +- src/diagrams/er/erRenderer.js | 98 +++++++++++++++++++++++------------ 2 files changed, 65 insertions(+), 36 deletions(-) diff --git a/src/diagrams/er/erMarkers.js b/src/diagrams/er/erMarkers.js index af9edbc9c..96933693b 100644 --- a/src/diagrams/er/erMarkers.js +++ b/src/diagrams/er/erMarkers.js @@ -1,4 +1,4 @@ -import * as d3 from 'd3'; +//import * as d3 from 'd3'; const ERMarkers = { ONLY_ONE_START: 'ONLY_ONE_START', @@ -20,7 +20,6 @@ const ERMarkers = { const insertMarkers = function(elem, conf) { let marker; - const markerWidth = elem .append('defs') .append('marker') diff --git a/src/diagrams/er/erRenderer.js b/src/diagrams/er/erRenderer.js index ba010dab7..858f6b9cf 100644 --- a/src/diagrams/er/erRenderer.js +++ b/src/diagrams/er/erRenderer.js @@ -110,7 +110,7 @@ const drawRelationshipFromLayout = function(diagram, rel, g) { 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? + 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 @@ -146,7 +146,7 @@ const drawRelationshipFromLayout = function(diagram, rel, g) { // TODO: change the way enums are imported // Decide which start and end markers it needs switch (rel.cardinality) { - case erDb.Cardinality.ONLY_ONE_TO_ONE_OR_MORE: + case erDb.Cardinality.ONLY_ONE_TO_ONE_OR_MORE: svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_START + ')'); svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_END + ')'); break; @@ -155,31 +155,52 @@ const drawRelationshipFromLayout = function(diagram, rel, g) { svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_END + ')'); break; case erDb.Cardinality.ZERO_OR_ONE_TO_ZERO_OR_MORE: - svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_START + ')'); + svgPath.attr( + 'marker-start', + 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_START + ')' + ); svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_END + ')'); break; case erDb.Cardinality.ZERO_OR_ONE_TO_ONE_OR_MORE: - svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_START + ')'); + svgPath.attr( + 'marker-start', + 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_START + ')' + ); svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_END + ')'); break; case erDb.Cardinality.ONE_OR_MORE_TO_ONLY_ONE: - svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_START + ')'); + svgPath.attr( + 'marker-start', + 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_START + ')' + ); svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_END + ')'); break; case erDb.Cardinality.ZERO_OR_MORE_TO_ONLY_ONE: - svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_START + ')'); + svgPath.attr( + 'marker-start', + 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_START + ')' + ); svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_END + ')'); break; case erDb.Cardinality.ZERO_OR_MORE_TO_ZERO_OR_ONE: - svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_START + ')'); + svgPath.attr( + 'marker-start', + 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_START + ')' + ); svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_END + ')'); break; case erDb.Cardinality.ONE_OR_MORE_TO_ZERO_OR_ONE: - svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_START + ')'); + svgPath.attr( + 'marker-start', + 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_START + ')' + ); svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_END + ')'); break; case erDb.Cardinality.ZERO_OR_ONE_TO_ONLY_ONE: - svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_START + ')'); + svgPath.attr( + 'marker-start', + 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_START + ')' + ); svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_END + ')'); break; case erDb.Cardinality.ONLY_ONE_TO_ONLY_ONE: @@ -191,28 +212,44 @@ const drawRelationshipFromLayout = function(diagram, rel, g) { svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_END + ')'); break; case erDb.Cardinality.ZERO_OR_ONE_TO_ZERO_OR_ONE: - svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_START + ')'); + svgPath.attr( + 'marker-start', + 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_START + ')' + ); svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_END + ')'); break; case erDb.Cardinality.ZERO_OR_MORE_TO_ZERO_OR_MORE: - svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_START + ')'); + svgPath.attr( + 'marker-start', + 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_START + ')' + ); svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_END + ')'); break; case erDb.Cardinality.ZERO_OR_MORE_TO_ONE_OR_MORE: - svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_START + ')'); + svgPath.attr( + 'marker-start', + 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_START + ')' + ); svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_END + ')'); break; case erDb.Cardinality.ONE_OR_MORE_TO_ZERO_OR_MORE: - svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_START + ')'); + svgPath.attr( + 'marker-start', + 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_START + ')' + ); svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_END + ')'); break; case erDb.Cardinality.ONE_OR_MORE_TO_ONE_OR_MORE: - svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_START + ')'); + svgPath.attr( + 'marker-start', + 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_START + ')' + ); svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_END + ')'); break; } }; +/* const drawRelationship = function(diagram, relationship, g) { // Set the from and to co-ordinates using the graph vertices @@ -234,7 +271,9 @@ const drawRelationship = function(diagram, relationship, g) { .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 @@ -307,7 +346,8 @@ const getToePath = function(heel, toe, tip) { 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; @@ -347,8 +387,8 @@ const getToes = function(relationship, fromPoint, toPoint, distance) { from: fromPoint.x < toPoint.x ? upper : lower }; } - - /* +*/ +/* if (fromPoint.x < toPoint.x) { // Scenario A @@ -377,9 +417,11 @@ const getToes = function(relationship, fromPoint, toPoint, distance) { } 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); @@ -453,21 +495,9 @@ const getJoints = function(relationship, fromPoint, toPoint, distance) { to: { x: toX, y: toY } }; }; +*/ -// Calculate point pXDelta w.r.t. an intersect point - -// Calcualate point pYDelta w.r.t. an intersect point - -// Calculate point qXDelta w.r.t. an intersect point - -// Calculate point qYDelta w.r.t. an intersect point - -// Now draw from the heel to point P then to the centre of the target entity - -// Now do the same again using point Q instead of P - -// Now draw the ankle - +/* const getXDelta = function(hypotenuse, gradient) { return Math.sqrt((hypotenuse * hypotenuse) / (Math.abs(gradient) + 1)); }; @@ -493,7 +523,7 @@ const dot = function(diagram, p, color) { .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 From bab4649a1e8c1c5fc026e8e0cf6fce9bb822d6d6 Mon Sep 17 00:00:00 2001 From: Adrian Hall Date: Tue, 10 Mar 2020 13:48:53 +0000 Subject: [PATCH 07/14] Use markers with rounded crows feet --- src/diagrams/er/erMarkers.js | 53 +++-- src/diagrams/er/erRenderer.js | 353 ++++------------------------------ src/mermaidAPI.js | 51 +---- 3 files changed, 69 insertions(+), 388 deletions(-) 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%' } }; From 1e498eccb63ac9b7f72d66baec345e3145a7b856 Mon Sep 17 00:00:00 2001 From: Adrian Hall Date: Wed, 11 Mar 2020 22:17:11 +0000 Subject: [PATCH 08/14] Change rendering algorithm --- src/diagrams/er/erRenderer.js | 124 ++++++++++++++++++++++++---------- src/mermaidAPI.js | 7 +- 2 files changed, 93 insertions(+), 38 deletions(-) diff --git a/src/diagrams/er/erRenderer.js b/src/diagrams/er/erRenderer.js index 1933ffb95..901fb8568 100644 --- a/src/diagrams/er/erRenderer.js +++ b/src/diagrams/er/erRenderer.js @@ -21,6 +21,7 @@ export const setConf = function(cnf) { * @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); @@ -39,61 +40,91 @@ const addEntities = function(entities, g) { }); return entities; }; - +*/ /** * Use D3 to construct the svg elements for the entities - * @param diagram the svg node that contains the diagram + * @param svgNode the svg node that contains the diagram * @param entities the entities to be drawn * @param g the dagre graph that contains the vertex and edge definitions post-layout */ -const drawEntities = function(diagram, entities, g, svgId) { - // For each vertex in the graph: - // - append the text label centred in the right place - // - get it's bounding box and calculate the size of the enclosing rectangle - // - insert the enclosing rectangle +const drawEntities = function(svgNode, entities, svgId, graph) { + const keys = Object.keys(entities); + let firstOne; - g.nodes().forEach(function(v) { - console.debug('Handling node ', v); + keys.forEach(function(id) { + // Create a group for each entity + const groupNode = svgNode.append('g').attr('id', id); - // Get the centre co-ordinate of the node so that we can centre the entity name - const centre = { x: g.node(v).x, y: g.node(v).y }; + firstOne = firstOne === undefined ? id : firstOne; // Label the entity - this is done first so that we can get the bounding box // which then determines the size of the rectangle - const textId = 'entity-' + v + '-' + svgId; - const textNode = diagram + const textId = 'entity-' + id + '-' + svgId; + const textNode = groupNode .append('text') .attr('id', textId) - .attr('x', centre.x) - .attr('y', centre.y) + .attr('x', 0) + .attr('y', (conf.fontSize + 2 * conf.entityPadding) / 2) .attr('dominant-baseline', 'middle') .attr('text-anchor', 'middle') - .attr('style', 'font-family: ' + getConfig().fontFamily) - .text(v); + .attr('style', 'font-family: ' + getConfig().fontFamily + '; font-size: ' + conf.fontSize) + .text(id); + // Calculate the width and height of the entity const textBBox = textNode.node().getBBox(); const entityWidth = Math.max(conf.minEntityWidth, textBBox.width + conf.entityPadding * 2); const entityHeight = Math.max(conf.minEntityHeight, textBBox.height + conf.entityPadding * 2); - // Add info to the node so that we can retrieve it later when drawing relationships - g.node(v).width = entityWidth; - g.node(v).height = entityHeight; + // Make sure the text gets centred relative to the entity box + textNode.attr('transform', 'translate(' + entityWidth / 2 + ',' + entityHeight / 2 + ')'); // Draw the rectangle - insert it before the text so that the text is not obscured - const rectX = centre.x - entityWidth / 2; - const rectY = centre.y - entityHeight / 2; - diagram + const rectNode = groupNode .insert('rect', '#' + textId) .attr('fill', conf.fill) .attr('fill-opacity', conf.fillOpacity) .attr('stroke', conf.stroke) - .attr('x', rectX) - .attr('y', rectY) + .attr('x', 0) + .attr('y', 0) .attr('width', entityWidth) .attr('height', entityHeight); + + const rectBBox = rectNode.node().getBBox(); + + // Add the entity to the graph + // TODO: revisit this - need to understand properly + graph.setNode(id, { + labelType: 'svg', + width: rectBBox.width, + height: rectBBox.height, + shape: 'rect', + label: document.createElementNS('http://www.w3.org/2000/svg', 'text'), + id: id + }); }); + return firstOne; }; // drawEntities +const adjustEntities = function(svgNode, entities, graph) { + graph.nodes().forEach(function(v) { + if (typeof v !== 'undefined' && typeof graph.node(v) !== 'undefined') { + d3.select('#' + v).attr( + 'transform', + 'translate(' + + (graph.node(v).x - graph.node(v).width / 2) + + ',' + + (graph.node(v).y - graph.node(v).height / 2) + + ' )' + ); + } + }); + return; +}; + +const getEdgeName = function(rel) { + return (rel.entityA + rel.roleA + rel.roleB + rel.entityB).replace(/\s/g, ''); +}; + /** * Add each relationship to the graph * @param relationships the relationships to be added @@ -102,7 +133,7 @@ const drawEntities = function(diagram, entities, g, svgId) { */ const addRelationships = function(relationships, g) { relationships.forEach(function(r) { - g.setEdge(r.entityA, r.entityB, { relationship: r }); + g.setEdge(r.entityA, r.entityB, { relationship: r }, getEdgeName(r)); }); return relationships; }; // addRelationships @@ -110,9 +141,9 @@ const addRelationships = function(relationships, g) { /** * */ -const drawRelationships = function(diagram, relationships, g) { +const drawRelationships = function(diagram, relationships, g, insertId) { relationships.forEach(function(rel) { - drawRelationshipFromLayout(diagram, rel, g); + drawRelationshipFromLayout(diagram, rel, g, insertId); }); }; // drawRelationships @@ -122,9 +153,10 @@ const drawRelationships = function(diagram, relationships, g) { * @param rel the relationship to draw in the svg * @param g the graph containing the edge information */ -const drawRelationshipFromLayout = function(diagram, rel, g) { +const drawRelationshipFromLayout = function(diagram, rel, g, insert) { // Find the edge relating to this relationship - const edge = g.edge({ v: rel.entityA, w: rel.entityB }); + //const edge = g.edge({ v: rel.entityA, w: rel.entityB }); + const edge = g.edge(rel.entityA, rel.entityB, getEdgeName(rel)); // Get a function that will generate the line path const lineFunction = d3 @@ -137,9 +169,9 @@ const drawRelationshipFromLayout = function(diagram, rel, g) { }) .curve(d3.curveBasis); - // Append the line to the diagram node + // Insert the line at the right place const svgPath = diagram - .append('path') + .insert('path', '#' + insert) .attr('d', lineFunction(edge.points)) .attr('stroke', conf.stroke) .attr('fill', 'none'); @@ -281,12 +313,26 @@ export const draw = function(text, id) { logger.debug('Parsing failed'); } - // Get a reference to the diagram node + // Get a reference to the svg node that contains the text const svg = d3.select(`[id='${id}']`); // Add cardinality marker definitions to the svg erMarkers.insertMarkers(svg, conf); + // Now we have to construct the diagram in a specific way: + // --- + // 1. Create all the entities in the svg node at 0,0, but with the correct dimensions (allowing for text content) + // 2. Make sure they are all added to the graph + // 3. Add all the edges (relationships) to the graph aswell + // 4. Let dagre do its magic to layout the graph. This assigns: + // - the centre co-ordinates for each node, bearing in mind the dimensions and edge relationships + // - the path co-ordinates for each edge + // But it has no impact on the svg child nodes - the diagram remains with every entity rooted at 0,0 + // 5. Now assign a transform to each entity in the svg node so that it gets drawn in the correct place, as determined by + // its centre point, which is obtained from the graph, and it's width and height + // 6. And finally, create all the edges in the svg node using information from the graph + // --- + // Create the graph let g; @@ -311,16 +357,20 @@ export const draw = function(text, id) { return {}; }); - // Add the entities and relationships to the graph - const entities = addEntities(erDb.getEntities(), g); + const entities = erDb.getEntities(); + const firstEntity = drawEntities(svg, entities, id, g); + + //addEntities(erDb.getEntities(), g); const relationships = addRelationships(erDb.getRelationships(), g); dagre.layout(g); // Node and edge positions will be updated + adjustEntities(svg, entities, 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); + drawRelationships(svg, relationships, g, firstEntity); + //drawEntities(svg, entities, id); const padding = 8; // TODO: move this to config diff --git a/src/mermaidAPI.js b/src/mermaidAPI.js index fa9e7ece5..8bfa33a0e 100644 --- a/src/mermaidAPI.js +++ b/src/mermaidAPI.js @@ -381,7 +381,12 @@ const config = { * retain their elegant joins to the boxes regardless of the angle of incidence * then override this to something less than 100% */ - fillOpacity: '100%' + fillOpacity: '100%', + + /** + * Font size + */ + fontSize: '12px' } }; From 2d3b02df6a0e03e5c5c5ccb15f48a9e3fdd51a6a Mon Sep 17 00:00:00 2001 From: Adrian Hall Date: Mon, 16 Mar 2020 10:08:43 +0000 Subject: [PATCH 09/14] Minor tidy up --- src/diagrams/er/erRenderer.js | 58 +++++++++++++------------- src/diagrams/er/parser/erDiagram.jison | 2 +- 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/src/diagrams/er/erRenderer.js b/src/diagrams/er/erRenderer.js index 901fb8568..85e5df033 100644 --- a/src/diagrams/er/erRenderer.js +++ b/src/diagrams/er/erRenderer.js @@ -8,6 +8,12 @@ import { logger } from '../../logger'; import erMarkers from './erMarkers'; const conf = {}; + +/** + * Allows the top-level API module to inject config specific to this renderer, + * storing it in the local conf object. Note that generic config still needs to be + * retrieved using getConfig() imported from the config module + */ export const setConf = function(cnf) { const keys = Object.keys(cnf); for (let i = 0; i < keys.length; i++) { @@ -44,10 +50,11 @@ const addEntities = function(entities, g) { /** * Use D3 to construct the svg elements for the entities * @param svgNode the svg node that contains the diagram - * @param entities the entities to be drawn - * @param g the dagre graph that contains the vertex and edge definitions post-layout + * @param entities The entities to be drawn + * @param g The graph that contains the vertex and edge definitions post-layout + * @return The first entity that was inserted */ -const drawEntities = function(svgNode, entities, svgId, graph) { +const drawEntities = function(svgNode, entities, graph) { const keys = Object.keys(entities); let firstOne; @@ -59,7 +66,7 @@ const drawEntities = function(svgNode, entities, svgId, graph) { // Label the entity - this is done first so that we can get the bounding box // which then determines the size of the rectangle - const textId = 'entity-' + id + '-' + svgId; + const textId = 'entity-' + id; const textNode = groupNode .append('text') .attr('id', textId) @@ -92,20 +99,17 @@ const drawEntities = function(svgNode, entities, svgId, graph) { const rectBBox = rectNode.node().getBBox(); // Add the entity to the graph - // TODO: revisit this - need to understand properly graph.setNode(id, { - labelType: 'svg', width: rectBBox.width, height: rectBBox.height, shape: 'rect', - label: document.createElementNS('http://www.w3.org/2000/svg', 'text'), id: id }); }); return firstOne; }; // drawEntities -const adjustEntities = function(svgNode, entities, graph) { +const adjustEntities = function(svgNode, graph) { graph.nodes().forEach(function(v) { if (typeof v !== 'undefined' && typeof graph.node(v) !== 'undefined') { d3.select('#' + v).attr( @@ -138,24 +142,14 @@ const addRelationships = function(relationships, g) { return relationships; }; // addRelationships -/** - * - */ -const drawRelationships = function(diagram, relationships, g, insertId) { - relationships.forEach(function(rel) { - drawRelationshipFromLayout(diagram, rel, g, insertId); - }); -}; // drawRelationships - /** * Draw a relationship using edge information from the graph - * @param diagram the svg node + * @param svg 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, insert) { +const drawRelationshipFromLayout = function(svg, rel, g, insert) { // Find the edge relating to this relationship - //const edge = g.edge({ v: rel.entityA, w: rel.entityB }); const edge = g.edge(rel.entityA, rel.entityB, getEdgeName(rel)); // Get a function that will generate the line path @@ -170,7 +164,7 @@ const drawRelationshipFromLayout = function(diagram, rel, g, insert) { .curve(d3.curveBasis); // Insert the line at the right place - const svgPath = diagram + const svgPath = svg .insert('path', '#' + insert) .attr('d', lineFunction(edge.points)) .attr('stroke', conf.stroke) @@ -347,7 +341,7 @@ export const draw = function(text, id) { compound: false }) .setGraph({ - rankdir: 'LR', + rankdir: 'TB', marginx: 20, marginy: 20, nodesep: 100, @@ -357,20 +351,24 @@ export const draw = function(text, id) { return {}; }); - const entities = erDb.getEntities(); - const firstEntity = drawEntities(svg, entities, id, g); + // Draw the entities (at 0,0), returning the first svg node that got + // inserted - this represents the insertion point for relationship paths + const firstEntity = drawEntities(svg, erDb.getEntities(), g); - //addEntities(erDb.getEntities(), g); + // TODO: externalise the addition of entities to the graph - it's a bit 'buried' in the above + + // Add all the relationships to the graph const relationships = addRelationships(erDb.getRelationships(), g); dagre.layout(g); // Node and edge positions will be updated - adjustEntities(svg, entities, g); + // Adjust the positions of the entities so that they adhere to the layout + adjustEntities(svg, g); - // Draw the relationships first because their markers need to be - // clipped by the entity boxes - drawRelationships(svg, relationships, g, firstEntity); - //drawEntities(svg, entities, id); + // Draw the relationships + relationships.forEach(function(rel) { + drawRelationshipFromLayout(svg, rel, g, firstEntity); + }); const padding = 8; // TODO: move this to config diff --git a/src/diagrams/er/parser/erDiagram.jison b/src/diagrams/er/parser/erDiagram.jison index c254c9099..cdd6090cd 100644 --- a/src/diagrams/er/parser/erDiagram.jison +++ b/src/diagrams/er/parser/erDiagram.jison @@ -10,7 +10,7 @@ ["] { this.popState(); } [^"]* { return 'STR'; } "erDiagram" return 'ER_DIAGRAM'; -[A-Za-z][A-Za-z0-9]* return 'ALPHANUM'; +[A-Za-z][A-Za-z0-9\-]* return 'ALPHANUM'; \>\?\-\?\< return 'ZERO_OR_MORE_TO_ZERO_OR_MORE'; \>\?\-\!\< return 'ZERO_OR_MORE_TO_ONE_OR_MORE'; \>\!\-\!\< return 'ONE_OR_MORE_TO_ONE_OR_MORE'; From 50f983871b5b6fddd93cbfab13d557b2f072b5f9 Mon Sep 17 00:00:00 2001 From: Adrian Hall Date: Tue, 17 Mar 2020 09:25:16 +0000 Subject: [PATCH 10/14] Add labels to relationships (needs further tidy up) --- src/diagrams/er/erRenderer.js | 65 ++++++++++++++++++++--------------- src/mermaidAPI.js | 6 ++++ 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/src/diagrams/er/erRenderer.js b/src/diagrams/er/erRenderer.js index 85e5df033..42df1bb53 100644 --- a/src/diagrams/er/erRenderer.js +++ b/src/diagrams/er/erRenderer.js @@ -21,32 +21,6 @@ export const setConf = function(cnf) { } }; -/** - * 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); - - keys.forEach(function(id) { - const entity = entities[id]; - const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); - - g.setNode(entity, { - labelType: 'svg', - width: 100, - height: 75, - shape: 'rect', - label: svgLabel, - id: entity - }); - }); - return entities; -}; -*/ /** * Use D3 to construct the svg elements for the entities * @param svgNode the svg node that contains the diagram @@ -142,6 +116,7 @@ const addRelationships = function(relationships, g) { return relationships; }; // addRelationships +let relCnt = 0; /** * Draw a relationship using edge information from the graph * @param svg the svg node @@ -149,6 +124,8 @@ const addRelationships = function(relationships, g) { * @param g the graph containing the edge information */ const drawRelationshipFromLayout = function(svg, rel, g, insert) { + relCnt++; + // Find the edge relating to this relationship const edge = g.edge(rel.entityA, rel.entityB, getEdgeName(rel)); @@ -287,6 +264,40 @@ const drawRelationshipFromLayout = function(svg, rel, g, insert) { svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_END + ')'); break; } + + // Now label the relationship + + // Find the half-way point + const len = svgPath.node().getTotalLength(); + const labelPoint = svgPath.node().getPointAtLength(len * 0.5); + + // Append a text node containing the label + const labelId = 'rel' + relCnt; + + const labelNode = svg + .append('text') + .attr('id', labelId) + .attr('x', labelPoint.x) + .attr('y', labelPoint.y) + .attr('text-anchor', 'middle') + .attr('dominant-baseline', 'middle') + .attr('style', 'font-family: ' + getConfig().fontFamily + '; font-size: ' + conf.fontSize) + .text(rel.roleA); + + // Figure out how big the opaque 'container' rectangle needs to be + const labelBBox = labelNode.node().getBBox(); + + // Insert the opaque rectangle in front of the text label + svg + .insert('rect', '#' + labelId) + .attr('x', labelPoint.x - labelBBox.width / 2) + .attr('y', labelPoint.y - labelBBox.height / 2) + .attr('width', labelBBox.width) + .attr('height', labelBBox.height) + .attr('fill', 'white') + .attr('fill-opacity', '85%'); + + return; }; /** @@ -341,7 +352,7 @@ export const draw = function(text, id) { compound: false }) .setGraph({ - rankdir: 'TB', + rankdir: conf.layoutDirection, marginx: 20, marginy: 20, nodesep: 100, diff --git a/src/mermaidAPI.js b/src/mermaidAPI.js index 8bfa33a0e..0731ae166 100644 --- a/src/mermaidAPI.js +++ b/src/mermaidAPI.js @@ -351,6 +351,12 @@ const config = { * The object containing configurations specific for entity relationship diagrams */ er: { + /** + * Directional bias for layout of entities. Can be either 'TB', 'BT', 'LR', or 'RL', + * where T = top, B = bottom, L = left, and R = right. + */ + layoutDirection: 'TB', + /** * The mimimum width of an entity box */ From 4f50e36e5b1a5a2711dd1209a69162e6726dac56 Mon Sep 17 00:00:00 2001 From: Adrian Hall Date: Tue, 17 Mar 2020 09:48:32 +0000 Subject: [PATCH 11/14] Tidy up for lint --- src/diagrams/er/erRenderer.js | 2 +- src/mermaidAPI.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/diagrams/er/erRenderer.js b/src/diagrams/er/erRenderer.js index 42df1bb53..2e18b678c 100644 --- a/src/diagrams/er/erRenderer.js +++ b/src/diagrams/er/erRenderer.js @@ -283,7 +283,7 @@ const drawRelationshipFromLayout = function(svg, rel, g, insert) { .attr('dominant-baseline', 'middle') .attr('style', 'font-family: ' + getConfig().fontFamily + '; font-size: ' + conf.fontSize) .text(rel.roleA); - + // Figure out how big the opaque 'container' rectangle needs to be const labelBBox = labelNode.node().getBBox(); diff --git a/src/mermaidAPI.js b/src/mermaidAPI.js index af52927f0..10ead65cb 100644 --- a/src/mermaidAPI.js +++ b/src/mermaidAPI.js @@ -353,8 +353,8 @@ const config = { */ er: { /** - * Directional bias for layout of entities. Can be either 'TB', 'BT', 'LR', or 'RL', - * where T = top, B = bottom, L = left, and R = right. + * Directional bias for layout of entities. Can be either 'TB', 'BT', 'LR', or 'RL', + * where T = top, B = bottom, L = left, and R = right. */ layoutDirection: 'TB', From a3b97f7c24bc4bdb450755347ce2e308559d06bd Mon Sep 17 00:00:00 2001 From: Adrian Hall Date: Tue, 17 Mar 2020 10:16:19 +0000 Subject: [PATCH 12/14] Remove roleB - only roleA is labelled --- src/diagrams/er/erDb.js | 10 ++++- src/diagrams/er/erRenderer.js | 2 +- src/diagrams/er/parser/erDiagram.jison | 6 +-- src/diagrams/er/parser/erDiagram.spec.js | 54 ++++++++++++------------ 4 files changed, 38 insertions(+), 34 deletions(-) diff --git a/src/diagrams/er/erDb.js b/src/diagrams/er/erDb.js index d43442ad7..556eb2f4f 100644 --- a/src/diagrams/er/erDb.js +++ b/src/diagrams/er/erDb.js @@ -35,12 +35,18 @@ const addEntity = function(name) { const getEntities = () => entities; -const addRelationship = function(entA, rolA, entB, rolB, card) { +/** + * Add a relationship + * @param entA The first entity in the relationship + * @param rolA The role played by the first entity in relation to the second + * @param entB The second entity in the relationship + * @param card The cardinality of the relationship between the two entities + */ +const addRelationship = function(entA, rolA, entB, card) { let rel = { entityA: entA, roleA: rolA, entityB: entB, - roleB: rolB, cardinality: card }; diff --git a/src/diagrams/er/erRenderer.js b/src/diagrams/er/erRenderer.js index 2e18b678c..1ae5a8d6a 100644 --- a/src/diagrams/er/erRenderer.js +++ b/src/diagrams/er/erRenderer.js @@ -100,7 +100,7 @@ const adjustEntities = function(svgNode, graph) { }; const getEdgeName = function(rel) { - return (rel.entityA + rel.roleA + rel.roleB + rel.entityB).replace(/\s/g, ''); + return (rel.entityA + rel.roleA + rel.entityB).replace(/\s/g, ''); }; /** diff --git a/src/diagrams/er/parser/erDiagram.jison b/src/diagrams/er/parser/erDiagram.jison index cdd6090cd..52ac48d3e 100644 --- a/src/diagrams/er/parser/erDiagram.jison +++ b/src/diagrams/er/parser/erDiagram.jison @@ -46,12 +46,12 @@ document ; statement - : entityName relationship entityName ':' role ',' role + : entityName relationship entityName ':' role { yy.addEntity($1); yy.addEntity($3); - yy.addRelationship($1, $5, $3, $7, $2); - /*console.log($1 + $2 + $3 + ':' + $5 + ',' + $7);*/ + yy.addRelationship($1, $5, $3, $2); + /*console.log($1 + $2 + $3 + ':' + $5);*/ }; entityName diff --git a/src/diagrams/er/parser/erDiagram.spec.js b/src/diagrams/er/parser/erDiagram.spec.js index e04c7130d..accdc4a40 100644 --- a/src/diagrams/er/parser/erDiagram.spec.js +++ b/src/diagrams/er/parser/erDiagram.spec.js @@ -15,7 +15,7 @@ describe('when parsing ER diagram it...', function() { }); it('should associate two entities correctly', function() { - erDiagram.parser.parse('erDiagram\nCAR !-?< DRIVER : "insured for", "can drive"'); + erDiagram.parser.parse('erDiagram\nCAR !-?< DRIVER : "insured for"'); const entities = erDb.getEntities(); const relationships = erDb.getRelationships(); const carEntity = entities.CAR; @@ -28,33 +28,31 @@ describe('when parsing ER diagram it...', function() { }); it('should not create duplicate entities', function() { - const line1 = 'CAR !-?< DRIVER : "insured for", "can drive"'; - const line2 = 'DRIVER !-! LICENSE : has, "belongs to"'; + const line1 = 'CAR !-?< DRIVER : "insured for"'; + const line2 = 'DRIVER !-! LICENSE : has'; erDiagram.parser.parse(`erDiagram\n${line1}\n${line2}`); const entities = erDb.getEntities(); expect(Object.keys(entities).length).toBe(3); }); - it('should create the roles specified', function() { + it('should create the role specified', function() { const teacherRole = 'is teacher of'; - const studentRole = 'is student of'; - const line1 = `TEACHER >?-?< STUDENT : "${teacherRole}", "${studentRole}"`; + const line1 = `TEACHER >?-?< STUDENT : "${teacherRole}"`; erDiagram.parser.parse(`erDiagram\n${line1}`); const rels = erDb.getRelationships(); expect(rels[0].roleA).toBe(`${teacherRole}`); - expect(rels[0].roleB).toBe(`${studentRole}`); }); it('should allow recursive relationships', function() { - erDiagram.parser.parse('erDiagram\nNODE !-?< NODE : "leads to", "comes from"'); + erDiagram.parser.parse('erDiagram\nNODE !-?< NODE : "leads to"'); expect(Object.keys(erDb.getEntities()).length).toBe(1); }); it('should allow more than one relationship between the same two entities', function() { - const line1 = 'CAR !-?< PERSON : "insured for", "may drive"'; - const line2 = 'CAR >?-! PERSON : "owned by", "owns"'; + const line1 = 'CAR !-?< PERSON : "insured for"'; + const line2 = 'CAR >?-! PERSON : "owned by"'; erDiagram.parser.parse(`erDiagram\n${line1}\n${line2}`); const entities = erDb.getEntities(); const rels = erDb.getRelationships(); @@ -67,12 +65,12 @@ describe('when parsing ER diagram it...', function() { /* TODO */ }); - it ('should not allow relationships between the same two entities unless the roles are different', function() { + it ('should not allow multiple relationships between the same two entities unless the roles are different', function() { /* TODO */ }); it('should handle only-one-to-one-or-more relationships', function() { - erDiagram.parser.parse('erDiagram\nA !-!< B : has, has'); + erDiagram.parser.parse('erDiagram\nA !-!< B : has'); const rels = erDb.getRelationships(); expect(Object.keys(erDb.getEntities()).length).toBe(2); @@ -81,7 +79,7 @@ describe('when parsing ER diagram it...', function() { }); it('should handle only-one-to-zero-or-more relationships', function() { - erDiagram.parser.parse('erDiagram\nA !-?< B : has, has'); + erDiagram.parser.parse('erDiagram\nA !-?< B : has'); const rels = erDb.getRelationships(); expect(Object.keys(erDb.getEntities()).length).toBe(2); @@ -91,7 +89,7 @@ describe('when parsing ER diagram it...', function() { }); it('should handle zero-or-one-to-zero-or-more relationships', function() { - erDiagram.parser.parse('erDiagram\nA ?-?< B : has, has'); + erDiagram.parser.parse('erDiagram\nA ?-?< B : has'); const rels = erDb.getRelationships(); expect(Object.keys(erDb.getEntities()).length).toBe(2); @@ -100,7 +98,7 @@ describe('when parsing ER diagram it...', function() { }); it('should handle zero-or-one-to-one-or-more relationships', function() { - erDiagram.parser.parse('erDiagram\nA ?-!< B : has, has'); + erDiagram.parser.parse('erDiagram\nA ?-!< B : has'); const rels = erDb.getRelationships(); expect(Object.keys(erDb.getEntities()).length).toBe(2); @@ -109,7 +107,7 @@ describe('when parsing ER diagram it...', function() { }); it('should handle one-or-more-to-only-one relationships', function() { - erDiagram.parser.parse('erDiagram\nA >!-! B : has, has'); + erDiagram.parser.parse('erDiagram\nA >!-! B : has'); const rels = erDb.getRelationships(); expect(Object.keys(erDb.getEntities()).length).toBe(2); @@ -118,7 +116,7 @@ describe('when parsing ER diagram it...', function() { }); it('should handle zero-or-more-to-only-one relationships', function() { - erDiagram.parser.parse('erDiagram\nA >?-! B : has, has'); + erDiagram.parser.parse('erDiagram\nA >?-! B : has'); const rels = erDb.getRelationships(); expect(Object.keys(erDb.getEntities()).length).toBe(2); @@ -127,7 +125,7 @@ describe('when parsing ER diagram it...', function() { }); it('should handle zero-or-more-to-zero-or-one relationships', function() { - erDiagram.parser.parse('erDiagram\nA >?-? B : has, has'); + erDiagram.parser.parse('erDiagram\nA >?-? B : has'); const rels = erDb.getRelationships(); expect(Object.keys(erDb.getEntities()).length).toBe(2); @@ -136,7 +134,7 @@ describe('when parsing ER diagram it...', function() { }); it('should handle one-or-more-to-zero-or-one relationships', function() { - erDiagram.parser.parse('erDiagram\nA >!-? B : has, has'); + erDiagram.parser.parse('erDiagram\nA >!-? B : has'); const rels = erDb.getRelationships(); expect(Object.keys(erDb.getEntities()).length).toBe(2); @@ -145,7 +143,7 @@ describe('when parsing ER diagram it...', function() { }); it('should handle zero-or-one-to-only-one relationships', function() { - erDiagram.parser.parse('erDiagram\nA ?-! B : has, has'); + erDiagram.parser.parse('erDiagram\nA ?-! B : has'); const rels = erDb.getRelationships(); expect(Object.keys(erDb.getEntities()).length).toBe(2); @@ -154,7 +152,7 @@ describe('when parsing ER diagram it...', function() { }); it('should handle only-one-to-only-one relationships', function() { - erDiagram.parser.parse('erDiagram\nA !-! B : has, has'); + erDiagram.parser.parse('erDiagram\nA !-! B : has'); const rels = erDb.getRelationships(); expect(Object.keys(erDb.getEntities()).length).toBe(2); @@ -163,7 +161,7 @@ describe('when parsing ER diagram it...', function() { }); it('should handle only-one-to-zero-or-one relationships', function() { - erDiagram.parser.parse('erDiagram\nA !-? B : has, has'); + erDiagram.parser.parse('erDiagram\nA !-? B : has'); const rels = erDb.getRelationships(); expect(Object.keys(erDb.getEntities()).length).toBe(2); @@ -172,7 +170,7 @@ describe('when parsing ER diagram it...', function() { }); it('should handle zero-or-one-to-zero-or-one relationships', function() { - erDiagram.parser.parse('erDiagram\nA ?-? B : has, has'); + erDiagram.parser.parse('erDiagram\nA ?-? B : has'); const rels = erDb.getRelationships(); expect(Object.keys(erDb.getEntities()).length).toBe(2); @@ -181,7 +179,7 @@ describe('when parsing ER diagram it...', function() { }); it('should handle zero-or-more-to-zero-or-more relationships', function() { - erDiagram.parser.parse('erDiagram\nA >?-?< B : has, has'); + erDiagram.parser.parse('erDiagram\nA >?-?< B : has'); const rels = erDb.getRelationships(); expect(Object.keys(erDb.getEntities()).length).toBe(2); @@ -190,7 +188,7 @@ describe('when parsing ER diagram it...', function() { }); it('should handle one-or-more-to-one-or-more relationships', function() { - erDiagram.parser.parse('erDiagram\nA >!-!< B : has, has'); + erDiagram.parser.parse('erDiagram\nA >!-!< B : has'); const rels = erDb.getRelationships(); expect(Object.keys(erDb.getEntities()).length).toBe(2); @@ -199,7 +197,7 @@ describe('when parsing ER diagram it...', function() { }); it('should handle zero-or-more-to-one-or-more relationships', function() { - erDiagram.parser.parse('erDiagram\nA >?-!< B : has, has'); + erDiagram.parser.parse('erDiagram\nA >?-!< B : has'); const rels = erDb.getRelationships(); expect(Object.keys(erDb.getEntities()).length).toBe(2); @@ -208,7 +206,7 @@ describe('when parsing ER diagram it...', function() { }); it('should handle one-or-more-to-zero-or-more relationships', function() { - erDiagram.parser.parse('erDiagram\nA >!-?< B : has, has'); + erDiagram.parser.parse('erDiagram\nA >!-?< B : has'); const rels = erDb.getRelationships(); expect(Object.keys(erDb.getEntities()).length).toBe(2); @@ -217,7 +215,7 @@ describe('when parsing ER diagram it...', function() { }); it('should not accept a syntax error', function() { - const doc = 'erDiagram\nA xxx B : has, has'; + const doc = 'erDiagram\nA xxx B : has'; expect(() => { erDiagram.parser.parse(doc); }).toThrowError(); From 3e76b2374c09e172eef1a00b2a0892b98a526915 Mon Sep 17 00:00:00 2001 From: Adrian Hall Date: Wed, 18 Mar 2020 09:31:10 +0000 Subject: [PATCH 13/14] Initial documentation for ER diagrams --- docs/entityRelationshipDiagram.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 docs/entityRelationshipDiagram.md diff --git a/docs/entityRelationshipDiagram.md b/docs/entityRelationshipDiagram.md new file mode 100644 index 000000000..cce0e5702 --- /dev/null +++ b/docs/entityRelationshipDiagram.md @@ -0,0 +1,21 @@ +# Entity Relationship Diagrams + +> An entity–relationship model (or ER model) describes interrelated things of interest in a specific domain of knowledge. A basic ER model is composed of entity types (which classify the things of interest) and specifies relationships that can exist between entities (instances of those entity types). Wikipedia. + +Mermaid can render ER diagrams +``` +erDiagram + CUSTOMER !-?< ORDER : places + ORDER !-!< LINE-ITEM : contains +``` +```mermaid +erDiagram + CUSTOMER !-?< ORDER : places + ORDER !-!< LINE-ITEM : contains +``` + +## Syntax + +### Entities and Relationships + +To be completed From d3f78299e754dfeec99b732819ccc727455bfd0e Mon Sep 17 00:00:00 2001 From: Adrian Hall Date: Wed, 18 Mar 2020 16:53:26 +0000 Subject: [PATCH 14/14] Add integration tests, basic docs, and fix multi-diagram bug --- .../integration/rendering/erDiagram.spec.js | 92 +++++++++++++++++++ docs/entityRelationshipDiagram.md | 10 ++ src/diagrams/er/erRenderer.js | 19 ++-- 3 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 cypress/integration/rendering/erDiagram.spec.js diff --git a/cypress/integration/rendering/erDiagram.spec.js b/cypress/integration/rendering/erDiagram.spec.js new file mode 100644 index 000000000..1f625e2e9 --- /dev/null +++ b/cypress/integration/rendering/erDiagram.spec.js @@ -0,0 +1,92 @@ +/* eslint-env jest */ +import { imgSnapshotTest } from '../../helpers/util'; + +describe('Entity Relationship Diagram', () => { + it('should render a simple ER diagram', () => { + imgSnapshotTest( + ` + erDiagram + CUSTOMER !-?< ORDER : places + ORDER !-!< LINE-ITEM : contains + `, + {logLevel : 1} + ); + cy.get('svg'); + }); + + it('should render an ER diagram with a recursive relationship', () => { + imgSnapshotTest( + ` + erDiagram + CUSTOMER !-?< CUSTOMER : refers + CUSTOMER !-?< ORDER : places + ORDER !-!< LINE-ITEM : contains + `, + {logLevel : 1} + ); + cy.get('svg'); + }); + + it('should render an ER diagram with multiple relationships between the same two entities', () => { + imgSnapshotTest( + ` + erDiagram + CUSTOMER !-!< ADDRESS : "invoiced at" + CUSTOMER !-!< ADDRESS : "receives goods at" + `, + {logLevel : 1} + ); + cy.get('svg'); + }); + + it('should render a cyclical ER diagram', () => { + imgSnapshotTest( + ` + erDiagram + A !-!< B : likes + B !-!< C : likes + C !-!< A : likes + `, + {logLevel : 1} + ); + cy.get('svg'); + + }); + + it('should render a not-so-simple ER diagram', () => { + imgSnapshotTest( + ` + erDiagram + DELIVERY-ADDRESS !-?< ORDER : receives + CUSTOMER >!-!< DELIVERY-ADDRESS : has + CUSTOMER !-?< ORDER : places + CUSTOMER !-?< INVOICE : "liable for" + INVOICE !-!< ORDER : covers + ORDER !-!< ORDER-ITEM : includes + PRODUCT-CATEGORY !-!< PRODUCT : contains + PRODUCT !-?< ORDER-ITEM : "ordered in" + `, + {logLevel : 1} + ); + cy.get('svg'); + }); + + it('should render multiple ER diagrams', () => { + imgSnapshotTest( + [ + ` + erDiagram + CUSTOMER !-?< ORDER : places + ORDER !-!< LINE-ITEM : contains + `, + ` + erDiagram + CUSTOMER !-?< ORDER : places + ORDER !-!< LINE-ITEM : contains + ` + ], + {logLevel : 1} + ); + cy.get('svg'); + }); +}); diff --git a/docs/entityRelationshipDiagram.md b/docs/entityRelationshipDiagram.md index cce0e5702..6b5e46bb3 100644 --- a/docs/entityRelationshipDiagram.md +++ b/docs/entityRelationshipDiagram.md @@ -2,6 +2,8 @@ > An entity–relationship model (or ER model) describes interrelated things of interest in a specific domain of knowledge. A basic ER model is composed of entity types (which classify the things of interest) and specifies relationships that can exist between entities (instances of those entity types). Wikipedia. +Note that practitioners of ER modelling almost always refer to entity types simply as entities. For example the CUSTOMER entity type would be referred to simply as the CUSTOMER entity. This is so common it would be inadvisable to do anything else, but technically an entity is an abstract instance of an entity type, and this is what an ER diagram shows - abstract instances, and the relationships between them. This is why entities are always named using singular nouns. + Mermaid can render ER diagrams ``` erDiagram @@ -14,6 +16,14 @@ erDiagram ORDER !-!< LINE-ITEM : contains ``` +Entity names are often capitalised, although there is no accepted standard on this, and it is not required in Mermaid. + +Relationships between entities are represented by lines with end markers representing cardinality. Mermaid uses the most popular crow's foot notation. The crow's foot intuitively conveys the possibility of many instances of the entity that it connects to. + +## Status + +ER diagrams are a new feature in Mermaid and are **experimental**. There are likely to be a few bugs and constraints, and enhancements will be made in due course. + ## Syntax ### Entities and Relationships diff --git a/src/diagrams/er/erRenderer.js b/src/diagrams/er/erRenderer.js index 1ae5a8d6a..3f37fd296 100644 --- a/src/diagrams/er/erRenderer.js +++ b/src/diagrams/er/erRenderer.js @@ -86,14 +86,16 @@ const drawEntities = function(svgNode, entities, graph) { const adjustEntities = function(svgNode, graph) { graph.nodes().forEach(function(v) { if (typeof v !== 'undefined' && typeof graph.node(v) !== 'undefined') { - d3.select('#' + v).attr( - 'transform', - 'translate(' + - (graph.node(v).x - graph.node(v).width / 2) + - ',' + - (graph.node(v).y - graph.node(v).height / 2) + - ' )' - ); + svgNode + .select('#' + v) + .attr( + 'transform', + 'translate(' + + (graph.node(v).x - graph.node(v).width / 2) + + ',' + + (graph.node(v).y - graph.node(v).height / 2) + + ' )' + ); } }); return; @@ -356,6 +358,7 @@ export const draw = function(text, id) { marginx: 20, marginy: 20, nodesep: 100, + edgesep: 100, ranksep: 100 }) .setDefaultEdgeLabel(function() {