What is the difference btween handling f[i,j]=GridLayout() and g=f[i,j]=GridLayout()

I am trying to plot a 2x2 figure as shown below.


So I

import CairoMakie
for ir in 1:2, ic in 1:2
    g = f[ir,ic] = GridLayout()
    # handling g below
end

But if I try the following code:

 for ir in 1:2, ic in 1:2
     f[ir,ic] = GridLayout()
     # handling f[ir,ic] directly below
end

By handing I mean codes like:

Axis(g[1,1]);
colsize!(g, 1, Relative(2/3))
colgap!(g, 0)
Label(g[1, 1:2, Top()], gridtitles[ir][ic], textsize = 26)

, or

Axis(f[ir,ic][1,1]);
colsize!(f[ir,ic], 1, Relative(2/3))
colgap!(f[ir,ic], 0)
Label(f[ir,ic][1, 1:2, Top()], gridtitles[ir][ic], textsize = 26)

My question is, when handled by g everything works fine; but not by f[ir,ic]. Here is the error info:

ERROR: LoadError: MethodError: no method matching colsize!(::GridPosition, ::Int64, ::Relative)
Closest candidates are:
  colsize!(::GridLayout, ::Int64, ::GridLayoutBase.ContentSize) at /home/dongjx/.julia/packages/GridLayoutBase/nYdeK/src/gridlayout.jl:552
  colsize!(::GridLayout, ::Int64, ::Real) at /home/dongjx/.julia/packages/GridLayoutBase/nYdeK/src/gridlayout.jl:560

This is quite weird to me, because in my view g = f[ir,ic] = GridLayout() is nothing but renaming f[ir,ic] as g, and I should handle them the same.
Why is that?

1 Like

It’s explained here

http://makie.juliaplots.org/stable/documentation/figure/#retrieving_objects_from_a_figure

Thanks for your reply! I have read the documentation and some related material, but still kind of confused about GridLayout and GridPosition. The following are my naive understanding of them.

The GridLayout is the container of GridLayout and layoutables, just as the role played by directories, to contain directories and files. In this way, we build a tree structure to contain a bunch of elements.
The GridPostion is a label on GridLayout indicating its postion in a figure.

I draw the testing figure:

And the structure of GridLayout of f is:

infil> f.layout
GridLayout[2, 2] with 3 children
 ┣━ [1, 1] Axis
 ┣━ [1, 2] GridLayout[2, 2] with 3 children
 ┃ ┣━ [1, 1] Colorbar
 ┃ ┣━ [2, 1] Axis
 ┃ ┗━ [1:2, 2] Axis
 ┃
 ┗━ [2, 1:2] Axis

The brakets leading each line are the corresponding GridPositions.
When we using functions like colsize! and colgap, we need to indicate which GridLayout we are manipulating.
My questions are:

  1. why we bother introduce GridSubposition? As in my shollow understanding, GridPosition can do all the job.
  2. how can we refer to the GridLayout living in f[1,2]. I have tried something like below but failed(my initial question of this topic). Must I do something like g = f[1,2] = GridLayout() and refer to it by g? Because in my testing example GridLayout is built implicitly, I think this question is quite natural.
    infil> f[1,2].layout
    GridLayout[2, 2] with 3 children
    ┣━ [1, 1] Axis
    ┣━ [1, 2] GridLayout[2, 2] with 3 children
    ┃ ┣━ [1, 1] Colorbar
    ┃ ┣━ [2, 1] Axis
    ┃ ┗━ [1:2, 2] Axis
    ┃
    ┗━ [2, 1:2] Axis
    
  3. As the codes in 2 shows, the returning of f[1,2].layout is exatly f.layout. Does this make any sense?

A GridPosition stores a position in a specific GridLayout. gp.layout returns that layout. A GridSubposition refers to a position in a theoretical GridLayout that could exist in a GridSubposition or GridPosition. When an object is instantiated at a GridSubposition, possibly missing parent layouts are created implicitly. You can get a layout at a certain GridPosition with content(gp), this can retrieve any layoutable object that can be in a certain GridPosition.

There’s also the internal GridLayoutBase function get_layout_at! which handles the implicit GridLayout creation.

Ok, let me ask a simple a question.

Figure and GridPosition both has a field name :layout, so I guess they should contain the same kind of infomation.

julia> fieldnames(Figure)
(:scene, :layout, :content, :attributes, :current_axis)

julia> fieldnames(GridPosition)
(:layout, :span, :side)

Let’s make a simple figure:

f = Figure()
Axis(f[1,1])
Axis(f[1,2][1,1])

The layout of f makes perfect sense:

julia> f.layout
GridLayout[1, 2] with 2 children
 ┣━ [1, 1] Axis
 ┗━ [1, 2] GridLayout[1, 1] with 1 children
   ┗━ [1, 1] Axis

, while layout of f[1,2] doesn’t:

julia> f[1,2].layout
GridLayout[1, 2] with 2 children
 ┣━ [1, 1] Axis
 ┗━ [1, 2] GridLayout[1, 1] with 1 children
   ┗━ [1, 1] Axis

As shown in f.layout, f[1,2] is the GridPostion for the GridLayout[1, 1] with 1 children. But we cannot work with f[1,2] except when using it to build layoutables. Anyway, f.layout and f[1,2].layout return exactly the samething, which doesn’t make sense to me.

As I said, the layout field of the GridPosition refers to the parent layout, which is the same as the top layout of the figure. If there is a GridLayout at that position (that’s not a given that there is one, generally speaking) you can access it via content(gridposition) which looks up the object stored at that location.

Also, the struct field names (like usual in Julia) are not really part of the public api, they just happen to be useful sometimes so you read about them here and there.

But I cannot pass it to functions like colsize!, colgap! which takes a GridLayout. So I have to use this g = f[1,2] = GridLayout() statement, which is kind of tedious to me. I undertant that to make things easy f[1,2] always return a GridPosition; to get the contents in a gridposition we can use content(gp)/contents(gp); but why dont we just return the GridLayout in place by f[1,2].layout so that we can pass it directly as the layout to spare another annotation?