Skip to content

Add unit axis attribute, for Plots v1 #5098

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
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
86 changes: 25 additions & 61 deletions ext/UnitfulExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,22 @@ 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
if sp ≤ length(attr[:plot_object])
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)
Expand Down Expand Up @@ -159,8 +159,12 @@ function fixmarkercolor!(attr)
ustripattribute!(attr, :clims, u)
u == NoUnits || append_unit_if_needed!(attr, :colorbar_title, u)
end
function fixlinecolor!(attr)
u = ustripattribute!(attr, :line_z)
ustripattribute!(attr, :clims, u)
u == NoUnits || append_unit_if_needed!(attr, :colorbar_title, u)
end
fixmarkersize!(attr) = ustripattribute!(attr, :markersize)
fixlinecolor!(attr) = ustripattribute!(attr, :line_z)

# strip unit from attribute[key]
ustripattribute!(attr, key) =
Expand Down Expand Up @@ -188,34 +192,22 @@ end
Label string containing unit information
=======================================#

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, mostly,
since axes have their own handling
=====================================#

append_unit_if_needed!(attr, key, u) =
append_unit_if_needed!(attr, key, get(attr, key, 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::Plots.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()
Expand All @@ -229,7 +221,7 @@ function append_unit_if_needed!(attr, key, label::S, u) where {S<:AbstractString
if attr[:plot_object].backend == Plots.PGFPlotsXBackend()
attr[key] = UnitfulString(
LaTeXString(
format_unit_label(
Plots.format_unit_label(
label,
latexify(u),
get(attr, Symbol(get(attr, :letter, ""), :unitformat), :round),
Expand All @@ -240,7 +232,7 @@ function append_unit_if_needed!(attr, key, label::S, u) where {S<:AbstractString
else
attr[key] = UnitfulString(
S(
format_unit_label(
Plots.format_unit_label(
label,
u,
get(attr, Symbol(get(attr, :letter, ""), :unitformat), :round),
Expand All @@ -251,38 +243,10 @@ function append_unit_if_needed!(attr, key, label::S, u) where {S<:AbstractString
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(::Nothing) = nothing
getaxisunit(s::UnitfulString) = s.unit
getaxisunit(a::Axis) = getaxisunit(a[:guide])
getaxisunit(a::Axis) = getaxisunit(a[:unit])
getaxisunit(u) = u

#==============
Fix annotations
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
57 changes: 57 additions & 0 deletions src/axes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ 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)
plotattributes[k] = protectedstring(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 +118,56 @@ 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] == :none
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,
)

# 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
26 changes: 15 additions & 11 deletions src/backends/gr.jl
Original file line number Diff line number Diff line change
Expand Up @@ -765,7 +765,7 @@ function gr_axis_height(sp, axis)
ticks in (nothing, false, :none) ? 0 :
last(gr_get_ticks_size(ticks, axis[:rotation]))
)
if (guide = axis[:guide]) != ""
if (guide = Plots.get_guide(axis)) != ""
gr_set_font(guidefont(axis), sp)
h += last(gr_text_size(guide))
end
Expand All @@ -781,7 +781,7 @@ function gr_axis_width(sp, axis)
ticks in (nothing, false, :none) ? 0 :
first(gr_get_ticks_size(ticks, axis[:rotation]))
)
if (guide = axis[:guide]) != ""
if (guide = Plots.get_guide(axis)) != ""
gr_set_font(guidefont(axis), sp)
w += last(gr_text_size(guide))
end
Expand Down Expand Up @@ -846,7 +846,7 @@ function _update_min_padding!(sp::Subplot{GRBackend})
# Add margin for x or y label
m = 0mm
for ax in (xaxis, yaxis)
(guide = ax[:guide] == "") && continue
(guide = Plots.get_guide(ax) == "") && continue
gr_set_font(guidefont(ax), sp)
l = last(gr_text_size(guide))
m = max(m, 1mm + height * l * px)
Expand All @@ -856,7 +856,7 @@ function _update_min_padding!(sp::Subplot{GRBackend})
padding[mirrored(xaxis, :top) ? :top : :bottom][] += m
end
# Add margin for z label
if (guide = zaxis[:guide]) != ""
if (guide = Plots.get_guide(zaxis)) != ""
gr_set_font(guidefont(zaxis), sp)
l = last(gr_text_size(guide))
padding[mirrored(zaxis, :right) ? :right : :left][] += 1mm + height * l * px # NOTE: why `height` here ?
Expand All @@ -872,7 +872,7 @@ function _update_min_padding!(sp::Subplot{GRBackend})
l = 0.01 + (isy ? first(ts) : last(ts))
padding[ax[:mirror] ? a : b][] += 1mm + sp_size[isy ? 1 : 2] * l * px
end
if (guide = ax[:guide]) != ""
if (guide = Plots.get_guide(ax)) != ""
gr_set_font(guidefont(ax), sp)
l = last(gr_text_size(guide))
padding[mirrored(ax, a) ? a : b][] += 1mm + height * l * px # NOTE: using `height` is arbitrary
Expand Down Expand Up @@ -1645,8 +1645,9 @@ function gr_label_ticks_3d(sp, letter, ticks)
end
end

gr_label_axis(sp, letter, vp) =
if (ax = sp[get_attr_symbol(letter, :axis)])[:guide] != ""
function gr_label_axis(sp, letter, vp)
ax = sp[get_attr_symbol(letter, :axis)]
if Plots.get_guide(ax) != ""
mirror = ax[:mirror]
GR.savestate()
guide_position = ax[:guide_position]
Expand All @@ -1673,12 +1674,14 @@ gr_label_axis(sp, letter, vp) =
end
end
gr_set_font(guidefont(ax), sp; rotation, halign, valign)
gr_text(xpos, ypos, ax[:guide])
gr_text(xpos, ypos, Plots.get_guide(ax))
GR.restorestate()
end
end

gr_label_axis_3d(sp, letter) =
if (ax = sp[get_attr_symbol(letter, :axis)])[:guide] != ""
function gr_label_axis_3d(sp, letter)
ax = sp[get_attr_symbol(letter, :axis)]
if Plots.get_guide(ax) != ""
letters = axes_letters(sp, letter)
(amin, amax), (namin, namax), (famin, famax) = map(l -> axis_limits(sp, l), letters)
n0, n1 = letter === :y ? (namax, namin) : (namin, namax)
Expand Down Expand Up @@ -1706,9 +1709,10 @@ gr_label_axis_3d(sp, letter) =
end
letter === :z && GR.setcharup(-1, 0)
sgn = ax[:mirror] ? -1 : 1
gr_text(x + sgn * x_offset, y + sgn * y_offset, ax[:guide])
gr_text(x + sgn * x_offset, y + sgn * y_offset, Plots.get_guide(ax))
GR.restorestate()
end
end

gr_add_title(sp, vp_plt, vp_sp) =
if (title = sp[:title]) != ""
Expand Down
4 changes: 2 additions & 2 deletions src/backends/inspectdr.jl
Original file line number Diff line number Diff line change
Expand Up @@ -375,8 +375,8 @@ function _inspectdr_setupsubplot(sp::Subplot{InspectDRBackend})

a = plot.annotation
a.title = texmath2unicode(sp[:title])
a.xlabel = texmath2unicode(xaxis[:guide])
a.ylabels = [texmath2unicode(yaxis[:guide])]
a.xlabel = Plots.get_guide(texmath2unicode(xaxis))
a.ylabels = [texmath2unicode(Plots.getguide(yaxis))]

#Modify base layout of new object:
l = plot.layout.defaults = deepcopy(InspectDR.defaults.plotlayout)
Expand Down
2 changes: 1 addition & 1 deletion src/backends/pgfplotsx.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1158,7 +1158,7 @@ function pgfx_axis!(opt::Options, sp::Subplot, letter)
push!(
opt,
"scaled $(letter) ticks" => "false",
"$(letter)label" => axis[:guide],
"$(letter)label" => Plots.get_guide(axis),
"$(letter) tick style" =>
Options("color" => color(tick_color), "opacity" => alpha(tick_color)),
"$(letter) tick label style" => Options(
Expand Down
2 changes: 1 addition & 1 deletion src/backends/plotly.jl
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ function plotly_axis(axis, sp, anchor = nothing, domain = nothing)
framestyle = sp[:framestyle]
ax = KW(
:visible => framestyle !== :none,
:title => axis[:guide],
:title => Plots.get_guide(axis),
:showgrid => axis[:grid],
:gridcolor =>
rgba_string(plot_color(axis[:foreground_color_grid], axis[:gridalpha])),
Expand Down
2 changes: 1 addition & 1 deletion src/backends/pythonplot.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1118,7 +1118,7 @@ function _before_layout_calcs(plt::Plot{PythonPlotBackend})
pyaxis.set_major_locator(mpl.ticker.NullLocator())
end

getproperty(ax, set_axis(letter, :label))(axis[:guide])
getproperty(ax, set_axis(letter, :label))(Plots.get_guide(axis))
pyaxis.label.set_fontsize(_py_thickness_scale(plt, axis[:guidefontsize]))
pyaxis.label.set_family(axis[:guidefontfamily])
pyaxis.label.set_math_fontfamily(
Expand Down
Loading
Loading