From 080bb4fb3932485d6bda6612fda16cd007cadaed Mon Sep 17 00:00:00 2001 From: Joaquim Date: Wed, 25 Feb 2026 20:01:18 +0000 Subject: [PATCH 1/2] A bunch more of sheltering against multi-recompiles. --- src/GMT.jl | 3 ++- src/gdal_utils.jl | 4 ++-- src/gmt_types.jl | 1 + src/makecpt.jl | 5 +++-- src/psscale.jl | 8 +++++--- src/utils_types.jl | 11 ++++++++--- 6 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/GMT.jl b/src/GMT.jl index 0cce2fa8c..e7b376c25 100644 --- a/src/GMT.jl +++ b/src/GMT.jl @@ -168,7 +168,7 @@ export density, density!, boxplot, boxplot!, cornerplot, cornerplot!, cubeplot, cubeplot!, ecdfplot, ecdfplot!, fill_between, fill_between!, funcurve, marginalhist, marginalhist!, parallelplot, parallelplot!, piechart, piechart!, plotlinefit, plotlinefit!, qqplot, qqplot!, qqnorm, qqnorm!, remotegrid, sealand, squeeze, terramar, - violin, violin!, viz, vizpdf, windbarbs, whereami, maregrams, pastplates, seismicity, ecmwf, era5time, era5vars, + symlog, isymlog, violin, violin!, viz, vizpdf, windbarbs, whereami, maregrams, pastplates, seismicity, ecmwf, era5time, era5vars, listecmwfvars, meteostat, weather, wmsinfo, wmstest, wmsread, VSdisp, mad, info, kmeans, pca, mosaic, quadbounds, quadkey, geocoder, getprovider, zscores, bwhitmiss, binarize, bwareaopen, bwconncomp, bwdist, bwdist_idx, bwlabel, bwperim, bwskell, cc2bw, graydist, isodata, padarray, rgb2gray, rgb2lab, rgb2YCbCr, rgb2ycbcr, grid2img, img2grid, grays2cube, grays2rgb, imclose, @@ -315,6 +315,7 @@ include("splitxyz.jl") include("streamlines.jl") include("surface.jl") include("subplot.jl") +include("symlog.jl") include("PrettyTables.jl") include("show_pretty_datasets.jl") include("solids.jl") diff --git a/src/gdal_utils.jl b/src/gdal_utils.jl index 293400b0f..f9909a685 100644 --- a/src/gdal_utils.jl +++ b/src/gdal_utils.jl @@ -358,9 +358,9 @@ function gd2gmt(dataset::Gdal.AbstractDataset) end # ------------------------------------------------------------------------------------------------------------- -# This function is made apart because XLS and CSVs have geometries and the calling functio, as is, +# This function is made apart because XLS and CSVs have geometries and the calling function, as is, # would not be able to extract the data from the 'dataset' -function helper_read_XLSCSV(dataset::Gdal.AbstractDataset)::GDtype +function helper_read_XLSCSV(@nospecialize(dataset))::GDtype if (get(POSTMAN[], "meteostat", "") != "") delete!(POSTMAN[], "meteostat") return read_meteostat(dataset) diff --git a/src/gmt_types.jl b/src/gmt_types.jl index 6a8ca7fd4..6346733c4 100644 --- a/src/gmt_types.jl +++ b/src/gmt_types.jl @@ -347,6 +347,7 @@ mutable struct wrapDatasets td, tvd, tcpt, tfv = GMTdataset(), Vector{GMTdataset}(), GMTcpt(), GMTfv() if (arg1 !== "") new(arg1, td, tvd, tfv, tcpt) elseif (isa(arg2, Matrix{<:Real})) new("", mat2ds(arg2), tvd, tfv, tcpt) + elseif (isa(arg2, Vector{<:Real})) new("", mat2ds(arg2), tvd, tfv, tcpt) elseif (isa(arg2, GMTdataset)) new("", arg2, tvd, tfv, tcpt) elseif (isa(arg2, Vector{<:GMTdataset})) new("", td, arg2, tfv, tcpt) elseif (isa(arg2, GMTfv)) new("", td, tvd, arg2, tcpt) diff --git a/src/makecpt.jl b/src/makecpt.jl index cd631637d..8857f41c0 100644 --- a/src/makecpt.jl +++ b/src/makecpt.jl @@ -59,9 +59,10 @@ To see the full documentation type: ``@? makecpt`` makecpt(cmd0::Symbol; kwargs...) = makecpt(""; C=string(cmd0), kwargs...) # Ex: makecpt(:gray) function makecpt(cmd0::String="", arg1=nothing; kwargs...)::Union{String, GMTcpt} d = init_module(false, kwargs...)[1] # Also checks if the user wants ONLY the HELP mode - makecpt(cmd0, arg1, d) + makecpt(wrapDatasets(cmd0, arg1), d) end -function makecpt(cmd0::String, arg1, d::Dict)::Union{String, GMTcpt} +function makecpt(w::wrapDatasets, d::Dict)::Union{String, GMTcpt} + cmd0, arg1 = unwrapDatasets(w::wrapDatasets) cmd = parse_V_params(d, "") diff --git a/src/psscale.jl b/src/psscale.jl index 9dd23dd46..f7c3d9eb0 100644 --- a/src/psscale.jl +++ b/src/psscale.jl @@ -49,13 +49,15 @@ To see the full documentation type: ``@? colorbar`` """ function colorbar(arg1::Union{Nothing, GMTcpt}=nothing; first=true, kwargs...) d, K, O = init_module(first==1, kwargs...) # Also checks if the user wants ONLY the HELP mode - dbg_cmd, d, cmd, arg1 = colorbar_parser(isa(arg1, Nothing) ? GMTcpt() : arg1, O, d) + dbg_cmd, d, cmd, arg1 = colorbar_parser(wrapDatasets("", arg1), O, d) (dbg_cmd !== nothing) && return dbg_cmd r = prep_and_call_finish_PS_module(d, cmd, "", K, O, true, arg1) (!isa(r,String)) && gmt("destroy") # Probably because of the rasters in cpt end -function colorbar_parser(arg1::Union{Nothing, GMTcpt}, O::Bool, d::Dict{Symbol, Any}) +function colorbar_parser(w::wrapDatasets, O::Bool, d::Dict{Symbol, Any}) + arg1 = unwrapDatasets(w::wrapDatasets)[2] + gmt_proggy = (IamModern[]) ? "colorbar " : "psscale " parse_paper(d) # See if user asked to temporarily pass into paper mode coordinates @@ -73,7 +75,7 @@ function colorbar_parser(arg1::Union{Nothing, GMTcpt}, O::Bool, d::Dict{Symbol, (!isempty(opt_D)) && (!contains(opt_D, "DJ") && !contains(opt_D, "Dj") && !contains(opt_D, "Dg")) && (cmd = replace(cmd, "-R " => "")) cmd *= opt_D - cmd, arg1, = add_opt_cpt(d, cmd, CPTaliases, 'C', 0, isempty(arg1) ? nothing : arg1) + cmd, arg1, = add_opt_cpt(d, cmd, CPTaliases, 'C', 0, arg1) if (opt_D === "" && ((val = hlp_desnany_str(d, [:triangles])) !== "")) # User asked for triangles but did not set pos (val == "true") && (val = "tri") # Means just triangles anc_tris = colorbar_triangles(val) # Returns anchor+triangles string diff --git a/src/utils_types.jl b/src/utils_types.jl index c904d4f17..203197319 100644 --- a/src/utils_types.jl +++ b/src/utils_types.jl @@ -208,7 +208,7 @@ function mat2ds(mat::Array{T,N}, txt::Union{String,Vector{String}}=String[]; d = KW(kwargs) _mat2ds(mat, txt, isa(hdr, String) ? [hdr] : vec(hdr), Int(geom), d) end -function _mat2ds(mat::Array{T,N}, txt::Union{String,Vector{String}}, hdr::Vector{String}, geom::Int, d::Dict)::GDtype where {T<:Real, N} +function _mat2ds(@nospecialize(mat::Array{<:Real}), txt::Union{String,Vector{String}}, hdr::Vector{String}, geom::Int, d::Dict)::GDtype coln = hlp_desnany_vstr(d, [:colnames]) (!isempty(txt)) && return text_record(mat, txt, hdr) @@ -1292,7 +1292,7 @@ function mat2img16(mat::AbstractArray{<:Unsigned}; x=Float64[], y=Float64[], v=F d = KW(kw) helper_mat2img(mat, vec(Float64.(x)), vec(Float64.(y)), vec(Float64.(v)), vec(Float64.(hdr)), proj4, wkt, cmap, is_transposed, d) end -function helper_mat2img(mat, x::Vector{Float64}, y::Vector{Float64}, v::Vector{Float64}, hdr::Vector{Float64}, +function helper_mat2img(@nospecialize(mat), x::Vector{Float64}, y::Vector{Float64}, v::Vector{Float64}, hdr::Vector{Float64}, proj4::String, wkt::String, cmap::GMTcpt, is_transposed::Bool, d::Dict) nx = size(mat, 2); ny = size(mat, 1); if (is_transposed) nx, ny = ny, nx end @@ -2443,9 +2443,14 @@ end # --------------------------------------------------------------------------------------------------- function grdimg_hdr_xy(mat, reg, hdr, x=Float64[], y=Float64[], is_transposed=false) + # Thin wrapper: convert to concrete types, then call the single-compilation implementation + _grdimg_hdr_xy(mat, Int(reg)::Int, Float64.(vec(hdr)), Float64.(vec(x)), Float64.(vec(y)), is_transposed == 1) +end + +function _grdimg_hdr_xy(@nospecialize(mat), reg::Int, hdr::Vector{Float64}, x::Vector{Float64}, y::Vector{Float64}, is_transposed::Bool) # Generate x,y coords array and compute/update header plus increments for grids/images # Arrays coming from GDAL are often scanline so they are transposed. In that case is_transposed should be true - row_dim, col_dim = (is_transposed) ? (2,1) : (1,2) + row_dim, col_dim = (is_transposed) ? (2,1) : (1,2) nx = size(mat, col_dim); ny = size(mat, row_dim); one_or_zero = reg == 0 ? 1 : 0 From 6706860769b9c31d79b0f8a5bad890f5158324f8 Mon Sep 17 00:00:00 2001 From: Joaquim Date: Wed, 25 Feb 2026 20:22:23 +0000 Subject: [PATCH 2/2] Add symlog.jl file. --- src/symlog.jl | 163 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 src/symlog.jl diff --git a/src/symlog.jl b/src/symlog.jl new file mode 100644 index 000000000..4455a92e3 --- /dev/null +++ b/src/symlog.jl @@ -0,0 +1,163 @@ +# Written by Claude.ai + +""" + symlog(x, y; axis=:y, linthresh=1, linscale=1, base=10, kwargs...) + symlog(D::GMTdataset; axis=:y, linthresh=1, linscale=1, base=10, kwargs...) + +Plot data with a symmetric logarithmic scale, like matplotlib's symlog. +Linear in [-linthresh, linthresh], logarithmic beyond. + +- `axis`: which axis to transform: `:y` (default), `:x`, or `:xy` for both +- `linthresh`: threshold below which the scale is linear (must be > 0) +- `linscale`: scale factor for the linear region +- `base`: logarithm base (default 10) +- All other kwargs are forwarded to `plot()` + +### Example +```julia +x = 1:100 +y = @. 10.0^(x/20) - 10.0^((101-x)/20) +symlog(x, y, linthresh=10, lw=1) +``` +""" +function symlog(x::AbstractVector{<:Real}, y::AbstractVector{<:Real}; axis::Symbol=:y, + linthresh::Real=1, linscale::Real=1, base::Real=10, first::Bool=true, kwargs...) + d = KW(kwargs) + lt, ls, b = Float64(linthresh), Float64(linscale), Float64(base) + _symlog_plot(Float64.(x), Float64.(y), axis, lt, ls, b, first, d) +end + +function symlog(D::GMTdataset; axis::Symbol=:y, + linthresh::Real=1, linscale::Real=1, base::Real=10, first::Bool=true, kwargs...) + d = KW(kwargs) + lt, ls, b = Float64(linthresh), Float64(linscale), Float64(base) + _symlog_plot(Float64.(view(D.data,:,1)), Float64.(view(D.data,:,2)), axis, lt, ls, b, first, d) +end + +function symlog(D::Vector{<:GMTdataset}; axis::Symbol=:y, + linthresh::Real=1, linscale::Real=1, base::Real=10, first::Bool=true, kwargs...) + d = KW(kwargs) + lt, ls, b = Float64(linthresh), Float64(linscale), Float64(base) + # Transform each dataset and plot; first one creates, rest overlay + for (i, Di) in enumerate(D) + xx, yy = Float64.(view(Di.data,:,1)), Float64.(view(Di.data,:,2)) + _symlog_plot(xx, yy, axis, lt, ls, b, i == 1 && first, d) + (i == 1) && (d = Dict{Symbol,Any}()) # Only pass user opts on first call + end +end + +function _symlog_plot(x::Vector{Float64}, y::Vector{Float64}, axis::Symbol, + lt::Float64, ls::Float64, b::Float64, first::Bool, d::Dict) + + # Transform the data + xt = (axis == :x || axis == :xy) ? [_symlog(v, lt, ls, b) for v in x] : x + yt = (axis == :y || axis == :xy) ? [_symlog(v, lt, ls, b) for v in y] : y + + # Build custom tick annotations for the transformed axis/axes + if (axis == :y || axis == :xy) + tpos, tlab = _symlog_ticks(y, lt, ls, b) + typ = ["a " * l for l in tlab] + d[:yaxis] = (custom = (pos=tpos, type=typ),) + end + if (axis == :x || axis == :xy) + tpos, tlab = _symlog_ticks(x, lt, ls, b) + typ = ["a " * l for l in tlab] + d[:xaxis] = (custom = (pos=tpos, type=typ),) + end + + # Ensure the non-transformed axis gets default annotations + if (axis == :y && !haskey(d, :xaxis) && !haskey(d, :frame) && !haskey(d, :B)) + d[:xaxis] = (annot=:auto,) + elseif (axis == :x && !haskey(d, :yaxis) && !haskey(d, :frame) && !haskey(d, :B)) + d[:yaxis] = (annot=:auto,) + end + + plot(mat2ds(hcat(xt, yt)); first=first, d...) +end + +# --------------------------------------------------------------------------- +# Core transform (single compilation, no type specialization) +# --------------------------------------------------------------------------- +function _symlog(x::Float64, linthresh::Float64, linscale::Float64, base::Float64)::Float64 + log_base = log(base) + linscale_adj = linscale / (1.0 - 1.0 / base) + abs_x = abs(x) + if abs_x <= linthresh + return x * linscale_adj / linthresh + else + return sign(x) * (linscale_adj + linscale * log(abs_x / linthresh) / log_base) + end +end + +""" + isymlog(y; linthresh=1, linscale=1, base=10) + +Inverse of the symlog transform. Converts transformed values back to original scale. +""" +function isymlog(y::Real; linthresh::Real=1, linscale::Real=1, base::Real=10) + _isymlog(Float64(y), Float64(linthresh), Float64(linscale), Float64(base)) +end + +function isymlog(v::AbstractArray{<:Real}; linthresh::Real=1, linscale::Real=1, base::Real=10) + lt, ls, b = Float64(linthresh), Float64(linscale), Float64(base) + [_isymlog(Float64(y), lt, ls, b) for y in v] +end + +function _isymlog(y::Float64, linthresh::Float64, linscale::Float64, base::Float64)::Float64 + log_base = log(base) + linscale_adj = linscale / (1.0 - 1.0 / base) + if abs(y) <= linscale_adj + return y * linthresh / linscale_adj + else + return sign(y) * linthresh * exp((abs(y) - linscale_adj) * log_base / linscale) + end +end + +# --------------------------------------------------------------------------- +# Tick generation — produces positions in transformed space + original-value labels +# --------------------------------------------------------------------------- +function _symlog_ticks(data::Vector{Float64}, lt::Float64, ls::Float64, b::Float64) + vmin, vmax = extrema(data) + ticks = Float64[] + labels = String[] + + function add_tick!(val::Float64) + push!(ticks, _symlog(val, lt, ls, b)) + push!(labels, _fmt_tick(val)) + end + + (vmin <= 0 <= vmax) && add_tick!(0.0) + + # +/- linthresh boundaries + (vmin <= -lt && vmax >= -lt) && add_tick!(-lt) + (vmax >= lt && vmin <= lt) && add_tick!(lt) + + # Positive log ticks + if vmax > lt + for e in floor(Int, log(b, lt)):ceil(Int, log(b, vmax)) + val = Float64(b^e) + (val >= lt && val <= vmax) && add_tick!(val) + end + end + + # Negative log ticks + if vmin < -lt + for e in floor(Int, log(b, lt)):ceil(Int, log(b, abs(vmin))) + val = -Float64(b^e) + (val <= -lt && val >= vmin) && add_tick!(val) + end + end + + perm = sortperm(ticks) + return ticks[perm], labels[perm] +end + +function _fmt_tick(v::Float64)::String + if v == 0 + return "0" + elseif abs(v) >= 1 && abs(v) == round(abs(v)) + return string(Int(v)) + else + return @sprintf("%.4g", v) + end +end