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+ =#
0 commit comments