• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mermaid-js / mermaid / 5071601472

pending completion
5071601472

push

github

Knut Sveidqvist
Merge branch 'release/10.2.0'

1633 of 2064 branches covered (79.12%)

Branch coverage included in aggregate %.

2701 of 2701 new or added lines in 128 files covered. (100.0%)

19402 of 34929 relevant lines covered (55.55%)

418.23 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

21.07
/packages/mermaid/src/diagrams/er/erRenderer.js
1
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
1✔
2
import { line, curveBasis, select } from 'd3';
1✔
3
import { layout as dagreLayout } from 'dagre-d3-es/src/dagre/index.js';
1✔
4
import { getConfig } from '../../config.js';
1✔
5
import { log } from '../../logger.js';
1✔
6
import utils from '../../utils.js';
1✔
7
import erMarkers from './erMarkers.js';
1✔
8
import { configureSvgSize } from '../../setupGraphViewbox.js';
1✔
9
import { parseGenericTypes } from '../common/common.js';
1✔
10
import { v5 as uuid5 } from 'uuid';
1✔
11

1✔
12
/** Regex used to remove chars from the entity name so the result can be used in an id */
1✔
13
const BAD_ID_CHARS_REGEXP = /[^\dA-Za-z](\W)*/g;
1✔
14

1✔
15
// Configuration
1✔
16
let conf = {};
1✔
17

1✔
18
// Map so we can look up the id of an entity based on the name
1✔
19
let entityNameIds = new Map();
1✔
20

1✔
21
/**
1✔
22
 * Allows the top-level API module to inject config specific to this renderer, storing it in the
1✔
23
 * local conf object. Note that generic config still needs to be retrieved using getConfig()
1✔
24
 * imported from the config module
1✔
25
 *
1✔
26
 * @param cnf
1✔
27
 */
1✔
28
export const setConf = function (cnf) {
1✔
29
  const keys = Object.keys(cnf);
×
30
  for (const key of keys) {
×
31
    conf[key] = cnf[key];
×
32
  }
×
33
};
×
34

1✔
35
/**
1✔
36
 * Draw attributes for an entity
1✔
37
 *
1✔
38
 * @param groupNode The svg group node for the entity
1✔
39
 * @param entityTextNode The svg node for the entity label text
1✔
40
 * @param attributes An array of attributes defined for the entity (each attribute has a type and a
1✔
41
 *   name)
1✔
42
 * @returns {object} The bounding box of the entity, after attributes have been added. The bounding
1✔
43
 *   box has a .width and .height
1✔
44
 */
1✔
45
const drawAttributes = (groupNode, entityTextNode, attributes) => {
1✔
46
  const heightPadding = conf.entityPadding / 3; // Padding internal to attribute boxes
×
47
  const widthPadding = conf.entityPadding / 3; // Ditto
×
48
  const attrFontSize = conf.fontSize * 0.85;
×
49
  const labelBBox = entityTextNode.node().getBBox();
×
50
  const attributeNodes = []; // Intermediate storage for attribute nodes created so that we can do a second pass
×
51
  let hasKeyType = false;
×
52
  let hasComment = false;
×
53
  let maxTypeWidth = 0;
×
54
  let maxNameWidth = 0;
×
55
  let maxKeyWidth = 0;
×
56
  let maxCommentWidth = 0;
×
57
  let cumulativeHeight = labelBBox.height + heightPadding * 2;
×
58
  let attrNum = 1;
×
59

×
60
  // Check to see if any of the attributes has a key or a comment
×
61
  attributes.forEach((item) => {
×
62
    if (item.attributeKeyTypeList !== undefined && item.attributeKeyTypeList.length > 0) {
×
63
      hasKeyType = true;
×
64
    }
×
65

×
66
    if (item.attributeComment !== undefined) {
×
67
      hasComment = true;
×
68
    }
×
69
  });
×
70

×
71
  attributes.forEach((item) => {
×
72
    const attrPrefix = `${entityTextNode.node().id}-attr-${attrNum}`;
×
73
    let nodeHeight = 0;
×
74

×
75
    const attributeType = parseGenericTypes(item.attributeType);
×
76

×
77
    // Add a text node for the attribute type
×
78
    const typeNode = groupNode
×
79
      .append('text')
×
80
      .classed('er entityLabel', true)
×
81
      .attr('id', `${attrPrefix}-type`)
×
82
      .attr('x', 0)
×
83
      .attr('y', 0)
×
84
      .style('dominant-baseline', 'middle')
×
85
      .style('text-anchor', 'left')
×
86
      .style('font-family', getConfig().fontFamily)
×
87
      .style('font-size', attrFontSize + 'px')
×
88
      .text(attributeType);
×
89

×
90
    // Add a text node for the attribute name
×
91
    const nameNode = groupNode
×
92
      .append('text')
×
93
      .classed('er entityLabel', true)
×
94
      .attr('id', `${attrPrefix}-name`)
×
95
      .attr('x', 0)
×
96
      .attr('y', 0)
×
97
      .style('dominant-baseline', 'middle')
×
98
      .style('text-anchor', 'left')
×
99
      .style('font-family', getConfig().fontFamily)
×
100
      .style('font-size', attrFontSize + 'px')
×
101
      .text(item.attributeName);
×
102

×
103
    const attributeNode = {};
×
104
    attributeNode.tn = typeNode;
×
105
    attributeNode.nn = nameNode;
×
106

×
107
    const typeBBox = typeNode.node().getBBox();
×
108
    const nameBBox = nameNode.node().getBBox();
×
109
    maxTypeWidth = Math.max(maxTypeWidth, typeBBox.width);
×
110
    maxNameWidth = Math.max(maxNameWidth, nameBBox.width);
×
111

×
112
    nodeHeight = Math.max(typeBBox.height, nameBBox.height);
×
113

×
114
    if (hasKeyType) {
×
115
      const keyTypeNodeText =
×
116
        item.attributeKeyTypeList !== undefined ? item.attributeKeyTypeList.join(',') : '';
×
117

×
118
      const keyTypeNode = groupNode
×
119
        .append('text')
×
120
        .classed('er entityLabel', true)
×
121
        .attr('id', `${attrPrefix}-key`)
×
122
        .attr('x', 0)
×
123
        .attr('y', 0)
×
124
        .style('dominant-baseline', 'middle')
×
125
        .style('text-anchor', 'left')
×
126
        .style('font-family', getConfig().fontFamily)
×
127
        .style('font-size', attrFontSize + 'px')
×
128
        .text(keyTypeNodeText);
×
129

×
130
      attributeNode.kn = keyTypeNode;
×
131
      const keyTypeBBox = keyTypeNode.node().getBBox();
×
132
      maxKeyWidth = Math.max(maxKeyWidth, keyTypeBBox.width);
×
133
      nodeHeight = Math.max(nodeHeight, keyTypeBBox.height);
×
134
    }
×
135

×
136
    if (hasComment) {
×
137
      const commentNode = groupNode
×
138
        .append('text')
×
139
        .classed('er entityLabel', true)
×
140
        .attr('id', `${attrPrefix}-comment`)
×
141
        .attr('x', 0)
×
142
        .attr('y', 0)
×
143
        .style('dominant-baseline', 'middle')
×
144
        .style('text-anchor', 'left')
×
145
        .style('font-family', getConfig().fontFamily)
×
146
        .style('font-size', attrFontSize + 'px')
×
147
        .text(item.attributeComment || '');
×
148

×
149
      attributeNode.cn = commentNode;
×
150
      const commentNodeBBox = commentNode.node().getBBox();
×
151
      maxCommentWidth = Math.max(maxCommentWidth, commentNodeBBox.width);
×
152
      nodeHeight = Math.max(nodeHeight, commentNodeBBox.height);
×
153
    }
×
154

×
155
    attributeNode.height = nodeHeight;
×
156
    // Keep a reference to the nodes so that we can iterate through them later
×
157
    attributeNodes.push(attributeNode);
×
158
    cumulativeHeight += nodeHeight + heightPadding * 2;
×
159
    attrNum += 1;
×
160
  });
×
161

×
162
  let widthPaddingFactor = 4;
×
163
  if (hasKeyType) {
×
164
    widthPaddingFactor += 2;
×
165
  }
×
166
  if (hasComment) {
×
167
    widthPaddingFactor += 2;
×
168
  }
×
169

×
170
  const maxWidth = maxTypeWidth + maxNameWidth + maxKeyWidth + maxCommentWidth;
×
171

×
172
  // Calculate the new bounding box of the overall entity, now that attributes have been added
×
173
  const bBox = {
×
174
    width: Math.max(
×
175
      conf.minEntityWidth,
×
176
      Math.max(
×
177
        labelBBox.width + conf.entityPadding * 2,
×
178
        maxWidth + widthPadding * widthPaddingFactor
×
179
      )
×
180
    ),
×
181
    height:
×
182
      attributes.length > 0
×
183
        ? cumulativeHeight
×
184
        : Math.max(conf.minEntityHeight, labelBBox.height + conf.entityPadding * 2),
×
185
  };
×
186

×
187
  if (attributes.length > 0) {
×
188
    // There might be some spare width for padding out attributes if the entity name is very long
×
189
    const spareColumnWidth = Math.max(
×
190
      0,
×
191
      (bBox.width - maxWidth - widthPadding * widthPaddingFactor) / (widthPaddingFactor / 2)
×
192
    );
×
193

×
194
    // Position the entity label near the top of the entity bounding box
×
195
    entityTextNode.attr(
×
196
      'transform',
×
197
      'translate(' + bBox.width / 2 + ',' + (heightPadding + labelBBox.height / 2) + ')'
×
198
    );
×
199

×
200
    // Add rectangular boxes for the attribute types/names
×
201
    let heightOffset = labelBBox.height + heightPadding * 2; // Start at the bottom of the entity label
×
202
    let attribStyle = 'attributeBoxOdd'; // We will flip the style on alternate rows to achieve a banded effect
×
203

×
204
    attributeNodes.forEach((attributeNode) => {
×
205
      // Calculate the alignment y co-ordinate for the type/name of the attribute
×
206
      const alignY = heightOffset + heightPadding + attributeNode.height / 2;
×
207

×
208
      // Position the type attribute
×
209
      attributeNode.tn.attr('transform', 'translate(' + widthPadding + ',' + alignY + ')');
×
210

×
211
      // TODO Handle spareWidth in attr('width')
×
212
      // Insert a rectangle for the type
×
213
      const typeRect = groupNode
×
214
        .insert('rect', '#' + attributeNode.tn.node().id)
×
215
        .classed(`er ${attribStyle}`, true)
×
216
        .attr('x', 0)
×
217
        .attr('y', heightOffset)
×
218
        .attr('width', maxTypeWidth + widthPadding * 2 + spareColumnWidth)
×
219
        .attr('height', attributeNode.height + heightPadding * 2);
×
220

×
221
      const nameXOffset = parseFloat(typeRect.attr('x')) + parseFloat(typeRect.attr('width'));
×
222

×
223
      // Position the name attribute
×
224
      attributeNode.nn.attr(
×
225
        'transform',
×
226
        'translate(' + (nameXOffset + widthPadding) + ',' + alignY + ')'
×
227
      );
×
228

×
229
      // Insert a rectangle for the name
×
230
      const nameRect = groupNode
×
231
        .insert('rect', '#' + attributeNode.nn.node().id)
×
232
        .classed(`er ${attribStyle}`, true)
×
233
        .attr('x', nameXOffset)
×
234
        .attr('y', heightOffset)
×
235
        .attr('width', maxNameWidth + widthPadding * 2 + spareColumnWidth)
×
236
        .attr('height', attributeNode.height + heightPadding * 2);
×
237

×
238
      let keyTypeAndCommentXOffset =
×
239
        parseFloat(nameRect.attr('x')) + parseFloat(nameRect.attr('width'));
×
240

×
241
      if (hasKeyType) {
×
242
        // Position the key type attribute
×
243
        attributeNode.kn.attr(
×
244
          'transform',
×
245
          'translate(' + (keyTypeAndCommentXOffset + widthPadding) + ',' + alignY + ')'
×
246
        );
×
247

×
248
        // Insert a rectangle for the key type
×
249
        const keyTypeRect = groupNode
×
250
          .insert('rect', '#' + attributeNode.kn.node().id)
×
251
          .classed(`er ${attribStyle}`, true)
×
252
          .attr('x', keyTypeAndCommentXOffset)
×
253
          .attr('y', heightOffset)
×
254
          .attr('width', maxKeyWidth + widthPadding * 2 + spareColumnWidth)
×
255
          .attr('height', attributeNode.height + heightPadding * 2);
×
256

×
257
        keyTypeAndCommentXOffset =
×
258
          parseFloat(keyTypeRect.attr('x')) + parseFloat(keyTypeRect.attr('width'));
×
259
      }
×
260

×
261
      if (hasComment) {
×
262
        // Position the comment attribute
×
263
        attributeNode.cn.attr(
×
264
          'transform',
×
265
          'translate(' + (keyTypeAndCommentXOffset + widthPadding) + ',' + alignY + ')'
×
266
        );
×
267

×
268
        // Insert a rectangle for the comment
×
269
        groupNode
×
270
          .insert('rect', '#' + attributeNode.cn.node().id)
×
271
          .classed(`er ${attribStyle}`, 'true')
×
272
          .attr('x', keyTypeAndCommentXOffset)
×
273
          .attr('y', heightOffset)
×
274
          .attr('width', maxCommentWidth + widthPadding * 2 + spareColumnWidth)
×
275
          .attr('height', attributeNode.height + heightPadding * 2);
×
276
      }
×
277

×
278
      // Increment the height offset to move to the next row
×
279
      heightOffset += attributeNode.height + heightPadding * 2;
×
280

×
281
      // Flip the attribute style for row banding
×
282
      attribStyle = attribStyle === 'attributeBoxOdd' ? 'attributeBoxEven' : 'attributeBoxOdd';
×
283
    });
×
284
  } else {
×
285
    // Ensure the entity box is a decent size without any attributes
×
286
    bBox.height = Math.max(conf.minEntityHeight, cumulativeHeight);
×
287

×
288
    // Position the entity label in the middle of the box
×
289
    entityTextNode.attr('transform', 'translate(' + bBox.width / 2 + ',' + bBox.height / 2 + ')');
×
290
  }
×
291

×
292
  return bBox;
×
293
};
×
294

1✔
295
/**
1✔
296
 * Use D3 to construct the svg elements for the entities
1✔
297
 *
1✔
298
 * @param svgNode The svg node that contains the diagram
1✔
299
 * @param entities The entities to be drawn
1✔
300
 * @param graph The graph that contains the vertex and edge definitions post-layout
1✔
301
 * @returns {object} The first entity that was inserted
1✔
302
 */
1✔
303
const drawEntities = function (svgNode, entities, graph) {
1✔
304
  const keys = Object.keys(entities);
×
305
  let firstOne;
×
306

×
307
  keys.forEach(function (entityName) {
×
308
    const entityId = generateId(entityName, 'entity');
×
309
    entityNameIds.set(entityName, entityId);
×
310

×
311
    // Create a group for each entity
×
312
    const groupNode = svgNode.append('g').attr('id', entityId);
×
313

×
314
    firstOne = firstOne === undefined ? entityId : firstOne;
×
315

×
316
    // Label the entity - this is done first so that we can get the bounding box
×
317
    // which then determines the size of the rectangle
×
318
    const textId = 'text-' + entityId;
×
319
    const textNode = groupNode
×
320
      .append('text')
×
321
      .classed('er entityLabel', true)
×
322
      .attr('id', textId)
×
323
      .attr('x', 0)
×
324
      .attr('y', 0)
×
325
      .style('dominant-baseline', 'middle')
×
326
      .style('text-anchor', 'middle')
×
327
      .style('font-family', getConfig().fontFamily)
×
328
      .style('font-size', conf.fontSize + 'px')
×
329
      .text(entityName);
×
330

×
331
    const { width: entityWidth, height: entityHeight } = drawAttributes(
×
332
      groupNode,
×
333
      textNode,
×
334
      entities[entityName].attributes
×
335
    );
×
336

×
337
    // Draw the rectangle - insert it before the text so that the text is not obscured
×
338
    const rectNode = groupNode
×
339
      .insert('rect', '#' + textId)
×
340
      .classed('er entityBox', true)
×
341
      .attr('x', 0)
×
342
      .attr('y', 0)
×
343
      .attr('width', entityWidth)
×
344
      .attr('height', entityHeight);
×
345

×
346
    const rectBBox = rectNode.node().getBBox();
×
347

×
348
    // Add the entity to the graph using the entityId
×
349
    graph.setNode(entityId, {
×
350
      width: rectBBox.width,
×
351
      height: rectBBox.height,
×
352
      shape: 'rect',
×
353
      id: entityId,
×
354
    });
×
355
  });
×
356
  return firstOne;
×
357
}; // drawEntities
1✔
358

1✔
359
const adjustEntities = function (svgNode, graph) {
1✔
360
  graph.nodes().forEach(function (v) {
×
361
    if (v !== undefined && graph.node(v) !== undefined) {
×
362
      svgNode
×
363
        .select('#' + v)
×
364
        .attr(
×
365
          'transform',
×
366
          'translate(' +
×
367
            (graph.node(v).x - graph.node(v).width / 2) +
×
368
            ',' +
×
369
            (graph.node(v).y - graph.node(v).height / 2) +
×
370
            ' )'
×
371
        );
×
372
    }
×
373
  });
×
374
};
×
375

1✔
376
/**
1✔
377
 * Construct a name for an edge based on the names of the 2 entities and the role (relationship)
1✔
378
 * between them. Remove any spaces from it
1✔
379
 *
1✔
380
 * @param rel - A (parsed) relationship (e.g. one of the objects in the list returned by
1✔
381
 *   erDb.getRelationships)
1✔
382
 * @returns {string}
1✔
383
 */
1✔
384
const getEdgeName = function (rel) {
1✔
385
  return (rel.entityA + rel.roleA + rel.entityB).replace(/\s/g, '');
×
386
};
×
387

1✔
388
/**
1✔
389
 * Add each relationship to the graph
1✔
390
 *
1✔
391
 * @param relationships The relationships to be added
1✔
392
 * @param g The graph
1✔
393
 * @returns {Array} The array of relationships
1✔
394
 */
1✔
395
const addRelationships = function (relationships, g) {
1✔
396
  relationships.forEach(function (r) {
×
397
    g.setEdge(
×
398
      entityNameIds.get(r.entityA),
×
399
      entityNameIds.get(r.entityB),
×
400
      { relationship: r },
×
401
      getEdgeName(r)
×
402
    );
×
403
  });
×
404
  return relationships;
×
405
}; // addRelationships
1✔
406

1✔
407
let relCnt = 0;
1✔
408
/**
1✔
409
 * Draw a relationship using edge information from the graph
1✔
410
 *
1✔
411
 * @param svg The svg node
1✔
412
 * @param rel The relationship to draw in the svg
1✔
413
 * @param g The graph containing the edge information
1✔
414
 * @param insert The insertion point in the svg DOM (because relationships have markers that need to
1✔
415
 *   sit 'behind' opaque entity boxes)
1✔
416
 * @param diagObj
1✔
417
 */
1✔
418
const drawRelationshipFromLayout = function (svg, rel, g, insert, diagObj) {
1✔
419
  relCnt++;
×
420

×
421
  // Find the edge relating to this relationship
×
422
  const edge = g.edge(
×
423
    entityNameIds.get(rel.entityA),
×
424
    entityNameIds.get(rel.entityB),
×
425
    getEdgeName(rel)
×
426
  );
×
427

×
428
  // Get a function that will generate the line path
×
429
  const lineFunction = line()
×
430
    .x(function (d) {
×
431
      return d.x;
×
432
    })
×
433
    .y(function (d) {
×
434
      return d.y;
×
435
    })
×
436
    .curve(curveBasis);
×
437

×
438
  // Insert the line at the right place
×
439
  const svgPath = svg
×
440
    .insert('path', '#' + insert)
×
441
    .classed('er relationshipLine', true)
×
442
    .attr('d', lineFunction(edge.points))
×
443
    .style('stroke', conf.stroke)
×
444
    .style('fill', 'none');
×
445

×
446
  // ...and with dashes if necessary
×
447
  if (rel.relSpec.relType === diagObj.db.Identification.NON_IDENTIFYING) {
×
448
    svgPath.attr('stroke-dasharray', '8,8');
×
449
  }
×
450

×
451
  // TODO: Understand this better
×
452
  let url = '';
×
453
  if (conf.arrowMarkerAbsolute) {
×
454
    url =
×
455
      window.location.protocol +
×
456
      '//' +
×
457
      window.location.host +
×
458
      window.location.pathname +
×
459
      window.location.search;
×
460
    url = url.replace(/\(/g, '\\(');
×
461
    url = url.replace(/\)/g, '\\)');
×
462
  }
×
463

×
464
  // Decide which start and end markers it needs. It may be possible to be more concise here
×
465
  // by reversing a start marker to make an end marker...but this will do for now
×
466

×
467
  // Note that the 'A' entity's marker is at the end of the relationship and the 'B' entity's marker is at the start
×
468
  switch (rel.relSpec.cardA) {
×
469
    case diagObj.db.Cardinality.ZERO_OR_ONE:
×
470
      svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_END + ')');
×
471
      break;
×
472
    case diagObj.db.Cardinality.ZERO_OR_MORE:
×
473
      svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_END + ')');
×
474
      break;
×
475
    case diagObj.db.Cardinality.ONE_OR_MORE:
×
476
      svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_END + ')');
×
477
      break;
×
478
    case diagObj.db.Cardinality.ONLY_ONE:
×
479
      svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_END + ')');
×
480
      break;
×
481
    case diagObj.db.Cardinality.MD_PARENT:
×
482
      svgPath.attr('marker-end', 'url(' + url + '#' + erMarkers.ERMarkers.MD_PARENT_END + ')');
×
483
      break;
×
484
  }
×
485

×
486
  switch (rel.relSpec.cardB) {
×
487
    case diagObj.db.Cardinality.ZERO_OR_ONE:
×
488
      svgPath.attr(
×
489
        'marker-start',
×
490
        'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_ONE_START + ')'
×
491
      );
×
492
      break;
×
493
    case diagObj.db.Cardinality.ZERO_OR_MORE:
×
494
      svgPath.attr(
×
495
        'marker-start',
×
496
        'url(' + url + '#' + erMarkers.ERMarkers.ZERO_OR_MORE_START + ')'
×
497
      );
×
498
      break;
×
499
    case diagObj.db.Cardinality.ONE_OR_MORE:
×
500
      svgPath.attr(
×
501
        'marker-start',
×
502
        'url(' + url + '#' + erMarkers.ERMarkers.ONE_OR_MORE_START + ')'
×
503
      );
×
504
      break;
×
505
    case diagObj.db.Cardinality.ONLY_ONE:
×
506
      svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.ONLY_ONE_START + ')');
×
507
      break;
×
508
    case diagObj.db.Cardinality.MD_PARENT:
×
509
      svgPath.attr('marker-start', 'url(' + url + '#' + erMarkers.ERMarkers.MD_PARENT_START + ')');
×
510
      break;
×
511
  }
×
512

×
513
  // Now label the relationship
×
514

×
515
  // Find the half-way point
×
516
  const len = svgPath.node().getTotalLength();
×
517
  const labelPoint = svgPath.node().getPointAtLength(len * 0.5);
×
518

×
519
  // Append a text node containing the label
×
520
  const labelId = 'rel' + relCnt;
×
521

×
522
  const labelNode = svg
×
523
    .append('text')
×
524
    .classed('er relationshipLabel', true)
×
525
    .attr('id', labelId)
×
526
    .attr('x', labelPoint.x)
×
527
    .attr('y', labelPoint.y)
×
528
    .style('text-anchor', 'middle')
×
529
    .style('dominant-baseline', 'middle')
×
530
    .style('font-family', getConfig().fontFamily)
×
531
    .style('font-size', conf.fontSize + 'px')
×
532
    .text(rel.roleA);
×
533

×
534
  // Figure out how big the opaque 'container' rectangle needs to be
×
535
  const labelBBox = labelNode.node().getBBox();
×
536

×
537
  // Insert the opaque rectangle before the text label
×
538
  svg
×
539
    .insert('rect', '#' + labelId)
×
540
    .classed('er relationshipLabelBox', true)
×
541
    .attr('x', labelPoint.x - labelBBox.width / 2)
×
542
    .attr('y', labelPoint.y - labelBBox.height / 2)
×
543
    .attr('width', labelBBox.width)
×
544
    .attr('height', labelBBox.height);
×
545
};
×
546

1✔
547
/**
1✔
548
 * Draw en E-R diagram in the tag with id: id based on the text definition of the diagram
1✔
549
 *
1✔
550
 * @param text The text of the diagram
1✔
551
 * @param id The unique id of the DOM node that contains the diagram
1✔
552
 * @param _version
1✔
553
 * @param diagObj
1✔
554
 */
1✔
555
export const draw = function (text, id, _version, diagObj) {
1✔
556
  conf = getConfig().er;
×
557
  log.info('Drawing ER diagram');
×
558
  //  diag.db.clear();
×
559
  const securityLevel = getConfig().securityLevel;
×
560
  // Handle root and Document for when rendering in sandbox mode
×
561
  let sandboxElement;
×
562
  if (securityLevel === 'sandbox') {
×
563
    sandboxElement = select('#i' + id);
×
564
  }
×
565
  const root =
×
566
    securityLevel === 'sandbox'
×
567
      ? select(sandboxElement.nodes()[0].contentDocument.body)
×
568
      : select('body');
×
569
  // const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document;
×
570

×
571
  // Parse the text to populate erDb
×
572
  // try {
×
573
  //   parser.parse(text);
×
574
  // } catch (err) {
×
575
  //   log.debug('Parsing failed');
×
576
  // }
×
577

×
578
  // Get a reference to the svg node that contains the text
×
579
  const svg = root.select(`[id='${id}']`);
×
580

×
581
  // Add cardinality marker definitions to the svg
×
582
  erMarkers.insertMarkers(svg, conf);
×
583

×
584
  // Now we have to construct the diagram in a specific way:
×
585
  // ---
×
586
  // 1. Create all the entities in the svg node at 0,0, but with the correct dimensions (allowing for text content)
×
587
  // 2. Make sure they are all added to the graph
×
588
  // 3. Add all the edges (relationships) to the graph as well
×
589
  // 4. Let dagre do its magic to lay out the graph.  This assigns:
×
590
  //    - the centre co-ordinates for each node, bearing in mind the dimensions and edge relationships
×
591
  //    - the path co-ordinates for each edge
×
592
  //    But it has no impact on the svg child nodes - the diagram remains with every entity rooted at 0,0
×
593
  // 5. Now assign a transform to each entity in the svg node so that it gets drawn in the correct place, as determined by
×
594
  //    its centre point, which is obtained from the graph, and it's width and height
×
595
  // 6. And finally, create all the edges in the svg node using information from the graph
×
596
  // ---
×
597

×
598
  // Create the graph
×
599
  let g;
×
600

×
601
  // TODO: Explore directed vs undirected graphs, and how the layout is affected
×
602
  // An E-R diagram could be said to be undirected, but there is merit in setting
×
603
  // the direction from parent to child in a one-to-many as this influences graphlib to
×
604
  // put the parent above the child (does it?), which is intuitive.  Most relationships
×
605
  // in ER diagrams are one-to-many.
×
606
  g = new graphlib.Graph({
×
607
    multigraph: true,
×
608
    directed: true,
×
609
    compound: false,
×
610
  })
×
611
    .setGraph({
×
612
      rankdir: conf.layoutDirection,
×
613
      marginx: 20,
×
614
      marginy: 20,
×
615
      nodesep: 100,
×
616
      edgesep: 100,
×
617
      ranksep: 100,
×
618
    })
×
619
    .setDefaultEdgeLabel(function () {
×
620
      return {};
×
621
    });
×
622

×
623
  // Draw the entities (at 0,0), returning the first svg node that got
×
624
  // inserted - this represents the insertion point for relationship paths
×
625
  const firstEntity = drawEntities(svg, diagObj.db.getEntities(), g);
×
626

×
627
  // TODO: externalize the addition of entities to the graph - it's a bit 'buried' in the above
×
628

×
629
  // Add all the relationships to the graph
×
630
  const relationships = addRelationships(diagObj.db.getRelationships(), g);
×
631

×
632
  dagreLayout(g); // Node and edge positions will be updated
×
633

×
634
  // Adjust the positions of the entities so that they adhere to the layout
×
635
  adjustEntities(svg, g);
×
636

×
637
  // Draw the relationships
×
638
  relationships.forEach(function (rel) {
×
639
    drawRelationshipFromLayout(svg, rel, g, firstEntity, diagObj);
×
640
  });
×
641

×
642
  const padding = conf.diagramPadding;
×
643

×
644
  utils.insertTitle(svg, 'entityTitleText', conf.titleTopMargin, diagObj.db.getDiagramTitle());
×
645

×
646
  const svgBounds = svg.node().getBBox();
×
647
  const width = svgBounds.width + padding * 2;
×
648
  const height = svgBounds.height + padding * 2;
×
649

×
650
  configureSvgSize(svg, height, width, conf.useMaxWidth);
×
651

×
652
  svg.attr('viewBox', `${svgBounds.x - padding} ${svgBounds.y - padding} ${width} ${height}`);
×
653
}; // draw
×
654

1✔
655
/**
1✔
656
 * UUID namespace for ER diagram IDs
1✔
657
 *
1✔
658
 * This can be generated via running:
1✔
659
 *
1✔
660
 * ```js
1✔
661
 * const { v5: uuid5 } = await import('uuid');
1✔
662
 * uuid5(
1✔
663
 *   'https://mermaid-js.github.io/mermaid/syntax/entityRelationshipDiagram.html',
1✔
664
 *   uuid5.URL
1✔
665
 * );
1✔
666
 * ```
1✔
667
 */
1✔
668
const MERMAID_ERDIAGRAM_UUID = '28e9f9db-3c8d-5aa5-9faf-44286ae5937c';
1✔
669

1✔
670
/**
1✔
671
 * Return a unique id based on the given string. Start with the prefix, then a hyphen, then the
1✔
672
 * simplified str, then a hyphen, then a unique uuid based on the str. (Hyphens are only included if needed.)
1✔
673
 * Although the official XML standard for ids says that many more characters are valid in the id,
1✔
674
 * this keeps things simple by accepting only A-Za-z0-9.
1✔
675
 *
1✔
676
 * @param {string} str Given string to use as the basis for the id. Default is `''`
1✔
677
 * @param {string} prefix String to put at the start, followed by '-'. Default is `''`
1✔
678
 * @returns {string}
1✔
679
 * @see https://www.w3.org/TR/xml/#NT-Name
1✔
680
 */
1✔
681
export function generateId(str = '', prefix = '') {
1✔
682
  const simplifiedStr = str.replace(BAD_ID_CHARS_REGEXP, '');
2✔
683
  // we use `uuid v5` so that UUIDs are consistent given a string.
2✔
684
  return `${strWithHyphen(prefix)}${strWithHyphen(simplifiedStr)}${uuid5(
2✔
685
    str,
2✔
686
    MERMAID_ERDIAGRAM_UUID
2✔
687
  )}`;
2✔
688
}
2✔
689

1✔
690
/**
1✔
691
 * Append a hyphen to a string only if the string isn't empty
1✔
692
 *
1✔
693
 * @param {string} str
1✔
694
 * @returns {string}
1✔
695
 * @todo This could be moved into a string utility file/class.
1✔
696
 */
1✔
697
function strWithHyphen(str = '') {
4✔
698
  return str.length > 0 ? `${str}-` : '';
4!
699
}
4✔
700

1✔
701
export default {
1✔
702
  setConf,
1✔
703
  draw,
1✔
704
};
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc