Skip to content

Commit eaefb5c

Browse files
authored
Merge pull request #1983 from GenericMappingTools/brokenaxes
Add options to plot() that let create plots with broken axes.
2 parents 5706f09 + a42fc26 commit eaefb5c

6 files changed

Lines changed: 300 additions & 0 deletions

File tree

src/GMT.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ include("grd_operations.jl")
204204
include("common_options.jl")
205205
const LEGEND_TYPE = Ref{legend_bag}(legend_bag())# To store Legends info
206206
include("beziers.jl")
207+
include("brokenaxes.jl")
207208
include("circfit.jl")
208209
include("crop.jl")
209210
include("custom_symb_funs.jl")
@@ -446,6 +447,7 @@ end
446447
#Base.precompile(Tuple{typeof(upGMT),Bool, Bool}) # Here it doesn't print anything.
447448
#Base.precompile(Tuple{Dict{Symbol, Any}, Vector{String}}) # Here it doesn't print anything.
448449
#Base.precompile(Tuple{typeof(Base.vect), Array{String, 1}, Vararg{Array{String, 1}}})
450+
Base.precompile(Tuple{typeof(GMT.axis), Base.Dict{Symbol, Any}, Bool, Bool, Bool, Bool, Base.Dict{Symbol, Any}})
449451

450452
function __init__(test::Bool=false)
451453
clear_sessions(3600)# Delete stray sessions dirs older than 1 hour

src/brokenaxes.jl

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
# ─────────────────────────────────────────────────────────────────────────────
2+
# Broken-axis feature, activated from plot() when breakx/breaky/xranges/yranges
3+
# are present in d.
4+
#
5+
# Broken-axis-specific options (all others pass through to plot() as normal):
6+
# breakx=(x1,x2) — interval to skip on X; panels from data bbox
7+
# xranges=[(a,b),(c,d),...] — explicit X panel ranges
8+
# breaky=(y1,y2) — interval to skip on Y; panels from data bbox
9+
# yranges=[(a,b),(c,d),...] — explicit Y panel ranges (bottom to top)
10+
# gap=0.5 — gap between panels in cm
11+
# widths=[...] — explicit panel widths in cm (X-broken)
12+
# heights=[...] — explicit panel heights in cm (Y-broken)
13+
# break_angle=70 — angle of break marks from horizontal (degrees)
14+
# break_size=0.35 — length of each break mark in cm
15+
# break_spacing=0.15 — perpendicular distance between the two marks in cm
16+
# no_break_symbols=false — skip drawing break symbols
17+
#
18+
# All other plot() options (region, figsize, title, xlabel, ...) stay in d and are
19+
# consumed by parse_R, parse_J, parse_B, etc. inside common_plot_xyz as normal.
20+
# ─────────────────────────────────────────────────────────────────────────────
21+
function _brokenplot(arg1, first::Bool, d::Dict{Symbol,Any})
22+
23+
breakx = pop!(d, :breakx, nothing)
24+
breaky = pop!(d, :breaky, nothing)
25+
xranges = pop!(d, :xranges, nothing)
26+
yranges = pop!(d, :yranges, nothing)
27+
widths_arg = pop!(d, :widths, nothing)
28+
heights_arg = pop!(d, :heights, nothing)
29+
gap = Float64(pop!(d, :gap, 0.5))
30+
break_angle = Float64(pop!(d, :break_angle, 70.0))
31+
break_size = Float64(pop!(d, :break_size, 0.35))
32+
break_spacing = Float64(pop!(d, :break_spacing, 0.15))
33+
no_break_symbols = pop!(d, :no_break_symbols, false)
34+
35+
bb = getregion(arg1) # (xmin, xmax, ymin, ymax)
36+
37+
has_xbreak = (breakx !== nothing) || (xranges !== nothing)
38+
has_ybreak = (breaky !== nothing) || (yranges !== nothing)
39+
40+
(!has_xbreak && !has_ybreak) && error("brokenplot: provide breakx/xranges or breaky/yranges")
41+
(has_xbreak && has_ybreak) && error("brokenplot: provide breakx/xranges OR breaky/yranges (not both simultaneously)")
42+
43+
do_show, fmt, savefig = get_show_fmt_savefig(d, false)
44+
axis = has_xbreak ? :x : :y
45+
46+
# Call parse_R to consume region from d (same as common_plot_xyz would).
47+
# After this, CTRL.limits[7:10] = (xmin, xmax, ymin, ymax) if region was given.
48+
opt_R = parse_R(d, "")[2]
49+
has_region = (opt_R != "")
50+
51+
# ── Build broken-axis ranges ──────────────────────────────────────────
52+
if axis === :x
53+
if (xranges === nothing)
54+
(breakx === nothing) && error("plot: provide `breakx=(a,b)` or `xranges=[(a,b),...]`")
55+
xranges = [(bb[1], Float64(breakx[1])), (Float64(breakx[2]), bb[2])]
56+
end
57+
brkranges = [(Float64(r[1]), Float64(r[2])) for r in xranges]
58+
fixed_range = has_region ? (CTRL.limits[9], CTRL.limits[10]) :
59+
(bb[3] - max((bb[4]-bb[3])*0.05, 1e-10), bb[4] + max((bb[4]-bb[3])*0.05, 1e-10))
60+
else
61+
if (yranges === nothing)
62+
(breaky === nothing) && error("plot: provide `breaky=(a,b)` or `yranges=[(a,b),...]`")
63+
yranges = [(bb[3], Float64(breaky[1])), (Float64(breaky[2]), bb[4])]
64+
end
65+
brkranges = [(Float64(r[1]), Float64(r[2])) for r in yranges]
66+
fixed_range = has_region ? (CTRL.limits[7], CTRL.limits[8]) :
67+
(bb[1] - max((bb[2]-bb[1])*0.05, 1e-10), bb[2] + max((bb[2]-bb[1])*0.05, 1e-10))
68+
end
69+
70+
# ── Compute variable panel sizes ──────────────────────────────────────
71+
# Default total: 15 cm wide × 10 cm tall. Users needing other sizes provide widths=/heights=.
72+
nranges = length(brkranges)
73+
range_spans = [r[2] - r[1] for r in brkranges]
74+
if axis === :x
75+
avail = 15.0 - gap * (nranges - 1)
76+
sizes_arg = widths_arg
77+
fixed_sz = 10.0
78+
else
79+
avail = 10.0 - gap * (nranges - 1)
80+
sizes_arg = heights_arg
81+
fixed_sz = 15.0
82+
end
83+
t = avail / sum(range_spans)
84+
panel_sizes = (sizes_arg === nothing) ? [t * s for s in range_spans] : Float64.(sizes_arg)
85+
scale_fixed = fixed_sz / (fixed_range[2] - fixed_range[1])
86+
87+
_brokenplot_core(arg1, first, axis, brkranges, fixed_range, panel_sizes, gap,
88+
fixed_sz, scale_fixed, break_angle, break_size, break_spacing,
89+
no_break_symbols, d)
90+
91+
(do_show || fmt !== "" || savefig !== "") && showfig(show=do_show, fmt=fmt, savefig=savefig)
92+
end
93+
94+
# ─────────────────────────────────────────────────────────────────────────────
95+
# Generic core: axis = :x → side-by-side panels; axis = :y → stacked panels.
96+
#
97+
# brkranges — ranges along the broken axis [(lo,hi), ...]
98+
# fixed_range — (lo, hi) of the fixed axis
99+
# panel_sizes — cm size of each panel along the broken axis
100+
# fixed_sz — cm size along the fixed axis (constant across panels)
101+
# scale_fixed — cm per data unit on the fixed axis
102+
# ─────────────────────────────────────────────────────────────────────────────
103+
function _brokenplot_core(arg1, first::Bool, axis::Symbol, brkranges, fixed_range, panel_sizes, gap, fixed_sz, scale_fixed,
104+
break_angle, break_size, break_spacing, no_break_symbols, d)
105+
106+
nranges = length(brkranges)
107+
range_spans = [r[2] - r[1] for r in brkranges]
108+
scale_brks = [panel_sizes[i] / range_spans[i] for i in 1:nranges]
109+
flo, fhi = fixed_range[1], fixed_range[2]
110+
shift_key = axis === :x ? :X : :Y
111+
112+
# Projection strings: broken-axis size varies per panel, fixed-axis size is constant
113+
projs = (axis === :x) ?
114+
["X$(panel_sizes[i])c/$(fixed_sz)c" for i in 1:nranges] :
115+
["X$(fixed_sz)c/$(panel_sizes[i])c" for i in 1:nranges]
116+
117+
# Frame sides: suppress inner borders on the broken-axis direction
118+
sides_1st = axis === :x ? "WSN" : "WSe"
119+
sides_lst = axis === :x ? "ESN" : "WNe"
120+
sides_mid = axis === :x ? "SN" : "We"
121+
122+
# X-broken: title on panel 1; Y-broken: title on topmost panel (nranges)
123+
title_panel = (axis === :x) ? 1 : nranges
124+
125+
# ── Draw panels ───────────────────────────────────────────────────────
126+
# Keep a master copy so style options (lw, lc, pen, …) survive across panels.
127+
# Each panel gets a fresh copy; common_plot_xyz consumes from the copy only.
128+
d0 = copy(d)
129+
for i in 1:nranges
130+
di = copy(d0)
131+
blo, bhi = brkranges[i]
132+
di[:region] = axis === :x ? (blo, bhi, flo, fhi) : (flo, fhi, blo, bhi)
133+
di[:proj] = projs[i]
134+
sides = (nranges == 1) ? "WSEN" : (i == 1) ? sides_1st : (i == nranges) ? sides_lst : sides_mid
135+
di[:frame] = (axes = sides, annot = :auto, ticks = :auto)
136+
i > 1 && (di[shift_key] = "$(panel_sizes[i-1] + gap)c")
137+
# title/subtitle only on title_panel; xlabel/ylabel only on panel 1
138+
i != title_panel && (delete!(di, :title); delete!(di, :subtitle))
139+
i != 1 && (delete!(di, :xlabel); delete!(di, :ylabel))
140+
common_plot_xyz("", arg1, "plot", i == 1 && first, false, di)
141+
end
142+
143+
#=
144+
no_break_symbols && return nothing
145+
146+
# ── Draw break symbols ────────────────────────────────────────────────
147+
# cumulative[i] = offset (cm) of panel i along the broken axis from panel 1
148+
cumulative = zeros(nranges)
149+
for i in 2:nranges
150+
cumulative[i] = cumulative[i-1] + panel_sizes[i-1] + gap
151+
end
152+
current_panel = nranges # PS origin is currently at the last panel
153+
154+
for i in 1:(nranges - 1)
155+
edge_L = brkranges[i][2]; sc_L = scale_brks[i]
156+
edge_R = brkranges[i+1][1]; sc_R = scale_brks[i+1]
157+
158+
reg_L = axis === :x ? (brkranges[i][1], edge_L, flo, fhi) : (flo, fhi, brkranges[i][1], edge_L)
159+
reg_R = axis === :x ? (edge_R, brkranges[i+1][2], flo, fhi) : (flo, fhi, edge_R, brkranges[i+1][2])
160+
161+
for fpos in (flo, fhi)
162+
bx_L, by_L, sx_L, sy_L = (axis === :x) ? (edge_L, fpos, sc_L, scale_fixed) : (fpos, edge_L, scale_fixed, sc_L)
163+
dshift = cumulative[i] - cumulative[current_panel]
164+
_ba_break_symbol!(bx_L, by_L, dshift, reg_L, projs[i], sx_L, sy_L, break_angle, break_size, break_spacing, axis)
165+
current_panel = i
166+
167+
bx_R, by_R, sx_R, sy_R = (axis === :x) ? (edge_R, fpos, sc_R, scale_fixed) : (fpos, edge_R, scale_fixed, sc_R)
168+
dshift = cumulative[i+1] - cumulative[current_panel]
169+
_ba_break_symbol!(bx_R, by_R, dshift, reg_R, projs[i+1], sx_R, sy_R, break_angle, break_size, break_spacing, axis)
170+
current_panel = i + 1
171+
end
172+
end
173+
=#
174+
return nothing
175+
end
176+
177+
#= ─────────────────────────────────────────────────────────────────────────────
178+
"""
179+
Draw a double-slash break symbol centred at (data_x, data_y).
180+
181+
- `dshift`: relative shift (cm) to reach this panel from the current PS origin.
182+
- `axis`: `:x` — X-break (eraser horizontal, navigate via `X=`);
183+
`:y` — Y-break (eraser vertical, navigate via `Y=`).
184+
"""
185+
function _ba_break_symbol!(data_x::Float64, data_y::Float64,
186+
dshift::Float64,
187+
reg::Tuple, prj::String,
188+
sx::Float64, sy::Float64,
189+
break_angle::Float64,
190+
break_size::Float64,
191+
break_spacing::Float64,
192+
axis::Symbol = :x)
193+
a = break_angle * π / 180.0
194+
ca, sa = cos(a), sin(a)
195+
196+
hl_x = break_size / 2.0 * ca / sx
197+
hl_y = break_size / 2.0 * sa / sy
198+
hs_x = break_spacing / 2.0 * (-sa) / sx
199+
hs_y = break_spacing / 2.0 * ca / sy
200+
201+
if axis === :x
202+
erase_half = (break_size * 0.7) / sx
203+
plot!([data_x - erase_half, data_x + erase_half], [data_y, data_y]; region=reg, proj=prj, lw=6, lc=:white, X="$(dshift)c")
204+
else
205+
erase_half = (break_size * 0.7) / sy
206+
plot!([data_x, data_x], [data_y - erase_half, data_y + erase_half]; region=reg, proj=prj, lw=6, lc=:white, Y="$(dshift)c")
207+
end
208+
209+
for sign in (-1.0, 1.0)
210+
cx = data_x + sign * hs_x
211+
cy = data_y + sign * hs_y
212+
plot!([cx - hl_x, cx + hl_x], [cy - hl_y, cy + hl_y]; region=reg, proj=prj, lw=1.5, lc=:black, X="0c")
213+
end
214+
end
215+
=#

src/plot.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ function plot(arg1; first=true, kw...)
103103
_plot(arg1, first==1, d)
104104
end
105105
function _plot(arg1, first::Bool, d::Dict{Symbol, Any})
106+
# Broken axis — intercept before anything else
107+
is_in_dict(d, [:breakx :breaky :xranges :yranges]; del=false) !== nothing && return _brokenplot(mat2ds(arg1), first, d)
106108
# First check if arg1 is a GMTds of a linear fit and if yes, call the plotlinefit() fun
107109
if (isa(arg1, GDtype) && is_in_dict(d, [:linefit :regress]; del=false) !== nothing)
108110
att = isa(arg1, GMTdataset) ? arg1.attrib : arg1[1].attrib

src/utils.jl

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1807,3 +1807,54 @@ end
18071807
include("makeDCWs.jl")
18081808
include("getdcw.jl")
18091809
#include("tttAPI.jl")
1810+
1811+
#=
1812+
function nada(offset=10)
1813+
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]
1814+
labels = ["P1", "P2", "P3", "P4", "P5", "P6"]
1815+
1816+
scatter(pts, region=[3, 7, 3, 7], marker=:circle, ms="8p", fill=:tomato, ml=:thin, frame=:af)
1817+
1818+
pos = textrepel(pts, labels, fontsize=10, offset=offset)
1819+
1820+
lines = [mat2ds([pts[k,1] pts[k,2]; pos[k,1] pos[k,2]]) for k in 1:6]
1821+
plot!(lines, pen="0.3p,gray50,dashed")
1822+
text!(mat2ds(pos, text=labels), font=(10,:Helvetica), justify=:CM, fill=:white, pen=:thin, clearance="1p", show=1)
1823+
end
1824+
1825+
function nada2(offset=10)
1826+
cities = [
1827+
-9.14 38.74; # Lisbon
1828+
-8.61 41.15; # Porto
1829+
-3.70 40.42; # Madrid
1830+
-3.68 40.48; # nearby Madrid (Alcobendas)
1831+
-0.38 39.47; # Valencia
1832+
2.17 41.39; # Barcelona
1833+
2.10 41.35; # nearby Barcelona (Hospitalet)
1834+
-8.43 43.37; # A Coruña
1835+
-5.98 37.39; # Seville
1836+
-1.13 37.99; # Murcia
1837+
]
1838+
1839+
names = ["Lisbon", "Porto", "Madrid", "Alcobendas",
1840+
"Valencia", "Barcelona", "Hospitalet",
1841+
"A Coruña", "Seville", "Murcia"]
1842+
1843+
# Plot the coast as background
1844+
coast(region=[-11, 4, 35, 45], proj=:Mercator, shore=true,
1845+
land=:lightyellow, water=:lightblue, borders=(1,:thinnest))
1846+
1847+
# Plot city points
1848+
scatter!(cities, marker=:circle, ms="5p", fill=:red, ml=:thinnest)
1849+
1850+
# Compute repelled label positions
1851+
tic()
1852+
pos = textrepel(cities, names, fontsize=8, offset=offset, max_iter=200)
1853+
toc()
1854+
1855+
# Draw leader lines (one multi-segment dataset) and place labels
1856+
lines = [mat2ds([cities[k,1] cities[k,2]; pos[k,1] pos[k,2]]) for k in 1:length(names)]
1857+
plot!(lines, pen="0.3p,gray50,dashed")
1858+
text!(mat2ds(pos, text=names), font=(8,:Helvetica,:black), justify=:CM, fill=:white, pen=:thinnest, clearance="1p", show=1)
1859+
end
1860+
=#

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ using InteractiveUtils
1717
API = GMT.GMT_Create_Session("GMT", 2, GMT.GMT_SESSION_NOEXIT + GMT.GMT_SESSION_EXTERNAL);
1818
GMT.GMT_Get_Ctrl(API);
1919

20+
include("test_brokenaxes.jl")
2021
include("test_wave_travel_time.jl")
2122
include("test_imgmorph.jl")
2223
include("test_PT_alignments.jl")

test/test_brokenaxes.jl

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Test file for broken-axis via plot() — run from the repo root:
2+
# julia --project=. test_brokenaxes.jl
3+
4+
using GMT
5+
6+
# ── Example 1: simple sin wave, skip the middle stretch ───────────────────
7+
x = collect(0.0:0.05:10.0)
8+
D1 = mat2ds([x sin.(x)])
9+
10+
plot(D1; breakx=(2, 8), lw=1.5, lc=:blue, region=(0, 10, -1.2, 1.2),
11+
xlabel="x", ylabel="sin(x)", title="Broken x-axis (breakx)")
12+
13+
# ── Example 2: explicit xranges, three panels ─────────────────────────────
14+
x2 = collect(0.0:0.1:30.0)
15+
D2 = mat2ds([x2 sin.(x2 ./ 3) .* exp.(-x2 ./ 20)])
16+
17+
plot(D2; xranges=[(0,4),(10,14),(24,30)], gap=0.4, lw=1, title="Three panels (xranges=)")
18+
19+
# ── Example 3: broken Y axis (breaky) ─────────────────────────────────────
20+
x3 = collect(0.0:0.1:10.0)
21+
D3 = mat2ds([x3 [fill(1.0, 61); fill(100.0, 40)]])
22+
23+
plot(D3; breaky=(5, 95), lw=1.5, lc=:blue, title="Broken y-axis (breaky)")
24+
25+
# ── Example 4: explicit yranges, three panels ─────────────────────────────
26+
x4 = collect(0.0:0.1:10.0)
27+
D4 = mat2ds([x4 [fill(0.0, 41); fill(50.0, 30); fill(200.0, 30)]])
28+
29+
plot(D4; yranges=[(-2,6),(44,56),(194,206)], gap=0.4, lw=1, title="Three y-panels (yranges=)")

0 commit comments

Comments
 (0)