JuliaHealth / KomaMRI.jl

Koma is a Pulseq-compatible framework to efficiently simulate Magnetic Resonance Imaging (MRI) acquisitions. The main focus of this package is to simulate general scenarios that could arise in pulse sequence development.
https://JuliaHealth.github.io/KomaMRI.jl
MIT License
118 stars 21 forks source link

Subplots from plot_image results produce unexpected aspect ratio changes #230

Open gsahonero opened 1 year ago

gsahonero commented 1 year ago

Hi,

When using the SyncPlots that plot_image returns to plot several images in a subplot manner, the aspect ratio of the last images is not respected. Is there a better way to make subplots using plot_image results?

This code should reproduce the issue, where only the first plot_image works as expected (see issue_1_1.png and issue_1_2.png, both generated by changing the size of the plotting window in VS Code and only the first image respecting the required aspect ratio), and the others do not. It also shows that the problem is similar to when heatmap is used (see issue_1_3.png, where all the plots change their aspect ratio).

using KomaMRI
using PlotlyJS

slice_abs_1 = zeros(64,64)

max_scale = findmax([findmax(slice_abs_1)])[1][1]
firstPlotTrace = plot_image(slice_abs_1, title="An image", zmin=0, zmax=max_scale);
secondPlotTrace = plot_image(slice_abs_1, title="An image", zmin=0, zmax=max_scale);
thirdPlotTrace = plot_image(slice_abs_1, title="An image", zmin=0, zmax=max_scale);
fourthPlotTrace = plot_image(slice_abs_1, title="An image", zmin=0, zmax=max_scale);
plotData = [firstPlotTrace secondPlotTrace; thirdPlotTrace fourthPlotTrace];
PlotlyJS.relayout!(plotData, title_text="Comparison")
display(plotData)

fig1 = make_subplots(rows=2, cols=2)
add_trace!(fig1, heatmap(z=slice_abs_1,transpose=false,zmin=0,zmax=max_scale,colorscale="Greys", aspect_ratio=:equal), row=1, col=1)
add_trace!(fig1, heatmap(z=slice_abs_1,transpose=false,zmin=0,zmax=max_scale,colorscale="Greys", aspect_ratio=:equal), row=1, col=2)
add_trace!(fig1, heatmap(z=slice_abs_1,transpose=false,zmin=0,zmax=max_scale,colorscale="Greys", aspect_ratio=:equal), row=2, col=1)
add_trace!(fig1, heatmap(z=slice_abs_1,transpose=false,zmin=0,zmax=max_scale,colorscale="Greys", aspect_ratio=:equal), row=2, col=2)
display(fig1)

issue_1_1.png: issue_1_1

issue_1_2.png: issue_1_2

issue_1_3.png: issue_1_3

For context, I ran into this problem when using Windows 11, Julia 1.9.3, KomaMRI 0.7.4, PlotlyJS 0.18.10, and VS Code 1.84.2 (the code is executed using the terminal)

cncastillo commented 1 year ago

Hi, thanks for looking into this. I modified the code to have a minimal working example to reproduce the issue.

I think this is a problem with PlotlyJS and how it deals with subplots. Apparently it does not preserves the layouts, as the subplot itself is a layout (i think). One quick solution would be to do something like this:

for k = 2:4
    plotData.plot.layout[Symbol("yaxis$k")][:scaleanchor] = "x$k"
    plotData.plot.layout[Symbol("yaxis$k")][:scaleratio] = 1
end

Not sure if there is something we can do to preserve the layouts when using subplots @beorostica ?

I believe this could be affecting other types of plots.

cncastillo commented 1 year ago

It is weird because it is preserving some information about the layout (?)

using KomaMRI
using PlotlyJS

slice_abs_1 = zeros(64,64)

max_scale = findmax([findmax(slice_abs_1)])[1][1]
firstPlotTrace = plot_image(slice_abs_1, title="darkmode=true", zmin=0, zmax=max_scale, darkmode=true);
secondPlotTrace = plot_image(slice_abs_1, title="darkmode=true", zmin=0, zmax=max_scale, darkmode=true);
thirdPlotTrace = plot_image(slice_abs_1, title="darkmode=false", zmin=0, zmax=max_scale, darkmode=false);
fourthPlotTrace = plot_image(slice_abs_1, title="darkmode=false", zmin=0, zmax=max_scale, darkmode=false);
plotData = [firstPlotTrace secondPlotTrace; thirdPlotTrace fourthPlotTrace];
for k = 2:4
    plotData.plot.layout[Symbol("yaxis$k")][:scaleanchor] = "x$k"
    plotData.plot.layout[Symbol("yaxis$k")][:scaleratio] = 1
end
display(plotData)

image

gsahonero commented 11 months ago

Hi,

In case it is useful, I ended up writing an "alternative" function using the plotly() backend for Plots instead of using several plot_image calls:

function multiple_plots(images, titles, main_title, aspect_ratio; plot_params = Dict("colorbar"=>"individual"))
    plotly()
    plots = []
    index = 1    
    if haskey(plot_params, "show_axis")
        show_axis = plot_params["show_axis"]
    else
        show_axis = false
    end
    if haskey(plot_params, "show_grid")
        show_grid = plot_params["show_grid"]
    else
        show_grid = false
    end

    if plot_params["colorbar"] == "global"
        clims = extrema(vcat(images...))
        if aspect_ratio != 1
            if haskey(plot_params, "force_aspect_ratio")
                if plot_params["force_aspect_ratio"] == true
                    println("Warning, forcing aspect ratio to be another value rather than 1.")
                else
                    println("Making aspect ratio equal to 1, use plot_params[\"force_aspect_ratio\"]=true to avoid this.")
                    aspect_ratio = 1
                end
            else
                println("Making aspect ratio equal to 1, use plot_params[\"force_aspect_ratio\"]=true to avoid this.")
                aspect_ratio = 1
            end
        end

        for image in images
            plot_i = Plots.heatmap(image', color=:grays, aspect_ratio=aspect_ratio, showaxis = show_axis, grid = show_grid, title = titles[index], plot_titlevspan = 0.1, top_margin=10*Plots.px, clims=clims, colorbar=false)
            push!(plots, plot_i)
            index = index + 1
        end
        trace = clims[1]:0.01:clims[2]

        display(Plots.plot(plots..., Plots.heatmap((trace).*ones(size(trace)[1],1), legend=:none, c=:grays,  xticks=:none, yticks=(1:size(trace)[1]/10:size(trace)[1], string.(round.(LinRange(clims[1], clims[2], 10), digits=1)))), layout=@layout[grid(2,2) a{0.03w}], plot_title=main_title))
    else
        for image in images
            plot_i = Plots.heatmap(image', color=:grays, aspect_ratio=aspect_ratio, showaxis = show_axis, grid = show_grid, title = titles[index], plot_titlevspan = 0.1, top_margin=10*Plots.px)
            push!(plots, plot_i)
            index = index + 1
        end

          display(Plots.plot(plots..., plot_title = main_title, plot_titlevspan=0.1))
    end

end

A simple usage would be just:

using Plots

slice_abs_1 = zeros(64,64)
slice_abs_2 = zeros(64,64)
slice_abs_3 = zeros(64,64)
slice_abs_4 = zeros(64,64)

multiple_plots([slice_abs_1, slice_abs_2, slice_abs_3, slice_abs_4], ["1", "2", "3", "4"], "Multiple plots", 1.13; plot_params=Dict("colorbar"=>"global", "force_aspect_ratio"=>true, "show_axis"=>true, "show_grid" => true))

The result:

issue_1_4

Some things I learned in the process and are unfortunately yet to be solved:

I guess that there should be another way to make this right, but so far I haven't dedicated too much time to it. Moreover, this function should work with any other "image" and it does not require Koma at all. But, that makes me wonder whether it would be a good idea to have a plot_images function in Koma and solve this issue programmatically. Or, to make plot_image compliant with aspect_ratio changes for multi-plot figures? Or, if the recommendation should be to manually adjust aspect_ratio while calling plot_image?

Anyways, hope it is useful for anybody.