Skip to content

Axis Hints#5375

Open
ffreyer wants to merge 13 commits intomasterfrom
ff/axis-hints
Open

Axis Hints#5375
ffreyer wants to merge 13 commits intomasterfrom
ff/axis-hints

Conversation

@ffreyer
Copy link
Copy Markdown
Collaborator

@ffreyer ffreyer commented Oct 30, 2025

Description

This pr adds

Makie.preferred_axis_attributes(AxisType, [plot], [args...])

as an extendable function for providing axis attributes to a generated axis. This is used with non-mutating plot() calls just like preferred_axis_type() / args_preferred_axis(). When plot(..., axis = (...)) is used, both sources are merged with the attributes from the plot call taking priority . If a different axis type is set there, the new type will be used to generate default attributes. The method call order here is:

  1. preferred_axis_attributes(AxisType, plot, args...)
  2. preferred_axis_attributes(AxisType, plot)
  3. preferred_axis_attributes(AxisType, args...)

The first one to return a non-empty collection gets to set axis attribute defaults.

This pr also cleans up the dispatch for args_preferred_axis(). Before we had

  1. preferred_axis_type(::Plot)
  2. args_preferred_axis(::Type, args)
  3. args_preferred_axis(::Type)
  4. args_preferred_axis(args[i]) (iterate arguments)
  5. args_preferred_axis(::Type, converted)
  6. args_preferred_axis(::Type)
  7. args_preferred_axis(converted[i]) (iterate converted arguments)

where the first method not returning nothing would set the axis type. We also had args_preferred_axis(::AbstractVector, ::AbstractVector, ::AbstractVector, ::Function) as a method which was unreachable.

After my changes we now have:

  1. preferred_axis_type(::Plot, input_args...)
  2. preferred_axis_type(::Type{<: Plot}, input_args...)
  3. preferred_axis_type(::Plot, converted_args...)
  4. preferred_axis_type(::Type{<: Plot}, converted_args...)
  5. preferred_axis_type(::Plot)
  6. preferred_axis_type(::Type{<: Plot})
  7. preferred_axis_type(input_args...)
  8. preferred_axis_type(converted_args...)
  9. preferred_axis_type(arg) for each argument
  10. preferred_axis_type(converted) for each converted argument

where the first valid method still sets the axis type. This is now a different priority order (e.g. args no longer beat plot type + converted). My thoughts here are to prioritize more specific information over less specific information. That way you can have a general "plot -> Axis" and a specific "plot, angle_args, radial_args -> PolarAxis`.

I've also chosen to soft-deprecate args_preferred_axis, meaning that they are not longer mentioned in docs but still work without errors or warnings. (Each preferred_axis_method defaults to trying args_preferred_axis.) Imo preferred_axis_type is a better name here, as there are a few methods that don't include args and many that use converted arguments. It also fits more nicely with preferred_axis_attributes(), though that's a name I chose. (If we want to actually deprecate this I would suggest doing that in breaking 0.25)

Closes #5361

Related: #5345, #5205, #1524, #379, #4174

TODO:

  • tests
  • docs

Type of change

Delete options that do not apply:

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Checklist

  • Added an entry in CHANGELOG.md (for new features and breaking changes)
  • Added or changed relevant sections in the documentation
  • Added unit tests for new algorithms, conversion methods, etc.
  • Added reference image tests for new plotting functions, recipes, visual options, etc.

@github-project-automation github-project-automation Bot moved this to Work in progress in PR review Oct 30, 2025
@MakieBot
Copy link
Copy Markdown
Collaborator

MakieBot commented Oct 30, 2025

Benchmark Results

SHA: 2456696c43e1b65a2c968b96a22ff74bd9117123

Warning

These results are subject to substantial noise because GitHub's CI runs on shared machines that are not ideally suited for benchmarking.

GLMakie
CairoMakie
WGLMakie

@ffreyer ffreyer marked this pull request as ready for review October 31, 2025 21:47
@asinghvi17
Copy link
Copy Markdown
Member

How would this fit in with an auto-legending or auto-colorbar request?

@jkrumbiegel
Copy link
Copy Markdown
Member

Can this be used to define axis attributes that update with the data? If so, should that be based on compute graph rather than observables for a later point when we might switch blocks to that infra as well? Because currently you'll have to convert to observables at the border but we might want to hide that from the user.

@ffreyer
Copy link
Copy Markdown
Collaborator Author

ffreyer commented Nov 1, 2025

How would this fit in with an auto-legending or auto-colorbar request?

We had a discussion internally about axis hints and auto legends and whatnot a while ago. The conclusion was that axis hints and auto legends should be separate, with axis hints generating defaults based on one plot and auto legends being part of some higher level system that may include multiple plots and layouting.
The dim converts pr #5323 also came from that discussion, because setting axis labels to units seems like a fairly common hint/default you'd want to give an axis.

Can this be used to define axis attributes that update with the data? If so, should that be based on compute graph rather than observables for a later point when we might switch blocks to that infra as well? Because currently you'll have to convert to observables at the border but we might want to hide that from the user.

The plot you get in preferred_axis_attributes() is partially constructed. It already has attributes, arguments and converted arguments which you can react to. It doesn't have anything scene related yet, because the axis does not exist yet, and as a consequence it also didn't run plot!(plot) yet. (Because that can depend on the scene via projections for example).

I could try delaying the call to preferred_axis_attributes() until after the plot is fully initialized, but I'm not sure if that's worth the extra book-keeping and Observable (or future compute graph) surgery required. (If you need a computation that's defined in plot!(plot) you could also just copy it over to preferred_axis_attributes(). If the inputs, output and callback are the same the compute graph will just skip the second map!/register_computation!.)

@ffreyer ffreyer moved this from Work in progress to Ready to review in PR review Nov 11, 2025
@ffreyer ffreyer mentioned this pull request Jan 3, 2026
22 tasks
@juliohm
Copy link
Copy Markdown
Member

juliohm commented Mar 9, 2026

@ffreyer what is the status of this PR?

@ffreyer
Copy link
Copy Markdown
Collaborator Author

ffreyer commented Mar 10, 2026

I think I was just waiting for review/feedback on this

@juliohm
Copy link
Copy Markdown
Member

juliohm commented Mar 12, 2026

I think I was just waiting for review/feedback on this

It would be great if someone could review this PR.

@juliohm
Copy link
Copy Markdown
Member

juliohm commented Mar 15, 2026

ping @SimonDanisch

@juliohm
Copy link
Copy Markdown
Member

juliohm commented Apr 4, 2026

We are looking forward to this feature. Is there any chance that someone could review this PR in the following weeks?

@SimonDanisch
Copy link
Copy Markdown
Member

Maybe you can review it as a user and try it out if it covers your use cases? That would be most valuable!

@juliohm
Copy link
Copy Markdown
Member

juliohm commented Apr 4, 2026 via email

@SimonDanisch
Copy link
Copy Markdown
Member

I think we're mainly unsure if this is a good user facing API ;)

@juliohm
Copy link
Copy Markdown
Member

juliohm commented Apr 11, 2026

@ffreyer the new function works like a charm. Could we also have a method that dispatches on the args like we do for args_preferred_axis? JuliaGeometry/Meshes.jl#1357

If we could dispatch on the args, we could retrieve additional information about the object being plotted. For example, if we know that the object being plotted has coordinates in cm we can set the xlabel to X [cm] as opposed to the generic X.

@juliohm
Copy link
Copy Markdown
Member

juliohm commented Apr 16, 2026

@SimonDanisch do you have an opinion about this method with args? I believe that is the only missing detail for the uses cases we have in mind. We need to set default attributes based on the object being plotted, not the recipe type.

@jariji
Copy link
Copy Markdown
Contributor

jariji commented Apr 16, 2026

A slightly modified API is proposed in https://github.com/MakieOrg/Makie.jl/pull/5581/changes

@juliohm
Copy link
Copy Markdown
Member

juliohm commented Apr 16, 2026

Thank you @jariji. It is good to see that other people already identified this need in other contexts as well.

jariji and others added 4 commits April 18, 2026 16:02
* Add argument-level preferred_axis_attributes hook

Extend the axis hints system so that data types passed as plot arguments
can provide default axis attributes (labels, scales, limits) by defining
preferred_axis_attributes(arg). This mirrors args_preferred_axis(arg)
which already determines the axis *type* from individual arguments.

Recipe-level hints (2-arg form) take priority over argument-level hints,
and user-provided axis keywords override both.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Unify preferred_axis_attributes into (AxisType, plot) / (AxisType, args...)

Address ffreyer's review: include axis type, pass all args at once, and
unify recipe-level and arg-level forms into a single dispatch chain.

The (AxisType, plot::Plot) form defaults to unpacking plot.args[] and
delegating to (AxisType, args...), so recipe authors override the first
and data type authors override the second.

This also flips argument order to (AxisType, plot) so the axis type is
available for dispatch (different axis types use different attribute names).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: jariji <jariji@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ffreyer
Copy link
Copy Markdown
Collaborator Author

ffreyer commented Apr 18, 2026

I merged the changes from #5581 though I also reordered the methods. Instead of

  1. preferred_axis_attributes(AxisType, plot)
  2. preferred_axis_attributes(AxisType, args...)

it is now

  1. preferred_axis_attributes(AxisType, plot, args...)
  2. preferred_axis_attributes(AxisType, plot)
  3. preferred_axis_attributes(AxisType, args...)

That way you can have plot based defaults and still specialize to (plot and) argument based defaults. I'm basically thinking that the more detail you add to these methods, the priority you should get for setting attributes.

I also did the same thing to preferred_axis_type as noted in the pr description.

@juliohm
Copy link
Copy Markdown
Member

juliohm commented Apr 20, 2026

Thank you @ffreyer for the update. I will be able to test it again on Wednesday or Thursday.

@juliohm
Copy link
Copy Markdown
Member

juliohm commented Apr 24, 2026

@ffreyer the latest changes work beautifully:

https://github.com/JuliaGeometry/Meshes.jl/pull/1357/changes

Looking forward to the next Makie.jl release with this new feature.

@aplavin
Copy link
Copy Markdown
Contributor

aplavin commented Apr 24, 2026

Eager to try using this to replace hacky axis attributes setup in packages like MakieExtra and AxisKeysExtra!
Some questions that I couldn't fully understand from the docs:

  • preferred_axis_type is documented for both "original" and converted arguments, but preferred_axis_attributes doesn't explicitly say it supports both – is it just a docs omission? A useful natural piece of this feature.
  • How to utilize these attributes for exclamation-suffixed plot calls, when plotting onto an existing axis? I find this quite a common pattern to create an axis first with some geometry/layout options, and then plot stuff onto it later:
ax = Axis(fig[1,1], width=200, height=300)
hidespines!()

...

image!(ax, data)

and automatic labels would be natural here, based on data.

@juliohm
Copy link
Copy Markdown
Member

juliohm commented Apr 24, 2026

  • How to utilize these attributes for exclamation-suffixed plot calls, when plotting onto an existing axis?

Does it make sense to overwrite the attributes of an already created axis with preferred_axis_attributes though? I think that the new function is designed for cases where we are creating the axis automatically from arguments. If the axis is already created, it already has attributes set. IMHO, it is dangerous to allow the plot recipe to set attributes of an already created axis.

@aplavin
Copy link
Copy Markdown
Contributor

aplavin commented Apr 24, 2026

@juliohm note that I haven't said that plot! calls should automatically set axis attributes – any other convenient way to utilize them would work just as well.
Although I personally don't see major downsides to setting axis attributes when they weren't set before.

That's how things work now:

data = [
       KeyedArray(1:5, time=10:10:50),
       KeyedArray(1:5, time=15:10:55),
]
ax = Axis(Figure()[1,1])

for d in data
       scatterlines!(d)
end
Image

image

And I'd be happy to transition the implementation to preferred_axis_attributes if it could support this scenario in some way.
I find this pattern quite common, and it becomes inconvenient without plot! funcs.

@ffreyer
Copy link
Copy Markdown
Collaborator Author

ffreyer commented Apr 25, 2026

Eager to try using this to replace hacky axis attributes setup in packages like MakieExtra and AxisKeysExtra! Some questions that I couldn't fully understand from the docs:

  • preferred_axis_type is documented for both "original" and converted arguments, but preferred_axis_attributes doesn't explicitly say it supports both – is it just a docs omission? A useful natural piece of this feature.

Is it actually useful? I'd imagine axis related information is one of those things that convert_argumentsis very likely to remove, if implemented. (And you don't need to extract dimensionality here anymore, since that's already encoded in the axis type choice)

  • How to utilize these attributes for exclamation-suffixed plot calls, when plotting onto an existing axis?

Well, you'd need to figure out what attributes the user set for that. I don't think diffing current with default attributes would be enough for that, because it would fail to catch the user setting an attribute to its default value to avoid plot-based overwriting. (Or maybe that's an acceptable shortcoming?) Another option would be to track the state of each attribute through construction and setindex/setproperty. Which seems kind of bloaty but maybe that's ok?

In general, when we discussed axis hints we were kind of doubtful about how useful they would be. For units you can use dim converts, probably even with something like KeyedArray. (0.25/#5323 adds functionality for having dim converts affect axis labels and also allows them to hint whether they should be in tick labels or axis labels.) For more complicated things 0.25/#5465 adds complex recipes. They also allow you to add automatic legends, colorbars or anything layout related which seems like the thing people actually want.

@aplavin
Copy link
Copy Markdown
Contributor

aplavin commented Apr 25, 2026

Is it (support for converted args) actually useful?

Of course – if my convert_arguments converts MyType to AnotherType, then I kinda want the plotting result to be the same as if AnotherType was plotted. Why wouldn't this be useful?
I don't personally rely on this aspect super often, but definitely do sometimes when converting to KeyedArrays.

Well, you'd need to figure out what attributes the user set for that.

Would be nice, yes – but even some simple callsite opt-in would help.
Eg, scatter!(data, axis_attributes=true) / with_axattrs(scatter!)(data) / whatever else, these are just initial variants.

Another option would be to track the state of each attribute through construction and setindex/setproperty. Which seems kind of bloaty but maybe that's ok?

IMO tracking "default" vs "explicitly assigned" could generally be useful, not just for this purpose.


Haven't read other recent PRs you linked in detail yet, do you suggest that those advanced recipes make these "axis hints" obsolete?

@ffreyer
Copy link
Copy Markdown
Collaborator Author

ffreyer commented Apr 26, 2026

Is it (support for converted args) actually useful?

Of course – if my convert_arguments converts MyType to AnotherType, then I kinda want the plotting result to be the same as if AnotherType was plotted. Why wouldn't this be useful? I don't personally rely on this aspect super often, but definitely do sometimes when converting to KeyedArrays.

Hmm, if you're converting between types that have extra information then that makes sense.

I view convert_arguments as something that changes data into a plot specific format, so I would expect it to remove that information. In fact, for primitive plots that is required to a degree by type annotations in @recipe (or trait based target type). This will expand to non-primitive plots with 0.25, because we use that information to figure out when conversions are done, which in turn is used to figure out if dim converts have already applied and if the user input is valid. For PointBased plots this forces you to convert to AbstractVector{<: Point{D, <:Real}} where D is 2 or 3, for example.

Well, you'd need to figure out what attributes the user set for that.

Would be nice, yes – but even some simple callsite opt-in would help. Eg, scatter!(data, axis_attributes=true) / with_axattrs(scatter!)(data) / whatever else, these are just initial variants.

An opt-in kwarg sounds good to me. That would skip all the extra complexity. Might also be nice to be able to opt-out in the non-mutating calls.

Haven't read other recent PRs you linked in detail yet, do you suggest that those advanced recipes make these "axis hints" obsolete?

tl;dr for complex/block recipes is that @Block can now be used as some sort of layout recipe

@Block MyBlock (x, y) begin ... end

function Makie.initialize_block!(b::MyBlock)
    ax = Axis(b[1, 1])
    map!(eachindex, b, :x, :scatter_colors)
    p = scatter!(ax, b.x, b.y, color = b.scatter_colors)
    Colorbar(b[1, 2], p)
end

f, b = MyBlock(rand(10), rand(10))

and if you are creating the axis manually in such a recipe, then you can of course also set all the axis attributes there. It doesn't make axis hints obsolete, but it is another way to get there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Ready to review

Development

Successfully merging this pull request may close these issues.

Allow customization of axis in recipes

8 participants