Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .zenodo.json
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,12 @@
"name": "Patrick Jaap",
"type": "Other"
},
{
"affiliation": "Purdue University",
"name": "Isaac Wheeler",
"orcid": "0000-0002-9717-073X",
"type": "Other"
},
{
"affiliation": "European XFEL",
"name": "James Wrigley",
Expand Down
151 changes: 69 additions & 82 deletions ext/UnitfulExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,38 +22,38 @@ Main recipe
if axisletter === :z && get(plotattributes, :seriestype, :nothing) ∈ clims_types
u = get(plotattributes, :zunit, _unit(eltype(x)))
ustripattribute!(plotattributes, :clims, u)
append_unit_if_needed!(plotattributes, :colorbar_title, u)
append_cbar_unit_if_needed!(plotattributes, u)
end
fixaxis!(plotattributes, x, axisletter)
end

function fixaxis!(attr, x, axisletter)
# Attribute keys
axislabel = Symbol(axisletter, :guide) # xguide, yguide, zguide
axislims = Symbol(axisletter, :lims) # xlims, ylims, zlims
axisticks = Symbol(axisletter, :ticks) # xticks, yticks, zticks
err = Symbol(axisletter, :error) # xerror, yerror, zerror
axisunit = Symbol(axisletter, :unit) # xunit, yunit, zunit
axis = Symbol(axisletter, :axis) # xaxis, yaxis, zaxis
u = pop!(attr, axisunit, _unit(eltype(x))) # get the unit
# if the subplot already exists with data, get its unit
# if the subplot already exists with data, use that unit
sp = get(attr, :subplot, 1)
if sp ≤ length(attr[:plot_object]) && attr[:plot_object].n > 0
label = attr[:plot_object][sp][axis][:guide]
u = getaxisunit(label)
get!(attr, axislabel, label) # if label was not given as an argument, reuse
spu = getaxisunit(attr[:plot_object][sp][axis])
if !isnothing(spu)
u = spu
else # Subplot exists but doesn't have a unit yet
u = get!(attr, axisunit, _unit(eltype(x))) # get the unit
end
else # Subplot doesn't exist yet, so create it with given unit
u = get!(attr, axisunit, _unit(eltype(x))) # get the unit
end
# fix the attributes: labels, lims, ticks, marker/line stuff, etc.
append_unit_if_needed!(attr, axislabel, u)
ustripattribute!(attr, err, u)
if axisletter === :y
ustripattribute!(attr, :ribbon, u)
ustripattribute!(attr, :fillrange, u)
end
fixaspectratio!(attr, u, axisletter)
fixmarkercolor!(attr)
fixseriescolor!(attr, :marker_z)
fixseriescolor!(attr, :line_z)
fixmarkersize!(attr)
fixlinecolor!(attr)
_ustrip.(u, x) # strip the unit
end

Expand All @@ -62,7 +62,7 @@ end
u = get(plotattributes, :zunit, _unit(eltype(z)))
ustripattribute!(plotattributes, :clims, u)
z = fixaxis!(plotattributes, z, :z)
append_unit_if_needed!(plotattributes, :colorbar_title, u)
append_cbar_unit_if_needed!(plotattributes, u)
x, y, z
end

Expand Down Expand Up @@ -154,13 +154,32 @@ function fixaspectratio!(attr, u, axisletter)
end

# Markers / lines
function fixmarkercolor!(attr)
u = ustripattribute!(attr, :marker_z)
function fixseriescolor!(attr, key)
sp = get(attr, :subplot, 1)
# Precedence to user-passed zunit
if haskey(attr, :zunit)
u = attr[:zunit]
ustripattribute!(attr, key, u)
# Then to an existing subplot's colorbar title
elseif sp ≤ length(attr[:plot_object]) && attr[:plot_object].n > 0
cbar_title = get(attr[:plot_object][sp], :colorbar_title, nothing)
spu = (cbar_title isa UnitfulString ? cbar_title.unit : nothing)
if !isnothing(spu)
u = spu
ustripattribute!(attr, key, u)
else
u = ustripattribute!(attr, key)
end
# Otherwise, get from the attribute
else
u = ustripattribute!(attr, key)
end
ustripattribute!(attr, :clims, u)
u == NoUnits || append_unit_if_needed!(attr, :colorbar_title, u)
# fixseriescolor! is called for each axis, so after the first pass,
# u will be NoUnits and we don't want to append unit again
u == NoUnits || append_cbar_unit_if_needed!(attr, u)
end
fixmarkersize!(attr) = ustripattribute!(attr, :markersize)
fixlinecolor!(attr) = ustripattribute!(attr, :line_z)

# strip unit from attribute[key]
ustripattribute!(attr, key) =
Expand All @@ -179,110 +198,76 @@ function ustripattribute!(attr, key, u)
v = attr[key]
if eltype(v) <: Quantity
attr[key] = _ustrip.(u, v)
elseif v isa Tuple
attr[key] = Tuple([(eltype(vi) <: Quantity ? _ustrip.(u, vi) : vi) for vi in v])
end
end
u
end

#=======================================
Label string containing unit information
Used only for colorbars, etc., which don't
have a bettter place for storing units
=======================================#

abstract type AbstractProtectedString <: AbstractString end
struct ProtectedString{S} <: AbstractProtectedString
content::S
end
struct UnitfulString{S,U} <: AbstractProtectedString
const APS = Plots.AbstractProtectedString
struct UnitfulString{S,U} <: APS
content::S
unit::U
end
# Minimum required AbstractString interface to work with Plots
const S = AbstractProtectedString
Base.iterate(n::S) = iterate(n.content)
Base.iterate(n::S, i::Integer) = iterate(n.content, i)
Base.codeunit(n::S) = codeunit(n.content)
Base.ncodeunits(n::S) = ncodeunits(n.content)
Base.isvalid(n::S, i::Integer) = isvalid(n.content, i)
Base.pointer(n::S) = pointer(n.content)
Base.pointer(n::S, i::Integer) = pointer(n.content, i)

Plots.protectedstring(s) = ProtectedString(s)

#=====================================
Append unit to labels when appropriate
This is needed for colorbars, etc., since axes have
distinct unit handling
=====================================#

append_unit_if_needed!(attr, key, u) =
append_unit_if_needed!(attr, key, get(attr, key, nothing), u)
append_cbar_unit_if_needed!(attr, u) =
append_cbar_unit_if_needed!(attr, get(attr, :colorbar_title, nothing), u)
# dispatch on the type of `label`
append_unit_if_needed!(attr, key, label::ProtectedString, u) = nothing
append_unit_if_needed!(attr, key, label::UnitfulString, u) = nothing
function append_unit_if_needed!(attr, key, label::Nothing, u)
attr[key] = if attr[:plot_object].backend == Plots.PGFPlotsXBackend()
append_cbar_unit_if_needed!(attr, label::UnitfulString, u) = nothing
function append_cbar_unit_if_needed!(attr, label::Nothing, u)
unitformat = get(attr, Symbol(:z, :unitformat), :round)
if unitformat ∈ [:nounit, :none, false, nothing]
return attr[:colorbar_title] = UnitfulString("", u)
end
attr[:colorbar_title] = if Plots.backend_name() === :pgfplotsx
UnitfulString(LaTeXString(latexify(u)), u)
else
UnitfulString(string(u), u)
end
end
function append_unit_if_needed!(attr, key, label::S, u) where {S<:AbstractString}
isempty(label) && return attr[key] = UnitfulString(label, u)
if attr[:plot_object].backend == Plots.PGFPlotsXBackend()
attr[key] = UnitfulString(
function append_cbar_unit_if_needed!(attr, label::S, u) where {S<:AbstractString}
isempty(label) && return attr[:colorbar_title] = UnitfulString(label, u)
attr[:colorbar_title] = if Plots.backend_name() ≡ :pgfplotsx
UnitfulString(
LaTeXString(
format_unit_label(
Plots.format_unit_label(
label,
latexify(u),
get(attr, Symbol(get(attr, :letter, ""), :unitformat), :round),
get(attr, :zunitformat, :round),
),
),
u,
)
else
attr[key] = UnitfulString(
UnitfulString(
S(
format_unit_label(
Plots.format_unit_label(
label,
u,
get(attr, Symbol(get(attr, :letter, ""), :unitformat), :round),
get(attr, :zunitformat, :round),
),
),
u,
)
end
end

#=============================================
Surround unit string with specified delimiters
=============================================#

const UNIT_FORMATS = Dict(
:round => ('(', ')'),
:square => ('[', ']'),
:curly => ('{', '}'),
:angle => ('<', '>'),
:slash => '/',
:slashround => (" / (", ")"),
:slashsquare => (" / [", "]"),
:slashcurly => (" / {", "}"),
:slashangle => (" / <", ">"),
:verbose => " in units of ",
:none => nothing,
)

format_unit_label(l, u, f::Nothing) = string(l, ' ', u)
format_unit_label(l, u, f::Function) = f(l, u)
format_unit_label(l, u, f::AbstractString) = string(l, f, u)
format_unit_label(l, u, f::NTuple{2,<:AbstractString}) = string(l, f[1], u, f[2])
format_unit_label(l, u, f::NTuple{3,<:AbstractString}) = string(f[1], l, f[2], u, f[3])
format_unit_label(l, u, f::Char) = string(l, ' ', f, ' ', u)
format_unit_label(l, u, f::NTuple{2,Char}) = string(l, ' ', f[1], u, f[2])
format_unit_label(l, u, f::NTuple{3,Char}) = string(f[1], l, ' ', f[2], u, f[3])
format_unit_label(l, u, f::Bool) = f ? format_unit_label(l, u, :round) : format_unit_label(l, u, nothing)
format_unit_label(l, u, f::Symbol) = format_unit_label(l, u, UNIT_FORMATS[f])

getaxisunit(::AbstractString) = NoUnits
getaxisunit(s::UnitfulString) = s.unit
getaxisunit(a::Axis) = getaxisunit(a[:guide])
getaxisunit(::Nothing) = nothing
getaxisunit(u) = u
getaxisunit(a::Axis) = getaxisunit(a[:unit])

#==============
Fix annotations
Expand Down Expand Up @@ -314,8 +299,10 @@ function Plots.locate_annotation(
rel::NTuple{N,<:MissingOrQuantity},
label,
) where {N}
units = getaxisunit(sp.attr[:xaxis], sp.attr[:yaxis], sp.attr[:zaxis])
Plots.locate_annotation(sp, _ustrip.(zip(units, rel)), label)
units = getaxisunit(sp.attr[:xaxis]),
getaxisunit(sp.attr[:yaxis]),
getaxisunit(sp.attr[:zaxis])
PlotsBase.locate_annotation(sp, _ustrip.(zip(units, rel)), label)
end

#==================#
Expand Down
1 change: 1 addition & 0 deletions src/args.jl
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,7 @@ const _axis_defaults = KW(
:showaxis => true,
:widen => :auto,
:draw_arrow => false,
:unit => nothing,
:unitformat => :round,
)

Expand Down
60 changes: 60 additions & 0 deletions src/axes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ function attr!(axis::Axis, args...; kw...)
foreach(x -> discrete_value!(axis, x), v) # add these discrete values to the axis
elseif k === :lims && isa(v, NTuple{2,TimeType})
plotattributes[k] = (Dates.value(v[1]), Dates.value(v[2]))
elseif k === :guide && v isa AbstractString && isempty(v) &&
!haskey(kw, :unitformat)
plotattributes[:unitformat] = :nounit
plotattributes[k] = v
elseif k === :unit
if !isnothing(plotattributes[k]) && plotattributes[k] != v
@warn "Overriding unit for $(axis[:letter]) axis: $(plotattributes[k]) -> $v. This will produce a plot, but series plotted before the override cannot update and will therefore be incorrectly treated as if they had the new units."
end
plotattributes[k] = v
else
plotattributes[k] = v
end
Expand All @@ -111,6 +120,57 @@ end
Base.show(io::IO, axis::Axis) = dumpdict(io, axis.plotattributes, "Axis")
ignorenan_extrema(axis::Axis) = (ex = axis[:extrema]; (ex.emin, ex.emax))

function get_guide(axis::Axis)
if isnothing(axis[:guide])
return ""
elseif isnothing(axis[:unit]) || axis[:guide] isa ProtectedString ||
axis[:unitformat] == :nounit
return axis[:guide]
else
ustr = if Plots.backend_name() ≡ :pgfplotsx
Latexify.latexify(axis[:unit])
else
string(axis[:unit])
end
if isempty(axis[:guide])
return ustr
end
return format_unit_label(
axis[:guide],
ustr,
axis[:unitformat])
end
end


# Keyword options for unit formats
const UNIT_FORMATS = Dict(
:round => ('(', ')'),
:square => ('[', ']'),
:curly => ('{', '}'),
:angle => ('<', '>'),
:slash => '/',
:slashround => (" / (", ")"),
:slashsquare => (" / [", "]"),
:slashcurly => (" / {", "}"),
:slashangle => (" / <", ">"),
:verbose => " in units of ",
:none => nothing,
:nounit => (l,u)->l
)

# All options for unit formats
format_unit_label(l, u, f::Nothing) = string(l, ' ', u)
format_unit_label(l, u, f::Function) = f(l, u)
format_unit_label(l, u, f::AbstractString) = string(l, f, u)
format_unit_label(l, u, f::NTuple{2,<:AbstractString}) = string(l, f[1], u, f[2])
format_unit_label(l, u, f::NTuple{3,<:AbstractString}) = string(f[1], l, f[2], u, f[3])
format_unit_label(l, u, f::Char) = string(l, ' ', f, ' ', u)
format_unit_label(l, u, f::NTuple{2,Char}) = string(l, ' ', f[1], u, f[2])
format_unit_label(l, u, f::NTuple{3,Char}) = string(f[1], l, ' ', f[2], u, f[3])
format_unit_label(l, u, f::Bool) = f ? format_unit_label(l, u, :round) : format_unit_label(l, u, nothing)
format_unit_label(l, u, f::Symbol) = format_unit_label(l, u, UNIT_FORMATS[f])

const _label_func =
Dict{Symbol,Function}(:log10 => x -> "10^$x", :log2 => x -> "2^$x", :ln => x -> "e^$x")
labelfunc(scale::Symbol, backend::AbstractBackend) = get(_label_func, scale, string)
Expand Down
2 changes: 1 addition & 1 deletion src/backends.jl
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ _series_updated(plt::Plot, series::Series) = nothing
_before_layout_calcs(plt::Plot) = nothing

title_padding(sp::Subplot) = sp[:title] == "" ? 0mm : sp[:titlefontsize] * pt
guide_padding(axis::Axis) = axis[:guide] == "" ? 0mm : axis[:guidefontsize] * pt
guide_padding(axis::Axis) = Plots.get_guide(axis) == "" ? 0mm : axis[:guidefontsize] * pt

closeall(::AbstractBackend) = nothing

Expand Down
2 changes: 1 addition & 1 deletion src/backends/deprecated/pgfplots.jl
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ function pgf_axis(sp::Subplot, letter)
framestyle = pgf_framestyle(sp[:framestyle])

# axis guide
kw[get_attr_symbol(letter, :label)] = axis[:guide]
kw[get_attr_symbol(letter, :label)] = Plots.get_guide(axis)

# axis label position
labelpos = ""
Expand Down
2 changes: 1 addition & 1 deletion src/backends/deprecated/pyplot.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1211,7 +1211,7 @@ function _before_layout_calcs(plt::Plot{PyPlotBackend})
5py_thickness_scale(plt, intensity),
)

getproperty(ax, Symbol("set_", letter, "label"))(axis[:guide])
getproperty(ax, Symbol("set_", letter, "label"))(Plots.get_guide(axis))
if get(axis.plotattributes, :flip, false)
getproperty(ax, Symbol("invert_", letter, "axis"))()
end
Expand Down
2 changes: 1 addition & 1 deletion src/backends/gaston.jl
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ function gaston_parse_axes_args(
end
push!(
axesconf,
"set $(letter)$(I)label '$(axis[:guide])' $(gaston_font(guide_font))",
"set $(letter)$(I)label '$(Plots.get_guide(axis))' $(gaston_font(guide_font))",
)

logscale, base = if (scale = axis[:scale]) === :identity
Expand Down
Loading
Loading