Skip to content

feat!: Use Makie.jl as plotting backend - #119

Open
icweaver wants to merge 38 commits into
v0.6from
makie
Open

feat!: Use Makie.jl as plotting backend#119
icweaver wants to merge 38 commits into
v0.6from
makie

Conversation

@icweaver

@icweaver icweaver commented Jul 11, 2026

Copy link
Copy Markdown
Member

Doc preview: https://juliaastro.org/AstroImages.jl/previews/PR119/

Replaces the RecipesBase/Plots.jl recipes with Makie, targeting the ff/breaking-0.25 branch (MakieOrg/Makie.jl#5484).

What changed

  • RecipesBase is gone. The backend-agnostic WCS machinery (WCSGrid, wcsgridspec, wcslabels, ctype_label, …) moved unchanged from src/plot-recipes.jl to src/wcs-grid.jl; the recipes themselves were removed. implot/implot!/implotview/polquiver/polquiver! are now exported function stubs (src/plot-interface.jl).
  • Makie is a weak dependency. ext/AstroImagesMakieExt.jl implements the stubs as Makie 0.25 compute-graph recipes. Loading any Makie backend activates it.

Design notes

Native colorbars with non-linear stretches. Instead of pre-rendering RGBA via imview, raw data flows through Makie's colormapping pipeline:

  • clims (incl. Percent/Zscale) resolve to colorrange.
  • stretch becomes a colorscale: a ReversibleScale closure that applies the stretch to the clims-normalized value (the DS9/imview convention. This matters because astro data is routinely negative, where raw-data log scaling fails). All exported stretches have analytic inverses, so Colorbar tick placement works.
  • contrast/bias fold into a resampled colormap (Makie renormalizes colorscale output affinely, so they cannot live in the scale).

Makie.Colorbar(fig[1, 2], plt) therefore shows real data units with correctly warped tick spacing, something the Plots recipe could only fake with a second image subplot. A Makie._extract_colormap(::ImPlot) method points Colorbar at the right child (the recipe also draws grid lines).

WCS axes. The recipe configures auto-created axes through the new preferred_axis_attributes hook: sexagesimal ticks and labels from wcsgridspec, title from refdims, tight limits, DataAspect. The (curved) WCS grid is overplotted by the recipe. Makie's image convention (first dim = x, y-up) matches FITS, so all the flip/transpose gymnastics from the Plots recipe are gone.

Dynamic zoom-aware WCS ticks (WCSTicks). Tick positions and sexagesimal labels recompute from the current axis limits through an overload of Makie's get_ticks(locator, scale, formatter, vmin, vmax) protocol. Zooming from the full frame into a 100-pixel window re-ticks from arcminute to arcsecond steps automatically, something the static Plots recipe (and astropy's WCSAxes) doesn't do. Because WCS tick positions on one axis depend on the extent of both axes, the x/y locators share a mutable extent state that each call refreshes for its own dimension (at most one interaction stale in the other, exact for unrotated WCS). Labels are generated in screen order so the full-length anchor label sits at the axis start even where world coordinates run right-to-left (RA). The context-bearing x-axis labels (the anchor tick and roll-overs) are stacked onto two lines, with the finest component over full context, e.g. 48.00ˢ over 23ʰ23ᵐ, following the pattern of Makie's DateTimeTicks, so that dense tick labels don't crowd each other.

ImPlotView block recipe. Makie 0.25's new @Block complex recipes bundle an Axis + WCS ticks + labeled Colorbar in one call (ImPlotView(fig[1, 1], img)), replacing the old @layout hack. Colorbar label defaults to the UNIT/BUNIT header. The block also syncs the tick state and the recipe's viewextent attribute from the axis finallimits, so the curved grid overlay refines together with the ticks on zoom/pan. (A plain implot into a user-created Axis keeps the static full-extent grid, since a plot cannot see its axis's limits.)

Direct world-coordinate overplotting (world_transform) (7ee4d8a). The analogue of matplotlib WCSAxes' ax.scatter(..., transform=ax.get_transform("world")): pass the new exported world_transform to any plot's transformation keyword to plot world-coordinate data (in the WCS's world units, typically degrees) directly over an image, with no manual world_to_pixel round-trip — scatter!(iv.ax, ra, dec; transformation = world_transform(iv)). It returns a Makie.Transformation whose transform func maps plotted-dim world coordinates into the (platescale-scaled, parent-frame) pixel space the recipe draws in, reusing the grid code's _world_to_plotted_pixel so frozen slice axes get the same fixed-point refinement. Following GeoMakie's precedent for nonlinear projections, points are NaN/error-guarded and Rect bounding boxes are densified by grid sampling instead of corner-mapping — those Rects feed axis autolimits, which is what makes world overlays co-register with the pixel-space image automatically. The transform is invertible (Makie.inverse_transform wraps pixel_to_world(...; parent = true)), so picking and inspection tooling keep working. world_transform(plt)/world_transform(iv) derive the image, wcsn, and platescale from an existing plot or view so they cannot drift out of sync with what is displayed; world_transform(img; wcsn, platescale) covers transform-first use. Angular wrap-around (RA crossing 0°/360°) is not special-cased, matching the grid code's standing limitation.

Dynamic updates work through the compute graph: update!(plt, stretch = asinhstretch, cmap = :viridis) re-renders live.

WCS grid correctness for sliced and non-square images (a18bec2). The position–velocity docs figure (HIcube[Y = 45]) exposed three latent bugs, now fixed with regression tests: tick registration compared gridline/edge intersections with exact float == (separable frames registered no ticks at all). The grid layout inherited the constant frozen-axis fill of world_to_pixel(...; parent = true), which drifts off the slice plane on curved transforms (now refined onto the exact frozen-pixel plane by fixed-point iteration); and the WCSGrid(img) convenience constructor had its x/y extents transposed since the Plots era, invisible on square images. wcsticks also now maps plotted axis --> WCS axis for sliced cubes, and DataAspect is only applied when both plotted axes are angular, so mixed longitude/velocity panels fill their layout cell.

Panel layout and aspect ratio (bf79dc3, 11dcd9e). An Axis aspect shrinks the axis inside its layout cell, so the cell keeps whatever shape the layout hands it and the leftover space opens up as a gap between the image and the colorbar. ImPlotView now shapes the cell instead (the approach in Makie's aspect tutorial), so the axis fills it and the colorbar sits flush. Two wrinkles that the obvious colsize!(layout, 1, Aspect(1, r)) + resize_to_layout! pair does not survive:

  • Aspect derives one cell dimension from the other and the derived one is unbounded, so deriving the width from the height overflows the layout for wide images (and vice versa for tall ones) — a nested view would grow straight over its neighbours. The block now picks the direction that fits the space it was actually given, and revisits it whenever that space changes.
  • resize_to_layout! is a no-op here: an Aspect row or column leaves the enclosing layout's size undetermined, so a block containing one reports no preferred size and the figure has nothing to shrink onto. When implotview creates its own figure it now sizes it from the block's solved content box instead (an explicit figure = (; size = ...) still wins, and a view placed into someone else's figure never resizes it).

Sized panels: figures that shrink-wrap around their content. The Aspect-based cell shaping above is the fill-the-window mode: the panel takes whatever space the layout gives it, which is right for interactive use. The complementary mode is now supported too: implot(fig[1, 1], img; width = 400) (new recipe attributes width/height, injected as axis layout sizes) and implotview(fig[1, 2], img; axis = (; height = 300)) fix the panel's image-box size, deriving a missing dimension from the image extent so the panel keeps the data aspect. Fixed sizes make the layout fully determined, which flips the sizing direction: resize_to_layout!(fig) shrink-wraps the figure around any arrangement of panels instead of the panels depending on the figure size, eliminating hand-tuned Figure(size = ...) from every multi-panel docs figure. Makie 0.25 blocks report a determinable inner layout as their own autosize, so this works through ImPlotView panels in grids with no extra machinery, and a standalone sized view shrink-wraps its own figure automatically; unsized views keep the aspect-locked fill behavior above.

Blank CUNIT means degrees (bf79dc3). CUNIT is optional, and the WCS standard makes deg the default unit of a celestial axis. Testing cunit == "deg" therefore misread any image that omits it, including the HST Eagle Nebula frame that headlines the docs, which was drawn with a distorted sky aspect and decimal-degree RA ticks (two of which rounded to the same label). isangular now falls back to the ctype (RA/DEC, the xLON/xLAT family, and the generic yzLN/yzLT form).

Smaller additions

  • imview gained a nan_color keyword (default transparent, unchanged): NaN/missing pixels can be rendered opaque, e.g. imview(clipped; nan_color = :black) — without it, transparent holes read as whatever backdrop the image lands on (white axis panel vs. page background), which made sigma-clipped stars look like bright sources. ImPlotView forwards the matching nan_color, lowclip, highclip, and alpha attributes to its internal plot, completing its claim of accepting all implot rendering kwargs (with the clip colors set, the Colorbar grows Makie's triangular out-of-range indicators automatically).
  • ImPlotView exposes its Axis as iv.ax for overplotting (e.g. lines!(iv.ax, apertures)) and the image plot as iv.plt.
  • Docs: the photometry guide is rebuilt end-to-end on the Makie stack. implotview panels with colorbars throughout, aperture overlays via Photometry.jl's new Makie extension, extract_sources output feeding apertures directly (paired with the Photometry 0.10 x/y convention fix below), and an imview mosaic via Images.mosaic. A consistency pass applies the sized-panel pattern across the manual and guide pages, and the custom docs stylesheet is gone: the 450px image cap it existed for is superseded by letting figures fill the content column under Documenter's stock max-width: 100% styling. The contours guide's overlays go through implotview as well, and its Contour.jl section is reframed around what still requires that package in the Makie era: Access to the contour geometry itself (Makie vendors the same marching-squares algorithm internally but never exposes the computed polylines) demonstrated by re-projecting contour lines into world coordinates, with a colorbar of its own.

Upstream requirements

Deferred / follow-ups

  • Tick step selection still uses the optimize_ticks(6v)/6 heuristic rather than true sexagesimal steps (30°, 10°, 1°, 30′, …). A generic SexagesimalTicks locator would fix this and could be upstreamed to Makie next to DateTimeTicks/AngularTicks once 0.25 settles. Tick density is part of the same gap: the step comes from the world extent with no notion of how many pixels the axis has, so an extreme aspect ratio (now that panels honour it) crowds the short axis's labels into each other.
  • A rounding-aware angle formatter in AstroAngles (à la astropy's Angle.to_string(precision=...), e.g. format_angle(deg; precision=:mas) snapping to the target component by integer arithmetic before decomposing). This would subsume AstroImages' private deg2dmsmμ helper, which we deliberately did not upstream as-is: its floor chain amplifies float fuzz (-46°40′ decomposes to (-46, 39, 59, 999, 999.99…)), it isn't a standard notation, and after the snappedparts fix it is only the fallback path. Pairs naturally with the SexagesimalTicks item above, since nice sexagesimal steps guarantee component-exact tick values.
  • Complex-valued images error with guidance (the Plots recipe rendered a magnitude/phase dual layout. An ImPlotView variant could restore this).
  • maxpixels/restrict downsampling not ported (Makie backends handle large images well).
  • Aside: the old polquiver called imresize without any package providing it (broken on main). The Makie port uses its own block-averaging.
  • FITSWCS (known, predates this PR): wcslib translates pre-WCS DSS plate-solution headers (PLT*/PPO*/AMDX/Y*) into a standard FK5 solution, which FITSWCS does not implement, so the photometry guide's M67 DSS cutout shows pixel axes where the pre-migration docs showed RA/Dec. A visible figure difference vs. stable, not a plotting change in this PR.

icweaver and others added 7 commits July 11, 2026 01:01
…id.jl

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Requires Makie 0.25 (compute-graph recipes; unreleased).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@icweaver icweaver changed the title Makie feat! Use Makie.jl as plotting backend Jul 11, 2026
@icweaver icweaver linked an issue Jul 11, 2026 that may be closed by this pull request
icweaver and others added 5 commits July 11, 2026 16:15
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ngle

Requires AstroAngles 0.3 (JuliaAstro/AstroAngles.jl#33).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@icweaver icweaver linked an issue Jul 12, 2026 that may be closed by this pull request
icweaver and others added 5 commits July 12, 2026 11:56
Ticks on exact component multiples (e.g. 3°20' steps) are decomposed
by integer arithmetic, fixing truncation (-46.67° shown as -46°) and
deg2dmsmu float fuzz ((-46, 39, 59, 999, 999.99...)). Labels now
display down to the deepest nonzero component.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Tick registration compared gridline/edge intersections with exact
  float ==, so separable frames (e.g. longitude/velocity slices)
  registered no ticks; now tolerance-based.
- world_to_pixel(parent=true) fills frozen world axes with a constant
  from pixel (1,1,...), drifting off the slice plane on curved
  transforms; the grid layout now refines the inverse onto the exact
  frozen-pixel plane by fixed-point iteration.
- WCSGrid(img) had x/y extents transposed (invisible on square images).
- wcsticks now maps plotted axis -> WCS axis for sliced cubes.
- DataAspect is only applied when both plotted axes are angular.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CUNIT is optional and the WCS standard makes `deg` the default unit of a
celestial axis, so an absent CUNIT means degrees, not "unknown".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Constrain the layout cell instead of the axis box, so the colorbar sits
flush against the image, and size the owned figure onto the result.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Base automatically changed from wcs-slicing to main July 13, 2026 03:50
@codecov

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.73134% with 42 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.00%. Comparing base (538c1fb) to head (0fe58ee).

Files with missing lines Patch % Lines
src/wcs-grid.jl 89.33% 37 Missing ⚠️
ext/AstroImagesMakieExt.jl 98.72% 4 Missing ⚠️
src/imview.jl 88.88% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##             v0.6     #119       +/-   ##
===========================================
+ Coverage   61.95%   80.00%   +18.04%     
===========================================
  Files           9       10        +1     
  Lines        1062     1200      +138     
===========================================
+ Hits          658      960      +302     
+ Misses        404      240      -164     

☔ View full report in Codecov by Harness.
📢 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.

@icweaver icweaver changed the title feat! Use Makie.jl as plotting backend feat!: Use Makie.jl as plotting backend Jul 19, 2026
@sefffal

sefffal commented Jul 21, 2026

Copy link
Copy Markdown
Member

@icweaver this is amazing! I'm so excited to use this!

@icweaver

Copy link
Copy Markdown
Member Author

Thanks @sefffal! This draft is still super messy, but we have some tentative usage examples here (repo: https://github.com/JuliaAstro/learn-JuliaAstro) that we'd love to get your input on. Happy to make any changes!

@icweaver
icweaver marked this pull request as ready for review July 24, 2026 23:13
@icweaver
icweaver changed the base branch from main to v0.6 July 29, 2026 20:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Plot extensions implot ignoring gridcolor argument if cmap is defined

2 participants