From 116ec82778563af0bfb5201fb2a6a15e7d232d48 Mon Sep 17 00:00:00 2001 From: Isaac Wheeler Date: Fri, 6 Jun 2025 16:01:42 +0200 Subject: [PATCH 01/10] add myself to .zenodo.json --- .zenodo.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.zenodo.json b/.zenodo.json index eb7341d1f..4ba607526 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -779,6 +779,12 @@ { "name": "Patrick Jaap", "type": "Other" + }, + { + "affiliation": "Purdue University", + "name": "Isaac Wheeler", + "orcid": "0000-0002-9717-073X", + "type": "Other" } ], "upload_type": "software" From 8d8d8ced335d0be80245ce1f32339f722e3080df Mon Sep 17 00:00:00 2001 From: Isaac Wheeler Date: Fri, 6 Jun 2025 16:02:43 +0200 Subject: [PATCH 02/10] Rework unit functionality --- ext/UnitfulExt.jl | 81 ++++++++++++++--------------------------------- src/args.jl | 1 + src/axes.jl | 50 +++++++++++++++++++++++++++++ src/utils.jl | 19 +++++++++-- 4 files changed, 92 insertions(+), 59 deletions(-) diff --git a/ext/UnitfulExt.jl b/ext/UnitfulExt.jl index 34b2fc950..9f1fea423 100644 --- a/ext/UnitfulExt.jl +++ b/ext/UnitfulExt.jl @@ -35,16 +35,17 @@ function fixaxis!(attr, x, axisletter) 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 + u = get!(attr, axisunit, _unit(eltype(x))) # get the unit + # if the subplot already exists with data, use that unit instead 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 + end + attr[axisunit] = u # update the unit in the attributes 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) @@ -159,8 +160,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) = @@ -188,36 +193,25 @@ 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) + @info "append unit to nothing" key label u attr[key] = if attr[:plot_object].backend == Plots.PGFPlotsXBackend() UnitfulString(LaTeXString(latexify(u)), u) else @@ -225,11 +219,12 @@ function append_unit_if_needed!(attr, key, label::Nothing, u) end end function append_unit_if_needed!(attr, key, label::S, u) where {S<:AbstractString} + @info "append unit to label" key label u isempty(label) && return attr[key] = UnitfulString(label, u) 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), @@ -240,7 +235,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), @@ -251,38 +246,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 diff --git a/src/args.jl b/src/args.jl index 94c44f10b..8c5e5ade1 100644 --- a/src/args.jl +++ b/src/args.jl @@ -519,6 +519,7 @@ const _axis_defaults = KW( :showaxis => true, :widen => :auto, :draw_arrow => false, + :unit => nothing, :unitformat => :round, ) diff --git a/src/axes.jl b/src/axes.jl index 1f3a8c94e..7c92a0590 100644 --- a/src/axes.jl +++ b/src/axes.jl @@ -93,6 +93,8 @@ 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) else plotattributes[k] = v end @@ -111,6 +113,54 @@ 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[: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) diff --git a/src/utils.jl b/src/utils.jl index faf7b1880..9c3437a89 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1250,8 +1250,23 @@ macro ext_imp_use(imp_use::QuoteNode, mod::Symbol, args...) Expr(imp_use.value, ex) |> esc end -# for UnitfulExt - cannot reside in `UnitfulExt` (macro) -function protectedstring end # COV_EXCL_LINE +# for UnitfulExt +abstract type AbstractProtectedString <: AbstractString end +struct ProtectedString{S} <: AbstractProtectedString + content::S +end +const APS = AbstractProtectedString +# Minimum required AbstractString interface to work with PlotsBase +Base.iterate(n::APS) = iterate(n.content) +Base.iterate(n::APS, i::Integer) = iterate(n.content, i) +Base.codeunit(n::APS) = codeunit(n.content) +Base.ncodeunits(n::APS) = ncodeunits(n.content) +Base.isvalid(n::APS, i::Integer) = isvalid(n.content, i) +Base.pointer(n::APS) = pointer(n.content) +Base.pointer(n::APS, i::Integer) = pointer(n.content, i) +protectedstring(s) = ProtectedString(s) + + """ P_str(s) From c897a9ac034b2646fca7b6f0d21c7c2348137c50 Mon Sep 17 00:00:00 2001 From: Isaac Wheeler Date: Fri, 6 Jun 2025 16:03:01 +0200 Subject: [PATCH 03/10] Make twin axes work with units --- src/layouts.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/layouts.jl b/src/layouts.jl index 8c98ee165..8b6ee6fe3 100644 --- a/src/layouts.jl +++ b/src/layouts.jl @@ -569,6 +569,8 @@ function link_axes!(axes::Axis...) a1 = axes[1] for i in 2:length(axes) a2 = axes[i] + a1[:unit] ≡ a2[:unit] || + error( "Cannot link axes with different units: $(a1[:unit]) and $(a2[:unit])",) expand_extrema!(a1, ignorenan_extrema(a2)) for k in (:extrema, :discrete_values, :continuous_values, :discrete_map) a2[k] = a1[k] @@ -662,6 +664,8 @@ function twin(sp, letter) tax[:grid] = false tax[:showaxis] = false tax[:ticks] = :none + tax[:unitformat] = :none + tax[:unit] = orig_sp[get_attr_symbol(letter, :axis)][:unit] oax[:grid] = false oax[:mirror] = true twin_sp[:background_color_inside] = RGBA{Float64}(0, 0, 0, 0) From d3455f7254be27b9bae12dff19114997a8e338e8 Mon Sep 17 00:00:00 2001 From: Isaac Wheeler Date: Fri, 6 Jun 2025 16:03:32 +0200 Subject: [PATCH 04/10] Use get_guide(axis) rather than axis[:guide] --- src/backends.jl | 2 +- src/backends/deprecated/pgfplots.jl | 2 +- src/backends/deprecated/pyplot.jl | 2 +- src/backends/gr.jl | 26 +++++++++++++++----------- src/backends/inspectdr.jl | 4 ++-- src/backends/pgfplotsx.jl | 2 +- src/backends/plotly.jl | 2 +- src/backends/pythonplot.jl | 2 +- src/backends/unicodeplots.jl | 4 ++-- test/test_axes.jl | 2 +- test/test_shorthands.jl | 6 +++--- test/test_unitful.jl | 6 +++--- 12 files changed, 32 insertions(+), 28 deletions(-) diff --git a/src/backends.jl b/src/backends.jl index e88b25ae1..80680b612 100644 --- a/src/backends.jl +++ b/src/backends.jl @@ -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 diff --git a/src/backends/deprecated/pgfplots.jl b/src/backends/deprecated/pgfplots.jl index 3e992dba8..a70ebceef 100644 --- a/src/backends/deprecated/pgfplots.jl +++ b/src/backends/deprecated/pgfplots.jl @@ -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 = "" diff --git a/src/backends/deprecated/pyplot.jl b/src/backends/deprecated/pyplot.jl index 4fc33cfd2..35ce40f8d 100644 --- a/src/backends/deprecated/pyplot.jl +++ b/src/backends/deprecated/pyplot.jl @@ -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 diff --git a/src/backends/gr.jl b/src/backends/gr.jl index 584ded397..a029709f5 100644 --- a/src/backends/gr.jl +++ b/src/backends/gr.jl @@ -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 @@ -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 @@ -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) @@ -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 ? @@ -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 @@ -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] @@ -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) @@ -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]) != "" diff --git a/src/backends/inspectdr.jl b/src/backends/inspectdr.jl index 07a435fa0..696413fbb 100644 --- a/src/backends/inspectdr.jl +++ b/src/backends/inspectdr.jl @@ -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) diff --git a/src/backends/pgfplotsx.jl b/src/backends/pgfplotsx.jl index 8913f8738..44d6bb495 100644 --- a/src/backends/pgfplotsx.jl +++ b/src/backends/pgfplotsx.jl @@ -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( diff --git a/src/backends/plotly.jl b/src/backends/plotly.jl index b2c33a85c..f223c8d7a 100644 --- a/src/backends/plotly.jl +++ b/src/backends/plotly.jl @@ -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])), diff --git a/src/backends/pythonplot.jl b/src/backends/pythonplot.jl index 38ef52574..d1b94ff80 100644 --- a/src/backends/pythonplot.jl +++ b/src/backends/pythonplot.jl @@ -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( diff --git a/src/backends/unicodeplots.jl b/src/backends/unicodeplots.jl index 057c68d09..6b275005f 100644 --- a/src/backends/unicodeplots.jl +++ b/src/backends/unicodeplots.jl @@ -97,8 +97,8 @@ function _before_layout_calcs(plt::Plot{UnicodePlotsBackend}) kw = ( compact = true, title = texmath2unicode(sp[:title]), - xlabel = texmath2unicode(xaxis[:guide]), - ylabel = texmath2unicode(yaxis[:guide]), + xlabel = texmath2unicode(Plots.get_guide(xaxis)), + ylabel = texmath2unicode(Plots.get_guide(yaxis)), labels = !plot_3d, # guide labels and limits do not make sense in 3d xscale = xaxis[:scale], yscale = yaxis[:scale], diff --git a/test/test_axes.jl b/test/test_axes.jl index 01df32ecd..cd5990c7c 100644 --- a/test/test_axes.jl +++ b/test/test_axes.jl @@ -153,7 +153,7 @@ end @test haskey(Plots._keyAliases, :x_guide_position) @test !haskey(Plots._keyAliases, :xguide_position) pl = plot(1:2, xl = "x label") - @test pl[1][:xaxis][:guide] === "x label" + @test Plots.get_guide(pl[1][:xaxis]) === "x label" pl = plot(1:2, xrange = (0, 3)) @test xlims(pl) === (0, 3) pl = plot(1:2, xtick = [1.25, 1.5, 1.75]) diff --git a/test/test_shorthands.jl b/test/test_shorthands.jl index 8b3846583..897dcd3a1 100644 --- a/test/test_shorthands.jl +++ b/test/test_shorthands.jl @@ -33,11 +33,11 @@ end sp = pl[1] @test sp[:title] == "Foo" xlabel!(pl, "xlabel") - @test sp[:xaxis][:guide] == "xlabel" + @test Plots.get_guide(sp[:xaxis]) == "xlabel" ylabel!(pl, "ylabel") - @test sp[:yaxis][:guide] == "ylabel" + @test Plots.get_guide(sp[:yaxis]) == "ylabel" zlabel!(pl, "zlabel") - @test sp[:zaxis][:guide] == "zlabel" + @test Plots.get_guide(sp[:zaxis]) == "zlabel" end @testset "Misc" begin diff --git a/test/test_unitful.jl b/test/test_unitful.jl index b0d79fdee..5218360f2 100644 --- a/test/test_unitful.jl +++ b/test/test_unitful.jl @@ -2,9 +2,9 @@ using Plots, Test using Unitful using Unitful: m, cm, s, DimensionError # Some helper functions to access the subplot labels and the series inside each test plot -xguide(pl, idx = length(pl.subplots)) = pl.subplots[idx].attr[:xaxis].plotattributes[:guide] -yguide(pl, idx = length(pl.subplots)) = pl.subplots[idx].attr[:yaxis].plotattributes[:guide] -zguide(pl, idx = length(pl.subplots)) = pl.subplots[idx].attr[:zaxis].plotattributes[:guide] +xguide(pl, idx = length(pl.subplots)) = Plots.get_guide(pl.subplots[idx].attr[:xaxis]) +yguide(pl, idx = length(pl.subplots)) = Plots.get_guide(pl.subplots[idx].attr[:yaxis]) +zguide(pl, idx = length(pl.subplots)) = Plots.get_guide(pl.subplots[idx].attr[:zaxis]) xseries(pl, idx = length(pl.series_list)) = pl.series_list[idx].plotattributes[:x] yseries(pl, idx = length(pl.series_list)) = pl.series_list[idx].plotattributes[:y] zseries(pl, idx = length(pl.series_list)) = pl.series_list[idx].plotattributes[:z] From d01e7146a4819e5b1746d49c79873afc0b9e723b Mon Sep 17 00:00:00 2001 From: Isaac Wheeler Date: Fri, 6 Jun 2025 16:08:58 +0200 Subject: [PATCH 05/10] debug statement, oops --- ext/UnitfulExt.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/ext/UnitfulExt.jl b/ext/UnitfulExt.jl index 9f1fea423..d63086b4d 100644 --- a/ext/UnitfulExt.jl +++ b/ext/UnitfulExt.jl @@ -211,7 +211,6 @@ append_unit_if_needed!(attr, key, u) = 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) - @info "append unit to nothing" key label u attr[key] = if attr[:plot_object].backend == Plots.PGFPlotsXBackend() UnitfulString(LaTeXString(latexify(u)), u) else @@ -219,7 +218,6 @@ function append_unit_if_needed!(attr, key, label::Nothing, u) end end function append_unit_if_needed!(attr, key, label::S, u) where {S<:AbstractString} - @info "append unit to label" key label u isempty(label) && return attr[key] = UnitfulString(label, u) if attr[:plot_object].backend == Plots.PGFPlotsXBackend() attr[key] = UnitfulString( From 27145df05b38c13361af06028a4a15f2d3f24508 Mon Sep 17 00:00:00 2001 From: Isaac Wheeler Date: Fri, 6 Jun 2025 16:30:26 +0200 Subject: [PATCH 06/10] missed a spot --- src/backends/gaston.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/gaston.jl b/src/backends/gaston.jl index 10b055ca7..4d440d0f8 100644 --- a/src/backends/gaston.jl +++ b/src/backends/gaston.jl @@ -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 From 84f6995db0a612729234cf2457acf43a934817f6 Mon Sep 17 00:00:00 2001 From: Isaac Wheeler Date: Mon, 9 Jun 2025 13:41:45 +0200 Subject: [PATCH 07/10] Use xunit, yunit kwargs better; make guide=nothing produce no label at all --- ext/UnitfulExt.jl | 13 ++++++------- src/axes.jl | 9 ++++++++- src/layouts.jl | 2 +- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/ext/UnitfulExt.jl b/ext/UnitfulExt.jl index d63086b4d..6d9067c24 100644 --- a/ext/UnitfulExt.jl +++ b/ext/UnitfulExt.jl @@ -29,21 +29,20 @@ 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 = get!(attr, axisunit, _unit(eltype(x))) # get the unit - # if the subplot already exists with data, use that unit instead + # 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 + 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 - attr[axisunit] = u # update the unit in the attributes + 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. ustripattribute!(attr, err, u) diff --git a/src/axes.jl b/src/axes.jl index 7c92a0590..68f34d74b 100644 --- a/src/axes.jl +++ b/src/axes.jl @@ -95,6 +95,11 @@ function attr!(axis::Axis, args...; kw...) 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 @@ -114,7 +119,9 @@ 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[:unit]) || axis[:guide] isa ProtectedString || + if isnothing(axis[:guide]) + return "" + elseif isnothing(axis[:unit]) || axis[:guide] isa ProtectedString || axis[:unitformat] == :none return axis[:guide] else diff --git a/src/layouts.jl b/src/layouts.jl index 8b6ee6fe3..7cf2c8982 100644 --- a/src/layouts.jl +++ b/src/layouts.jl @@ -664,8 +664,8 @@ function twin(sp, letter) tax[:grid] = false tax[:showaxis] = false tax[:ticks] = :none - tax[:unitformat] = :none tax[:unit] = orig_sp[get_attr_symbol(letter, :axis)][:unit] + tax[:guide] = nothing oax[:grid] = false oax[:mirror] = true twin_sp[:background_color_inside] = RGBA{Float64}(0, 0, 0, 0) From 2a16ab1e0e0d89fbebe723a8978a44ba0077fda1 Mon Sep 17 00:00:00 2001 From: Isaac Wheeler Date: Wed, 11 Jun 2025 16:17:33 +0200 Subject: [PATCH 08/10] Add some tests for twinx and changing labels after units --- src/layouts.jl | 2 +- test/test_unitful.jl | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/layouts.jl b/src/layouts.jl index 7cf2c8982..05aa1e352 100644 --- a/src/layouts.jl +++ b/src/layouts.jl @@ -570,7 +570,7 @@ function link_axes!(axes::Axis...) for i in 2:length(axes) a2 = axes[i] a1[:unit] ≡ a2[:unit] || - error( "Cannot link axes with different units: $(a1[:unit]) and $(a2[:unit])",) + error("Cannot link axes with different units: $(a1[:unit]) and $(a2[:unit])",) expand_extrema!(a1, ignorenan_extrema(a2)) for k in (:extrema, :discrete_values, :continuous_values, :discrete_map) a2[k] = a1[k] diff --git a/test/test_unitful.jl b/test/test_unitful.jl index 5218360f2..563c8d773 100644 --- a/test/test_unitful.jl +++ b/test/test_unitful.jl @@ -34,18 +34,29 @@ end @testset "ylabel" begin @test yguide(plot(y, ylabel = "hello")) == "hello (m)" @test yguide(plot(y, ylabel = P"hello")) == "hello" + @test yguide(plot(y, ylabel = "hello", unitformat=:none)) == "hello" pl = plot(y, ylabel = "") @test yguide(pl) == "" @test yguide(plot!(pl, -y)) == "" pl = plot(y; ylabel = "hello") plot!(pl, -y) @test yguide(pl) == "hello (m)" + plot!(pl, -y; ylabel = "goodbye") + @test yguide(pl) == "goodbye (m)" + pl = plot(y) + plot!(pl, -y; ylabel = "hello") + @test yguide(pl) == "hello (m)" end @testset "yunit" begin @test yguide(plot(y, yunit = cm)) == "cm" @test yseries(plot(y, yunit = cm)) ≈ ustrip.(cm, y) @test plot([copy(y), copy(y)], yunit = cm) |> pl -> yseries(pl, 1) ≈ yseries(pl, 2) + pl = plot(y) + @test_logs (:warn, "Overriding unit") plot!(pl; yunit = cm) + @test yguide(pl) == "cm" + plot!(pl; ylabel="hello") + @test yguide(pl) == "hello (cm)" end @testset "ylims" begin # Using all(lims .≈ lims) because of uncontrolled type conversions? @@ -272,6 +283,24 @@ end y = rand(10) * u"m" @test plot(y, label = P"meters") isa Plots.Plot end + + @testset "twinx (#4750)" begin + y = rand(10) * u"m" + pl = plot(y; ylabel = "hello") + pl2 = twinx(pl) + plot!(pl2, 1 ./ y; ylabel = "goodbye", yunit = u"cm^-1") + @test pl isa Plots.Plot + @test pl2 isa Plots.Subplot + @test yguide(pl) == "hello (m)" + @test yguide(pl, 2) == "goodbye (cm^-1)" + end + + @testset "bad link" begin + pl1 = plot(rand(10)*u"m") + pl2 = plot(rand(10)*u"s") + @test_throws "Cannot link axes" plot(pl1, pl2; link = :y) + end + end @testset "Comparing apples and oranges" begin From dc628dba0a26b380874681f64819605d9d947524 Mon Sep 17 00:00:00 2001 From: Isaac Wheeler Date: Mon, 16 Jun 2025 14:11:54 +0200 Subject: [PATCH 09/10] Work on tests passing --- test/test_unitful.jl | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test/test_unitful.jl b/test/test_unitful.jl index 563c8d773..f2906a04f 100644 --- a/test/test_unitful.jl +++ b/test/test_unitful.jl @@ -53,7 +53,7 @@ end @test yseries(plot(y, yunit = cm)) ≈ ustrip.(cm, y) @test plot([copy(y), copy(y)], yunit = cm) |> pl -> yseries(pl, 1) ≈ yseries(pl, 2) pl = plot(y) - @test_logs (:warn, "Overriding unit") plot!(pl; yunit = cm) + @test_logs (:warn, r"Overriding unit") plot!(pl; yunit = cm) @test yguide(pl) == "cm" plot!(pl; ylabel="hello") @test yguide(pl) == "hello (cm)" @@ -286,19 +286,22 @@ end @testset "twinx (#4750)" begin y = rand(10) * u"m" - pl = plot(y; ylabel = "hello") + pl = plot(y; xlabel = "check", ylabel = "hello") pl2 = twinx(pl) plot!(pl2, 1 ./ y; ylabel = "goodbye", yunit = u"cm^-1") @test pl isa Plots.Plot @test pl2 isa Plots.Subplot - @test yguide(pl) == "hello (m)" + @test yguide(pl, 1) == "hello (m)" @test yguide(pl, 2) == "goodbye (cm^-1)" + @test xguide(pl, 1) == "check" + @test xguide(pl, 2) == "" end @testset "bad link" begin pl1 = plot(rand(10)*u"m") pl2 = plot(rand(10)*u"s") - @test_throws "Cannot link axes" plot(pl1, pl2; link = :y) + # TODO: On Julia 1.8 and above, can replace ErrorException with part of error message. + @test_throws ErrorException plot(pl1, pl2; link = :y) end end From 75c8bd5b10412b7b450151ee376b892e26da5e03 Mon Sep 17 00:00:00 2001 From: Isaac Wheeler Date: Mon, 16 Jun 2025 14:37:34 +0200 Subject: [PATCH 10/10] MacOS superscript unicode --- test/test_unitful.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_unitful.jl b/test/test_unitful.jl index f2906a04f..8f39b275e 100644 --- a/test/test_unitful.jl +++ b/test/test_unitful.jl @@ -292,7 +292,8 @@ end @test pl isa Plots.Plot @test pl2 isa Plots.Subplot @test yguide(pl, 1) == "hello (m)" - @test yguide(pl, 2) == "goodbye (cm^-1)" + # on MacOS the superscript gets rendered with Unicode, on Ubuntu and Windows no + @test yguide(pl, 2) ∈ ["goodbye (cm^-1)", "goodbye (cm⁻¹)"] @test xguide(pl, 1) == "check" @test xguide(pl, 2) == "" end