You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Feature request: Handle WCS projections for plotting astronomical images. #606
I am assuming that UltraPlot cannot handle WCS projections, hence this issue is framed as a feature request. As far as I am aware, UltraPlot has a lot of features focused on plotting geographical data. It would be great if features related to plotting astronomical data and images could also be added, especially since many of the plots we make are quite similar to ones made for geographical data.
I am suspecting that the transform is not properly registered by the package -- will have a look. The error indicates that it is assuming the default transform object (which does not take an argument) but the astropy backends do take an argument. The registration should fix this normally -- but it is not properly registering at ultraplot's level.
Hi @cvanelteren! Thanks for noticing, I completely missed that. Here is a more complete example as a Jupyter notebook, which should run standalone: mwe.ipynb. Let me know if there are any issues accessing it. I basically create the WCS myself in this case, but I think there should be a similar issue if one tries this with a FITS image.
Ok had some time to look at this. The issue is that UltraPlot has a tight control over custom axes to control how it looks like to the reader. The implementation is provided for 2D axes, 3D axes, and GeoAxes, with some recent addition of hosting external axes (e.g. mpltern); but the support is a bit yanky. Namely, for mpl tern we are hosting those axes as a subplot within a native UltraPlot axis and just hiding its axes. Here, a feedback loop is created because astropy handles its layout differerently.
When implementing the external container class for mpl tern I was under the impression that not that many axes would exist outside geo and ternary plots -- but astropy is proving me wrong. The best integration would be to write a custom adapter for Astropy to play nice with UltraPlot -- which would cost time and effort I don't have at this moment.
I can probably jank it in, but not sure if that is the way to go and it is generally a problem for another package that will provide another layout mechanism. The reason why matplotlib does not have this issue it that it does not have methods like
panel creation
sharing logic
abc/title management
guide/colorbar/legend integration
Locally I have a fix that can render the astropy axes, but it is not very pretty and I cannot test it to the full extend as this is outside my field of expertise.
What would be necessary (in case somebody wants to work on this) is:
The most plausible shape is a dedicated AstroAxes class modeled after ThreeAxes and GeoAxes:966, registered alongside
the built-in projections in axes/init.py.
What would be needed:
Define a native axes class.
The first thing to test is a cooperative MRO class like AstroAxes(shared._SharedAxes, plot.PlotAxes, WCSAxes).
This is the hard part, because PlotAxes:1885 and Astropy WCSAxes both sit on top of Matplotlib Axes.
If that MRO is unstable, the fallback is a thinner class more like ThreeAxes that does not try to inherit all
plotting overrides in v1.
Register it as a real UltraPlot projection.
Add it to axes/init.py with _name / aliases so UltraPlot treats it as a first-class axes, not an external child.
Teach figure creation to route WCS objects to it.
Update Figure._parse_proj():1398 so projection=wcs resolves to ultraplot_astropy instead of falling through to the
external container path.
Decide the supported Astropy scope up front.
I would keep v1 narrow:
2D celestial WCS only
astropy.wcs.WCS and maybe high-level WCS API objects
no spectral cubes / sliced N-D support beyond what Astropy already exposes cleanly
Implement Astro-specific init and state.
The class would need to accept and persist Astropy-specific inputs such as:
wcs
slices
coord_meta
frame_class
maybe transform
This needs to line up with WCSAxes.init(..., wcs=..., slices=..., ...), not with the current generic container
constructor.
Build an UltraPlot-aware format() bridge.
This is where most of the work is.
A native Astropy axes cannot just reuse Cartesian formatting blindly because WCS axes semantics live in coords[...],
not just xaxis / yaxis.
You would need to decide how UltraPlot keywords map to:
grid visibility
RA/Dec labels
tick visibility and placement
tick formatters/locators
frame styling
titles and abc labels
Define transform semantics for plotting commands.
The class should make ax.get_transform("icrs") work natively, but also decide what UltraPlot plotting defaults mean:
should imshow() stay pixel-native by default?
should plot()/scatter() assume pixel coordinates unless transform= is passed?
do we want any world-coordinate convenience defaults like GeoAxes does?
Revisit sharing, panels, and guides.
UltraPlot’s layout/panel/sharing logic expects native UltraPlot axes, which is the main reason to do this work at
all.
But some features may need to be limited initially:
@cvanelteren As requested, here is a minimal working example. Note that this is the same code as the Jupyter notebook I shared earlier, and I think it should be stand alone. In case it isn't, do let me know:
@cvanelteren Sure, I can help with some of the testing. I will see if I can test it with my own use cases, as well as some FITS images. Also, maybe @beckermr can also have a look at this if he has some time? He should be more familiar with WCS projections, and could test this more rigorously than I. I don't deal with that many astronomical images in my line of work, I am more of a time-domain astronomy guy.
On #607 astropy support is added. My main concern is that I (i) forces a bundle on the conda install and (ii) I will not daily drive it so I cannot catch bugs early is they arrive.
Activity
cvanelteren commentedon Mar 7, 2026
I think the projection is handled properly by our backend; should be easy fix.
cvanelteren commentedon Mar 7, 2026
I am suspecting that the transform is not properly registered by the package -- will have a look. The error indicates that it is assuming the default transform object (which does not take an argument) but the astropy backends do take an argument. The registration should fix this normally -- but it is not properly registering at ultraplot's level.
Handle WCS projection objects in external containers
cvanelteren commentedon Mar 7, 2026
Can you checkout #607 @astrogewgaw -- I don't have access to the image you have above (code is not complete).
astrogewgaw commentedon Mar 9, 2026
Hi @cvanelteren! Thanks for noticing, I completely missed that. Here is a more complete example as a Jupyter notebook, which should run standalone: mwe.ipynb. Let me know if there are any issues accessing it. I basically create the WCS myself in this case, but I think there should be a similar issue if one tries this with a FITS image.
cvanelteren commentedon Mar 9, 2026
Easier would be to provide a minimal example to use. But the fix on #607 should address the problem.
The first plot shows this:
the second looks fine, not sure if that is normal
cvanelteren commentedon Mar 9, 2026
Ok I made a small test code; ther is something odd happening with our external axes wrapper. Would need to spend a bit more time on this.
cvanelteren commentedon Mar 9, 2026
Ok had some time to look at this. The issue is that UltraPlot has a tight control over custom axes to control how it looks like to the reader. The implementation is provided for 2D axes, 3D axes, and GeoAxes, with some recent addition of hosting external axes (e.g. mpltern); but the support is a bit yanky. Namely, for mpl tern we are hosting those axes as a subplot within a native UltraPlot axis and just hiding its axes. Here, a feedback loop is created because astropy handles its layout differerently.
When implementing the external container class for mpl tern I was under the impression that not that many axes would exist outside geo and ternary plots -- but astropy is proving me wrong. The best integration would be to write a custom adapter for Astropy to play nice with UltraPlot -- which would cost time and effort I don't have at this moment.
I can probably jank it in, but not sure if that is the way to go and it is generally a problem for another package that will provide another layout mechanism. The reason why matplotlib does not have this issue it that it does not have methods like
Locally I have a fix that can render the astropy axes, but it is not very pretty and I cannot test it to the full extend as this is outside my field of expertise.
What would be necessary (in case somebody wants to work on this) is:
The most plausible shape is a dedicated AstroAxes class modeled after ThreeAxes and GeoAxes:966, registered alongside
the built-in projections in axes/init.py.
What would be needed:
Define a native axes class.
The first thing to test is a cooperative MRO class like AstroAxes(shared._SharedAxes, plot.PlotAxes, WCSAxes).
This is the hard part, because PlotAxes:1885 and Astropy WCSAxes both sit on top of Matplotlib Axes.
If that MRO is unstable, the fallback is a thinner class more like ThreeAxes that does not try to inherit all
plotting overrides in v1.
Register it as a real UltraPlot projection.
Add it to axes/init.py with _name / aliases so UltraPlot treats it as a first-class axes, not an external child.
Teach figure creation to route WCS objects to it.
Update Figure._parse_proj():1398 so projection=wcs resolves to ultraplot_astropy instead of falling through to the
external container path.
Decide the supported Astropy scope up front.
I would keep v1 narrow:
Implement Astro-specific init and state.
The class would need to accept and persist Astropy-specific inputs such as:
This needs to line up with WCSAxes.init(..., wcs=..., slices=..., ...), not with the current generic container
constructor.
Build an UltraPlot-aware format() bridge.
This is where most of the work is.
A native Astropy axes cannot just reuse Cartesian formatting blindly because WCS axes semantics live in coords[...],
not just xaxis / yaxis.
You would need to decide how UltraPlot keywords map to:
Define transform semantics for plotting commands.
The class should make ax.get_transform("icrs") work natively, but also decide what UltraPlot plotting defaults mean:
Revisit sharing, panels, and guides.
UltraPlot’s layout/panel/sharing logic expects native UltraPlot axes, which is the main reason to do this work at
all.
But some features may need to be limited initially:
Add a serious regression matrix.
At minimum:
astrogewgaw commentedon Mar 12, 2026
@cvanelteren As requested, here is a minimal working example. Note that this is the same code as the Jupyter notebook I shared earlier, and I think it should be stand alone. In case it isn't, do let me know:
cvanelteren commentedon Jul 14, 2026
@astrogewgaw I am going to pick this up again, are you around for review by any chance?
astrogewgaw commentedon Jul 16, 2026
@cvanelteren Sure, I can help with some of the testing. I will see if I can test it with my own use cases, as well as some FITS images. Also, maybe @beckermr can also have a look at this if he has some time? He should be more familiar with WCS projections, and could test this more rigorously than I. I don't deal with that many astronomical images in my line of work, I am more of a time-domain astronomy guy.
cvanelteren commentedon Jul 16, 2026
On #607 astropy support is added. My main concern is that I (i) forces a bundle on the conda install and (ii) I will not daily drive it so I cannot catch bugs early is they arrive.
Handle WCS projection objects in external containers
Handle WCS projection objects in external containers
Handle WCS projection objects in external containers