Skip to content

Commit 416ae8b

Browse files
refactor(plot): restructure plot building for active curves and observables
1 parent e8df3a8 commit 416ae8b

1 file changed

Lines changed: 164 additions & 129 deletions

File tree

src/engine/plot.jl

Lines changed: 164 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -841,117 +841,165 @@ function _get_axis_label(base_label::String, exponent::Int, scale_func::Function
841841
end
842842

843843
function _build_plot!(fig_ctx, ctx, axis, spec::LineParametersPlotSpec)
844-
# Set initial axis scales from the spec. These will be the source of truth.
844+
# ---- Axis title & initial labels ----------------------------------------
845+
axis.title = spec.title
846+
axis.xlabel = _get_axis_label(spec.xlabel, spec.x_exp, spec.xscale[])
847+
axis.ylabel = _get_axis_label(spec.ylabel, spec.y_exp, spec.yscale[])
848+
849+
# ---- Helpers ------------------------------------------------------------
850+
sanitize_log!(v::AbstractVector, is_log::Bool) =
851+
(is_log && !isempty(v)) ? (v[v .<= 0] .= NaN; v) : v
852+
853+
_x_data_for(scale) = begin
854+
xd = _get_axis_data(spec.raw_freqs, spec.freqs, scale)
855+
sanitize_log!(xd.values, scale == Makie.log10)
856+
xd
857+
end
845858

846-
axis.title = spec.title
859+
_y_data_for(i::Int, scale) = begin
860+
yd = _get_axis_data(spec.raw_curves[i], spec.curves[i], scale)
861+
sanitize_log!(yd.values, scale == Makie.log10)
862+
yd
863+
end
847864

848-
# This helper function redraws everything based on the current axis scales.
849-
function _draw_axis!()
850-
# 1. Clear the axis completely
851-
empty!(axis)
852-
is_empty = true
865+
# safe max(abs(.)) ignoring non-finite
866+
_finite_max_abs(v) = begin
867+
buf = (x -> abs(x)).(value.(v))
868+
any(isfinite, buf) ? maximum(x for x in buf if isfinite(x)) : 0.0
869+
end
853870

854-
# 2. Get data and labels based on the current scale stored in the axis
855-
axis.xlabel = _get_axis_label(spec.xlabel, spec.x_exp, spec.xscale[])
856-
axis.ylabel = _get_axis_label(spec.ylabel, spec.y_exp, spec.yscale[])
857-
x_data = _get_axis_data(spec.raw_freqs, spec.freqs, spec.xscale[])
871+
# ---- Select active (non-noise) curves by EPS -------------------------------
872+
ncurves = length(spec.curves)
873+
active_idx = Int[]
858874

859-
# Sanitize x-axis data for log scale if needed
860-
if spec.xscale[] == Makie.log10
861-
x_data.values[x_data.values .<= 0] .= NaN
875+
@inbounds for i in 1:ncurves
876+
# max magnitude of raw curve; works for Real, Complex, and Measurement types
877+
maxmag = maximum(value.(abs.(spec.raw_curves[i])))
878+
if maxmag > eps(Float64) # keep only if anything rises above machine eps
879+
push!(active_idx, i)
862880
end
881+
end
863882

864-
palette = Makie.wong_colors()
865-
ncolors = length(palette)
866-
867-
868-
# 3. Draw all lines and error bars from scratch
869-
for (idx, (raw_curve, scaled_curve)) in enumerate(zip(spec.raw_curves, spec.curves))
870-
871-
# If all raw values are below the threshold,
872-
# consider it numerical noise and skip plotting this curve entirely.
873-
if !any(abs.(value.(raw_curve)) .> eps())
874-
continue
875-
end
876-
is_empty = false
877-
878-
color = palette[mod1(idx, ncolors)]
879-
label = spec.labels[idx]
880-
y_data = _get_axis_data(raw_curve, scaled_curve, spec.yscale[])
881-
882-
# LOG-SCALE SANITIZATION: Only for log plots, replace non-positive
883-
# values with NaN to create gaps.
884-
if spec.yscale[] == Makie.log10
885-
y_data.values[y_data.values .<= 0] .= NaN
886-
end
887-
888-
lines!(
889-
axis,
890-
x_data.values,
891-
y_data.values;
892-
color = color,
893-
label = label,
894-
linewidth = 2,
883+
any_real_curve = !isempty(active_idx)
884+
885+
# ---- Initial data (x) ---------------------------------------------------
886+
x_init = _x_data_for(spec.xscale[])
887+
x_vals_obs = Observable(copy(x_init.values))
888+
x_errs_obs = x_init.errors === nothing ? nothing : Observable(copy(x_init.errors))
889+
890+
# ---- Per-curve allocs only for active curves ---------------------------
891+
palette = Makie.wong_colors()
892+
ncolors = length(palette)
893+
nact = length(active_idx)
894+
895+
y_vals_obs = Vector{Observable}(undef, nact)
896+
y_errs_obs = Vector{Union{Nothing, Observable}}(undef, nact)
897+
line_plots = Vector{Any}(undef, nact)
898+
yerr_plots = Vector{Any}(undef, nact)
899+
xerr_plots = Vector{Any}(undef, nact)
900+
901+
# ---- Draw active curves -------------------------------------------------
902+
for k in 1:nact
903+
i = active_idx[k]
904+
color = palette[mod1(k, ncolors)] # color by active order
905+
label = spec.labels[i]
906+
907+
yd = _y_data_for(i, spec.yscale[])
908+
909+
y_vals_obs[k] = Observable(copy(yd.values))
910+
y_errs_obs[k] = yd.errors === nothing ? nothing : Observable(copy(yd.errors))
911+
912+
# line
913+
ln = lines!(
914+
axis,
915+
x_vals_obs,
916+
y_vals_obs[k];
917+
color = color,
918+
label = label,
919+
linewidth = 2,
920+
)
921+
line_plots[k] = ln
922+
923+
# Y errorbars: stems only + caps; both follow the line’s visibility
924+
if y_errs_obs[k] !== nothing
925+
yerr_plots[k] = errorbars!(
926+
axis, x_vals_obs, y_vals_obs[k], y_errs_obs[k];
927+
color = color, direction = :y, whiskerwidth = 3,
928+
visible = lift(identity, ln.visible),
895929
)
930+
else
931+
yerr_plots[k] = nothing
932+
end
896933

897-
if y_data.errors !== nothing
898-
errorbars!(
899-
axis,
900-
x_data.values,
901-
y_data.values,
902-
y_data.errors;
903-
color = color,
904-
whiskerwidth = 5,
905-
direction = :y,
906-
)
907-
end
908-
if x_data.errors !== nothing
909-
errorbars!(
910-
axis,
911-
x_data.values,
912-
y_data.values,
913-
x_data.errors;
914-
color = color,
915-
whiskerwidth = 5,
916-
direction = :x,
917-
)
918-
end
934+
# X errorbars: stems only + caps
935+
if x_errs_obs !== nothing
936+
xerr_plots[k] = errorbars!(
937+
axis, x_vals_obs, y_vals_obs[k], x_errs_obs;
938+
color = color, direction = :x, whiskerwidth = 3,
939+
visible = lift(identity, ln.visible),
940+
)
941+
else
942+
xerr_plots[k] = nothing
919943
end
920944

921-
# 4. Handle the "no data" case internally
922-
if is_empty
923-
lines!(axis, [NaN], [NaN]; color = :transparent, label = "No data")
945+
end
946+
947+
# If nothing to draw, add transparent dummy without legend entry
948+
if !any_real_curve
949+
lines!(axis, [NaN], [NaN]; color = :transparent, label = "No data")
950+
end
951+
952+
# ---- Apply initial scales safely ---------------------------------------
953+
try
954+
axis.xscale[] = spec.xscale[]
955+
axis.yscale[] = spec.yscale[]
956+
catch
957+
axis.xscale[] = Makie.identity
958+
axis.yscale[] = Makie.identity
959+
@warn "Failed to set axis scale; reverted to linear scale."
960+
end
961+
Makie.autolimits!(axis)
962+
963+
# ---- Refreshers (update Observables only) ------------------------------
964+
function _refresh_x!(scale)
965+
spec.xscale[] = scale
966+
axis.xscale[] = scale
967+
axis.xlabel = _get_axis_label(spec.xlabel, spec.x_exp, scale)
968+
969+
xd = _x_data_for(scale)
970+
x_vals_obs[] = xd.values
971+
if x_errs_obs !== nothing
972+
x_errs_obs[] = xd.errors
924973
end
974+
Makie.autolimits!(axis)
975+
nothing
976+
end
925977

926-
# 5. Reset limits
927-
try
928-
axis.xscale[] = spec.xscale[]
929-
axis.yscale[] = spec.yscale[]
930-
catch
931-
# If setting the scale fails (e.g., log scale with non-positive values),
932-
# revert to linear scale and notify the user.
933-
axis.xscale[] = Makie.identity
934-
axis.yscale[] = Makie.identity
935-
@warn "Failed to set axis scale; reverted to linear scale."
978+
function _refresh_y!(scale)
979+
spec.yscale[] = scale
980+
axis.yscale[] = scale
981+
axis.ylabel = _get_axis_label(spec.ylabel, spec.y_exp, scale)
982+
983+
@inbounds for k in 1:nact
984+
i = active_idx[k]
985+
yd = _y_data_for(i, scale)
986+
y_vals_obs[k][] = yd.values
987+
if y_errs_obs[k] !== nothing
988+
y_errs_obs[k][] = yd.errors
989+
end
936990
end
937991
Makie.autolimits!(axis)
938-
return is_empty
992+
nothing
939993
end
940994

941-
# Initial drawing
942-
is_empty_axis = _draw_axis!()
995+
# ---- Buttons ------------------------------------------------------------
943996
buttons =
944-
is_empty_axis ? [] :
997+
any_real_curve ?
945998
[
946999
ControlButtonSpec(
947-
(_ctx, _btn) -> begin
948-
Makie.reset_limits!(axis)
949-
return nothing
950-
end;
1000+
(_ctx, _btn) -> (Makie.reset_limits!(axis); nothing);
9511001
icon = MI_REFRESH,
952-
on_success = ControlReaction(
953-
status_string = "Axis limits reset",
954-
),
1002+
on_success = ControlReaction(status_string = "Axis limits reset"),
9551003
),
9561004
ControlButtonSpec(
9571005
(_ctx, _btn) -> _save_plot_export(spec, axis);
@@ -960,70 +1008,57 @@ function _build_plot!(fig_ctx, ctx, axis, spec::LineParametersPlotSpec)
9601008
status_string = path -> string("Saved SVG to ", basename(path)),
9611009
),
9621010
),
963-
]
964-
965-
# --- Define control toggles ---
966-
# The callbacks are now trivial: just update the scale and redraw.
1011+
] : Any[]
9671012

1013+
# ---- Toggles ------------------------------------------------------------
9681014
toggles =
969-
is_empty_axis ? [] :
1015+
any_real_curve ?
9701016
[
9711017
ControlToggleSpec(
972-
# Action when toggled ON (log scale)
973-
(_ctx, _toggle) -> begin
974-
spec.xscale[] = Makie.log10
975-
_draw_axis!()
976-
end,
977-
# Action when toggled OFF (linear scale)
978-
(_ctx, _toggle) -> begin
979-
spec.xscale[] = Makie.identity
980-
_draw_axis!()
981-
end;
1018+
(_ctx, _t) -> _refresh_x!(Makie.log10),
1019+
(_ctx, _t) -> _refresh_x!(Makie.identity);
9821020
label = "log x-axis",
9831021
start_active = spec.xscale[] == Makie.log10,
9841022
on_success_on = ControlReaction(status_string = "x-axis scale set to log"),
9851023
on_success_off = ControlReaction(
9861024
status_string = "x-axis scale set to linear",
9871025
),
988-
on_failure = ControlReaction(
989-
status_string = err_msg -> err_msg,
990-
),
1026+
on_failure = ControlReaction(status_string = err -> err),
9911027
),
9921028
ControlToggleSpec(
993-
# Action when toggled ON (log scale)
994-
(_ctx, _toggle) -> begin
995-
spec.yscale[] = Makie.log10
996-
_draw_axis!()
997-
end,
998-
# Action when toggled OFF (linear scale)
999-
(_ctx, _toggle) -> begin
1000-
spec.yscale[] = Makie.identity
1001-
_draw_axis!()
1002-
end;
1029+
(_ctx, _t) -> _refresh_y!(Makie.log10),
1030+
(_ctx, _t) -> _refresh_y!(Makie.identity);
10031031
label = "log y-axis",
10041032
start_active = spec.yscale[] == Makie.log10,
10051033
on_success_on = ControlReaction(status_string = "y-axis scale set to log"),
10061034
on_success_off = ControlReaction(
10071035
status_string = "y-axis scale set to linear",
10081036
),
1009-
on_failure = ControlReaction(
1010-
status_string = err_msg -> err_msg,
1011-
),
1037+
on_failure = ControlReaction(status_string = err -> err),
10121038
),
1013-
]
1014-
1015-
legend_builder = parent -> Makie.Legend(parent, axis; orientation = :vertical)
1039+
] : Any[]
1040+
1041+
# ---- Legend -------------------------------------------------------------
1042+
legend_builder =
1043+
parent ->
1044+
Makie.Legend(
1045+
parent,
1046+
axis;
1047+
orientation = :vertical,
1048+
)
10161049

10171050
return PlotBuildArtifacts(
1018-
axis = axis,
1019-
legends = legend_builder,
1020-
colorbars = Any[],
1051+
axis = axis,
1052+
legends = legend_builder,
1053+
colorbars = Any[],
10211054
control_buttons = buttons,
10221055
control_toggles = toggles,
1023-
status_message = nothing,
1056+
status_message = nothing,
10241057
)
10251058
end
10261059

1060+
1061+
10271062
function _display!(backend_ctx, fig::Makie.Figure; title::AbstractString = "")
10281063
if backend_ctx.interactive && backend_ctx.window !== nothing
10291064
display(backend_ctx.window, fig)

0 commit comments

Comments
 (0)