Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/GMT.jl
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ include("grd_operations.jl")
include("common_options.jl")
const LEGEND_TYPE = Ref{legend_bag}(legend_bag())# To store Legends info
include("beziers.jl")
include("brokenaxes.jl")
include("circfit.jl")
include("crop.jl")
include("custom_symb_funs.jl")
Expand Down Expand Up @@ -446,6 +447,7 @@ end
#Base.precompile(Tuple{typeof(upGMT),Bool, Bool}) # Here it doesn't print anything.
#Base.precompile(Tuple{Dict{Symbol, Any}, Vector{String}}) # Here it doesn't print anything.
#Base.precompile(Tuple{typeof(Base.vect), Array{String, 1}, Vararg{Array{String, 1}}})
Base.precompile(Tuple{typeof(GMT.axis), Base.Dict{Symbol, Any}, Bool, Bool, Bool, Bool, Base.Dict{Symbol, Any}})

function __init__(test::Bool=false)
clear_sessions(3600)# Delete stray sessions dirs older than 1 hour
Expand Down
215 changes: 215 additions & 0 deletions src/brokenaxes.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
# ─────────────────────────────────────────────────────────────────────────────
# Broken-axis feature, activated from plot() when breakx/breaky/xranges/yranges
# are present in d.
#
# Broken-axis-specific options (all others pass through to plot() as normal):
# breakx=(x1,x2) — interval to skip on X; panels from data bbox
# xranges=[(a,b),(c,d),...] — explicit X panel ranges
# breaky=(y1,y2) — interval to skip on Y; panels from data bbox
# yranges=[(a,b),(c,d),...] — explicit Y panel ranges (bottom to top)
# gap=0.5 — gap between panels in cm
# widths=[...] — explicit panel widths in cm (X-broken)
# heights=[...] — explicit panel heights in cm (Y-broken)
# break_angle=70 — angle of break marks from horizontal (degrees)
# break_size=0.35 — length of each break mark in cm
# break_spacing=0.15 — perpendicular distance between the two marks in cm
# no_break_symbols=false — skip drawing break symbols
#
# All other plot() options (region, figsize, title, xlabel, ...) stay in d and are
# consumed by parse_R, parse_J, parse_B, etc. inside common_plot_xyz as normal.
# ─────────────────────────────────────────────────────────────────────────────
function _brokenplot(arg1, first::Bool, d::Dict{Symbol,Any})

breakx = pop!(d, :breakx, nothing)
breaky = pop!(d, :breaky, nothing)
xranges = pop!(d, :xranges, nothing)
yranges = pop!(d, :yranges, nothing)
widths_arg = pop!(d, :widths, nothing)
heights_arg = pop!(d, :heights, nothing)
gap = Float64(pop!(d, :gap, 0.5))
break_angle = Float64(pop!(d, :break_angle, 70.0))
break_size = Float64(pop!(d, :break_size, 0.35))
break_spacing = Float64(pop!(d, :break_spacing, 0.15))
no_break_symbols = pop!(d, :no_break_symbols, false)

bb = getregion(arg1) # (xmin, xmax, ymin, ymax)

has_xbreak = (breakx !== nothing) || (xranges !== nothing)
has_ybreak = (breaky !== nothing) || (yranges !== nothing)

(!has_xbreak && !has_ybreak) && error("brokenplot: provide breakx/xranges or breaky/yranges")
(has_xbreak && has_ybreak) && error("brokenplot: provide breakx/xranges OR breaky/yranges (not both simultaneously)")

do_show, fmt, savefig = get_show_fmt_savefig(d, false)
axis = has_xbreak ? :x : :y

# Call parse_R to consume region from d (same as common_plot_xyz would).
# After this, CTRL.limits[7:10] = (xmin, xmax, ymin, ymax) if region was given.
opt_R = parse_R(d, "")[2]
has_region = (opt_R != "")

# ── Build broken-axis ranges ──────────────────────────────────────────
if axis === :x
if (xranges === nothing)
(breakx === nothing) && error("plot: provide `breakx=(a,b)` or `xranges=[(a,b),...]`")
xranges = [(bb[1], Float64(breakx[1])), (Float64(breakx[2]), bb[2])]
end
brkranges = [(Float64(r[1]), Float64(r[2])) for r in xranges]
fixed_range = has_region ? (CTRL.limits[9], CTRL.limits[10]) :
(bb[3] - max((bb[4]-bb[3])*0.05, 1e-10), bb[4] + max((bb[4]-bb[3])*0.05, 1e-10))
else
if (yranges === nothing)
(breaky === nothing) && error("plot: provide `breaky=(a,b)` or `yranges=[(a,b),...]`")
yranges = [(bb[3], Float64(breaky[1])), (Float64(breaky[2]), bb[4])]
end
brkranges = [(Float64(r[1]), Float64(r[2])) for r in yranges]
fixed_range = has_region ? (CTRL.limits[7], CTRL.limits[8]) :
(bb[1] - max((bb[2]-bb[1])*0.05, 1e-10), bb[2] + max((bb[2]-bb[1])*0.05, 1e-10))
end

# ── Compute variable panel sizes ──────────────────────────────────────
# Default total: 15 cm wide × 10 cm tall. Users needing other sizes provide widths=/heights=.
nranges = length(brkranges)
range_spans = [r[2] - r[1] for r in brkranges]
if axis === :x
avail = 15.0 - gap * (nranges - 1)
sizes_arg = widths_arg
fixed_sz = 10.0
else
avail = 10.0 - gap * (nranges - 1)
sizes_arg = heights_arg
fixed_sz = 15.0
end
t = avail / sum(range_spans)
panel_sizes = (sizes_arg === nothing) ? [t * s for s in range_spans] : Float64.(sizes_arg)
scale_fixed = fixed_sz / (fixed_range[2] - fixed_range[1])

_brokenplot_core(arg1, first, axis, brkranges, fixed_range, panel_sizes, gap,
fixed_sz, scale_fixed, break_angle, break_size, break_spacing,
no_break_symbols, d)

(do_show || fmt !== "" || savefig !== "") && showfig(show=do_show, fmt=fmt, savefig=savefig)
end

# ─────────────────────────────────────────────────────────────────────────────
# Generic core: axis = :x → side-by-side panels; axis = :y → stacked panels.
#
# brkranges — ranges along the broken axis [(lo,hi), ...]
# fixed_range — (lo, hi) of the fixed axis
# panel_sizes — cm size of each panel along the broken axis
# fixed_sz — cm size along the fixed axis (constant across panels)
# scale_fixed — cm per data unit on the fixed axis
# ─────────────────────────────────────────────────────────────────────────────
function _brokenplot_core(arg1, first::Bool, axis::Symbol, brkranges, fixed_range, panel_sizes, gap, fixed_sz, scale_fixed,
break_angle, break_size, break_spacing, no_break_symbols, d)

nranges = length(brkranges)
range_spans = [r[2] - r[1] for r in brkranges]
scale_brks = [panel_sizes[i] / range_spans[i] for i in 1:nranges]
flo, fhi = fixed_range[1], fixed_range[2]
shift_key = axis === :x ? :X : :Y

# Projection strings: broken-axis size varies per panel, fixed-axis size is constant
projs = (axis === :x) ?
["X$(panel_sizes[i])c/$(fixed_sz)c" for i in 1:nranges] :
["X$(fixed_sz)c/$(panel_sizes[i])c" for i in 1:nranges]

# Frame sides: suppress inner borders on the broken-axis direction
sides_1st = axis === :x ? "WSN" : "WSe"
sides_lst = axis === :x ? "ESN" : "WNe"
sides_mid = axis === :x ? "SN" : "We"

# X-broken: title on panel 1; Y-broken: title on topmost panel (nranges)
title_panel = (axis === :x) ? 1 : nranges

# ── Draw panels ───────────────────────────────────────────────────────
# Keep a master copy so style options (lw, lc, pen, …) survive across panels.
# Each panel gets a fresh copy; common_plot_xyz consumes from the copy only.
d0 = copy(d)
for i in 1:nranges
di = copy(d0)
blo, bhi = brkranges[i]
di[:region] = axis === :x ? (blo, bhi, flo, fhi) : (flo, fhi, blo, bhi)
di[:proj] = projs[i]
sides = (nranges == 1) ? "WSEN" : (i == 1) ? sides_1st : (i == nranges) ? sides_lst : sides_mid
di[:frame] = (axes = sides, annot = :auto, ticks = :auto)
i > 1 && (di[shift_key] = "$(panel_sizes[i-1] + gap)c")
# title/subtitle only on title_panel; xlabel/ylabel only on panel 1
i != title_panel && (delete!(di, :title); delete!(di, :subtitle))
i != 1 && (delete!(di, :xlabel); delete!(di, :ylabel))
common_plot_xyz("", arg1, "plot", i == 1 && first, false, di)
end

#=
no_break_symbols && return nothing

# ── Draw break symbols ────────────────────────────────────────────────
# cumulative[i] = offset (cm) of panel i along the broken axis from panel 1
cumulative = zeros(nranges)
for i in 2:nranges
cumulative[i] = cumulative[i-1] + panel_sizes[i-1] + gap
end
current_panel = nranges # PS origin is currently at the last panel

for i in 1:(nranges - 1)
edge_L = brkranges[i][2]; sc_L = scale_brks[i]
edge_R = brkranges[i+1][1]; sc_R = scale_brks[i+1]

reg_L = axis === :x ? (brkranges[i][1], edge_L, flo, fhi) : (flo, fhi, brkranges[i][1], edge_L)
reg_R = axis === :x ? (edge_R, brkranges[i+1][2], flo, fhi) : (flo, fhi, edge_R, brkranges[i+1][2])

for fpos in (flo, fhi)
bx_L, by_L, sx_L, sy_L = (axis === :x) ? (edge_L, fpos, sc_L, scale_fixed) : (fpos, edge_L, scale_fixed, sc_L)
dshift = cumulative[i] - cumulative[current_panel]
_ba_break_symbol!(bx_L, by_L, dshift, reg_L, projs[i], sx_L, sy_L, break_angle, break_size, break_spacing, axis)
current_panel = i

bx_R, by_R, sx_R, sy_R = (axis === :x) ? (edge_R, fpos, sc_R, scale_fixed) : (fpos, edge_R, scale_fixed, sc_R)
dshift = cumulative[i+1] - cumulative[current_panel]
_ba_break_symbol!(bx_R, by_R, dshift, reg_R, projs[i+1], sx_R, sy_R, break_angle, break_size, break_spacing, axis)
current_panel = i + 1
end
end
=#
return nothing
end

#= ─────────────────────────────────────────────────────────────────────────────
"""
Draw a double-slash break symbol centred at (data_x, data_y).

- `dshift`: relative shift (cm) to reach this panel from the current PS origin.
- `axis`: `:x` — X-break (eraser horizontal, navigate via `X=`);
`:y` — Y-break (eraser vertical, navigate via `Y=`).
"""
function _ba_break_symbol!(data_x::Float64, data_y::Float64,
dshift::Float64,
reg::Tuple, prj::String,
sx::Float64, sy::Float64,
break_angle::Float64,
break_size::Float64,
break_spacing::Float64,
axis::Symbol = :x)
a = break_angle * π / 180.0
ca, sa = cos(a), sin(a)

hl_x = break_size / 2.0 * ca / sx
hl_y = break_size / 2.0 * sa / sy
hs_x = break_spacing / 2.0 * (-sa) / sx
hs_y = break_spacing / 2.0 * ca / sy

if axis === :x
erase_half = (break_size * 0.7) / sx
plot!([data_x - erase_half, data_x + erase_half], [data_y, data_y]; region=reg, proj=prj, lw=6, lc=:white, X="$(dshift)c")
else
erase_half = (break_size * 0.7) / sy
plot!([data_x, data_x], [data_y - erase_half, data_y + erase_half]; region=reg, proj=prj, lw=6, lc=:white, Y="$(dshift)c")
end

for sign in (-1.0, 1.0)
cx = data_x + sign * hs_x
cy = data_y + sign * hs_y
plot!([cx - hl_x, cx + hl_x], [cy - hl_y, cy + hl_y]; region=reg, proj=prj, lw=1.5, lc=:black, X="0c")
end
end
=#
2 changes: 2 additions & 0 deletions src/plot.jl
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ function plot(arg1; first=true, kw...)
_plot(arg1, first==1, d)
end
function _plot(arg1, first::Bool, d::Dict{Symbol, Any})
# Broken axis — intercept before anything else
is_in_dict(d, [:breakx :breaky :xranges :yranges]; del=false) !== nothing && return _brokenplot(mat2ds(arg1), first, d)
# First check if arg1 is a GMTds of a linear fit and if yes, call the plotlinefit() fun
if (isa(arg1, GDtype) && is_in_dict(d, [:linefit :regress]; del=false) !== nothing)
att = isa(arg1, GMTdataset) ? arg1.attrib : arg1[1].attrib
Expand Down
51 changes: 51 additions & 0 deletions src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1807,3 +1807,54 @@ end
include("makeDCWs.jl")
include("getdcw.jl")
#include("tttAPI.jl")

#=
function nada(offset=10)
pts = [5.0 5.0; 5.1 5.1; 4.9 5.0; 5.0 4.9; 5.1 4.9; 4.9 5.1]
labels = ["P1", "P2", "P3", "P4", "P5", "P6"]

scatter(pts, region=[3, 7, 3, 7], marker=:circle, ms="8p", fill=:tomato, ml=:thin, frame=:af)

pos = textrepel(pts, labels, fontsize=10, offset=offset)

lines = [mat2ds([pts[k,1] pts[k,2]; pos[k,1] pos[k,2]]) for k in 1:6]
plot!(lines, pen="0.3p,gray50,dashed")
text!(mat2ds(pos, text=labels), font=(10,:Helvetica), justify=:CM, fill=:white, pen=:thin, clearance="1p", show=1)
end

function nada2(offset=10)
cities = [
-9.14 38.74; # Lisbon
-8.61 41.15; # Porto
-3.70 40.42; # Madrid
-3.68 40.48; # nearby Madrid (Alcobendas)
-0.38 39.47; # Valencia
2.17 41.39; # Barcelona
2.10 41.35; # nearby Barcelona (Hospitalet)
-8.43 43.37; # A Coruña
-5.98 37.39; # Seville
-1.13 37.99; # Murcia
]

names = ["Lisbon", "Porto", "Madrid", "Alcobendas",
"Valencia", "Barcelona", "Hospitalet",
"A Coruña", "Seville", "Murcia"]

# Plot the coast as background
coast(region=[-11, 4, 35, 45], proj=:Mercator, shore=true,
land=:lightyellow, water=:lightblue, borders=(1,:thinnest))

# Plot city points
scatter!(cities, marker=:circle, ms="5p", fill=:red, ml=:thinnest)

# Compute repelled label positions
tic()
pos = textrepel(cities, names, fontsize=8, offset=offset, max_iter=200)
toc()

# Draw leader lines (one multi-segment dataset) and place labels
lines = [mat2ds([cities[k,1] cities[k,2]; pos[k,1] pos[k,2]]) for k in 1:length(names)]
plot!(lines, pen="0.3p,gray50,dashed")
text!(mat2ds(pos, text=names), font=(8,:Helvetica,:black), justify=:CM, fill=:white, pen=:thinnest, clearance="1p", show=1)
end
=#
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ using InteractiveUtils
API = GMT.GMT_Create_Session("GMT", 2, GMT.GMT_SESSION_NOEXIT + GMT.GMT_SESSION_EXTERNAL);
GMT.GMT_Get_Ctrl(API);

include("test_brokenaxes.jl")
include("test_wave_travel_time.jl")
include("test_imgmorph.jl")
include("test_PT_alignments.jl")
Expand Down
29 changes: 29 additions & 0 deletions test/test_brokenaxes.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Test file for broken-axis via plot() — run from the repo root:
# julia --project=. test_brokenaxes.jl

using GMT

# ── Example 1: simple sin wave, skip the middle stretch ───────────────────
x = collect(0.0:0.05:10.0)
D1 = mat2ds([x sin.(x)])

plot(D1; breakx=(2, 8), lw=1.5, lc=:blue, region=(0, 10, -1.2, 1.2),
xlabel="x", ylabel="sin(x)", title="Broken x-axis (breakx)")

# ── Example 2: explicit xranges, three panels ─────────────────────────────
x2 = collect(0.0:0.1:30.0)
D2 = mat2ds([x2 sin.(x2 ./ 3) .* exp.(-x2 ./ 20)])

plot(D2; xranges=[(0,4),(10,14),(24,30)], gap=0.4, lw=1, title="Three panels (xranges=)")

# ── Example 3: broken Y axis (breaky) ─────────────────────────────────────
x3 = collect(0.0:0.1:10.0)
D3 = mat2ds([x3 [fill(1.0, 61); fill(100.0, 40)]])

plot(D3; breaky=(5, 95), lw=1.5, lc=:blue, title="Broken y-axis (breaky)")

# ── Example 4: explicit yranges, three panels ─────────────────────────────
x4 = collect(0.0:0.1:10.0)
D4 = mat2ds([x4 [fill(0.0, 41); fill(50.0, 30); fill(200.0, 30)]])

plot(D4; yranges=[(-2,6),(44,56),(194,206)], gap=0.4, lw=1, title="Three y-panels (yranges=)")
Loading