957957
958958# --------------------------------------------------------------------------------------------------
959959"""
960- text_repel (points, labels; fontsize=10, force_push=1.0, force_pull=0.01,
960+ textrepel (points, labels; fontsize=10, force_push=1.0, force_pull=0.01,
961961 max_iter=500, padding=0.15) -> Matrix{Float64}
962962
963963Compute adjusted text label positions so that they do not overlap each other or the data points,
@@ -976,10 +976,31 @@ similar to R's `ggrepel`. Uses a force-directed simulation: labels repel each ot
976976### Returns
977977A `Matrix{Float64}` of size `(N, 2)` with adjusted `(x, y)` positions in data coordinates.
978978"""
979- function text_repel (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 )
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 )
981+ ax, ay, pw, ph, sx, sy, xmin, ymin, n = _repel_setup (points)
982+ n != length (labels) && error (" Number of points ($n ) must match number of labels ($(length (labels)) )" )
983+ fs = fontsize * 2.54 / 72
984+ pad = Float64 (padding)
985+ hws = [length (l) * 0.55 * fs / 2 + pad for l in labels]
986+ 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)
989+ end
981990
982- # Extract point coordinates
991+ # --------------------------------------------------------------------------------------------------
992+ function circlerepel (points; diameter:: Real = 10 , force_push:: Real = 1.0 , force_pull:: Real = 0.01 ,
993+ max_iter:: Int = 500 , offset= 10 )
994+ 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)
1000+ end
1001+
1002+ # --------------------------------------------------------------------------------------------------
1003+ function _repel_setup (points)
9831004 if isa (points, GMTdataset)
9841005 px = Float64 .(points[:,1 ]); py = Float64 .(points[:,2 ])
9851006 elseif isa (points, Vector{<: GMTdataset })
@@ -988,9 +1009,6 @@ function text_repel(points, labels::Vector{<:AbstractString}; fontsize::Int=10,
9881009 px = Float64 .(points[:,1 ]); py = Float64 .(points[:,2 ])
9891010 end
9901011 n = length (px)
991- n != length (labels) && error (" Number of points ($n ) must match number of labels ($(length (labels)) )" )
992-
993- # Plot region from CTRL.limits, fallback to data bbox
9941012 xmin, xmax, ymin, ymax = (CTRL. limits[7 ] != 0 || CTRL. limits[8 ] != 0 ) ?
9951013 (CTRL. limits[7 ], CTRL. limits[8 ], CTRL. limits[9 ], CTRL. limits[10 ]) :
9961014 (CTRL. limits[1 ] != CTRL. limits[2 ]) ?
@@ -1001,39 +1019,33 @@ function text_repel(points, labels::Vector{<:AbstractString}; fontsize::Int=10,
10011019 (dy == 0 ) && (ymin -= 0.5 ; ymax += 0.5 ; dy = 1.0 )
10021020 pw, ph = _get_plotsize ()
10031021 sx, sy = pw / dx, ph / dy
1004-
1005- # Convert anchor points to cm space
10061022 ax = (px .- xmin) .* sx
10071023 ay = (py .- ymin) .* sy
1024+ return ax, ay, pw, ph, sx, sy, xmin, ymin, n
1025+ end
10081026
1009- # Text box half-dimensions in cm (with padding)
1010- fs = fontsize * 2.54 / 72
1011- char_w = 0.55 * fs
1012- char_h = fs
1013- pad = Float64 (padding)
1014- hws = [length (l) * char_w / 2 + pad for l in labels]
1015- hhs = fill (char_h / 2 + pad, n)
1016-
1017- # Initialize label positions at anchor points
1027+ # --------------------------------------------------------------------------------------------------
1028+ function _repel_core (ax, ay, hws, hhs, pw, ph, sx, sy, xmin, ymin, fp, fa, min_off, max_iter)
1029+ n = length (ax)
10181030 lx = copy (ax)
10191031 ly = copy (ay)
1020- vx = zeros (n) # velocities
1032+ vx = zeros (n)
10211033 vy = zeros (n)
1034+ fx = zeros (n)
1035+ fy = zeros (n)
1036+ decay = 0.7
10221037
1023- fp = Float64 (force_push)
1024- fa = Float64 (force_pull)
1025- decay = 0.7 # velocity damping
1026-
1038+ global iters = 0
1039+ move_hist = fill (Inf , 10 )
10271040 for _iter in 1 : max_iter
1028- fx = zeros (n )
1029- fy = zeros (n )
1041+ fill! (fx, 0.0 )
1042+ fill! (fy, 0.0 )
10301043
10311044 # Repulsion between label pairs
10321045 for i in 1 : n- 1 , j in i+ 1 : n
1033- ox = (hws[i] + hws[j]) - abs (lx[i] - lx[j]) # overlap in x
1034- oy = (hhs[i] + hhs[j]) - abs (ly[i] - ly[j]) # overlap in y
1035- (ox <= 0 || oy <= 0 ) && continue # no overlap
1036- # Push proportional to overlap area
1046+ ox = (hws[i] + hws[j]) - abs (lx[i] - lx[j])
1047+ oy = (hhs[i] + hhs[j]) - abs (ly[i] - ly[j])
1048+ (ox <= 0 || oy <= 0 ) && continue
10371049 area = ox * oy
10381050 dx = lx[i] - lx[j]
10391051 dy = ly[i] - ly[j]
@@ -1045,40 +1057,114 @@ function text_repel(points, labels::Vector{<:AbstractString}; fontsize::Int=10,
10451057 fx[j] -= fdx; fy[j] -= fdy
10461058 end
10471059
1048- # Repulsion from data points (push labels away from all anchor points)
1060+ # Repulsion from data points
10491061 for i in 1 : n, j in 1 : n
10501062 dx = lx[i] - ax[j]
10511063 dy = ly[i] - ay[j]
1052- # Check if point j is inside label i's box
10531064 (abs (dx) > hws[i] || abs (dy) > hhs[i]) && continue
10541065 dist = max (hypot (dx, dy), 1e-6 )
10551066 force = fp * 0.5 / dist
10561067 fx[i] += force * dx / dist
10571068 fy[i] += force * dy / dist
10581069 end
10591070
1060- # Attraction back to own anchor (spring force)
1071+ # Attraction back to own anchor
10611072 for i in 1 : n
10621073 fx[i] -= fa * (lx[i] - ax[i])
10631074 fy[i] -= fa * (ly[i] - ay[i])
10641075 end
10651076
10661077 # Update velocities and positions
1067- any_movement = false
1078+ max_move = 0.0
10681079 for i in 1 : n
10691080 vx[i] = (vx[i] + fx[i]) * decay
10701081 vy[i] = (vy[i] + fy[i]) * decay
1082+ spd = hypot (vx[i], vy[i])
1083+ if spd > 0.5
1084+ vx[i] *= 0.5 / spd
1085+ vy[i] *= 0.5 / spd
1086+ end
1087+ old_x, old_y = lx[i], ly[i]
10711088 lx[i] += vx[i]
10721089 ly[i] += vy[i]
1073- # Clamp to plot region (cm space)
1074- lx[i] = clamp (lx[i], hws[i], pw - hws[i])
1075- ly[i] = clamp (ly[i], hhs[i], ph - hhs[i])
1076- (abs (vx[i]) > 1e-4 || abs (vy[i]) > 1e-4 ) && (any_movement = true )
1090+ cx = clamp (lx[i], hws[i], pw - hws[i])
1091+ cy = clamp (ly[i], hhs[i], ph - hhs[i])
1092+ (cx != lx[i]) && (vx[i] = 0.0 )
1093+ (cy != ly[i]) && (vy[i] = 0.0 )
1094+ lx[i] = cx; ly[i] = cy
1095+ max_move = max (max_move, abs (lx[i] - old_x), abs (ly[i] - old_y))
1096+ end
1097+ iters += 1
1098+ # println(" iter $iters: max_move = $max_move")
1099+ move_hist[mod1 (_iter, 10 )] = max_move
1100+ (_iter >= 50 && minimum (move_hist) >= maximum (move_hist) * 0.9 ) && break
1101+ end
1102+ # println("textrepel: completed $iters iterations")
1103+
1104+ # Enforce minimum offset
1105+ if min_off > 0
1106+ for i in 1 : n
1107+ dx = lx[i] - ax[i]
1108+ dy = ly[i] - ay[i]
1109+ dist = hypot (dx, dy)
1110+ if dist < min_off && dist > 1e-6
1111+ scale = min_off / dist
1112+ lx[i] = ax[i] + dx * scale
1113+ ly[i] = ay[i] + dy * scale
1114+ lx[i] = clamp (lx[i], hws[i], pw - hws[i])
1115+ ly[i] = clamp (ly[i], hhs[i], ph - hhs[i])
1116+ elseif dist < 1e-6
1117+ ly[i] = ay[i] + min_off
1118+ ly[i] = clamp (ly[i], hhs[i], ph - hhs[i])
1119+ end
1120+ end
1121+ end
1122+
1123+ # Pull-back: minimize offset without creating overlaps
1124+ for _pull in 1 : 50
1125+ moved = false
1126+ for i in 1 : n
1127+ tox = ax[i] - lx[i]
1128+ toy = ay[i] - ly[i]
1129+ cur_dist = hypot (tox, toy)
1130+ (cur_dist < 1e-6 ) && continue
1131+ max_pull = min_off > 0 ? max (cur_dist - min_off, 0.0 ) / cur_dist : 1.0
1132+ (max_pull < 1e-6 ) && continue
1133+
1134+ lo, hi, best_t = 0.0 , max_pull, 0.0
1135+ for _ in 1 : 20
1136+ t = (lo + hi) * 0.5
1137+ nx = lx[i] + t * tox
1138+ ny = ly[i] + t * toy
1139+ ok = true
1140+ for j in 1 : n
1141+ (i == j) && continue
1142+ if (hws[i] + hws[j]) - abs (nx - lx[j]) > 0 && (hhs[i] + hhs[j]) - abs (ny - ly[j]) > 0
1143+ ok = false ; break
1144+ end
1145+ end
1146+ if ok
1147+ for j in 1 : n
1148+ if abs (nx - ax[j]) < hws[i] && abs (ny - ay[j]) < hhs[i]
1149+ ok = false ; break
1150+ end
1151+ end
1152+ end
1153+ if ok
1154+ best_t = t; lo = t
1155+ else
1156+ hi = t
1157+ end
1158+ end
1159+ if best_t > 1e-6
1160+ lx[i] += best_t * tox
1161+ ly[i] += best_t * toy
1162+ moved = true
1163+ end
10771164 end
1078- ! any_movement && break
1165+ ! moved && break
10791166 end
10801167
1081- # Convert back to data coordinates
10821168 result = Matrix {Float64} (undef, n, 2 )
10831169 for i in 1 : n
10841170 result[i, 1 ] = lx[i] / sx + xmin
0 commit comments