@@ -425,12 +425,23 @@ function best_label_pos(curves::GDtype, labels::Vector{<:AbstractString}; fontsi
425425end
426426
427427function _best_label_pos (D:: Vector{<:GMTdataset} , labels:: Vector{String} , nc:: Int , fontsize:: Int , prefer:: Symbol )
428- xmin, xmax, ymin, ymax = getregion (D)
428+ # Use the actual plot region (not data bbox) so cm↔data conversion matches the axes
429+ if CTRL. limits[7 ] != 0
430+ xmin, xmax, ymin, ymax = CTRL. limits[7 : 10 ]
431+ else
432+ xmin, xmax, ymin, ymax = getregion (D)
433+ end
429434 pw, ph = _get_plotsize ()
430435 sx, sy = pw / (xmax - xmin), ph / (ymax - ymin)
431436
432- # Convert to cm space for accurate visual distance/angle calculations
433- crv = [hcat ((D[k][:,1 ] .- xmin) .* sx, (D[k][:,2 ] .- ymin) .* sy) for k in 1 : nc]
437+ # Convert to cm space — only the visible portion of each curve (inside the plot region)
438+ crv = Vector {Matrix{Float64}} (undef, nc)
439+ for k in 1 : nc
440+ pts = D[k]. data
441+ mask = vec ((pts[:,1 ] .>= xmin) .& (pts[:,1 ] .<= xmax) .& (pts[:,2 ] .>= ymin) .& (pts[:,2 ] .<= ymax))
442+ vis = sum (mask) >= 2 ? pts[mask, :] : pts
443+ crv[k] = hcat ((vis[:,1 ] .- xmin) .* sx, (vis[:,2 ] .- ymin) .* sy)
444+ end
434445
435446 # Text half-dimensions in cm
436447 pt2cm = 2.54 / 72
@@ -439,22 +450,31 @@ function _best_label_pos(D::Vector{<:GMTdataset}, labels::Vector{String}, nc::In
439450 hws = [length (l) * char_w / 2 for l in labels] # half-width per label
440451 hh = char_h * 0.9 # half-height (shared)
441452
442- # Candidate range along arc length depends on preference
453+ # Preferred zone for targets (where labels WANT to be)
443454 prefer in (:begin , :middle , :end ) || error (" prefer must be :begin, :middle, or :end" )
444- frac_lo, frac_hi, frac_target = prefer == :begin ? (0.05 , 0.55 , 0.15 ) :
445- prefer == :end ? (0.45 , 0.95 , 0.85 ) :
446- (0.10 , 0.90 , 0.50 )
455+ frac_lo, frac_hi = prefer == :begin ? (0.05 , 0.30 ) :
456+ prefer == :end ? (0.60 , 0.95 ) : (0.20 , 0.80 )
457+ center = prefer == :begin ? 0.17 : prefer == :end ? 0.83 : 0.50
458+ if nc == 1
459+ frac_targets = [center]
460+ else
461+ half_spread = min (0.20 , 0.04 * nc)
462+ frac_targets = [center + half_spread * (2.0 * (i - 1 ) / (nc - 1 ) - 1.0 ) for i in 1 : nc]
463+ end
447464
448- # Generate candidate positions along each curve
449- ncand = 19
450- cands = [_bla_gen_candidates (crv[i], ncand, frac_lo, frac_hi) for i in 1 : nc]
465+ # Candidate range is wider than target zone — labels prefer the target but CAN escape if all
466+ # candidates in the preferred zone have crossings (e.g. steep curves bunched at the center)
467+ cand_lo = max (0.05 , frac_lo - 0.15 )
468+ cand_hi = min (0.95 , frac_hi + 0.15 )
469+ ncand = 29
470+ cands = [_bla_gen_candidates (crv[i], ncand, cand_lo, cand_hi) for i in 1 : nc]
451471
452472 # Greedy placement: maximize minimum clearance to other curves
453473 placed_cx = Float64[]; placed_cy = Float64[]; placed_a = Float64[]
454474 placed_hw = Float64[]; placed_hh = Float64[]
455475 result = Matrix {Float64} (undef, nc, 4 ) # each row: x1, y1, x2, y2
456476
457- half_cross = 0.3 # half-length of crossing segment in cm (short to avoid multi-intersection)
477+ half_cross = 0.3 # half-length of crossing segment in cm
458478
459479 for i in 1 : nc
460480 hw_i = hws[i]
@@ -465,13 +485,31 @@ function _best_label_pos(D::Vector{<:GMTdataset}, labels::Vector{String}, nc::In
465485 curvs = Vector {Float64} (undef, nj)
466486 fracs = Vector {Float64} (undef, nj)
467487
488+ # For each candidate: count crossings with other curves through the label bbox, measure clearance
489+ ncross_j = Vector {Int} (undef, nj)
490+ clears_j = Vector {Float64} (undef, nj)
491+ overlaps_j = falses (nj)
492+
468493 for j in 1 : nj
469494 cx, cy, ang, curv = cands[i][j]
470- fracs[j] = frac_lo + (frac_hi - frac_lo) * (j - 1 ) / max (nj - 1 , 1 )
471- quality = 0.0
495+ fracs[j] = cand_lo + (cand_hi - cand_lo) * (j - 1 ) / max (nj - 1 , 1 )
472496 curvs[j] = curv
473497
474- # Min distance from (cx,cy) to any segment of any other curve
498+ # Count how many OTHER curves cross through the label bounding box
499+ ncross = 0
500+ if nc > 1
501+ corners = _bla_corners (cx, cy, ang, hw_i, hh)
502+ for k in 1 : nc
503+ k == i && continue
504+ for ei in 1 : 4
505+ eni = mod1 (ei + 1 , 4 )
506+ ncross += _bla_crossing_count (corners[ei][1 ], corners[ei][2 ], corners[eni][1 ], corners[eni][2 ], crv[k])
507+ end
508+ end
509+ end
510+ ncross_j[j] = ncross
511+
512+ # Min distance to nearest other curve
475513 clearance = nc > 1 ? 1e10 : 0.0
476514 for k in 1 : nc
477515 k == i && continue
@@ -481,70 +519,112 @@ function _best_label_pos(D::Vector{<:GMTdataset}, labels::Vector{String}, nc::In
481519 clearance = min (clearance, d)
482520 end
483521 end
522+ clears_j[j] = clearance
484523
485- # Penalty when another curve is very close to the label center
486- if clearance < hh * 2
487- quality -= (hh * 2 - clearance) * 10.0
524+ # Reject candidates too close to plot edges (label or crossing segment would be clipped)
525+ margin = hh * 2.0
526+ if cx < margin || cy < margin || cx > pw - margin || cy > ph - margin
527+ overlaps_j[j] = true # reuse flag to mark as unusable
528+ continue
488529 end
489530
490- # Penalty if perpendicular crosses own curve more than once
491- nx, ny = - sin (ang), cos (ang)
492- x1c, y1c = cx - nx * half_cross, cy - ny * half_cross
493- x2c, y2c = cx + nx * half_cross, cy + ny * half_cross
494- nself = _bla_crossing_count (x1c, y1c, x2c, y2c, crv[i])
495- nself > 1 && (quality -= nself * 20.0 )
496-
497- # Penalty if any OTHER curve crosses through the label bounding box
498- if nc > 1
499- corners = _bla_corners (cx, cy, ang, hw_i * 1.3 , hh * 1.3 )
500- for k in 1 : nc
501- k == i && continue
502- for ei in 1 : 4
503- eni = mod1 (ei + 1 , 4 )
504- ncx = _bla_crossing_count (corners[ei][1 ], corners[ei][2 ], corners[eni][1 ], corners[eni][2 ], crv[k])
505- ncx > 0 && (quality -= ncx * 30.0 )
506- end
531+ # Check overlap with already-placed labels (with padding so labels don't get too close)
532+ pad = hh * 0.5 # extra margin around each label box
533+ for p in eachindex (placed_cx)
534+ if _bla_rboxes_overlap (cx, cy, ang, hw_i + pad, hh + pad,
535+ placed_cx[p], placed_cy[p], placed_a[p], placed_hw[p] + pad, placed_hh[p] + pad)
536+ overlaps_j[j] = true ; break
507537 end
508538 end
539+ end
509540
510- # Penalty for overlap with already-placed labels
511- for p in eachindex (placed_cx)
512- if _bla_rboxes_overlap (cx, cy, ang, hw_i, hh,
513- placed_cx[p], placed_cy[p], placed_a[p], placed_hw[p], placed_hh[p])
514- quality -= 100.0 ; break
541+ # Pick best: filter by crossings → label overlap → clearance → curvature; pick closest to frac_target
542+ min_ncross = minimum (ncross_j)
543+ clear_thresh = hh * 3.0 # min clearance from other curves (not squeezed)
544+ sorted_curvs = sort (curvs)
545+ curv_thresh = sorted_curvs[round (Int, nj * 0.75 )] # 75th percentile — only reject extreme curvature
546+
547+ # Minimum clearance: curve must NOT touch label box — use diagonal of half-dimensions
548+ min_clear = hypot (hw_i, hh) * 1.2
549+
550+ # Try with progressively relaxed clearance, but NEVER below min_clear
551+ best_j = 1
552+ best_dist = Inf
553+ for ct in (clear_thresh, (clear_thresh + min_clear) / 2 , min_clear)
554+ for j in 1 : nj
555+ ncross_j[j] > min_ncross && continue
556+ overlaps_j[j] && continue
557+ clears_j[j] < ct && continue
558+ curvs[j] > curv_thresh && continue
559+ frac_dist = abs (fracs[j] - frac_targets[i])
560+ if frac_dist < best_dist
561+ best_dist = frac_dist; best_j = j
515562 end
516563 end
564+ best_dist < Inf && break
565+ end
517566
518- qualities[j] = quality
567+ if best_dist == Inf # relax curvature filter, keep min_clear
568+ for ct in (clear_thresh, min_clear)
569+ for j in 1 : nj
570+ ncross_j[j] > min_ncross && continue
571+ overlaps_j[j] && continue
572+ clears_j[j] < ct && continue
573+ frac_dist = abs (fracs[j] - frac_targets[i])
574+ if frac_dist < best_dist
575+ best_dist = frac_dist; best_j = j
576+ end
577+ end
578+ best_dist < Inf && break
579+ end
519580 end
520581
521- # Phase 2: among candidates with acceptable quality, pick the best balance of
522- # proximity to frac_target and low curvature (prefer straight sections).
523- best_quality = maximum (qualities)
524- threshold = best_quality - 5.0
582+ if best_dist == Inf # all labels overlap — farthest from placed labels, then closest to target
583+ max_sep = - Inf
584+ for j in 1 : nj
585+ ncross_j[j] > min_ncross && continue
586+ cx_j, cy_j = cands[i][j][1 ], cands[i][j][2 ]
587+ min_d = isempty (placed_cx) ? Inf : minimum (hypot (cx_j - placed_cx[p], cy_j - placed_cy[p]) for p in eachindex (placed_cx))
588+ max_sep = max (max_sep, min_d)
589+ end
590+ thresh_sep = max_sep * 0.8
591+ best_dist = Inf
592+ for j in 1 : nj
593+ ncross_j[j] > min_ncross && continue
594+ cx_j, cy_j = cands[i][j][1 ], cands[i][j][2 ]
595+ min_d = isempty (placed_cx) ? Inf : minimum (hypot (cx_j - placed_cx[p], cy_j - placed_cy[p]) for p in eachindex (placed_cx))
596+ min_d < thresh_sep && continue
597+ frac_dist = abs (fracs[j] - frac_targets[i])
598+ if frac_dist < best_dist
599+ best_dist = frac_dist; best_j = j
600+ end
601+ end
602+ end
525603
526- best_j = 1
527- best_score2 = - Inf
528- for j in 1 : nj
529- qualities[j] < threshold && continue
530- frac_dist = abs (fracs[j] - frac_target) # 0..0.4 typically
531- score2 = - frac_dist * 10.0 - curvs[j] * 8.0 # balance preference vs curvature
532- if score2 > best_score2
533- best_score2 = score2
534- best_j = j
604+ if best_dist == Inf # absolute last resort: closest to target, NO filters at all
605+ for j in 1 : nj
606+ frac_dist = abs (fracs[j] - frac_targets[i])
607+ if frac_dist < best_dist
608+ best_dist = frac_dist; best_j = j
609+ end
535610 end
536611 end
537612
538613 cx, cy, ang = cands[i][best_j][1 ], cands[i][best_j][2 ], cands[i][best_j][3 ]
539614 push! (placed_cx, cx); push! (placed_cy, cy); push! (placed_a, ang)
540615 push! (placed_hw, hw_i); push! (placed_hh, hh)
541616
542- # Short perpendicular crossing segment, convert to data coords
543- nx, ny = - sin (ang), cos (ang)
544- result[i,1 ] = (cx - nx * half_cross) / sx + xmin
545- result[i,2 ] = (cy - ny * half_cross) / sy + ymin
546- result[i,3 ] = (cx + nx * half_cross) / sx + xmin
547- result[i,4 ] = (cy + ny * half_cross) / sy + ymin
617+ # Convert to data coords; perpendicular in data-space from the cm-space tangent angle
618+ cx_dat = cx / sx + xmin
619+ cy_dat = cy / sy + ymin
620+ nx_d, ny_d = - sin (ang) / sy, cos (ang) / sx # perpendicular to tangent in data-space
621+ nd = hypot (nx_d, ny_d)
622+ nx_d /= nd; ny_d /= nd
623+ half_d = min (xmax - xmin, ymax - ymin) * 0.015
624+ result[i,1 ] = clamp (cx_dat - nx_d * half_d, xmin, xmax)
625+ result[i,2 ] = clamp (cy_dat - ny_d * half_d, ymin, ymax)
626+ result[i,3 ] = clamp (cx_dat + nx_d * half_d, xmin, xmax)
627+ result[i,4 ] = clamp (cy_dat + ny_d * half_d, ymin, ymax)
548628 end
549629 return result
550630end
@@ -601,6 +681,19 @@ function _label_pos_at_vals(D::Vector{<:GMTdataset}, nc::Int, xvals::Vector{Floa
601681 result[i,3 ] = best_px + nx * scale
602682 result[i,4 ] = best_py + ny * scale
603683 end
684+
685+ # Clamp all insertion points to the plot region so GMT doesn't skip labels outside it
686+ if CTRL. limits[7 ] != 0
687+ xmin, xmax, ymin, ymax = CTRL. limits[7 : 10 ]
688+ else
689+ xmin, xmax, ymin, ymax = getregion (D)
690+ end
691+ for i in 1 : nc
692+ result[i,1 ] = clamp (result[i,1 ], xmin, xmax)
693+ result[i,2 ] = clamp (result[i,2 ], ymin, ymax)
694+ result[i,3 ] = clamp (result[i,3 ], xmin, xmax)
695+ result[i,4 ] = clamp (result[i,4 ], ymin, ymax)
696+ end
604697 return result
605698end
606699
@@ -627,25 +720,19 @@ end
627720# Returns Vector of (x, y, tangent_angle, curvature) where curvature = max angle change (rad) over a local window.
628721function _bla_gen_candidates (c:: Matrix{Float64} , ncand:: Int , frac_lo:: Float64 = 0.1 , frac_hi:: Float64 = 0.9 )
629722 n = size (c, 1 )
630- arclen = Vector {Float64} (undef, n)
631- arclen[1 ] = 0.0
632- @inbounds for i in 2 : n
633- arclen[i] = arclen[i- 1 ] + hypot (c[i,1 ] - c[i- 1 ,1 ], c[i,2 ] - c[i- 1 ,2 ])
634- end
635723 # Per-segment angles
636724 seg_ang = Vector {Float64} (undef, max (n - 1 , 1 ))
637725 @inbounds for i in 1 : n- 1
638726 seg_ang[i] = atan (c[i+ 1 ,2 ] - c[i,2 ], c[i+ 1 ,1 ] - c[i,1 ])
639727 end
640- total = arclen[end ]
728+ # Distribute candidates evenly by INDEX (not arc-length) so that steep curves
729+ # don't bunch all candidates in the same visual spot.
641730 result = Vector {NTuple{4,Float64}} (undef, ncand)
642731 @inbounds for j in 1 : ncand
643732 frac = frac_lo + (frac_hi - frac_lo) * (j - 1 ) / max (ncand - 1 , 1 )
644- target = frac * total
645- idx = searchsortedlast (arclen, target)
646- idx = clamp (idx, 1 , n - 1 )
647- seg_len = arclen[idx+ 1 ] - arclen[idx]
648- t = seg_len > 0 ? (target - arclen[idx]) / seg_len : 0.0
733+ fidx = frac * (n - 1 ) + 1 # 1-based float index
734+ idx = clamp (floor (Int, fidx), 1 , n - 1 )
735+ t = fidx - idx
649736 px = c[idx,1 ] + t * (c[idx+ 1 ,1 ] - c[idx,1 ])
650737 py = c[idx,2 ] + t * (c[idx+ 1 ,2 ] - c[idx,2 ])
651738 # Smoothed tangent over a few neighboring segments
@@ -764,59 +851,36 @@ function _add_labellines_apply(arg1::Vector{<:GMTdataset}, _cmd::Vector{String},
764851 color = _extract_W_color (arg1[k]. header)
765852 font = color != " " ? " $(fontsize) p,,$color " : " $(fontsize) p"
766853 lbl = occursin (' ' , labels[k]) ? " \" $(labels[k]) \" " : labels[k]
767- sq = @sprintf (" -Sql%.10g/%.10g/%.10g/%.10g:+l%s+f%s+v" , pos[k,1 ], pos[k,2 ], pos[k,3 ], pos[k,4 ], lbl, font)
768- hdr = arg1[k]. header
769- hdr = replace (hdr, r" -Sq(?:[^\s\" ]|\" [^\" ]*\" )*" => " " ) # Remove any previous -Sq option
854+ sq = @sprintf (" -Sql%.7g/%.7g/%.7g/%.7g:+l%s+f%s+v" , pos[k,1 ], pos[k,2 ], pos[k,3 ], pos[k,4 ], lbl, font)
855+ hdr = replace (arg1[k]. header, r" -Sq(?:[^\s\" ]|\" [^\" ]*\" )*" => " " ) # Remove any previous -Sq option
770856 arg1[k]. header = string (hdr, " " , sq)
771857 end
772858end
773859
774860# Inject outside labels into d[:text] so they are processed by finish_PS_nested as a nested text call.
775861function _inject_outside_labels! (d:: Dict{Symbol,Any} , D:: Vector{<:GMTdataset} , labels:: Vector{String} , fontsize:: Int , _cmd:: Vector{String} )
776- info = _outside_label_data (D, labels, fontsize, _has_right_axis (_cmd[ 1 ]) )
862+ info = _outside_label_data (D, labels, fontsize)
777863 Dt = mat2ds (hcat (info. x, info. y), info. labels)
778864 color = info. colors[1 ]
779865 fnt = color != " " ? " $(fontsize) p,,$color " : " $(fontsize) p"
780866 d[:text ] = (data= Dt, font= fnt, justify= " ML" , offset= (0.15 , 0.0 ), noclip= true )
781867end
782868
783- # Check if the right axis frame is drawn by looking for 'E', 'e' or 'r' in the -B axes spec.
784- function _has_right_axis (cmd:: String ):: Bool
785- # Find -B options that specify axes (contain frame letters, not just intervals like -Baf)
786- for m in eachmatch (r" -B([A-Za-z]*)" , cmd)
787- s = m. captures[1 ]
788- # Axes specs contain W/w/S/s/E/e/N/n/l/r/t/b/u/d — skip pure interval specs like "af", "xaf", "p"
789- any (c -> c in " WSENwsen" , s) && return any (c -> c in " Ee" , s)
790- end
791- return false # No axes spec found, default frames don't draw right axis
792- end
793-
794869# Compute label positions outside the plot (at the right edge), with vertical repel to avoid overlaps.
795- function _outside_label_data (D:: Vector{<:GMTdataset} , labels:: Vector{String} , fontsize:: Int , right_axis :: Bool )
870+ function _outside_label_data (D:: Vector{<:GMTdataset} , labels:: Vector{String} , fontsize:: Int )
796871 nc = length (D)
797872
798873 # Get plot region limits from CTRL.pocket_R e.g. " -R0/10/-1.5/1.5"
799- r = split (CTRL. pocket_R[1 ][4 : end ], ' /' ) # skip " -R"
800- ymin = parse (Float64, r[3 ])
801- ymax = parse (Float64, r[4 ])
874+ ymin, ymax = CTRL. limits[9 ], CTRL. limits[10 ]
802875
803876 # x positions: at plot region edge if right axis is drawn, otherwise at each curve's last point
804877 xs = Vector {Float64} (undef, nc)
805878 ys = Vector {Float64} (undef, nc)
806879 colors = Vector {String} (undef, nc)
807- if right_axis
808- xmax = parse (Float64, r[2 ])
809- for k in 1 : nc
810- xs[k] = xmax
811- ys[k] = D[k]. data[end , 2 ]
812- colors[k] = _extract_W_color (D[k]. header)
813- end
814- else
815- for k in 1 : nc
816- xs[k] = D[k]. data[end , 1 ]
817- ys[k] = D[k]. data[end , 2 ]
818- colors[k] = _extract_W_color (D[k]. header)
819- end
880+ for k in 1 : nc
881+ xs[k] = D[k]. data[end , 1 ]
882+ ys[k] = D[k]. data[end , 2 ]
883+ colors[k] = _extract_W_color (D[k]. header)
820884 end
821885
822886 # Repel overlapping labels vertically
0 commit comments