Coveralls logob
Coveralls logo
  • Home
  • Features
  • Pricing
  • Docs
  • Sign In

knsv / mermaid / 753

24 May 2019 - 15:14 coverage increased (+0.06%) to 54.316%
753

Pull #845

travis-ci

9181eb84f9c35729a3bad740fb7f9d93?size=18&default=identiconweb-flow
Support styling of subgraphs
Pull Request #845: Support styling of subgraphs

933 of 1744 branches covered (53.5%)

Branch coverage included in aggregate %.

38 of 54 new or added lines in 3 files covered. (70.37%)

2062 of 3770 relevant lines covered (54.69%)

206.94 hits per line

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

18.35
/src/diagrams/flowchart/flowRenderer.js
1
import graphlib from 'graphlibrary'
2
import * as d3 from 'd3'
3

4
import flowDb from './flowDb'
5
import flow from './parser/flow'
6
import dagreD3 from 'dagre-d3-renderer'
7
import { logger } from '../../logger'
8
import { interpolateToCurve } from '../../utils'
9

10
const conf = {
2×
11
}
12
export const setConf = function (cnf) {
2×
13
  const keys = Object.keys(cnf)
!
14
  for (let i = 0; i < keys.length; i++) {
!
15
    conf[keys[i]] = cnf[keys[i]]
!
16
  }
17
}
18

19
/**
20
 * Function that adds the vertices found in the graph definition to the graph to be rendered.
21
 * @param vert Object containing the vertices.
22
 * @param g The graph that is to be drawn.
23
 */
24
export const addVertices = function (vert, g) {
2×
25
  const keys = Object.keys(vert)
!
26

27
  const styleFromStyleArr = function (styleStr, arr) {
!
28
    // Create a compound style definition from the style definitions found for the node in the graph definition
29
    for (let i = 0; i < arr.length; i++) {
!
30
      if (typeof arr[i] !== 'undefined') {
Branches [[0, 0], [0, 1]] missed. !
31
        styleStr = styleStr + arr[i] + ';'
!
32
      }
33
    }
34

35
    return styleStr
!
36
  }
37

38
  // Iterate through each item in the vertice object (containing all the vertices found) in the graph definition
39
  keys.forEach(function (id) {
!
40
    const vertice = vert[id]
!
41
    let verticeText
42

43
    /**
44
     * Variable for storing the classes for the vertice
45
     * @type {string}
46
     */
47
    let classStr = ''
!
48
    if (vertice.classes.length > 0) {
Branches [[1, 0], [1, 1]] missed. !
49
      classStr = vertice.classes.join(' ')
!
50
    }
51

52
    /**
53
     * Variable for storing the extracted style for the vertice
54
     * @type {string}
55
     */
56
    let style = ''
!
57
    // Create a compound style definition from the style definitions found for the node in the graph definition
58
    style = styleFromStyleArr(style, vertice.styles)
!
59

60
    // Use vertice id as text in the box if no text is provided by the graph definition
61
    if (typeof vertice.text === 'undefined') {
Branches [[2, 0], [2, 1]] missed. !
62
      verticeText = vertice.id
!
63
    } else {
64
      verticeText = vertice.text
!
65
    }
66

67
    let labelTypeStr = ''
!
68
    if (conf.htmlLabels) {
Branches [[3, 0], [3, 1]] missed. !
69
      labelTypeStr = 'html'
!
70
      verticeText = verticeText.replace(/fa[lrsb]?:fa-[\w-]+/g, s => `<i class='${s.replace(':', ' ')}'></i>`)
!
71
      if (vertice.link) {
Branches [[4, 0], [4, 1]] missed. !
72
        verticeText = '<a href="' + vertice.link + '" rel="noopener">' + verticeText + '</a>'
!
73
      }
74
    } else {
75
      const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text')
!
76

77
      const rows = verticeText.split(/<br>/)
!
78

79
      for (let j = 0; j < rows.length; j++) {
!
80
        const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan')
!
81
        tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve')
!
82
        tspan.setAttribute('dy', '1em')
!
83
        tspan.setAttribute('x', '1')
!
84
        tspan.textContent = rows[j]
!
85
        svgLabel.appendChild(tspan)
!
86
      }
87

88
      labelTypeStr = 'svg'
!
89
      if (vertice.link) {
Branches [[5, 0], [5, 1]] missed. !
90
        const link = document.createElementNS('http://www.w3.org/2000/svg', 'a')
!
91
        link.setAttributeNS('http://www.w3.org/2000/svg', 'href', vertice.link)
!
92
        link.setAttributeNS('http://www.w3.org/2000/svg', 'rel', 'noopener')
!
93
        verticeText = link
!
94
      } else {
95
        verticeText = svgLabel
!
96
      }
97
    }
98

99
    let radious = 0
!
100
    let _shape = ''
!
101
    // Set the shape based parameters
102
    switch (vertice.type) {
Branches [[6, 0], [6, 1], [6, 2], [6, 3], [6, 4], [6, 5], [6, 6], [6, 7], [6, 8]] missed. !
103
      case 'round':
104
        radious = 5
!
105
        _shape = 'rect'
!
106
        break
!
107
      case 'square':
108
        _shape = 'rect'
!
109
        break
!
110
      case 'diamond':
111
        _shape = 'question'
!
112
        break
!
113
      case 'odd':
114
        _shape = 'rect_left_inv_arrow'
!
115
        break
!
116
      case 'odd_right':
117
        _shape = 'rect_left_inv_arrow'
!
118
        break
!
119
      case 'circle':
120
        _shape = 'circle'
!
121
        break
!
122
      case 'ellipse':
123
        _shape = 'ellipse'
!
124
        break
!
125
      case 'group':
126
        _shape = 'rect'
!
127
        // Need to create a text node if using svg labels, see #367
128
        verticeText = conf.htmlLabels ? '' : document.createElementNS('http://www.w3.org/2000/svg', 'text')
Branches [[7, 0], [7, 1]] missed. !
129
        break
!
130
      default:
131
        _shape = 'rect'
!
132
    }
133
    // Add the node
134
    g.setNode(vertice.id, { labelType: labelTypeStr, shape: _shape, label: verticeText, rx: radious, ry: radious, 'class': classStr, style: style, id: vertice.id })
!
135
  })
136
}
137

138
/**
139
 * Add edges to graph based on parsed graph defninition
140
 * @param {Object} edges The edges to add to the graph
141
 * @param {Object} g The graph object
142
 */
143
export const addEdges = function (edges, g) {
2×
144
  let cnt = 0
8×
145

146
  let defaultStyle
147
  if (typeof edges.defaultStyle !== 'undefined') {
Branches [[8, 0]] missed. 8×
148
    defaultStyle = edges.defaultStyle.toString().replace(/,/g, ';')
!
149
  }
150

151
  edges.forEach(function (edge) {
8×
152
    cnt++
8×
153
    const edgeData = {}
8×
154

155
    // Set link type for rendering
156
    if (edge.type === 'arrow_open') {
8×
157
      edgeData.arrowhead = 'none'
6×
158
    } else {
159
      edgeData.arrowhead = 'normal'
2×
160
    }
161

162
    let style = ''
8×
163
    if (typeof edge.style !== 'undefined') {
8×
164
      edge.style.forEach(function (s) {
4×
165
        style = style + s + ';'
12×
166
      })
167
    } else {
168
      switch (edge.stroke) {
Branches [[11, 1], [11, 2]] missed. 4×
169
        case 'normal':
170
          style = 'fill:none'
4×
171
          if (typeof defaultStyle !== 'undefined') {
Branches [[12, 0]] missed. 4×
172
            style = defaultStyle
!
173
          }
174
          break
4×
175
        case 'dotted':
176
          style = 'stroke: #333; fill:none;stroke-width:2px;stroke-dasharray:3;'
!
177
          break
!
178
        case 'thick':
179
          style = 'stroke: #333; stroke-width: 3.5px;fill:none'
!
180
          break
!
181
      }
182
    }
183
    edgeData.style = style
8×
184

185
    if (typeof edge.interpolate !== 'undefined') {
8×
186
      edgeData.curve = interpolateToCurve(edge.interpolate, d3.curveLinear)
1×
187
    } else if (typeof edges.defaultInterpolate !== 'undefined') {
Branches [[14, 0]] missed. 7×
188
      edgeData.curve = interpolateToCurve(edges.defaultInterpolate, d3.curveLinear)
!
189
    } else {
190
      edgeData.curve = interpolateToCurve(conf.curve, d3.curveLinear)
7×
191
    }
192

193
    if (typeof edge.text === 'undefined') {
Branches [[15, 0]] missed. 8×
194
      if (typeof edge.style !== 'undefined') {
Branches [[16, 0], [16, 1]] missed. !
195
        edgeData.arrowheadStyle = 'fill: #333'
!
196
      }
197
    } else {
198
      edgeData.arrowheadStyle = 'fill: #333'
8×
199
      if (typeof edge.style === 'undefined') {
8×
200
        edgeData.labelpos = 'c'
4×
201
        if (conf.htmlLabels) {
Branches [[18, 0]] missed. 4×
202
          edgeData.labelType = 'html'
!
203
          edgeData.label = '<span class="edgeLabel">' + edge.text + '</span>'
!
204
        } else {
205
          edgeData.labelType = 'text'
4×
206
          edgeData.style = 'stroke: #333; stroke-width: 1.5px;fill:none'
4×
207
          edgeData.label = edge.text.replace(/<br>/g, '\n')
4×
208
        }
209
      } else {
210
        edgeData.label = edge.text.replace(/<br>/g, '\n')
4×
211
      }
212
    }
213
    // Add the edge to the graph
214
    g.setEdge(edge.start, edge.end, edgeData, cnt)
8×
215
  })
216
}
217

218
/**
219
 * Returns the all the styles from classDef statements in the graph definition.
220
 * @returns {object} classDef styles
221
 */
222
export const getClasses = function (text) {
2×
223
  flowDb.clear()
!
224
  const parser = flow.parser
!
225
  parser.yy = flowDb
!
226

227
  // Parse the graph definition
228
  parser.parse(text)
!
229
  return flowDb.getClasses()
!
230
}
231

232
/**
233
 * Draws a flowchart in the tag with id: id based on the graph definition in text.
234
 * @param text
235
 * @param id
236
 */
237
export const draw = function (text, id) {
2×
238
  logger.debug('Drawing flowchart')
!
239
  flowDb.clear()
!
240
  const parser = flow.parser
!
241
  parser.yy = flowDb
!
242

243
  // Parse the graph definition
244
  try {
!
245
    parser.parse(text)
!
246
  } catch (err) {
247
    logger.debug('Parsing failed')
!
248
  }
249

250
  // Fetch the default direction, use TD if none was found
251
  let dir = flowDb.getDirection()
!
252
  if (typeof dir === 'undefined') {
Branches [[19, 0], [19, 1]] missed. !
253
    dir = 'TD'
!
254
  }
255

256
  // Create the input mermaid.graph
257
  const g = new graphlib.Graph({
!
258
    multigraph: true,
259
    compound: true
260
  })
261
    .setGraph({
262
      rankdir: dir,
263
      marginx: 20,
264
      marginy: 20
265

266
    })
267
    .setDefaultEdgeLabel(function () {
268
      return {}
!
269
    })
270

271
  let subG
272
  const subGraphs = flowDb.getSubGraphs()
!
273
  for (let i = subGraphs.length - 1; i >= 0; i--) {
!
274
    subG = subGraphs[i]
!
NEW
275
    flowDb.addVertex(subG.id, subG.title, 'group', undefined, subG.classes)
!
276
  }
277

278
  // Fetch the verices/nodes and edges/links from the parsed graph definition
279
  const vert = flowDb.getVertices()
!
280

281
  const edges = flowDb.getEdges()
!
282

283
  let i = 0
!
284
  for (i = subGraphs.length - 1; i >= 0; i--) {
!
285
    subG = subGraphs[i]
!
286

287
    d3.selectAll('cluster').append('text')
!
288

289
    for (let j = 0; j < subG.nodes.length; j++) {
!
290
      g.setParent(subG.nodes[j], subG.id)
!
291
    }
292
  }
293
  addVertices(vert, g)
!
294
  addEdges(edges, g)
!
295

296
  // Create the renderer
297
  const Render = dagreD3.render
!
298
  const render = new Render()
!
299

300
  // Add custom shape for rhombus type of boc (decision)
301
  render.shapes().question = function (parent, bbox, node) {
!
302
    const w = bbox.width
!
303
    const h = bbox.height
!
304
    const s = (w + h) * 0.9
!
305
    const points = [
!
306
      { x: s / 2, y: 0 },
307
      { x: s, y: -s / 2 },
308
      { x: s / 2, y: -s },
309
      { x: 0, y: -s / 2 }
310
    ]
311
    const shapeSvg = parent.insert('polygon', ':first-child')
!
312
      .attr('points', points.map(function (d) {
313
        return d.x + ',' + d.y
!
314
      }).join(' '))
315
      .attr('rx', 5)
316
      .attr('ry', 5)
317
      .attr('transform', 'translate(' + (-s / 2) + ',' + (s * 2 / 4) + ')')
318
    node.intersect = function (point) {
!
319
      return dagreD3.intersect.polygon(node, points, point)
!
320
    }
321
    return shapeSvg
!
322
  }
323

324
  // Add custom shape for box with inverted arrow on left side
325
  render.shapes().rect_left_inv_arrow = function (parent, bbox, node) {
!
326
    const w = bbox.width
!
327
    const h = bbox.height
!
328
    const points = [
!
329
      { x: -h / 2, y: 0 },
330
      { x: w, y: 0 },
331
      { x: w, y: -h },
332
      { x: -h / 2, y: -h },
333
      { x: 0, y: -h / 2 }
334
    ]
335
    const shapeSvg = parent.insert('polygon', ':first-child')
!
336
      .attr('points', points.map(function (d) {
337
        return d.x + ',' + d.y
!
338
      }).join(' '))
339
      .attr('transform', 'translate(' + (-w / 2) + ',' + (h * 2 / 4) + ')')
340
    node.intersect = function (point) {
!
341
      return dagreD3.intersect.polygon(node, points, point)
!
342
    }
343
    return shapeSvg
!
344
  }
345

346
  // Add custom shape for box with inverted arrow on right side
347
  render.shapes().rect_right_inv_arrow = function (parent, bbox, node) {
!
348
    const w = bbox.width
!
349
    const h = bbox.height
!
350
    const points = [
!
351
      { x: 0, y: 0 },
352
      { x: w + h / 2, y: 0 },
353
      { x: w, y: -h / 2 },
354
      { x: w + h / 2, y: -h },
355
      { x: 0, y: -h }
356
    ]
357
    const shapeSvg = parent.insert('polygon', ':first-child')
!
358
      .attr('points', points.map(function (d) {
359
        return d.x + ',' + d.y
!
360
      }).join(' '))
361
      .attr('transform', 'translate(' + (-w / 2) + ',' + (h * 2 / 4) + ')')
362
    node.intersect = function (point) {
!
363
      return dagreD3.intersect.polygon(node, points, point)
!
364
    }
365
    return shapeSvg
!
366
  }
367

368
  // Add our custom arrow - an empty arrowhead
369
  render.arrows().none = function normal (parent, id, edge, type) {
!
370
    const marker = parent.append('marker')
!
371
      .attr('id', id)
372
      .attr('viewBox', '0 0 10 10')
373
      .attr('refX', 9)
374
      .attr('refY', 5)
375
      .attr('markerUnits', 'strokeWidth')
376
      .attr('markerWidth', 8)
377
      .attr('markerHeight', 6)
378
      .attr('orient', 'auto')
379

380
    const path = marker.append('path')
!
381
      .attr('d', 'M 0 0 L 0 0 L 0 0 z')
382
    dagreD3.util.applyStyle(path, edge[type + 'Style'])
!
383
  }
384

385
  // Override normal arrowhead defined in d3. Remove style & add class to allow css styling.
386
  render.arrows().normal = function normal (parent, id, edge, type) {
!
387
    const marker = parent.append('marker')
!
388
      .attr('id', id)
389
      .attr('viewBox', '0 0 10 10')
390
      .attr('refX', 9)
391
      .attr('refY', 5)
392
      .attr('markerUnits', 'strokeWidth')
393
      .attr('markerWidth', 8)
394
      .attr('markerHeight', 6)
395
      .attr('orient', 'auto')
396

397
    marker.append('path')
!
398
      .attr('d', 'M 0 0 L 10 5 L 0 10 z')
399
      .attr('class', 'arrowheadPath')
400
      .style('stroke-width', 1)
401
      .style('stroke-dasharray', '1,0')
402
  }
403

404
  // Set up an SVG group so that we can translate the final graph.
405
  const svg = d3.select(`[id="${id}"]`)
!
406

407
  // Run the renderer. This is what draws the final graph.
408
  const element = d3.select('#' + id + ' g')
!
409
  render(element, g)
!
410

411
  element.selectAll('g.node')
!
412
    .attr('title', function () {
413
      return flowDb.getTooltip(this.id)
!
414
    })
415

416
  const padding = 8
!
417
  const width = g.maxX - g.minX + padding * 2
!
418
  const height = g.maxY - g.minY + padding * 2
!
419
  svg.attr('width', '100%')
!
420
  svg.attr('style', `max-width: ${width}px;`)
!
421
  svg.attr('viewBox', `0 0 ${width} ${height}`)
!
422
  svg.select('g').attr('transform', `translate(${padding - g.minX}, ${padding - g.minY})`)
!
423

424
  // Index nodes
425
  flowDb.indexNodes('subGraph' + i)
!
426

427
  for (i = 0; i < subGraphs.length; i++) {
!
428
    subG = subGraphs[i]
!
429

430
    if (subG.title !== 'undefined') {
Branches [[20, 0], [20, 1]] missed. !
431
      const clusterRects = document.querySelectorAll('#' + id + ' #' + subG.id + ' rect')
!
432
      const clusterEl = document.querySelectorAll('#' + id + ' #' + subG.id)
!
433

434
      const xPos = clusterRects[0].x.baseVal.value
!
435
      const yPos = clusterRects[0].y.baseVal.value
!
436
      const width = clusterRects[0].width.baseVal.value
!
437
      const cluster = d3.select(clusterEl[0])
!
438
      const te = cluster.append('text')
!
439
      te.attr('x', xPos + width / 2)
!
440
      te.attr('y', yPos + 14)
!
441
      te.attr('fill', 'black')
!
442
      te.attr('stroke', 'none')
!
443
      te.attr('id', id + 'Text')
!
444
      te.style('text-anchor', 'middle')
!
445

446
      if (typeof subG.title === 'undefined') {
Branches [[21, 0], [21, 1]] missed. !
447
        te.text('Undef')
!
448
      } else {
449
        te.text(subG.title)
!
450
      }
451
    }
452
  }
453

454
  // Add label rects for non html labels
455
  if (!conf.htmlLabels) {
Branches [[22, 0], [22, 1]] missed. !
456
    const labels = document.querySelectorAll('#' + id + ' .edgeLabel .label')
!
457
    for (let k = 0; k < labels.length; k++) {
!
458
      const label = labels[k]
!
459

460
      // Get dimensions of label
461
      const dim = label.getBBox()
!
462

463
      const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
!
464
      rect.setAttribute('rx', 0)
!
465
      rect.setAttribute('ry', 0)
!
466
      rect.setAttribute('width', dim.width)
!
467
      rect.setAttribute('height', dim.height)
!
468
      rect.setAttribute('style', 'fill:#e8e8e8;')
!
469

470
      label.insertBefore(rect, label.firstChild)
!
471
    }
472
  }
473
}
474

475
export default {
476
  setConf,
477
  addVertices,
478
  addEdges,
479
  getClasses,
480
  draw
481
}
Troubleshooting · Open an Issue · Sales · Support · ENTERPRISE · CAREERS · STATUS
BLOG · TWITTER · Legal & Privacy · Supported CI Services · What's a CI service? · Automated Testing

© 2019 Coveralls, LLC