Skip to content

Commit a307c1a

Browse files
Merge pull request #58 from PeronGH/master
2 parents b5d02c3 + 9f2194a commit a307c1a

18 files changed

Lines changed: 851 additions & 96 deletions

cmd/diagram.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ func (gd *GraphDiagram) Render(config *diagram.Config) (string, error) {
8282
if styleType == "" {
8383
styleType = "cli"
8484
}
85+
gd.properties.boxBorderPadding = config.BoxBorderPadding
86+
gd.properties.paddingX = config.PaddingBetweenX
87+
gd.properties.paddingY = config.PaddingBetweenY
8588
gd.properties.styleType = styleType
8689
gd.properties.useAscii = config.UseAscii
8790

cmd/direction.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,23 +45,23 @@ func (c drawingCoord) Direction(dir direction) drawingCoord {
4545
return drawingCoord{x: c.x + dir.x, y: c.y + dir.y}
4646
}
4747

48-
func selfReferenceDirection(e *edge) (direction, direction, direction, direction) {
49-
if graphDirection == "LR" {
48+
func (g graph) selfReferenceDirection() (direction, direction, direction, direction) {
49+
if g.graphDirection == "LR" {
5050
return Right, Down, Down, Right
5151
}
5252
return Down, Right, Right, Down
5353
}
5454

55-
func determineStartAndEndDir(e *edge) (direction, direction, direction, direction) {
55+
func (g graph) determineStartAndEndDir(e *edge) (direction, direction, direction, direction) {
5656
if e.from == e.to {
57-
return selfReferenceDirection(e)
57+
return g.selfReferenceDirection()
5858
}
5959
d := determineDirection(genericCoord(*e.from.gridCoord), genericCoord(*e.to.gridCoord))
6060
var preferredDir, preferredOppositeDir, alternativeDir, alternativeOppositeDir direction
6161

6262
// Check if this is a backwards flowing edge
6363
isBackwards := false
64-
if graphDirection == "LR" {
64+
if g.graphDirection == "LR" {
6565
// In LR mode, backwards flow is when edge goes from right to left (Left direction)
6666
isBackwards = (d == Left || d == UpperLeft || d == LowerLeft)
6767
} else { // TD mode
@@ -75,7 +75,7 @@ func determineStartAndEndDir(e *edge) (direction, direction, direction, directio
7575
// For backwards edges, use special start positions: Down in LR mode, Right in TD mode
7676
switch d {
7777
case LowerRight:
78-
if graphDirection == "LR" {
78+
if g.graphDirection == "LR" {
7979
preferredDir = Down
8080
preferredOppositeDir = Left
8181
alternativeDir = Right
@@ -87,7 +87,7 @@ func determineStartAndEndDir(e *edge) (direction, direction, direction, directio
8787
alternativeOppositeDir = Left
8888
}
8989
case UpperRight:
90-
if graphDirection == "LR" {
90+
if g.graphDirection == "LR" {
9191
preferredDir = Up
9292
preferredOppositeDir = Left
9393
alternativeDir = Right
@@ -99,7 +99,7 @@ func determineStartAndEndDir(e *edge) (direction, direction, direction, directio
9999
alternativeOppositeDir = Left
100100
}
101101
case LowerLeft:
102-
if graphDirection == "LR" {
102+
if g.graphDirection == "LR" {
103103
// Backwards flow in LR mode - start from Down, arrive at Down
104104
preferredDir = Down
105105
preferredOppositeDir = Down // Edge goes to bottom of destination
@@ -112,7 +112,7 @@ func determineStartAndEndDir(e *edge) (direction, direction, direction, directio
112112
alternativeOppositeDir = Right
113113
}
114114
case UpperLeft:
115-
if graphDirection == "LR" {
115+
if g.graphDirection == "LR" {
116116
// Backwards flow in LR mode - start from Down, arrive at Down
117117
preferredDir = Down
118118
preferredOppositeDir = Down // Edge goes to bottom of destination
@@ -128,13 +128,13 @@ func determineStartAndEndDir(e *edge) (direction, direction, direction, directio
128128
default:
129129
// Handle direct backwards flow cases
130130
if isBackwards {
131-
if graphDirection == "LR" && d == Left {
131+
if g.graphDirection == "LR" && d == Left {
132132
// Direct left flow in LR mode - start from Down, arrive at Down
133133
preferredDir = Down
134134
preferredOppositeDir = Down // Edge goes to bottom of destination
135135
alternativeDir = Left
136136
alternativeOppositeDir = Right
137-
} else if graphDirection == "TD" && d == Up {
137+
} else if g.graphDirection == "TD" && d == Up {
138138
// Direct up flow in TD mode - start from Right, arrive at Right
139139
preferredDir = Right
140140
preferredOppositeDir = Right // Edge goes to right of destination

cmd/draw.go

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"strings"
66

77
"github.com/gookit/color"
8+
"github.com/mattn/go-runewidth"
89
log "github.com/sirupsen/logrus"
910
)
1011

@@ -151,7 +152,7 @@ func (g *graph) drawLine(d *drawing, from drawingCoord, to drawingCoord, offsetF
151152
}
152153

153154
func drawMap(properties *graphProperties) string {
154-
g := mkGraph(properties.data)
155+
g := mkGraph(properties.data, properties.nodeSpecs)
155156
g.setStyleClasses(properties)
156157
g.paddingX = properties.paddingX
157158
g.paddingY = properties.paddingY
@@ -227,11 +228,22 @@ func drawBox(n *node, g graph) *drawing {
227228
boxDrawing[from.x][to.y] = "+" // Bottom left corner
228229
boxDrawing[to.x][to.y] = "+" // Bottom right corner
229230
}
230-
// Draw text
231-
textY := from.y + h/2
232-
textX := from.x + w/2 - CeilDiv(len(n.name), 2) + 1
233-
for x := 0; x < len(n.name); x++ {
234-
boxDrawing[textX+x][textY] = wrapTextInColor(string(n.name[x]), n.styleClass.styles["color"], g.styleType)
231+
// Draw label lines inside the padded content area.
232+
innerTop := from.y + 1
233+
innerHeight := h - 1
234+
contentTop := innerTop + (innerHeight-n.label.contentHeight())/2
235+
for lineIdx, line := range n.label.lines {
236+
textY := contentTop + lineIdx*(graphLabelLineGap+1)
237+
textWidth := runewidth.StringWidth(line)
238+
textX := from.x + w/2 - CeilDiv(textWidth, 2) + 1
239+
for _, r := range line {
240+
runeWidth := Max(runewidth.RuneWidth(r), 1)
241+
boxDrawing[textX][textY] = wrapTextInColor(string(r), n.styleClass.styles["color"], g.styleType)
242+
for offset := 1; offset < runeWidth; offset++ {
243+
boxDrawing[textX+offset][textY] = ""
244+
}
245+
textX += runeWidth
246+
}
235247
}
236248

237249
return &boxDrawing
@@ -316,15 +328,22 @@ func drawSubgraphLabel(sg *subgraph, g graph) (*drawing, drawingCoord) {
316328
to := drawingCoord{width, height}
317329
labelDrawing := *(mkDrawing(width, height))
318330

319-
// Draw label centered at top
320-
labelY := from.y + 1
321-
labelX := from.x + width/2 - len(sg.name)/2
322-
if labelX < from.x+1 {
323-
labelX = from.x + 1
324-
}
325-
for i, char := range sg.name {
326-
if labelX+i < to.x {
327-
labelDrawing[labelX+i][labelY] = string(char)
331+
// Draw label centered at top.
332+
for lineIdx, line := range sg.label.lines {
333+
labelY := from.y + 1 + lineIdx*(graphLabelLineGap+1)
334+
labelX := from.x + width/2 - runewidth.StringWidth(line)/2
335+
if labelX < from.x+1 {
336+
labelX = from.x + 1
337+
}
338+
for _, char := range line {
339+
runeWidth := Max(runewidth.RuneWidth(char), 1)
340+
if labelX < to.x {
341+
labelDrawing[labelX][labelY] = string(char)
342+
}
343+
for offset := 1; offset < runeWidth && labelX+offset < to.x; offset++ {
344+
labelDrawing[labelX+offset][labelY] = ""
345+
}
346+
labelX += runeWidth
328347
}
329348
}
330349

cmd/graph.go

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,24 +30,40 @@ func (g graph) lineToDrawing(line []gridCoord) []drawingCoord {
3030
}
3131

3232
type graph struct {
33-
nodes []*node
34-
edges []*edge
35-
drawing *drawing
36-
grid map[gridCoord]*node
37-
columnWidth map[int]int
38-
rowHeight map[int]int
39-
styleClasses map[string]styleClass
40-
styleType string
41-
paddingX int
42-
paddingY int
43-
subgraphs []*subgraph
44-
offsetX int
45-
offsetY int
46-
useAscii bool
33+
nodes []*node
34+
edges []*edge
35+
drawing *drawing
36+
grid map[gridCoord]*node
37+
edgeCounts map[edgePair]int
38+
columnWidth map[int]int
39+
rowHeight map[int]int
40+
styleClasses map[string]styleClass
41+
styleType string
42+
boxBorderPadding int
43+
graphDirection string
44+
paddingX int
45+
paddingY int
46+
subgraphs []*subgraph
47+
offsetX int
48+
offsetY int
49+
useAscii bool
50+
}
51+
52+
type edgePair struct {
53+
from int
54+
to int
55+
}
56+
57+
func newEdgePair(from, to int) edgePair {
58+
if from < to {
59+
return edgePair{from: from, to: to}
60+
}
61+
return edgePair{from: to, to: from}
4762
}
4863

4964
type subgraph struct {
5065
name string
66+
label graphLabel
5167
nodes []*node
5268
parent *subgraph
5369
children []*subgraph
@@ -58,28 +74,30 @@ type subgraph struct {
5874
maxY int
5975
}
6076

61-
func mkGraph(data *orderedmap.OrderedMap[string, []textEdge]) graph {
77+
func mkGraph(data *orderedmap.OrderedMap[string, []textEdge], nodeSpecs map[string]graphNodeSpec) graph {
6278
g := graph{drawing: mkDrawing(0, 0)}
6379
g.grid = make(map[gridCoord]*node)
80+
g.edgeCounts = make(map[edgePair]int)
6481
g.columnWidth = make(map[int]int)
6582
g.rowHeight = make(map[int]int)
6683
g.styleClasses = make(map[string]styleClass)
6784
index := 0
6885
for el := data.Front(); el != nil; el = el.Next() {
6986
nodeName := el.Key
7087
children := el.Value
88+
spec := nodeSpecs[nodeName]
7189
// Get or create parent node
7290
parentNode, err := g.getNode(nodeName)
7391
if err != nil {
74-
parentNode = &node{name: nodeName, index: index, styleClassName: ""}
92+
parentNode = &node{name: nodeName, label: spec.label, index: index, styleClassName: spec.styleClass}
7593
g.appendNode(parentNode)
7694
index += 1
7795
}
7896
for _, textEdge := range children {
97+
childSpec := nodeSpecs[textEdge.child.name]
7998
childNode, err := g.getNode(textEdge.child.name)
8099
if err != nil {
81-
childNode = &node{name: textEdge.child.name, index: index, styleClassName: textEdge.child.styleClass}
82-
parentNode.styleClassName = textEdge.parent.styleClass
100+
childNode = &node{name: textEdge.child.name, label: childSpec.label, index: index, styleClassName: childSpec.styleClass}
83101
g.appendNode(childNode)
84102
index += 1
85103
}
@@ -94,6 +112,8 @@ func (g *graph) setStyleClasses(properties *graphProperties) {
94112
log.Debugf("Setting style classes to %v", properties.styleClasses)
95113
g.styleClasses = *properties.styleClasses
96114
g.styleType = properties.styleType
115+
g.boxBorderPadding = properties.boxBorderPadding
116+
g.graphDirection = properties.graphDirection
97117
g.paddingX = properties.paddingX
98118
g.paddingY = properties.paddingY
99119
for _, n := range g.nodes {
@@ -111,6 +131,7 @@ func (g *graph) setSubgraphs(textSubgraphs []*textSubgraph) {
111131
for _, tsg := range textSubgraphs {
112132
sg := &subgraph{
113133
name: tsg.name,
134+
label: tsg.label,
114135
nodes: []*node{},
115136
children: []*subgraph{},
116137
}
@@ -194,7 +215,7 @@ func (g *graph) createMapping() {
194215

195216
// Separate root nodes by whether they're in subgraphs, but only if we have both types
196217
// AND there are edges in subgraphs (indicating intentional layout structure)
197-
shouldSeparate := graphDirection == "LR" && hasExternalRoots && hasSubgraphRootsWithEdges
218+
shouldSeparate := g.graphDirection == "LR" && hasExternalRoots && hasSubgraphRootsWithEdges
198219

199220
externalRootNodes := []*node{}
200221
subgraphRootNodes := []*node{}
@@ -214,7 +235,7 @@ func (g *graph) createMapping() {
214235
// Place external root nodes first at level 0
215236
for _, n := range externalRootNodes {
216237
var mappingCoord *gridCoord
217-
if graphDirection == "LR" {
238+
if g.graphDirection == "LR" {
218239
mappingCoord = g.reserveSpotInGrid(g.nodes[n.index], &gridCoord{x: 0, y: highestPositionPerLevel[0]})
219240
} else {
220241
mappingCoord = g.reserveSpotInGrid(g.nodes[n.index], &gridCoord{x: highestPositionPerLevel[0], y: 0})
@@ -230,7 +251,7 @@ func (g *graph) createMapping() {
230251
subgraphLevel := 4
231252
for _, n := range subgraphRootNodes {
232253
var mappingCoord *gridCoord
233-
if graphDirection == "LR" {
254+
if g.graphDirection == "LR" {
234255
mappingCoord = g.reserveSpotInGrid(g.nodes[n.index], &gridCoord{x: subgraphLevel, y: highestPositionPerLevel[subgraphLevel]})
235256
} else {
236257
mappingCoord = g.reserveSpotInGrid(g.nodes[n.index], &gridCoord{x: highestPositionPerLevel[subgraphLevel], y: subgraphLevel})
@@ -245,7 +266,7 @@ func (g *graph) createMapping() {
245266
log.Debugf("Creating mapping for node %s at %v", n.name, n.gridCoord)
246267
var childLevel int
247268
// Next column is 4 coords further. This is because every node is 3 coords wide + 1 coord inbetween.
248-
if graphDirection == "LR" {
269+
if g.graphDirection == "LR" {
249270
childLevel = n.gridCoord.x + 4
250271
} else {
251272
childLevel = n.gridCoord.y + 4
@@ -258,7 +279,7 @@ func (g *graph) createMapping() {
258279
}
259280

260281
var mappingCoord *gridCoord
261-
if graphDirection == "LR" {
282+
if g.graphDirection == "LR" {
262283
mappingCoord = g.reserveSpotInGrid(g.nodes[child.index], &gridCoord{x: childLevel, y: highestPosition})
263284
} else {
264285
mappingCoord = g.reserveSpotInGrid(g.nodes[child.index], &gridCoord{x: highestPosition, y: childLevel})
@@ -471,9 +492,18 @@ func (g *graph) calculateSubgraphBoundingBox(sg *subgraph) {
471492
maxY = Max(maxY, nodeMaxY)
472493
}
473494

495+
// Ensure the title fits inside the frame after padding is applied.
496+
currentWidth := maxX - minX
497+
currentInnerWidth := currentWidth + 3
498+
if currentInnerWidth < sg.label.width {
499+
extraWidth := sg.label.width - currentInnerWidth
500+
minX -= extraWidth / 2
501+
maxX += extraWidth - (extraWidth / 2)
502+
}
503+
474504
// Add padding (allow negative coordinates, we'll offset later)
475505
const subgraphPadding = 2
476-
const subgraphLabelSpace = 2 // Extra space for label at top
506+
subgraphLabelSpace := sg.label.contentHeight() + 1
477507
sg.minX = minX - subgraphPadding
478508
sg.minY = minY - subgraphPadding - subgraphLabelSpace
479509
sg.maxX = maxX + subgraphPadding

cmd/label.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package cmd
2+
3+
import (
4+
"regexp"
5+
"strings"
6+
7+
"github.com/mattn/go-runewidth"
8+
)
9+
10+
var htmlBreakPattern = regexp.MustCompile(`(?i)<br\s*/?>`)
11+
12+
const graphLabelLineGap = 1
13+
14+
type graphLabel struct {
15+
lines []string
16+
width int
17+
}
18+
19+
func newGraphLabel(raw string) graphLabel {
20+
normalized := htmlBreakPattern.ReplaceAllString(raw, "\n")
21+
normalized = strings.ReplaceAll(normalized, `\n`, "\n")
22+
23+
lines := strings.Split(normalized, "\n")
24+
if len(lines) == 0 {
25+
lines = []string{""}
26+
}
27+
28+
width := 0
29+
for _, line := range lines {
30+
width = Max(width, runewidth.StringWidth(line))
31+
}
32+
33+
return graphLabel{
34+
lines: lines,
35+
width: width,
36+
}
37+
}
38+
39+
func (l graphLabel) height() int {
40+
return len(l.lines)
41+
}
42+
43+
func (l graphLabel) contentHeight() int {
44+
if len(l.lines) == 0 {
45+
return 0
46+
}
47+
return len(l.lines) + (len(l.lines)-1)*graphLabelLineGap
48+
}

0 commit comments

Comments
 (0)