Skip to content

Refactor Axis to ComputeGraph#5546

Open
ffreyer wants to merge 53 commits intoff/breaking-0.25from
ff/compute-blocks
Open

Refactor Axis to ComputeGraph#5546
ffreyer wants to merge 53 commits intoff/breaking-0.25from
ff/compute-blocks

Conversation

@ffreyer
Copy link
Collaborator

@ffreyer ffreyer commented Mar 3, 2026

Description

Leftover Observables

Translates Axis to compute graph, including LineAxis. There are some exceptions that still use Observables to interface with other Observable structures:

  • Axis: layoutobservables.computedbbox and scene.viewport feed into the compute graph to calculate graph.scenearea with graph.finallimits and graph.aspect which then feeds back into scene.viewport
  • Axis: graph.layout_protrusion feeds into layoutobservables.protrusions
  • Axis: graph.xscale, graph.yscale indirectly update scene.transform_func (irrelevant)
  • Axis: graph.sharedxlimits and graph.sharedylimits each have an observable output to force them to evaluate whenever their inputs change. See (1)
  • Axis: graph.projectionmatrix feeds into scene.camera.projectionview etc which then feeds into scene.compute. We could probably update the Observable as a side effect of pulling camera matrices into backends without issues?
  • LineAxis: dim converts feed into the LineAxis subgraph (input only, irrelevant)
  • LineAxis: label boundingbox transitions to an Observable to translate the plot. I think we can avoid this without loops since the boundingbox used does not include text positions. But we don't have infrastructure to set this up yet. Might try...
  • LineAxis: ticklabel boundingboxes feed into an Observable to feed back into a graph input that's used beforehand. This might just be and ordering issue?

LineAxis

For the most part this is just a translation from Observables to compute graph. Some things got moved around, some split up, some defaults moved to generic_plot_attributes() which should all be irrelevant. In terms relevant changes:

  • calls to boundingbox() were replaced with raw_string_boundingboxes, which completely ignore projections, positions and offset. This should be fine since all these calls only care about widths(bb) anyway
  • a bunch of Observable fields got removed from LineAxis as those moved to compute nodes in ax.(x/y)axis.(...)

ComputeGraph

  • fixed an issue where graph.output -> Observable -> ... -> graph.input -> ... graph.output loops could discard other Observable updates
  • added force_update field to Input to allow propagation of equal values (2)
  • added ExplicitUpdate wrapper to control the propagation of an update from the outside. Updates can be forced or denied this way (or use the default rules)
  • added set_type!() for initializing just the type of a compute node (similar to unsafe_init!()

Axis Limits

Axis limits were difficult to translate because there are a lot of different things that need to work:

  • autolimits!() requires (nothing, nothing) to propagate up to the point where limits are determined from the plots in the Axis. This requires at least force_update (2), even if we kept using Observables immediately after ax.limits.
  • ticks in LineAxis require finallimits to update no later than xscale or yscale to avoid applying e.g. log10 to limits outside the domain of log10. Since Observables pulled from a compute graph only update after all edges evaluate, i.e. after ticks are calculated with the updated (x/y)scale, the whole path from (x/y)scale to finallimits must be compute nodes.
  • interactions (pan, zoom, rectangle select) must not apply to ax.limits but something later down the line to preserve user set limits for reset_limits!() and the LimitReset interaction
  • xlims!() and ylims!() must be to update one dimension of ax.limits without affecting any limits in the other dimension. This means they need to be able to deny an update in one dimension, which I added ExplicitUpdate (3) for.
  • Axis linking requires changes from one Axis to be communicated to all linked axes, per dimension. Since multiple linked axes could update before the backend pulls in updates, this must happen immediately. (More specifically, linked axes must get notified of limit changes before they start resolving their own limit changes.)

The way I set things up is like this:

  • ax.(x/y)scale compute transform_func which sets scene.transformations.transform_func as a side effect
  • ax.limits is marked to always propagate
  • ax.limits splits into localxlimits and localylimits together with transform_func, (x/y)autolimitmargin and _limit_update_rule. This step generates numeric limits based on the limits given to this axis, the bounding boxes of its plots and the defaultlimits() of the (x/y)scale (which is in transform_func). By default the resulting x and y limits will be wrapped in ExplicitUpdate(lims, :force) so they always make it to the next step. If xlims!() or ylims!() are called _update_limit_rule is updated alongside the limits, which will cause one of the limits to not propagate.
  • local(x/y)limits feeds into shared(x/y)limits. This computation validates the limits and sets the shared(x/y)limits of every linked axis. This node is also backed by a ComputePipeline.get_observable!() to force the linking code to run asap. Note that this node can desync with local(x/y)limits through updates of a linked axis. That's why local limits need to always update. Also note that updating local limits through linking would cause an update loop/recursion.
  • shared(x/y)limits recombine into targetlimits
  • targetlimits map to finallimits via adjustlimits() like before, including viewport, autolimitaspect and (x/y)autolimitmargin
  • finallimits map to projectionmatrix like before

Interactions must now read from shared(x/y)limits or later (e.g. targetlimits is also fine), and input into local(x/y)limits to work correctly with linked axes. The former is basically "limits with linking" and the latter is the input that gets communicated to linked axes. Note that this is affects everything that previously set targetlimits.

Other Changes

  • fixed automatic not being considered in get_label_suffix for DynamicQuantities
  • fixed GLMakie being reliant on projectionview updates to rerender when plots are added
  • changed SpecApi axis linking to use the union of Axis limits as its initial state
  • removed targetlimits and finallimits as fields from Axis. This can still be accessed as ax.targetlimits/finallimits but now return compute nodes
  • removed block_limit_linking from Axis as it's not needed
  • reworked autolimit calculation so that plot boundingboxes run once for both dimensions instead of per dimension
  • added early exit condition to mark_dirty!() which requires mark_resolved!() to resolve all parent edges

TODO:

  • consider giving LineAxis a separate ComputeGraph instead of a nested one
  • move limits to compute
  • check the remaining functions of LineAxis
  • finish cleanup of LineAxis type
  • Reorder LineAxis code to avoid intermediate Observable
  • check for issues with PolarAxis, Axis3 that require moving more things to computations
  • add/update notes on the changes made to this pr
  • test ExplicitUpdate and input.force_update
  • document ExplicitUpdate and input.force_update
  • add tests for issues around cyclic compute graph observables
  • maybe add dev docs on limits

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

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 Mar 3, 2026
@MakieBot
Copy link
Collaborator

MakieBot commented Mar 3, 2026

Benchmark Results

SHA: 2efd32737c3deee42303ac204b07ae1687b2a6fe

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 changed the title Refactor Axis (and maybe more) to ComputeGraph Refactor Axis to ComputeGraph Mar 6, 2026
@ffreyer ffreyer marked this pull request as ready for review March 9, 2026 18:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Work in progress

Development

Successfully merging this pull request may close these issues.

2 participants