Skip to content

Add unit axis attribute, for Plots v1#5098

Merged
BeastyBlacksmith merged 23 commits intoJuliaPlots:masterfrom
Ickaser:unitful-twinx_v1
Jun 26, 2025
Merged

Add unit axis attribute, for Plots v1#5098
BeastyBlacksmith merged 23 commits intoJuliaPlots:masterfrom
Ickaser:unitful-twinx_v1

Conversation

@Ickaser
Copy link
Contributor

@Ickaser Ickaser commented Jun 6, 2025

Description

Essentially identical to #5095 , but made against the master branch so that I'm not waiting for Plots v2--folder structures are different enough that it made sense to open as a separate PR.

@gustaphe and I discussed some in #5095; it would be helpful to get feedback from @t-bltg or @BeastyBlacksmith or someone else who is not as focused on Unitful usage.

A lot of the noise in the diff is a result of switching to Plots.get_guide(axis) instead of axis[:guide], because that new function allows us to more elegantly handle differing unit formats in the guide strings; in any case where the axis has no units, the result of get_guide(axis) is axis[:guide] so not a big change in behavior.

Other changes involve moving some of the machinery out of the UnitfulExt so that it is accessible within Plots for axis handling.

MWEs showing new behavior

using Plots
import GR; gr()

x = range(1u"s", 10u"s", length = 100)
y = @. x * 5u"m/s" 

# For now, ProtectedString is still supported but prints a depwarn
plot(x,y, xlabel="time", unitformat=:square, ylabel=P"distance")
# Previously this would not work--setting xlabel would kill the units
plot!(xlabel="new time")
plot!(x, -y)

plot_31
(So: this PR is fixing #4822.)

# Unitformat does nothing if no units are set
plot(rand(4), rand(4), xlabel="time", ylabel="distance", unitformat=:square)

plot_33

# Twinx: this did not used to work
plot(x, y, ylabel="distance")
pl2 = twinx()
plot!(pl2, x.+1u"s", 1 ./y, ylabel="invy", unitformat=:square)

plot_34
(So, this PR is fixing #4741 and #4750, although the MWEs in both of those issues do not plot correctly into the twinned axis.)

# This works as before; the new :nounit unitformat doesn't print any units,
# and zunitformat gets applied to the colorbar title
z = [yi/xi for xi in x, yi in y]
heatmap(x, y, z,  xlabel="time", ylabel="dist", cbar_title = "velocity?", 
    xunit=u"ms", zunit=u"km/hr", xunitformat=:nounit, zunitformat=:square)

plot_273

# Both marker_z and line_z work, and dimensionality is enforced on the color bar
plot(x, y, (x * permutedims(y))*u"C/m/s", zunit=u"C")
plot!(x, y, line_z=rand(length(y))*u"C", cbar_title="zaxis")
scatter!(x.+1u"s", y, marker_z=rand(length(y))*u"kC", zunitformat=:square)

plot!(x, y, line_z=rand(10)*u"s") #--> gives DimensionError

plot_274

Fix #4947.
Fix #4822.

Attribution

Things to consider

  • Does it work on log scales?
  • Does it work in layouts?
  • Does it work in recipes?
  • Does it work with multiple series in one call?
  • PR includes or updates tests?
  • PR includes or updates documentation?

@t-bltg
Copy link
Member

t-bltg commented Jun 7, 2025

CI is failing on this one.

@codecov
Copy link

codecov bot commented Jun 9, 2025

Codecov Report

Attention: Patch coverage is 98.30508% with 1 line in your changes missing coverage. Please review.

Project coverage is 89.83%. Comparing base (fa65e7d) to head (7875bc4).
Report is 49 commits behind head on master.

Files with missing lines Patch % Lines
src/axes.jl 96.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #5098      +/-   ##
==========================================
+ Coverage   89.77%   89.83%   +0.06%     
==========================================
  Files          40       40              
  Lines        8780     8872      +92     
==========================================
+ Hits         7882     7970      +88     
- Misses        898      902       +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Ickaser
Copy link
Contributor Author

Ickaser commented Jun 9, 2025

This PR isn't quite ready yet, because I think it deserves a few more tests for the UnitfulExt before merging, plus some additions to the docs. But @t-bltg the CI failures seem to be either 1) a resurface of a PlotlyJS>Blink>Electron issue #5075, which is randomly hitting only some of the Ubuntu test cases or 2) something to do with the 1.12 prerelease

@t-bltg
Copy link
Member

t-bltg commented Jun 9, 2025

  1. a resurface of a PlotlyJS>Blink>Electron issue fix PlotlyJS ECONNREFUSED #5075

#5100

  1. something to do with the 1.12 prerelease

Ignore those (they are development aids), we focus on julia release and lts.

@t-bltg t-bltg added the enhancement improving existing functionality label Jun 9, 2025
@Ickaser Ickaser mentioned this pull request Jun 13, 2025
7 tasks
@Ickaser
Copy link
Contributor Author

Ickaser commented Jun 16, 2025

Progress:

  • Tests added and passing
  • ProtectedString has a Base.depwarn
  • Added to the UnitfulExt docs
  • Double check that UnitfulExt docs are correct and have everything new

@Ickaser
Copy link
Contributor Author

Ickaser commented Jun 17, 2025

@gustaphe, I realized today that I was assuming the :none unitformat should put no unit in the axis label, where the current behavior is more like "no special symbols on the unit". Changing unitformat=:none from "no symbols with unit" to "no unit" is breaking, so think I will give it a new name like unitformat=:nounit at least for v1. But since we are making breaking changes in v2, what do you think of changing the behavior of unitformat=:none to print no unit at all?

Edit: unitformat=false follows the same code path, and I think I would also expect that to print no unit, so maybe change that at the same time.

@t-bltg
Copy link
Member

t-bltg commented Jun 17, 2025

@Ickaser can you post mwe code and resulting images in your PR description, so we can see in a glance what changes ?

@Ickaser
Copy link
Contributor Author

Ickaser commented Jun 17, 2025

@Ickaser can you post mwe code and resulting images in your PR description, so we can see in a glance what changes ?

Sure, I've added that to the PR description and linked to the relevant issues.

@t-bltg t-bltg added the UnitfulExt recipes for Unitful quantities label Jun 17, 2025
@gustaphe
Copy link
Collaborator

I was assuming the :none unitformat should put no unit in the axis label

Not an unfair assumption, that would probably have been a better design choice.

@t-bltg

This comment was marked as outdated.

@BeastyBlacksmith
Copy link
Member

We do need all of these for 1.x releases or don't we?

@t-bltg
Copy link
Member

t-bltg commented Jun 19, 2025

We do need all of these for 1.x releases or don't we?

You are right.

@Ickaser
Copy link
Contributor Author

Ickaser commented Jun 19, 2025

We do need all of these for 1.x releases or don't we?

You are right.

I think I did it right then--for 1.x, the docs changes are in JuliaPlots/PlotDocs.jl#360, and for 2.0(#5095), the docs changes are in the single PR to the monorepo.

Copy link
Member

@t-bltg t-bltg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Co-authored-by: t-bltg <tf.bltg@gmail.com>
@gustaphe
Copy link
Collaborator

I think this is generally good, but I'm hunting down a regression. The second argument to format_unit_label(l, u) is not supposed to be a string (if the axis unit is not, I guess). It matters, because latexify has a two-argument method intended specifically for formatting labels:

plot(rand(5)*u"m"; yguide="x", unitformat=latexify)

should have the yguide $x\;/\;\mathrm{m}$ (== latexify("x", u"m")), but with this PR it errors. I will suggest a few lines of edits that will fix this.

src/axes.jl Outdated
Comment on lines 130 to 139
ustr = if Plots.backend_name() ≡ :pgfplotsx
Latexify.latexify(axis[:unit])
else
string(axis[:unit])
end
isempty(axis[:guide]) && return ustr
return format_unit_label(
axis[:guide],
ustr,
axis[:unitformat])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ustr = if Plots.backend_name() :pgfplotsx
Latexify.latexify(axis[:unit])
else
string(axis[:unit])
end
isempty(axis[:guide]) && return ustr
return format_unit_label(
axis[:guide],
ustr,
axis[:unitformat])
isempty(axis[:guide]) && return string(axis[:unit])
return format_unit_label(
axis[:guide],
axis[:unit],
axis[:unitformat])

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This together with somehow setting the default unitformat for pgfplotsx to (l, u)->"\$$l\\;\\left($(Latexify.latexify(u; env=:raw))\\right)\$" solves my problem. I had success putting default(unitformat=(l, u)->"\$$l\\;\\left($(Latexify.latexify(u; env=:raw))\\right)\$") at line 1608 of src/backends.jl (inside _initialize_backend(pkg::PGFPLotsXBackend)), but I don't know if that's legal.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to find a solution that doesn't involve setting a default in the backend, if I can?... So far the best idea I am coming up with is that PGFPlotsX should have its own function (or set of methods) for format_unit_label(l, u), that puts \\left and \\right on all the characters.

That, or for all the cases except unitformat <: Function, latexify gets called on the unit before getting passed to format_unit_label. That way if unitformat=latexify gets passed, latexify gets to see the unit object itself, but if any of the defaults are used then things still work.

Increasingly I find myself of the opinion that in v2 it might be worth dropping most of the methods of format_unit_label--unless I'm using :round or :square, I almost always end up writing a function myself, and I think it's not too complicated an operation for most users. (Probably not more complicated than understanding how many symbols they want to pass for the current methods, at least.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went for the unitformat isa Function check and am letting tests run now: if this works in your cases, I might like this solution best since it doesn't involve setting any defaults, but if the resulting code is unclear I am open to other suggestions (like your original one).

@Ickaser
Copy link
Contributor Author

Ickaser commented Jun 19, 2025

I think this is generally good, but I'm hunting down a regression. The second argument to format_unit_label(l, u) is not supposed to be a string (if the axis unit is not, I guess). It matters, because latexify has a two-argument method intended specifically for formatting labels:

plot(rand(5)*u"m"; yguide="x", unitformat=latexify)

should have the yguide $x\;/\;\mathrm{m}$ (== latexify("x", u"m")), but with this PR it errors. I will suggest a few lines of edits that will fix this.

Sure enough, in Plots.get_guide (src/axes.jl, line 130 or thereabouts) I have the unit getting preemptively converted to a string, so that if we are in PGFPlotsX the unit is in LaTeX-friendly format. So a fix can start there

@t-bltg t-bltg added the bug label Jun 21, 2025
@Ickaser
Copy link
Contributor Author

Ickaser commented Jun 25, 2025

The latest CI failures are either 1) failure to precompile Gtk.jl on Windows (seems random: affected 1.10 and 1.11, not 1.6, and worked the last time CI ran) and 2) downstream GraphRecipes failing on a Julia type tree (which fails on 1.11 and 1.12 across branches, it seems).

I added one (1) test with latexify to the test suite, and it seems to work now, so if that's enough (and latexify solution looks good to @gustaphe) I think this is good to go.

@gustaphe
Copy link
Collaborator

Otherwise LGTM. Good work!

@BeastyBlacksmith BeastyBlacksmith merged commit 069318c into JuliaPlots:master Jun 26, 2025
15 of 19 checks passed
This was referenced Jul 2, 2025
Ickaser added a commit to Ickaser/Plots.jl that referenced this pull request Jul 4, 2025
Ickaser added a commit to Ickaser/Plots.jl that referenced this pull request Aug 28, 2025
BeastyBlacksmith pushed a commit that referenced this pull request Oct 5, 2025
* Catch some fixes from #5098 and #5127 for v2

* Copypaste whitespace formatting

* EOF whitespace format

* Load latexify for Unitful tests

* Add improvements from #5158

* PlotsBase, not Plots

* Fully excise UnitfulLatexify, and drop Latexify as a dep for Unitful

* no need for brackets

* Fix unitful latexify tests

* Make Latexify and LaTeXStrings full deps of PlotsBase, remove hack

* Add Colors and Contour as full deps of PlotsBase, so PGFPlotsXExt triggers with no extra imports

* Allow Colors, Contour, LaTeXStrings, Latexify to be stale deps for PlotsBase

* Add a test to improve coverage

* Another test suggested by coverage

* do that test right, whoops

* Drop "WEAKDEPS" for triggering extensions
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug enhancement improving existing functionality UnitfulExt recipes for Unitful quantities

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants