Skip to content

Commit

Permalink
Improve pcolor methods to plot Paerson's correlation matrices and Dat…
Browse files Browse the repository at this point in the history
…aFrames (#1682)

* Add and adapt some tests

* Let the annotation angle be passable via an extra member of xticks or xaxis=(custom=(...))

* Improve pcolor methods to plot Paerson's correlation matrices and DataFrames

* Update actions/cache
  • Loading branch information
joa-quim authored Mar 3, 2025
1 parent ffab0c7 commit ff400ac
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 39 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ jobs:
if: matrix.run_in_pr == true || github.event_name != 'pull_request'

- name: Cache GSHHG and DCW data
uses: actions/cache@v2
uses: actions/cache@v4
id: cache-coastline
with:
path: ${{ env.COASTLINEDIR }}
Expand Down
23 changes: 14 additions & 9 deletions src/common_options.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2920,10 +2920,8 @@ function axis(D::Dict=Dict(); x::Bool=false, y::Bool=false, z::Bool=false, secon
d = KW(kwargs) # These kwargs always come from the fields of a NamedTuple
axis(D, x, y, z, secondary, d)
end
#function axis(D::Dict=Dict(); x::Bool=false, y::Bool=false, z::Bool=false, secondary::Bool=false, kwargs...)::Tuple{String, Vector{Bool}}
function axis(D::Dict, x::Bool, y::Bool, z::Bool, secondary::Bool, d::Dict)::Tuple{String, Vector{Bool}}
# Build the (terrible) -B option
#d = KW(kwargs) # These kwargs always come from the fields of a NamedTuple

# Before anything else
(haskey(d, :none)) && return " -B0", [false, false]
Expand Down Expand Up @@ -3042,6 +3040,7 @@ function axis(D::Dict, x::Bool, y::Bool, z::Bool, secondary::Bool, d::Dict)::Tup
if (isa(d[:custom], String)) ints *= "c" * d[:custom]::String
else
if ((r = helper3_axes(d[:custom], primo, axe)) != "") ints *= "c" * r end
(axe == "x") && isa(d[:custom], NamedTuple) && ((_ang = get(d[:custom], :angle, nothing)) !== nothing) && (ints *= "+a$_ang")
end
elseif (haskey(d, :customticks)) # These ticks are custom axis
((r = ticks(d[:customticks]; axis=axe, primary=primo)) != "") && (ints *= "c" * r)
Expand Down Expand Up @@ -3176,8 +3175,8 @@ function helper3_axes(arg, primo::String, axe::String)::String
tipo = fill('a', n_annot) # Default to annotate
elseif (isa(arg, NamedTuple) || isa(arg, Dict))
if (isa(arg, NamedTuple)) d = nt2dict(arg) end
!haskey(d, :pos) && error("Custom annotations NamedTuple must contain the member 'pos'")
pos = isa(d[:pos], Vector{<:AbstractRange}) ? collect(d[:pos][1]) : d[:pos]
(!haskey(d, :pos) && !haskey(d, :label)) && error("Custom annotations NamedTuple must contain at least one of: 'label' or 'pos' members.")
pos = (!haskey(d, :pos)) ? collect(1:length(d[:label])) : (isa(d[:pos], Vector{<:AbstractRange}) ? collect(d[:pos][1]) : d[:pos])
n_annot = length(pos); got_tipo = false
if ((val = find_in_dict(d, [:type])[1]) !== nothing)
if (isa(val, Char) || isa(val, String) || isa(val, Symbol))
Expand Down Expand Up @@ -3239,12 +3238,18 @@ zticks(labels, pos=nothing) = ticks(labels, pos; axis="z")
function ticks(labels, pos=nothing; axis="x", primary="p")
# Simple plot of custom ticks.
# LABELS can be an Array or Tuple of strings or symbols with the labels to be plotted at ticks in POS
if (isa(labels, Tuple) && length(labels) == 2 && isa(labels[1], AbstractArray))
types_in = typeof.(labels) # If they are all equal it means we got a labels=(:a, :b, :c) or labels=("a", "b", "c")
if (isa(labels, Tuple) && length(labels) > 1 && !all(types_in .== types_in[1]))
# helper3 wants (Array{Real}, Array{String}) but here we accept both orders, just need to figure out which
inds = (eltype(labels[1]) <: AbstractString) ? [2,1] : [1,2]
!(isa(labels[inds[1]], AbstractArray) && eltype(labels[inds[1]][1]) <: Real) &&
error("Input must be: (Vector{Real}, Vector{String}) (in any order)")
r = helper3_axes((pos=labels[inds[1]], label=labels[inds[2]]), primary, axis)
# If 'labels contains an element that is a number, we interpret it to mean the annotations angle.'
((ind_s = findfirst(types_in .<: AbstractArray{String})) === nothing) &&
((inds_s = findfirst(eltype.(labels) .== String)) === nothing) &&
error("Must provide the annotation labels in form of a vector or tuple.")
pos = ((ind_p = findfirst(types_in .<: AbstractArray{<:Real})) !== nothing) ? labels[ind_p] : (1:length(labels[ind_s]))
ind_a = findfirst(types_in .<: Real)
(ind_a !== nothing && axis != "x") && (ind_a = nothing; @warn("Annotation angle can only be specified for the x axis."))
r = helper3_axes((pos=pos, label=labels[ind_s]), primary, axis)
(ind_a !== nothing) && (r *= "+a" * string(labels[ind_a]))
else
_pos = (pos === nothing) ? (1:length(labels)) : pos
r = helper3_axes((pos=_pos, label=labels), primary, axis)
Expand Down
100 changes: 77 additions & 23 deletions src/pcolor.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Creates a colored cells plot using the values in matrix `C`. The color of each c
value of `C` after consulting a color table (cpt). If a color table is not provided via option `cmap=xxx` we
compute a default one.
### Args
- `X`, `Y`: Vectors or 1 row matrices with the x- and y-coordinates for the vertices. The number of
elements of `X` must match the number of columns in `C` (is using the grid registration model) or exceed
it by one (pixel registration). The same for `Y` and the number of rows in `C`. Notice that `X` and `Y`
Expand All @@ -13,7 +14,9 @@ compute a default one.
m-by-n grid, then `C` should be an (m-1)-by-(n-1) matrix, though we also allow it to be m-by-n but we then
drop the last row and column from `C`
- `C`: A matrix with the values that will be used to color the cells.
- `kwargs`: This form of `pcolor` is in fact a wrap up of ``plot`` so any option of that module can be used here.
### Kwargs
This form of `pcolor` is in fact a wrap up of ``plot`` so any option of that module can be used here.
- `labels`: If this ``keyword`` is used then we plot the value of each node in the corresponding cell. Use `label=n`,
where ``n`` is integer and represents the number of printed decimals. Any other value like ``true``, ``"y"``
or ``:y`` tells the program to guess the number of decimals.
Expand All @@ -36,8 +39,17 @@ This form takes a grid (or the file name of one) as input an paints it's cell wi
- `kwargs`: This form of `pcolor` is a wrap of ``grdview`` so any option of that module can be used here.
One can for example control the tilling option via ``grdview's`` ``tiles`` option.
---
pcolor(GorD; kwargs...)
If `GorD` is either a GMTgrid or a GMTdataset containing a Pearson correlation matrix obtained with ``GMT.cor()``,
the processing recieves a special treatment. In this case, other than the `labels` keyword, user is also
interested in seing if the automatic choice of x-annotaions angle is correct. If not, one can force it
by setting the `rotx` (ot `slanted`) keywords.
### Examples
```julia
# Create an example grid
G = GMT.peaks(N=21);
Expand All @@ -56,6 +68,13 @@ This form takes a grid (or the file name of one) as input an paints it's cell wi
X,Y = meshgrid(-3:6/17:3);
XX = 2*X .* Y; YY = X.^2 .- Y.^2;
pcolor(XX,YY, reshape(repeat([1:18; 18:-1:1], 9,1), size(XX)), lc=:black, show=true)
```
Display a Pearson's correlation matrix
```julia
pcolor(GMT.cor(rand(4,4)), labels=:y, colorbar=1, show=true)
```
"""
function pcolor(X_::VMr, Y_::VMr, C::Union{Nothing, AbstractMatrix{<:Real}}=nothing; first::Bool=true, kwargs...)
pcolor(X_, Y_, C, first, KW(kwargs))
Expand Down Expand Up @@ -106,7 +125,7 @@ function pcolor(X_::VMr, Y_::VMr, C::Union{Nothing, AbstractMatrix{<:Real}}, fir
end

Z = istransposed(C) ? vec(copy(C)) : vec(C)
do_show, got_labels, ndigit, opt_F = helper_pcolor(d, Z)
do_show, got_labels, ndigit, opt_F = helper_pcolor(d, Z, round(Int, sqrt(length(Z))))

got_fn = ((fname = find_in_dict(d, [:name :figname :savefig])[1]) !== nothing)
d[:show] = got_labels ? false : do_show
Expand All @@ -133,26 +152,57 @@ pcolor!(X::VMr, Y::VMr, C::Matrix{<:Real}; kw...) = pcolor(X, Y, C; first=false,

# ---------------------------------------------------------------------------------------------------
function pcolor(cmd0::String="", arg1=nothing; first=true, kwargs...)
pcolor(cmd0, arg1, first, KW(kwargs))
(cmd0 != "") && (arg1 = gmtread(cmd0))
(isa(arg1, Matrix)) && (arg1 = mat2grid(Float32.(arg1)))
isdataframe(arg1) && (arg1 = df2ds(arg1))
if ((arg1 == arg1' && arg1[1] == 1 && arg1[end] == 1)) # A corr matrix (computed with GMT.cor())
return pcolor(mat2ds(arg1.z); first=first, kwargs...)
elseif (isa(arg1, GMTdataset)) # Arrive here when arg1 was originally a DataFrame
return pcolor(arg1; first=first, kwargs...)
end
pcolor(arg1, first, KW(kwargs))
end
function pcolor(cmd0::String, arg1, first::Bool, d::Dict{Symbol,Any})
# Method for grids

function get_grid_xy(reg, bbox, inc, nx, ny) # Return the grid registration x,y coord vectors.
_bbox = copy(bbox) # Make a copy to not risk to change the original
if (reg == 1)
_bbox[1] += inc[1] / 2; _bbox[2] -= inc[1] / 2;
_bbox[3] += inc[2] / 2; _bbox[4] -= inc[2] / 2;
function pcolor(D::GMTdataset; first=true, kwargs...)
d = KW(kwargs)
z = Float32.(D.data)
colnames = D.colnames[1:size(z,2)]
ang::Float64 = 90.0
if ((val = find_in_dict(d, [:slanted :rotx])[1]) !== nothing)
ang = val
elseif (is_in_dict(d, [:figscale :fig_scale :scale :figsize :fig_size]) === nothing)
maxchars = maximum(length.(colnames))
with_per_col = 15 / size(z,2)
ang = (with_per_col / 0.25) >= maxchars ? 0 : 90
end
d[:xticks] = (colnames, ang)
d[:yticks] = colnames[end:-1:1]
if (D == D' && D[1] == 1 && D[end] == 1) # A correlation matrix (that computed with GMT.cor())
(is_in_dict(d, CPTaliases) === nothing) && (d[:C] = makecpt(T=(-1,1.0,0.1), C="tomato,azure1,dodgerblue4", Z=true))
for n = 1:size(z,2), m = 1:size(z,1)
m < n && (z[m,n] = NaN) # Since the matrix is symmetric, remove the upper triangle
end
x, y = linspace(_bbox[1], _bbox[2], nx), linspace(_bbox[3], _bbox[4], ny)
return x,y
end
pcolor(mat2grid(flipud(z)), first, d)
end

function pcolor(G::GMTgrid, first::Bool, d::Dict{Symbol,Any})
# Method for grids

changed_reg = false
if (G.registration == 0) # If not pixel reg, make it so
G.registration = 1
range_bak = copy(G.range)
x_bak, y_bak = G.x, G.y
G.range[1:4] .= range_bak[1:4] .- [G.inc[1], -G.inc[1], G.inc[2], -G.inc[2]] / 2
G.x = linspace(G.range[1], G.range[2], size(G.z, 2)+1)
G.y = linspace(G.range[3], G.range[4], size(G.z, 1)+1)
changed_reg = true
end

got_labels = false
if (is_in_dict(d, [:labels]) !== nothing)
G = (isa(arg1, GMTgrid)) ? arg1 : gmtread(cmd0) # If fname we have to read the grid
x,y = (G.registration == 0) ? (G.x, G.y) : get_grid_xy(G.registration, G.range, G.inc, size(G,2), size(G,1))
do_show, got_labels, ndigit, opt_F = helper_pcolor(d, G.range[5:6])
do_show, got_labels, ndigit, opt_F = helper_pcolor(d, G.range[5:6], size(G.z, 2))
end

if (find_in_dict(d, [:T :no_interp :tiles])[1] === nothing) # If no -T, make one here
Expand All @@ -161,14 +211,17 @@ function pcolor(cmd0::String, arg1, first::Bool, d::Dict{Symbol,Any})
opt_T *= "+o" * add_opt_pen(Dict(:outline => val), [:outline])
end
d[:T] = opt_T
grdview_helper(cmd0, arg1, !first, true, d)
else
grdview_helper(cmd0, arg1, !first, true, d)
end
grdview_helper("", G, !first, true, d)

(changed_reg) && ((G.registration, G.range, G.x, G.y) = (0, range_bak, x_bak, y_bak)) # Undo the reg change

if (got_labels)
X,Y = meshgrid(x, y)
Dt = mat2ds([X[:] Y[:]], string.(round.(G.z[:], digits=ndigit)))
X,Y = (G.registration == 0) ? meshgrid(G.x, G.y) : meshgrid(G.x[1:end-1].+G.inc[1]/2, G.y[1:end-1].+G.inc[2]/2)
z = G.z[:]
ind = .!isnan.(z) # We don't want to plot NaNs
_X, _Y, _z = X[ind], Y[ind], z[ind]
Dt = mat2ds([_X _Y], string.(round.(_z, digits=ndigit)))
text!(Dt, F=opt_F, show=do_show)
end
end
Expand All @@ -179,16 +232,17 @@ pcolor!(cmd0::String="", arg1=nothing; kw...) = pcolor(cmd0, arg1; first=false,
pcolor!(arg1; kw...) = pcolor("", arg1; first=false, kw...)

# ---------------------------------------------------------------------------------------------------
function helper_pcolor(d::Dict{Symbol,Any}, Z)
function helper_pcolor(d::Dict{Symbol,Any}, Z, nc::Int)
# Lots of gymn to see if we have a show request and suspend it in case we also want to plot text labels
# Also fishes contents of the 'labels' and 'font' keywords.
do_show, got_labels = false, false
ndigit, opt_F = 2, "+f6p+jMC" # Just default value to always have these vars defined
opt_F = (nc < 5) ? "+f10p+jMC" : (nc < 8 ? "+f9p+jMC" : nc < 11 ? "+f8p+jMC" : nc <= 15 ? "+f7p+jMC" : nc < 30 ? "+f6p+jMC" : "+f5p+jMC")
ndigit = 2 # Just default value to always have these vars defined
if (is_in_dict(d, [:labels]) !== nothing)
got_labels = true
if ((isa(d[:labels], Bool) && d[:labels]) || isa(d[:labels], String) || isa(d[:labels], Symbol))
dif = (length(Z) == 2) ? abs(Z[2] - Z[1]) : abs(maximum_nan(Z) - minimum_nan(Z))
ndigit = (dif < 1) ? 3 : (dif <= 10 ? 2 : (dif < 100 ? 1 : 0))
ndigit = (dif < 1) ? 3 : (dif <= 10 ? 2 : (dif < 100 ? 1 : 0)) + Int(nc <= 15)
elseif (isa(d[:labels], Int))
ndigit = abs(d[:labels])
end
Expand Down
3 changes: 0 additions & 3 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,8 @@ using FFTW
include("test_lepto_funs.jl")
include("test_beziers.jl")
include("test_cody.jl")
println(" Entering: test_imgfuns.jl")
include("test_imgfuns.jl")
println(" Entering: test_imgtiles.jl")
include("test_imgtiles.jl")
println(" Entering: test_makecpts.jl")
include("test_makecpts.jl")
println(" Entering: test_avatars.jl")
include("test_avatars.jl")
Expand Down
3 changes: 2 additions & 1 deletion test/test_avatars.jl
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@
pcolor(G.x, G.y, G.z, labels=:y, show=false)
pcolor(G.x, G.y, G.z, labels=1)
pcolor(G, labels=1, font=(angle=45, font=(5,:red)))
pcolor(GMT.cor(rand(4,4)), labels=:y);
x = -20:5:40; y = 30:5:50;
GMT.boxes(x,y, grdlandmask=:water);
GMT.boxes(meshgrid(x,y)...);
Expand Down Expand Up @@ -288,7 +289,7 @@
bar(rand(20),hbar=(width=0.5,unit=:c, base=9), Vd=dbg2)
bar(rand(20),bar="0.5c+b9", Vd=dbg2)
bar(rand(20),hbar="0.5c+b9", Vd=dbg2)
bar(rand(10), xaxis=(custom=(pos=1:5,type="A"),), Vd=dbg2)
bar(rand(10), xaxis=(custom=(pos=1:5,type="A",angle=45),), Vd=dbg2)
bar(rand(10), axis=(custom=(pos=1:5,label=[:a :b :c :d :e]),), Vd=dbg2)
bar((1,2,3), Vd=dbg2)
bar((1,2,3), (1,2,3), Vd=dbg2)
Expand Down
4 changes: 2 additions & 2 deletions test/test_common_opts.jl
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@
@test GMT.parse_B(Dict(:frame => :full), "")[1] == " -Baf -BWSEN"
@test GMT.parse_B(Dict(:title => "BlaBla", :frame => :none), "")[1] == " -B+tBlaBla"
GMT.helper2_axes("lolo");
@test_throws ErrorException("Custom annotations NamedTuple must contain the member 'pos'") GMT.helper3_axes((a=0,),"","")
@test_throws ErrorException("Custom annotations NamedTuple must contain at least one of: 'label' or 'pos' members.") GMT.helper3_axes((a=0,),"","")

@test GMT.consolidate_Baxes(" -Baf -BWSen -BpxaUfg10 -BWSen+taiai -Bpx+lai+sBlaBla -Bpyclixo.txt -Bsxa5f1") ==
" -BWSen -BpxaUfg10 -BWSen+taiai -Bpx+lai+sBlaBla -Bpyclixo.txt -Bsxa5f1"
Expand Down Expand Up @@ -222,7 +222,7 @@
@test GMT.line_decorated_with_symbol(Dict()) == " -S~d0.88:+sc0.11+gwhite+p0.75,black"
@test_throws ErrorException("Argument of the *bar* keyword can be only a string or a NamedTuple.") GMT.parse_bar_cmd(Dict(:a => 0), :a, "", "")

@test_throws ErrorException("Custom annotations NamedTuple must contain the member 'pos'") GMT.helper3_axes((post=1:5,), "p", "x")
@test_throws ErrorException("Custom annotations NamedTuple must contain at least one of: 'label' or 'pos' members.") GMT.helper3_axes((post=1:5,), "p", "x")
GMT.helper3_axes(1,"","") # Trigger a warning

dt = collect(Dates.DateTime(Dates.now()):Dates.Month(6):Dates.DateTime(Dates.now() + Dates.Year(10)));
Expand Down

0 comments on commit ff400ac

Please sign in to comment.