Initial update to new renderer

This commit is contained in:
yari-dewalt
2024-10-08 08:12:42 -07:00
parent b3dee343d1
commit 10562e72f4
8 changed files with 364 additions and 107 deletions

View File

@@ -793,6 +793,8 @@ export interface ErDiagramConfig extends BaseDiagramConfig {
* *
*/ */
entityPadding?: number; entityPadding?: number;
nodeSpacing?: number;
rankSpacing?: number;
/** /**
* Stroke color of box edges and lines. * Stroke color of box edges and lines.
*/ */

View File

@@ -1,103 +0,0 @@
import { log } from '../../logger.js';
import { getConfig } from '../../diagram-api/diagramAPI.js';
import {
setAccTitle,
getAccTitle,
getAccDescription,
setAccDescription,
clear as commonClear,
setDiagramTitle,
getDiagramTitle,
} from '../common/commonDb.js';
let entities = new Map();
let relationships = [];
const Cardinality = {
ZERO_OR_ONE: 'ZERO_OR_ONE',
ZERO_OR_MORE: 'ZERO_OR_MORE',
ONE_OR_MORE: 'ONE_OR_MORE',
ONLY_ONE: 'ONLY_ONE',
MD_PARENT: 'MD_PARENT',
};
const Identification = {
NON_IDENTIFYING: 'NON_IDENTIFYING',
IDENTIFYING: 'IDENTIFYING',
};
/**
* Add entity
* @param {string} name - The name of the entity
* @param {string | undefined} alias - The alias of the entity
*/
const addEntity = function (name, alias = undefined) {
if (!entities.has(name)) {
entities.set(name, { attributes: [], alias });
log.info('Added new entity :', name);
} else if (!entities.get(name).alias && alias) {
entities.get(name).alias = alias;
log.info(`Add alias '${alias}' to entity '${name}'`);
}
return entities.get(name);
};
const getEntities = () => entities;
const addAttributes = function (entityName, attribs) {
let entity = addEntity(entityName); // May do nothing (if entity has already been added)
// Process attribs in reverse order due to effect of recursive construction (last attribute is first)
let i;
for (i = attribs.length - 1; i >= 0; i--) {
entity.attributes.push(attribs[i]);
log.debug('Added attribute ', attribs[i].attributeName);
}
};
/**
* 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 rSpec The details of the relationship between the two entities
*/
const addRelationship = function (entA, rolA, entB, rSpec) {
let rel = {
entityA: entA,
roleA: rolA,
entityB: entB,
relSpec: rSpec,
};
relationships.push(rel);
log.debug('Added new relationship :', rel);
};
const getRelationships = () => relationships;
const clear = function () {
entities = new Map();
relationships = [];
commonClear();
};
export default {
Cardinality,
Identification,
getConfig: () => getConfig().er,
addEntity,
addAttributes,
getEntities,
addRelationship,
getRelationships,
clear,
setAccTitle,
getAccTitle,
setAccDescription,
getAccDescription,
setDiagramTitle,
getDiagramTitle,
};

View File

@@ -0,0 +1,240 @@
import { log } from '../../logger.js';
import { getConfig } from '../../diagram-api/diagramAPI.js';
import type { Edge, Node } from '../../rendering-util/types.js';
import type { EntityNode, Attribute, Relationship, EntityClass, RelSpec } from './erTypes.js';
import {
setAccTitle,
getAccTitle,
getAccDescription,
setAccDescription,
clear as commonClear,
setDiagramTitle,
getDiagramTitle,
} from '../common/commonDb.js';
import { getEdgeId } from '../../utils.js';
let entities = new Map<string, EntityNode>();
let relationships: Relationship[] = [];
let classes = new Map<string, EntityClass>();
let direction = 'TB';
const Cardinality = {
ZERO_OR_ONE: 'ZERO_OR_ONE',
ZERO_OR_MORE: 'ZERO_OR_MORE',
ONE_OR_MORE: 'ONE_OR_MORE',
ONLY_ONE: 'ONLY_ONE',
MD_PARENT: 'MD_PARENT',
};
const Identification = {
NON_IDENTIFYING: 'NON_IDENTIFYING',
IDENTIFYING: 'IDENTIFYING',
};
/**
* Add entity
* @param name - The name of the entity
* @param alias - The alias of the entity
*/
const addEntity = function (name: string, alias = ''): EntityNode {
if (!entities.has(name)) {
entities.set(name, {
id: `entity-${name}-${entities.size}`,
label: name,
attributes: [],
alias,
shape: 'erBox',
look: getConfig().look || 'default',
cssClasses: [],
cssStyles: [],
});
log.info('Added new entity :', name);
} else if (!entities.get(name)?.alias && alias) {
entities.get(name)!.alias = alias;
log.info(`Add alias '${alias}' to entity '${name}'`);
}
return entities.get(name)!;
};
const getEntity = function (name: string) {
return entities.get(name);
};
const getEntities = () => entities;
const getClasses = () => classes;
const addAttributes = function (entityName: string, attribs: Attribute[]) {
const entity = addEntity(entityName); // May do nothing (if entity has already been added)
// Process attribs in reverse order due to effect of recursive construction (last attribute is first)
let i;
for (i = attribs.length - 1; i >= 0; i--) {
if (!attribs[i].keys) {
attribs[i].keys = [];
}
if (!attribs[i].comment) {
attribs[i].comment = '';
}
entity.attributes.push(attribs[i]);
log.debug('Added attribute ', attribs[i].name);
}
};
/**
* 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 rSpec - The details of the relationship between the two entities
*/
const addRelationship = function (entA: string, rolA: string, entB: string, rSpec: RelSpec) {
const entityA = entities.get(entA);
const entityB = entities.get(entB);
if (!entityA || !entityB) {
return;
}
const rel = {
entityA: entityA.id,
roleA: rolA,
entityB: entityB.id,
relSpec: rSpec,
};
relationships.push(rel);
log.debug('Added new relationship :', rel);
};
const getRelationships = () => relationships;
const getDirection = () => direction;
const setDirection = (dir: string) => {
direction = dir;
};
const clear = function () {
entities = new Map();
classes = new Map();
relationships = [];
commonClear();
};
export const getData = function () {
const nodes: Node[] = [];
const edges: Edge[] = [];
const config = getConfig();
for (const entityKey of entities.keys()) {
const entityNode = entities.get(entityKey);
if (entityNode) {
entityNode.cssCompiledStyles = getCompiledStyles(entityNode.cssClasses!);
nodes.push(entityNode as unknown as Node);
}
}
let cnt = 0;
for (const relationship of relationships) {
const edge: Edge = {
id: getEdgeId(relationship.entityA, relationship.entityB, { prefix: 'id', counter: cnt++ }),
type: 'normal',
start: relationship.entityA,
end: relationship.entityB,
label: relationship.roleA,
labelpos: 'c',
thickness: 'normal',
classes: 'relationshipLine',
arrowTypeStart: relationship.relSpec.cardB.toLowerCase(),
arrowTypeEnd: relationship.relSpec.cardA.toLowerCase(),
pattern: relationship.relSpec.relType == 'IDENTIFYING' ? 'solid' : 'dashed',
look: config.look,
};
edges.push(edge);
}
return { nodes, edges, other: {}, config, direction: 'TB' };
};
export const addCssStyles = function (ids: string[], styles: string[]) {
for (const id of ids) {
const entity = entities.get(id);
if (!styles || !entity) {
return;
}
for (const style of styles) {
entity.cssStyles!.push(style);
}
}
};
export const addClass = function (ids: string[], style: string[]) {
ids.forEach(function (id) {
let classNode = classes.get(id);
if (classNode === undefined) {
classNode = { id, styles: [], textStyles: [] };
classes.set(id, classNode);
}
if (style) {
style.forEach(function (s) {
if (/color/.exec(s)) {
const newStyle = s.replace('fill', 'bgFill'); // .replace('color', 'fill');
classNode.textStyles.push(newStyle);
}
classNode.styles.push(s);
});
}
});
};
export const setClass = function (ids: string[], classNames: string[]) {
for (const id of ids) {
const entity = entities.get(id);
if (entity) {
for (const className of classNames) {
entity.cssClasses!.push(className);
}
}
}
};
function getCompiledStyles(classDefs: string[]) {
let compiledStyles: string[] = [];
for (const customClass of classDefs) {
const cssClass = classes.get(customClass);
if (cssClass?.styles) {
compiledStyles = [...compiledStyles, ...(cssClass.styles ?? [])].map((s) => s.trim());
}
if (cssClass?.textStyles) {
compiledStyles = [...compiledStyles, ...(cssClass.textStyles ?? [])].map((s) => s.trim());
}
}
return compiledStyles;
}
export default {
Cardinality,
Identification,
getConfig: () => getConfig().er,
addEntity,
addAttributes,
getEntities,
getEntity,
getClasses,
addRelationship,
getRelationships,
clear,
getDirection,
setDirection,
setAccTitle,
getAccTitle,
setAccDescription,
getAccDescription,
setDiagramTitle,
getDiagramTitle,
getData,
addCssStyles,
addClass,
setClass,
};

View File

@@ -1,7 +1,7 @@
// @ts-ignore: TODO: Fix ts errors // @ts-ignore: TODO: Fix ts errors
import erParser from './parser/erDiagram.jison'; import erParser from './parser/erDiagram.jison';
import erDb from './erDb.js'; import erDb from './erDb.js';
import erRenderer from './erRenderer.js'; import erRenderer from './erRenderer-unified.js';
import erStyles from './styles.js'; import erStyles from './styles.js';
export const diagram = { export const diagram = {

View File

@@ -0,0 +1,46 @@
import { getConfig } from '../../diagram-api/diagramAPI.js';
import { log } from '../../logger.js';
import { getDiagramElement } from '../../rendering-util/insertElementsForSize.js';
import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js';
import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js';
import type { LayoutData } from '../../rendering-util/types.js';
import db from './erDb.js';
import utils from '../../utils.js';
export const draw = async function (text: string, id: string, _version: string, diag: any) {
log.info('REF0:');
log.info('Drawing er diagram (unified)', id);
const { securityLevel, er: conf, layout } = getConfig();
// The getData method provided in all supported diagrams is used to extract the data from the parsed structure
// into the Layout data format
const data4Layout = diag.db.getData() as LayoutData;
// Create the root SVG - the element is the div containing the SVG element
const svg = getDiagramElement(id, securityLevel);
data4Layout.type = diag.type;
data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layout);
// Workaround as when rendering and setting up the graph it uses flowchart spacing before data4Layout spacing?
data4Layout.config.flowchart!.nodeSpacing = conf?.nodeSpacing || 140;
data4Layout.config.flowchart!.rankSpacing = conf?.rankSpacing || 80;
data4Layout.direction = db.getDirection();
data4Layout.markers = ['only_one', 'zero_or_one', 'one_or_more', 'zero_or_more'];
data4Layout.diagramId = id;
await render(data4Layout, svg);
const padding = 8;
utils.insertTitle(
svg,
'erDiagramTitleText',
conf?.titleTopMargin ?? 25,
diag.db.getDiagramTitle()
);
setupViewPortForSVG(svg, padding, 'erDiagram', conf?.useMaxWidth ?? true);
};
export default {
draw,
};

View File

@@ -0,0 +1,37 @@
export interface EntityNode {
id: string;
label: string;
attributes: Attribute[];
alias: string;
shape: string;
look?: string;
cssClasses?: string[];
cssStyles?: string[];
cssCompiledStyles?: string[];
}
export interface Attribute {
type: string;
name: string;
keys: ('PK' | 'FK' | 'UK')[];
comment: string;
}
export interface Relationship {
entityA: string;
roleA: string;
entityB: string;
relSpec: RelSpec;
}
export interface RelSpec {
cardA: string;
cardB: string;
relType: string;
}
export interface EntityClass {
id: string;
styles: string[];
textStyles: string[];
}

View File

@@ -24,9 +24,36 @@ const getStyles = (options) =>
} }
} }
.relationshipLine { .edgeLabel .label {
stroke: ${options.lineColor}; fill: ${options.nodeBorder};
} font-size: 14px;
}
.edgeLabel .label .labelBkg {
background: ${options.mainBkg};
}
.node rect,
.node circle,
.node ellipse,
.node polygon,
.node path {
fill: ${options.mainBkg};
stroke: ${options.nodeBorder};
stroke-width: 1px;
}
.relationshipLine {
stroke: ${options.lineColor};
stroke-width: 1;
fill: none;
}
.marker {
fill: none !important;
stroke: ${options.lineColor} !important;
stroke-width: 1;
}
.entityTitleText { .entityTitleText {
text-anchor: middle; text-anchor: middle;

View File

@@ -1280,6 +1280,14 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
type: integer type: integer
default: 15 default: 15
minimum: 0 minimum: 0
nodeSpacing:
type: integer
default: 140
minimum: 0
rankSpacing:
type: integer
default: 80
minimum: 0
stroke: stroke:
description: Stroke color of box edges and lines. description: Stroke color of box edges and lines.
type: string type: string