MakieOrg / Makie.jl

Interactive data visualizations and plotting in Julia
https://docs.makie.org/stable
MIT License
2.4k stars 310 forks source link

[FR] subplotgrid function #1581

Open Datseris opened 2 years ago

Datseris commented 2 years ago

Would be nice to have something equivalent with PyPlot's subplots function:

fig, axs = subplotgrid((X, Y); sharex, sharey)

such a function would make an XxY grid layout, and optionaly share their x and/or y axis (also hiding ticks if axis are shared).

(I'll do this, assign me on GitHub please)

Datseris commented 2 years ago

@SimonDanisch I'm not making a PR yet because I just don't know where to put this function. But here is the code:

(EDIT: Code updated to be even better!)

using Makie
Text = Union{Symbol, <: AbstractString}

"""
    subplotgrid(m, n; kwargs...) -> fig, axs

Create a grid of `m` rows and `n` columns of axes in a new figure and return the figure and
the `Matrix` of axis.

## Keyword arguments

- `sharex/sharey = false`: make every row share the y axis and/or every row column
  share the x axis. In this case, tick labels are hidden from the shared axes.
- `titles::Vector{String}`: if given, they are used as titles
  for the axes of the top row.
- `xlabels::Vector{String}`: if given, they are used as x labels of the axes
  in the bottom row
- `ylabels::Vector{String}`: if given, they are used as y labels of the axes in the
  leftmost column
- `title::String`: if given, it is used as super-title for the entire figure
  using the `figuretitle!` function.
- `kwargs...`: all further keywords are propagated to `Figure`.
"""
function subplotgrid(m, n;
        sharex = false, sharey = false, titles = nothing,
        xlabels = nothing, ylabels = nothing, title = nothing, kwargs...
    )
    fig = Makie.Figure(; kwargs...)
    axs = Matrix{Axis}(undef, m, n)
    for i in 1:m
        for j in 1:n
            axs[i,j] = Axis(fig[i,j])
        end
    end
    if sharex
        for j in 1:n
            Makie.linkxaxes!(axs[:,j]...)
            for i in 1:m-1; Makie.hidexdecorations!(axs[i,j]; grid = false); end
        end
    end
    if sharey
        for i in 1:m # iterate through rows
            Makie.linkyaxes!(axs[i,:])
            for j in 2:n; Makie.hideydecorations!(axs[i,j]; grid = false); end
        end
    end
    if !isnothing(titles)
        for j in 1:n
            axs[1, j].title = titles[j]
        end
    end
    if !isnothing(xlabels)
        for j in 1:n
            axs[end, j].xlabel = xlabels isa Text ? xlabels : xlabels[j]
        end
    end
    if !isnothing(ylabels)
        for i in 1:m
            axs[i, 1].ylabel = ylabels isa Text ? ylabels : ylabels[i]
        end
    end
    !isnothing(title) && figuretitle!(fig, title)
    return fig, axs
end

"""
    figuretitle!(fig, title; kwargs...)

Add a title to a `Figure`, that looks the same as the title of an `Axis`
by using the same default font. `kwargs` are propagated to `Label`.
"""
function figuretitle!(fig, title;
        valign = :bottom, padding = (0, 0, 0, 0),
        font = "TeX Gyre Heros Bold", # same font as Axis titles
        kwargs...,
    )
    Label(fig[0, :], title;
        tellheight = true, tellwidth = false, valign, padding, font, kwargs...
    )
    return
end

feel free to give a review and/or tell me where to put it.

Datseris commented 2 years ago

image

Datseris commented 2 years ago

bump!

Datseris commented 2 years ago

I will also make an in-place version that takes an existing figure or GridLayout as an argument and adds the axis. Just tell me where to actually put it to make a PR :D

jkrumbiegel commented 2 years ago

Maybe this would be good for a Block, it could be called FacetGrid for example. That's because it could benefit from spanning labels on the axes and maybe boxes for the titles such as commonly found in ggplot.

Datseris commented 2 years ago

I am not aware of what a Block is (https://makie.juliaplots.org/stable/search/index.html?q=Block ). Is it internal?

jkrumbiegel commented 2 years ago

It's the new implementation of what Layoutable was, on master. A building block possibly made of other building blocks, that you can place in a grid as one rectangular object.

Datseris commented 2 years ago

Yes, I'll make it a block once I have the instructions of how to do so. Is it just a simple wrap of a GridLayout? In reality, the function I've coded above can just return a GridLayout.

jkrumbiegel commented 2 years ago

There's an initial implementation here https://github.com/JuliaPlots/Makie.jl/pull/1827

begin
    f = Figure()
    fg = MakieLayout.FacetGrid(f[1, 1], 3, 4, sharey = :all,
        rowlabels = ["Hi", "Whats", "Up"])
    fg.columnlabels = ["a", "b", "c", "d"]
    lines!(fg[1, 2], cumsum(randn(50)))
    lines!(fg[3, 4], cumsum(randn(50)))
    Label(f[0, :], "A FacetGrid demonstration", tellwidth = false)
    f
end

grafik

I'm not sure yet if I like making an object out of this or if it should just make a bunch of axes wherever and return those. The options to style this are potentially infinite, and it's not going to be practical to implement them all with overlapping keywords etc. For example position of label boxes, if it's a full grid or if there are positions missing, etc. However, using a Block does make the thing somewhat nicely self contained.

Datseris commented 2 years ago

I really hope the Makie developers strongly consider adding the above subplotgrid function to the API and exporting it, because it has become rather tedius to copy paste it in 20 different science projects and then include the file with this definition in every one scripts in these projects. Given the prevalence of "facet/grid plots" in science, this function is likely to be widespread used if available.

jkrumbiegel commented 2 years ago

Yes, definitely. I need a bit more time to think about how exactly to integrate it, as this function is only one example of a multi-axis (or object) plotting recipe, for which we need better infrastructure anyway. So I don't want to include this as is only to come up with a unified way a bit later, leading to more breaking changes.

Datseris commented 1 year ago

I've updated my original post with an even better and more general version of the code, that I Am particularly prooud about! I use this code in literally every science project I have!

jkrumbiegel commented 1 year ago

Yeah I agree, let me try to prepare a PR soon, I have some small ideas about the API that I'd want to try. But it would be very useful to have