Skip to content

Commit 8ca77a5

Browse files
fix: avoid widening node columns for edge labels
Edge label placement widened the grid column at the midpoint of the chosen path segment to fit the label text. When that midpoint fell on a column owned by a node's border, the column grew but the box's own width did not — leaving a gap between the box and any incoming arrowhead. Filter label-line candidates so the widened column is always a free edge corridor, falling back to a node column only if the path offers no other segment. Fixes #63 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a307c1a commit 8ca77a5

5 files changed

Lines changed: 125 additions & 28 deletions

File tree

cmd/mapping_edge.go

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -112,39 +112,74 @@ func (g *graph) determineLabelLine(e *edge) {
112112
if lenLabel == 0 {
113113
return
114114
}
115+
// Widening a column that is occupied by a node would push that node's
116+
// border out, leaving a visible gap between the box and any incoming
117+
// arrowhead. Prefer label-line candidates whose target column is a free
118+
// edge corridor; only fall back to a node column if no corridor segment
119+
// is available.
115120
prevStep := e.path[0]
121+
var largestLine []gridCoord
116122
var largestLineSize int
117-
// Init to first line if we find nothing else
118-
largestLine := []gridCoord{prevStep, e.path[1]}
119-
largestLineSize = 0
123+
var fallbackLine []gridCoord
124+
var fallbackLineSize int
120125
for _, step := range e.path[1:] {
121126
line := []gridCoord{gridCoord(prevStep), gridCoord(step)}
127+
prevStep = step
122128
lineWidth := g.calculateLineWidth(line)
129+
if g.isNodeColumn(labelMiddleX(line)) {
130+
if lineWidth > fallbackLineSize {
131+
fallbackLineSize = lineWidth
132+
fallbackLine = line
133+
}
134+
continue
135+
}
123136
if lineWidth >= lenLabel {
124137
largestLine = line
125138
break
126-
} else if lineWidth > largestLineSize {
139+
}
140+
if lineWidth > largestLineSize {
127141
largestLineSize = lineWidth
128142
largestLine = line
129143
}
130-
prevStep = step
131144
}
132-
133-
var maxX, minX int
134-
if largestLine[0].x > largestLine[1].x {
135-
maxX = largestLine[0].x
136-
minX = largestLine[1].x
137-
} else {
138-
maxX = largestLine[1].x
139-
minX = largestLine[0].x
145+
if largestLine == nil {
146+
largestLine = fallbackLine
140147
}
141-
middleX := minX + (maxX-minX)/2
148+
if largestLine == nil {
149+
// Path only had a single segment that lives on a node column; use it
150+
// rather than dropping the label entirely.
151+
largestLine = []gridCoord{e.path[0], e.path[1]}
152+
}
153+
154+
middleX := labelMiddleX(largestLine)
142155
log.Debugf("Increasing column width for column %v from size %v to %v", middleX, g.columnWidth[middleX], lenLabel+2)
143156
g.columnWidth[middleX] = Max(g.columnWidth[middleX], lenLabel+2) // Wrap with dashes + arrowhead
144157
log.Debugf("New column sizes: %v", g.columnWidth)
145158
e.labelLine = largestLine
146159
}
147160

161+
func labelMiddleX(line []gridCoord) int {
162+
minX, maxX := line[0].x, line[1].x
163+
if minX > maxX {
164+
minX, maxX = maxX, minX
165+
}
166+
return minX + (maxX-minX)/2
167+
}
168+
169+
// isNodeColumn reports whether grid column x is occupied by any node.
170+
// Widening such a column distorts the box that owns it.
171+
func (g *graph) isNodeColumn(x int) bool {
172+
for _, n := range g.nodes {
173+
if n.gridCoord == nil {
174+
continue
175+
}
176+
if x >= n.gridCoord.x && x <= n.gridCoord.x+2 {
177+
return true
178+
}
179+
}
180+
return false
181+
}
182+
148183
func (g graph) calculateLineWidth(line []gridCoord) int {
149184
totalSize := 0
150185
for _, c := range line {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
graph TD
2+
A --> B
3+
B --> C
4+
C -->|back1| A
5+
C -->|back2| B
6+
---
7+
+---+
8+
| |
9+
| A |<--+
10+
| | |
11+
+---+ |
12+
| |
13+
| |
14+
| |
15+
| |
16+
v |
17+
+---+ |
18+
| | |
19+
| B |<back1
20+
| | |
21+
+---+ |
22+
| |
23+
| |
24+
| back2
25+
| |
26+
v |
27+
+---+ |
28+
| | |
29+
| C |---+
30+
| |
31+
+---+

cmd/testdata/ascii/bidirectional_edge_labels_lr.txt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ graph LR
22
A -->|workload exits| B
33
B -->|run| A
44
---
5-
+---+ +-----+
6-
| | | |
7-
| A |workload-exits->| B |
8-
| | | |
9-
+---+ +-----+
10-
^ run
11-
+---------------------+
5+
+---+ +---+
6+
| | | |
7+
| A |workload-exits->| B |
8+
| | | |
9+
+---+ +---+
10+
^ |
11+
+--------run---------+

cmd/testdata/ascii/duplicate_edge_labels.txt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ graph LR
22
A -->|miss| B
33
A -->|hit| B
44
---
5-
+-----+ +---+
6-
| | | |
7-
| A |miss->| B |
8-
| | | |
9-
+-----+ +---+
10-
hit ^
11-
+-----------+
5+
+---+ +---+
6+
| | | |
7+
| A |miss->| B |
8+
| | | |
9+
+---+ +---+
10+
| ^
11+
+---hit----+
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
graph TD
2+
A --> B
3+
B --> C
4+
C -->|back1| A
5+
C -->|back2| B
6+
---
7+
┌───┐
8+
│ │
9+
│ A │◄──┐
10+
│ │ │
11+
└─┬─┘ │
12+
│ │
13+
│ │
14+
│ │
15+
│ │
16+
▼ │
17+
┌───┐ │
18+
│ │ │
19+
│ B │◄back1
20+
│ │ │
21+
└─┬─┘ │
22+
│ │
23+
│ │
24+
│ back2
25+
│ │
26+
▼ │
27+
┌───┐ │
28+
│ │ │
29+
│ C ├───┘
30+
│ │
31+
└───┘

0 commit comments

Comments
 (0)