Skip to content

Comments

Blocks as complex recipes#5465

Draft
ffreyer wants to merge 80 commits intomasterfrom
ff/complex-blocks
Draft

Blocks as complex recipes#5465
ffreyer wants to merge 80 commits intomasterfrom
ff/complex-blocks

Conversation

@ffreyer
Copy link
Collaborator

@ffreyer ffreyer commented Dec 13, 2025

Description

This is an alternative to #5454 which extends @Block instead of @recipe. @jkrumbiegel suggested this because Blocks and complex recipes both fill the purpose of placing something in the layout.

Changes relevant to existing Blocks

  • all attributes are now in a ComputeGraph, rather than fields
  • block.layout currently always exists, but isn't always initialized

Problems for existing Blocks

  • moving to ComputeGraph broke notify(block.attribute) block.attr[] = block.attr[]
  • setfield!, hasfield, getfield for attributes is broken since attributes aren't fields anymore

Blocks as complex recipes compared to ComplexRecipe from #5454

  • uses DualView(f[1, 1], args...; attributes...) instead of dualview(f[1, 1], args...; attributes...)
    • currently can't be used without a GridPosition
  • uses initialize_block!(b::DualView, args...; kwargs...) instead of plot!(cr::DualView)
    • currently this means that arguments aren't automatically in the compute graph and no convert_arguments is used @Block now allows arguments and initialize_block will automatically apply argument conversions with initialize_block!(block; kwargs...) defined (instead of the args... version)
    • no arguments is allowed here, but not for ComplexRecipe
  • attributes work, but are in a @attributes begin ... end block
  • this returns a ::DualView instead of ::AbstractAxis, ::ComplexRecipe
  • things like Axis(b[1, 1]) and heatmap(b[1, 1], ...) work, including from outside the recipe
  • b.blocks works with the list generated on demand
  • b.plots works with the list generated at init time I decided to drop plots because it's rather confusing to have them with Axis
  • Block also allows adding fields, which may be useful?

Example

This is the example from #5454 rewritten with this pr:

Makie.@Block DualView (x, y) begin
    @attributes begin
        "Color for scatter plot"
        scatter_color = :blue
        "Color for line plot"
        line_color = :red
    end
end

function Makie.initialize_block!(b::DualView)
    scatter(b[1, 1], b.x, b.y; color=b.scatter_color)
    lines(b[1, 2], b.x, b.y; color=b.line_color)
    return b
end

fig, b = DualView(1:10, rand(10))
DualView(fig[2, 1], 1:10, rand(10))

TODO

  • watch half the refimages fail and fix them
  • add Block() constructor that generates a figure and places the block at fig[1, 1]
  • improve argument support, specifically automatic adding to the compute graph (not sure if convert_arguments is worth bothering with?)
  • consider adding more onany/map!/map methods for Computed inputs with N preceding Observable inputs. I'd be surprised if we don't run into issues with those... doesn't seem to be necessary
  • export @Block
  • figure out what to do with block.layout for traditional blocks
  • consider types defined for attributes again
  • fix wrong layouting in "basic shading"

Type of change

  • New feature (non-breaking change which adds functionality)
  • Breaking change due to ComputeGraph breaking some notify() calls and similarly obs[] = obs[], setfield/hasfield/getfield for attributes

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 Dec 13, 2025
@MakieBot
Copy link
Collaborator

MakieBot commented Dec 13, 2025

Benchmark Results

SHA: be101287e417eafeb69368562cb374b74f465303

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

@jkrumbiegel
Copy link
Member

jkrumbiegel commented Dec 14, 2025

I would probably make the Block infrastructure more like the plot infrastructure in this case, to make it behave more like the other recipe code does. Rename it even. Because when I wrote them first, I didn't consider blocks in a recursive way like they would work now, where a complex recipe by some user would use some predefined blocks (then complex recipes) internally. But I think the similarities to the complex recipe idea have shown that it should have worked more like that from the beginning. I just didn't see the similarity between a button, a slider an axis and something like a whole facet plot with a colorbar.

@ffreyer
Copy link
Collaborator Author

ffreyer commented Dec 14, 2025

I would probably make the Block infrastructure more like the plot infrastructure in this case, to make it behave more like the other recipe code does. Rename it even. Because when I wrote them first, I didn't consider blocks in a recursive way like they would work now, where a complex recipe by some user would use some predefined blocks (then complex recipes) internally.

What do you mean with that? Have block.blocks like we have plot.plots? Because I see lots of differences between them:

  • blocks allow fields to be added
  • plots have the whole argument conversion pipeline, while blocks just pass them along
  • plots must have arguments, Blocks can have none
  • plots don't allow kwargs that aren't attributes, block do
  • plots are just one parametric type, while each Block is its own type
  • plots are defined recursively in a tree like structure via plot.plots, while (complex recipe style) blocks currently connect through block.layout
  • plots have plot.transformations, Blocks don't
  • plots have plot.kw, Blocks don't
  • plots are always added with lower case functions, Blocks always uppercase

And what would you rename it to? I kind of like Block for its simplicity.

@ffreyer
Copy link
Collaborator Author

ffreyer commented Dec 14, 2025

Only textbox fails now, because the bottom one resets its alignment differently. Imo that's better, because as far as I can tell it reflects the alignment without any translation (like the top one)

Before After
Textbox Textbox

Comment on lines +240 to +249
f, b1 = Box(S.GridLayout())
b2 = Box(f[1, 2], S.GridLayout())
st = Makie.Stepper(f)
sync_step!(st)
b1[1] = S.GridLayout(
[
S.Axis(; plots = [S.Lines(1:4; color = :black, linewidth = 5), S.Scatter(1:4; markersize = 20)])
S.Axis3(; plots = [S.Scatter(Rect3f(Vec3f(0), Vec3f(1)); color = :red, markersize = 50)])
]
)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

BlockSpec and GridLayoutSpec can now be passed to a Block, either directly or through convert_arguments.

The way I set it up it's more or less irrelevant which block is used. If it doesn't implement an argument-catching initialize_block!(block, args...; kwargs...) and there is a BlockSpec or GridLayoutSpec present after convert_arguments it switches to SpecApi block construction. That then treats the block like a figure and defers to existing SpecApi code for GridLayoutSpec.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Seems weird to me that something like Box(S.GridLayout()) works but I think that's fine if it's just a background quirk.
If it's useful to have some container block to pass a SpecApi GridLayoutSpec or BlockSpec to, then I think we should make a dedicated one. I think it could also be useful in general to have an empty placeholder block.

@jkrumbiegel
Copy link
Member

The FigureAxis type should be FigureBlock I would say?

@jkrumbiegel
Copy link
Member

jkrumbiegel commented Dec 30, 2025

For your example with the DualView I get this with CairoMakie

grafik

So the layout is off but also the limits are not right. This must be because the update_state_before_display! step doesn't run for the nested blocks. Things look okay if I add

autolimits!.(dualview.blocks)

@ffreyer
Copy link
Collaborator Author

ffreyer commented Dec 30, 2025

Huh, still? I added a method to propagate update_state_before_display through nested blocks to fix this. Seems like that only works for DualView(figpos, ...) though?

@jkrumbiegel
Copy link
Member

The overload for gridlayout sides isn't implemented, yet:

Label(b[1, 1:2, Top()], "Hello", font = :bold)

ERROR: MethodError: no method matching getindex(::DualView, ::Int64, ::UnitRange{Int64}, ::Top)

@jkrumbiegel
Copy link
Member

Thanks, found another one:

Colorbar(b[end+1, :], vertical = false)

ERROR: MethodError: no method matching axes(::DualView, ::Int64)

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.

3 participants