958958# --------------------------------------------------------------------------------------------------
959959"""
960960 textrepel(points, labels; fontsize=10, force_push=1.0, force_pull=0.01,
961- max_iter=500, padding =0.15) -> Matrix{Float64}
961+ max_iter=500, pad =0.15, offset=10, offsets=false ) -> Matrix{Float64}
962962
963963Compute adjusted text label positions so that they do not overlap each other or the data points,
964964similar to R's `ggrepel`. Uses a force-directed simulation: labels repel each other and data points
@@ -971,61 +971,69 @@ similar to R's `ggrepel`. Uses a force-directed simulation: labels repel each ot
971971- `force_push`: repulsion strength multiplier (default 1.0).
972972- `force_pull`: attraction strength back to anchor (default 0.01).
973973- `max_iter`: maximum number of simulation iterations (default 500).
974- - `padding`: extra padding around text boxes in cm (default 0.15).
974+ - `pad`: extra padding around text boxes in cm (default 0.15).
975+ - `offset`: minimum distance between label and anchor point in points (default 10).
976+ - `offsets`: if `true`, return label displacements in cm from each anchor point instead of
977+ absolute data coordinates (default `false`).
975978
976979### Returns
977- A `Matrix{Float64}` of size `(N, 2)` with adjusted `(x, y)` positions in data coordinates.
980+ A `Matrix{Float64}` of size `(N, 2)`. When `offsets=false` (default), values are adjusted
981+ `(x, y)` positions in data coordinates. When `offsets=true`, values are `(dx, dy)` offsets
982+ in centimetres from each original anchor point.
978983"""
979- function textrepel (points, labels:: Vector{<:AbstractString} ; fontsize:: Int = 10 ,
980- force_push:: Real = 1.0 , force_pull:: Real = 0.01 , max_iter:: Int = 500 , padding:: Real = 0.15 , offset= 10 )
984+ function textrepel (points:: Union{Matrix{<:Real}, GMTdataset} , labels:: Vector{<:AbstractString} ; fontsize:: Int = 10 , force_push:: Real = 1.0 ,
985+ force_pull:: Real = 0.01 , max_iter:: Int = 500 , pad:: Real = 0.15 , offset:: Int = 10 , offsets:: Bool = false )
986+ isa (points, Matrix) && (points = mat2ds (Float64 .(points)))
987+ _textrepel (points, labels; fontsize= fontsize, force_push= Float64 (force_push), force_pull= Float64 (force_pull),
988+ max_iter= max_iter, pad= Float64 (pad), offset= offset, offsets= offsets)
989+ end
990+ function _textrepel (points:: GMTdataset , labels:: Vector{<:AbstractString} ; fontsize:: Int = 10 , force_push:: Float64 = 1.0 ,
991+ force_pull:: Float64 = 0.01 , max_iter:: Int = 500 , pad:: Float64 = 0.15 , offset:: Int = 10 , offsets:: Bool = false )
981992 ax, ay, pw, ph, sx, sy, xmin, ymin, n = _repel_setup (points)
982993 n != length (labels) && error (" Number of points ($n ) must match number of labels ($(length (labels)) )" )
983994 fs = fontsize * 2.54 / 72
984- pad = Float64 (padding)
985995 hws = [length (l) * 0.55 * fs / 2 + pad for l in labels]
986996 hhs = fill (fs / 2 + pad, n)
987- return _repel_core (ax, ay, hws, hhs, pw, ph, sx, sy, xmin, ymin,
988- Float64 (force_push), Float64 ( force_pull), Float64 ( offset) * 2.54 / 72 , max_iter)
997+ return _repel_core (ax, ay, hws, hhs, pw, ph, sx, sy, xmin, ymin, Float64 (force_push),
998+ force_pull, offset * 2.54 / 72 , max_iter, offsets )
989999end
9901000
9911001# --------------------------------------------------------------------------------------------------
9921002function circlerepel (points; diameter:: Real = 10 , force_push:: Real = 1.0 , force_pull:: Real = 0.01 ,
993- max_iter:: Int = 500 , offset= 10 )
1003+ max_iter:: Int = 500 , offset= 10 , offsets:: Bool = false )
1004+ isa (points, Matrix) && (points = mat2ds (Float64 .(points)))
1005+ _circlerepel (points; diameter= Float64 (diameter), force_push= Float64 (force_push), force_pull= Float64 (force_pull),
1006+ max_iter= max_iter, offset= offset, offsets= offsets)
1007+ end
1008+ function _circlerepel (points:: GMTdataset ; diameter:: Float64 = 10 , force_push:: Float64 = 1.0 , force_pull:: Float64 = 0.01 ,
1009+ max_iter:: Int = 500 , offset:: Int = 10 , offsets:: Bool = false )
9941010 ax, ay, pw, ph, sx, sy, xmin, ymin, n = _repel_setup (points)
995- rad = Float64 (diameter) * 2.54 / 72 / 2
996- hws = fill (rad, n)
997- hhs = fill (rad, n)
998- return _repel_core (ax, ay, hws, hhs, pw, ph, sx, sy, xmin, ymin,
999- Float64 (force_push), Float64 (force_pull), Float64 (offset) * 2.54 / 72 , max_iter)
1011+ rad = diameter * 2.54 / 72 / 2
1012+ hws, hhs = fill (rad, n), fill (rad, n)
1013+ return _repel_core (ax, ay, hws, hhs, pw, ph, sx, sy, xmin, ymin, Float64 (force_push),
1014+ force_pull, offset * 2.54 / 72 , max_iter, offsets)
10001015end
10011016
10021017# --------------------------------------------------------------------------------------------------
1003- function _repel_setup (points)
1004- if isa (points, GMTdataset)
1005- px = Float64 .(points[:,1 ]); py = Float64 .(points[:,2 ])
1006- elseif isa (points, Vector{<: GMTdataset })
1007- px = Float64 .(points[1 ][:,1 ]); py = Float64 .(points[1 ][:,2 ])
1008- else
1009- px = Float64 .(points[:,1 ]); py = Float64 .(points[:,2 ])
1010- end
1011- n = length (px)
1018+ function _repel_setup (points:: GMTdataset )
1019+ n = size (points,1 )
10121020 xmin, xmax, ymin, ymax = (CTRL. limits[7 ] != 0 || CTRL. limits[8 ] != 0 ) ?
10131021 (CTRL. limits[7 ], CTRL. limits[8 ], CTRL. limits[9 ], CTRL. limits[10 ]) :
10141022 (CTRL. limits[1 ] != CTRL. limits[2 ]) ?
10151023 (CTRL. limits[1 ], CTRL. limits[2 ], CTRL. limits[3 ], CTRL. limits[4 ]) :
1016- (minimum (px), maximum (px), minimum (py), maximum (py) )
1024+ (points . bbox[ 1 ], points . bbox[ 2 ], points . bbox[ 3 ], points . bbox[ 4 ] )
10171025 dx = xmax - xmin; dy = ymax - ymin
10181026 (dx == 0 ) && (xmin -= 0.5 ; xmax += 0.5 ; dx = 1.0 )
10191027 (dy == 0 ) && (ymin -= 0.5 ; ymax += 0.5 ; dy = 1.0 )
10201028 pw, ph = _get_plotsize ()
10211029 sx, sy = pw / dx, ph / dy
1022- ax = (px .- xmin) .* sx
1023- ay = (py .- ymin) .* sy
1030+ ax = (points[:, 1 ] .- xmin) .* sx
1031+ ay = (points[:, 2 ] .- ymin) .* sy
10241032 return ax, ay, pw, ph, sx, sy, xmin, ymin, n
10251033end
10261034
10271035# --------------------------------------------------------------------------------------------------
1028- function _repel_core (ax, ay, hws, hhs, pw, ph, sx, sy, xmin, ymin, fp, fa, min_off, max_iter)
1036+ function _repel_core (ax, ay, hws, hhs, pw, ph, sx, sy, xmin, ymin, fp, fa, min_off, max_iter, offsets :: Bool = false )
10291037 n = length (ax)
10301038 lx = copy (ax)
10311039 ly = copy (ay)
@@ -1057,13 +1065,18 @@ function _repel_core(ax, ay, hws, hhs, pw, ph, sx, sy, xmin, ymin, fp, fa, min_o
10571065 fx[j] -= fdx; fy[j] -= fdy
10581066 end
10591067
1060- # Repulsion from data points
1068+ # Repulsion from data points — box-edge aware.
1069+ # Acts whenever the nearest box edge is within min_off of the anchor,
1070+ # so diagonal labels are pushed far enough even when anchor is outside the box.
10611071 for i in 1 : n, j in 1 : n
10621072 dx = lx[i] - ax[j]
10631073 dy = ly[i] - ay[j]
1064- (abs (dx) > hws[i] || abs (dy) > hhs[i]) && continue
1074+ near_x = clamp (ax[j], lx[i]- hws[i], lx[i]+ hws[i])
1075+ near_y = clamp (ay[j], ly[i]- hhs[i], ly[i]+ hhs[i])
1076+ edge_dist = hypot (ax[j]- near_x, ay[j]- near_y)
1077+ (edge_dist >= min_off) && continue
10651078 dist = max (hypot (dx, dy), 1e-6 )
1066- force = fp * 0.5 / dist
1079+ force = fp * 0.5 * ( 1.0 - edge_dist / max (min_off, 1e-10 )) / dist
10671080 fx[i] += force * dx / dist
10681081 fy[i] += force * dy / dist
10691082 end
@@ -1101,7 +1114,8 @@ function _repel_core(ax, ay, hws, hhs, pw, ph, sx, sy, xmin, ymin, fp, fa, min_o
11011114 end
11021115 # println("textrepel: completed $iters iterations")
11031116
1104- # Enforce minimum offset
1117+ # Enforce minimum center-distance between anchor and label center (original behaviour).
1118+ # This respects min_off for all label directions, especially horizontal/vertical.
11051119 if min_off > 0
11061120 for i in 1 : n
11071121 dx = lx[i] - ax[i]
@@ -1120,6 +1134,36 @@ function _repel_core(ax, ay, hws, hhs, pw, ph, sx, sy, xmin, ymin, fp, fa, min_o
11201134 end
11211135 end
11221136
1137+ # Diagonal correction: for labels at an angle (not near-horizontal/vertical),
1138+ # push the label further so the box does not visually overlap the anchor marker.
1139+ # The extra distance scales with the box height (∝ fontsize, independent of text length)
1140+ # and is applied ONLY when both x and y placement components are significant.
1141+ for i in 1 : n
1142+ dx = lx[i] - ax[i]
1143+ dy = ly[i] - ay[i]
1144+ dist = max (hypot (dx, dy), 1e-6 )
1145+ (abs (dx / dist) <= 0.25 || abs (dy / dist) <= 0.25 ) && continue # near-horiz/vert: skip
1146+ near_x = clamp (ax[i], lx[i]- hws[i], lx[i]+ hws[i])
1147+ near_y = clamp (ay[i], ly[i]- hhs[i], ly[i]+ hhs[i])
1148+ clr = hhs[i] / 2 # minimum edge-clearance for diagonal labels
1149+ (hypot (ax[i]- near_x, ay[i]- near_y) >= clr) && continue # already clear enough
1150+ ux_s = dx / dist; uy_s = dy / dist
1151+ lo, hi = 0.0 , hypot (hws[i], hhs[i]) + clr + dist
1152+ for _ in 1 : 30
1153+ mid = (lo + hi) / 2
1154+ nlx = lx[i] + mid * ux_s; nly = ly[i] + mid * uy_s
1155+ nnx = clamp (ax[i], nlx- hws[i], nlx+ hws[i])
1156+ nny = clamp (ay[i], nly- hhs[i], nly+ hhs[i])
1157+ if hypot (ax[i]- nnx, ay[i]- nny) < clr
1158+ lo = mid
1159+ else
1160+ hi = mid
1161+ end
1162+ end
1163+ lx[i] = clamp (lx[i] + hi * ux_s, hws[i], pw - hws[i])
1164+ ly[i] = clamp (ly[i] + hi * uy_s, hhs[i], ph - hhs[i])
1165+ end
1166+
11231167 # Pull-back: minimize offset without creating overlaps
11241168 for _pull in 1 : 50
11251169 moved = false
@@ -1145,7 +1189,20 @@ function _repel_core(ax, ay, hws, hhs, pw, ph, sx, sy, xmin, ymin, fp, fa, min_o
11451189 end
11461190 if ok
11471191 for j in 1 : n
1148- if abs (nx - ax[j]) < hws[i] && abs (ny - ay[j]) < hhs[i]
1192+ if j == i
1193+ # Own anchor: diagonal labels need edge-clearance; others just can't have anchor inside box
1194+ ddx = nx - ax[i]; ddy = ny - ay[i]
1195+ ddist = max (hypot (ddx, ddy), 1e-6 )
1196+ if abs (ddx/ ddist) > 0.25 && abs (ddy/ ddist) > 0.25
1197+ nnx = clamp (ax[i], nx- hws[i], nx+ hws[i])
1198+ nny = clamp (ay[i], ny- hhs[i], ny+ hhs[i])
1199+ if hypot (ax[i]- nnx, ay[i]- nny) < hhs[i]/ 2
1200+ ok = false ; break
1201+ end
1202+ elseif abs (nx - ax[i]) < hws[i] && abs (ny - ay[i]) < hhs[i]
1203+ ok = false ; break
1204+ end
1205+ elseif abs (nx - ax[j]) < hws[i] && abs (ny - ay[j]) < hhs[i]
11491206 ok = false ; break
11501207 end
11511208 end
@@ -1166,9 +1223,16 @@ function _repel_core(ax, ay, hws, hhs, pw, ph, sx, sy, xmin, ymin, fp, fa, min_o
11661223 end
11671224
11681225 result = Matrix {Float64} (undef, n, 2 )
1169- for i in 1 : n
1170- result[i, 1 ] = lx[i] / sx + xmin
1171- result[i, 2 ] = ly[i] / sy + ymin
1226+ if offsets
1227+ for i in 1 : n
1228+ result[i, 1 ] = lx[i] - ax[i] # cm offset from anchor
1229+ result[i, 2 ] = ly[i] - ay[i]
1230+ end
1231+ else
1232+ for i in 1 : n
1233+ result[i, 1 ] = lx[i] / sx + xmin
1234+ result[i, 2 ] = ly[i] / sy + ymin
1235+ end
11721236 end
11731237 return result
11741238end
0 commit comments