diff --git a/src/UnitfulRecipes.jl b/src/UnitfulRecipes.jl index 67ef9df..cf8ce4f 100644 --- a/src/UnitfulRecipes.jl +++ b/src/UnitfulRecipes.jl @@ -1,20 +1,22 @@ module UnitfulRecipes using RecipesBase -using Unitful: Quantity, unit, ustrip, Unitful, dimension, Units +using Unitful: Quantity, unit, ustrip, Unitful, dimension, Units, MixedUnits export @P_str +include("UnitfulWrapper.jl") + const clims_types = (:contour, :contourf, :heatmap, :surface) #========== Main recipe ==========# -@recipe function f(::Type{T}, x::T) where T <: AbstractArray{<:Union{Missing,<:Quantity}} +@recipe function f(::Type{T}, x::T) where T <: AbstractArray{<:Union{Missing,<:Quantity, <:LogScaled}} axisletter = plotattributes[:letter] # x, y, or z if (axisletter == :z) && get(plotattributes, :seriestype, :nothing) ∈ clims_types - u = get(plotattributes, :zunit, unit(eltype(x))) + u = get(plotattributes, :zunit, fullunit(eltype(x))) ustripattribute!(plotattributes, :clims, u) append_unit_if_needed!(plotattributes, :colorbar_title, u) end @@ -30,7 +32,7 @@ function fixaxis!(attr, x, axisletter) axisunit = Symbol(axisletter, :unit) # xunit, yunit, zunit axis = Symbol(axisletter, :axis) # xaxis, yaxis, zaxis # Get the unit - u = pop!(attr, axisunit, unit(eltype(x))) + u = pop!(attr, axisunit, fullunit(eltype(x))) # If the subplot already exists with data, get its unit sp = get(attr, :subplot, 1) if sp ≤ length(attr[:plot_object]) && attr[:plot_object].n > 0 @@ -54,14 +56,14 @@ function fixaxis!(attr, x, axisletter) fixmarkersize!(attr) fixlinecolor!(attr) # Strip the unit - ustrip.(u, x) + uwstrip.(u, x) end # Recipe for (x::AVec, y::AVec, z::Surface) types const AVec = AbstractVector const AMat{T} = AbstractArray{T,2} where T @recipe function f(x::AVec, y::AVec, z::AMat{T}) where T <: Quantity - u = get(plotattributes, :zunit, unit(eltype(z))) + u = get(plotattributes, :zunit, fullunit(eltype(z))) ustripattribute!(plotattributes, :clims, u) z = fixaxis!(plotattributes, z, :z) append_unit_if_needed!(plotattributes, :colorbar_title, u) @@ -69,34 +71,34 @@ const AMat{T} = AbstractArray{T,2} where T end # Recipe for vectors of vectors -@recipe function f(::Type{T}, x::T) where T <: AbstractVector{<:AbstractVector{<:Union{Missing,<:Quantity}}} +@recipe function f(::Type{T}, x::T) where T <: AbstractVector{<:AbstractVector{<:Union{Missing,<:Quantity, <:LogScaled}}} axisletter = plotattributes[:letter] # x, y, or z [fixaxis!(plotattributes, x, axisletter) for x in x] end -# Recipe for bare units -@recipe function f(::Type{T}, x::T) where T <: Units +# Recipe for bare Union{Units, MixedUnits} +@recipe function f(::Type{T}, x::T) where T <: Union{Units, MixedUnits} primary := false Float64[]*x end # Recipes for functions -@recipe function f(f::Function, x::T) where T <: AVec{<:Union{Missing,<:Quantity}} +@recipe function f(f::Function, x::T) where T <: AVec{<:Union{Missing,<:Quantity,<:LogScaled}} x, f.(x) end -@recipe function f(x::T, f::Function) where T <: AVec{<:Union{Missing,<:Quantity}} +@recipe function f(x::T, f::Function) where T <: AVec{<:Union{Missing,<:Quantity,<:LogScaled}} x, f.(x) end -@recipe function f(x::T, y::AVec, f::Function) where T <: AVec{<:Union{Missing,<:Quantity}} +@recipe function f(x::T, y::AVec, f::Function) where T <: AVec{<:Union{Missing,<:Quantity,<:LogScaled}} x, y, f.(x',y) end -@recipe function f(x::AVec, y::T, f::Function) where T <: AVec{<:Union{Missing,<:Quantity}} +@recipe function f(x::AVec, y::T, f::Function) where T <: AVec{<:Union{Missing,<:Quantity,<:LogScaled}} x, y, f.(x',y) end -@recipe function f(x::T1, y::T2, f::Function) where {T1<:AVec{<:Union{Missing,<:Quantity}}, T2<:AVec{<:Union{Missing,<:Quantity}}} +@recipe function f(x::T1, y::T2, f::Function) where {T1<:AVec{<:Union{Missing,<:Quantity,<:LogScaled}}, T2<:AVec{<:Union{Missing,<:Quantity,<:LogScaled}}} x, y, f.(x',y) end -@recipe function f(f::Function, u::Units) +@recipe function f(f::Function, u::Union{Units, MixedUnits}) uf = UnitFunction(f, [u]) recipedata = RecipesBase.apply_recipe(plotattributes, uf) (_, xmin, xmax) = recipedata[1].args @@ -117,7 +119,7 @@ uf(3, 2) == f(3u"m", 2u"m"^2) == 7u"m^2" """ struct UnitFunction <: Function f::Function - u::Vector{Units} + u::Vector{T} where T<: Union{Units, MixedUnits} end (f::UnitFunction)(args...) = f.f((args .* f.u)...) @@ -138,8 +140,8 @@ fixlinecolor!(attr) = ustripattribute!(attr, :line_z) function ustripattribute!(attr, key) if haskey(attr, key) v = attr[key] - u = unit(eltype(v)) - attr[key] = ustrip.(u, v) + u = fullunit(eltype(v)) + attr[key] = uwstrip.(u, v) return u else return Unitful.NoUnits @@ -150,7 +152,7 @@ function ustripattribute!(attr, key, u) if haskey(attr, key) v = attr[key] if eltype(v) <: Quantity - attr[key] = ustrip.(u, v) + attr[key] = uwstrip.(u, v) end end u @@ -198,7 +200,7 @@ end Append unit to labels when appropriate =====================================# -function append_unit_if_needed!(attr, key, u::Unitful.Units) +function append_unit_if_needed!(attr, key, u::T) where T<:Union{MixedUnits, Units} label = get(attr, key, nothing) append_unit_if_needed!(attr, key, label, u) end diff --git a/src/UnitfulWrapper.jl b/src/UnitfulWrapper.jl new file mode 100644 index 0000000..ad006be --- /dev/null +++ b/src/UnitfulWrapper.jl @@ -0,0 +1,61 @@ +using Unitful: Gain, Level, MixedUnits, LogScaled, NoUnits, uconvert +using Unitful: ustrip +export fullunit, uwstrip + +#===== +Extension to Unitful.jl, required to plot LogScaled units and +Quantities with LogScaled values (dB/Hz). + +Intruduces function: + fullunit + +and adds a new method to ustrip for LogScaled units. + +=====# + +#===== +fullunit(x) +------------ +Define new function fullunit which returns correct unit of Quantity and LogScaled. + +Unitful.unit(dB/Hz) == Hz⁻¹ +Unitful.unit(dB) == NoUnits +Unitful.logunit(dB/Hz) == dB + +expected behavior: +fullunit(dB/Hz) == dB Hz⁻¹ +fullunit(dB) == dB +fullunit(Hz) == Hz +=====# + +# general argument +fullunit(x::T) where T<: Quantity = unit(x) +fullunit(x::Type{T}) where T<:Quantity = unit(x) + +# gain +fullunit(x::Gain{L,S}) where {L,S} = fullunit(eltype(x)) +fullunit(::Type{T}) where {L,S,T<:Gain{L,S}} = MixedUnits{Gain{L,S}}() + +# level +fullunit(x::Level{L,S}) where {L,S} = fullunit(eltype(x)) +fullunit(::Type{T}) where {L,S,T<:Level{L,S}} = MixedUnits{Level{L,S}}() + +# quantity with mixed unit +fullunit(x::Quantity{T,D,U}) where {T<:LogScaled , D , U} = fullunit(eltype(x)) +fullunit(::Type{Quantity{T, D, U}}) where {T<:LogScaled , D , U} = fullunit(T) * U() + +# others from Unitful util.jl +fullunit(x::T) where T<: Number = NoUnits +fullunit(::Type{T}) where T<:Number= NoUnits +fullunit(::Type{Union{Missing, T}}) where T = fullunit(T) +fullunit(::Type{Missing}) = missing +fullunit(x::Missing) = missing + + +#===== +uwstrip(u, x) placeholder for ustrip +=====# +# level +uwstrip(u::T, x::L) where {T<:MixedUnits, L<:Union{Quantity, LogScaled}} = ustrip(uconvert(u, x)) +#all other +uwstrip(u, x) = ustrip(u, x) diff --git a/test/runtests.jl b/test/runtests.jl index 8ee0af0..c9d087d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,6 @@ using Test, Unitful, Plots -using Unitful: m, s, cm, DimensionError +using Unitful: m, s, cm, mm, DimensionError +using Unitful: dBm, dB, dBV, V, Hz, B, MHz using UnitfulRecipes # Some helper functions to access the subplot labels and the series inside each test plot @@ -299,3 +300,75 @@ end plot!(plt, (1:3)m) @test yguide(plt) == "m" end + +@testset "LogScaled plots" begin + x, y, dbv, v = randn(3)*dBm, randn(3)*dB, rand(3)*dBV, rand(3)V + + @testset "no keyword argument" begin + @test xguide(plot(x,y)) == "dBm" + @test xseries(plot(x,y)) ≈ ustrip.(x) + @test yguide(plot(x,y)) == "dB" + @test yseries(plot(x,y)) ≈ ustrip.(y) + plot(x, dbv) + @test yseries(plot!(x, v)) ≈ ustrip(uconvert.(u"dBV", v)) + plot(x, v) + @test yseries(plot!(x, dbv)) ≈ ustrip(uconvert.(u"V", dbv)) + end + + @testset "labels" begin + @test xguide(plot(x, y, xlabel= "hello")) == "hello (dBm)" + @test xguide(plot(x, y, xlabel=P"hello")) == "hello" + @test yguide(plot(x, y, ylabel= "hello")) == "hello (dB)" + @test yguide(plot(x, y, ylabel=P"hello")) == "hello" + @test xguide(plot(x, y, xlabel= "hello", ylabel= "hello")) == "hello (dBm)" + @test xguide(plot(x, y, xlabel=P"hello", ylabel=P"hello")) == "hello" + @test yguide(plot(x, y, xlabel= "hello", ylabel= "hello")) == "hello (dB)" + @test yguide(plot(x, y, xlabel=P"hello", ylabel=P"hello")) == "hello" + end +end + +@testset "mixed Log units" begin + x, y, x1, y1 = randn(3)*dB/Hz, randn(3)*dB*m, rand(3)*B/MHz, rand(3)*B*cm + + @testset "no keyword argument" begin + dbhz = Sys.isapple() ? "dB Hz⁻¹" : "dB Hz^-1" # expect fancy exponent or not? + @test xguide(plot(x,y)) == dbhz + @test xseries(plot(x,y)) ≈ ustrip.(x) + @test yguide(plot(x,y)) == "dB m" + @test yseries(plot(x,y)) ≈ ustrip.(y) + end + @testset "plot!" begin + plot(x, y) + @test xseries(plot!(x1, y)) ≈ ustrip(ustrip(uconvert.(u"dB/Hz", x1))) + @test yseries(plot!(x1, y1)) ≈ ustrip(ustrip(uconvert.(u"dB*m", y1))) + end + +end + +@testset "fullunit methods test" begin + @test @inferred(fullunit(1m^2)) === m^2 + @test @inferred(fullunit(1dB)) === dB + @test @inferred(fullunit(1dBm)) === dBm + @test @inferred(fullunit(1dB*m)) === dB*m + @test @inferred(fullunit(typeof(1m^2))) === m^2 + @test @inferred(fullunit(typeof(1dB))) === dB + @test @inferred(fullunit(typeof(1dBm))) === dBm + @test @inferred(fullunit(typeof(1dB*m))) === dB*m + @test @inferred(fullunit(Float64)) === NoUnits + @test @inferred(fullunit(Union{typeof(1m^2),Missing})) === m^2 + @test @inferred(fullunit(Union{Float64,Missing})) === NoUnits + @test @inferred(fullunit(missing)) === missing + @test @inferred(fullunit(Missing)) === missing +end + +@testset "uwstrip tests" begin + # ustrip with type and unit arguments + @test @inferred(uwstrip(m, 3.0m)) === 3.0 + @test @inferred(uwstrip(m, 2mm)) === 1//500 + @test @inferred(uwstrip(mm, 3.0m)) === 3000.0 + @test @inferred(uwstrip(NoUnits, 3.0m/1.0m)) === 3.0 + @test @inferred(uwstrip(NoUnits, 3.0m/1.0cm)) === 300.0 + @test @inferred(uwstrip(cm, missing)) === missing + @test @inferred(uwstrip(NoUnits, missing)) === missing + @test_throws DimensionError uwstrip(NoUnits, 3.0m/1.0s) +end \ No newline at end of file