GLMakie saving Axis

I have a complex figure with several Axis and I want to save an image of just one Axis. One of the Axis is named SpreadGrid_ax and I have tried…

FileIO.save("SpreadGrid_ax.png", SpreadGrid_ax.scene; size = size(SpreadGrid_ax.scene), px_per_unit = 1.0)

however, the image file this creates contains the entire figure with all Axis rendered. Is it possible to specify just one axis?

Thanks
Steve

This seems to work:

f = Figure()
for i in 1:3, j in 1:3
    Axis(f[i, j], title = "$i $j")
end

function outer_bbox(ax; padding)
    sbb = ax.layoutobservables.suggestedbbox[]
    prot = ax.layoutobservables.protrusions[]
    o = sbb.origin .- (prot.left, prot.bottom) .- padding
    w = sbb.widths .+ (prot.left + prot.right, prot.bottom + prot.top) .+ 2 * padding
    Rect2f(o, w)
end

Makie.update_state_before_display!(f)

function cropped_colorbuffer(fig, bb; kwargs...)
    full_bb = fig.scene.viewport[]

    mi = bb.origin
    ma = mi .+ bb.widths
    f_mi = (mi .- full_bb.origin) ./ full_bb.widths
    f_ma = (ma .- full_bb.origin) ./ full_bb.widths

    cb = Makie.colorbuffer(fig; kwargs...)
    px_mi = round.(Int, reverse(size(cb)) .* f_mi)
    px_ma = round.(Int, reverse(size(cb)) .* f_ma)
    px_h = size(cb, 1)
    cb[(px_h - px_ma[2]):(px_h - px_mi[2]), px_mi[1]:px_ma[1]]
end

Makie.save("test.png", cropped_colorbuffer(f, outer_bbox(content(f[2, 2]), padding = 10); px_per_unit = 4))

Thanks Jules, that worked.

When I call Makie.save using…

Makie.save("test.png", cropped_colorbuffer(fig,outer_bbox(content(fig[1:2,3]), padding = 10); px_per_unit = 2))

the image on the screen deteriorates. Its readable but only just. Is there a way to avoid this?

and, I want to save as an svg file so I can print these at good quality. So I added CairoMakie to the project and tried…

Makie.save("test.svg", cropped_colorbuffer(fig, backend=CairoMakie,outer_bbox(content(fig[1:2,3]), padding = 10); px_per_unit = 2))

but it seems that using GLMakie and CarioMakie at the same time causes issues, my plots don’t show and a file display.png opens. Is there a way to have an application based on GLMakie create svg files?

Hm the quality deteriorating is probably because the screen can only have one px_per_unit at the time, although you set 2 so that should usually be sufficient unless you have a very unusual screen setup. Maybe there’s a bug there, though.

With CairoMakie it would still only make sense to save a png with this technique as colorbuffer makes a bitmap. If you had said you wanted an svg I wouldn’t have suggested that solution. I’ll try and see if there’s a similar workaround that makes sense for svg.

Oh, this should work but seems to be broken:

using GLMakie

f = Figure()
for i in 1:3, j in 1:3
    Axis(f[i, j], title="$i $j")
end
Makie.save("test.png", colorbuffer(content(f[2, 2])))

Really weird, we even have a reference image test for this, which is totally borked…
Not sure how this regression got merged without the tests failing - I’m guessing it must have been in a PR where we updated all reference images, since the visual diff must be big enough even for our old thresholds to go off.

Just to me clear I can generate the png and the image file looks ok on the screen but it’s very poor quality when I print it. I saw a reference suggesting that for printing I should use svg but I’m not wedded to any particular format.

This is an example of one of the Axis that I want to print on it’s own. The others are more conventional charts.

Here is a variant that saves svg. You can’t crop an svg by throwing away pixels, so I do it by editing the width, height and viewBox attributes accordingly.

using CairoMakie


f = Figure()
for i in 1:3, j in 1:3
    Axis(f[i, j], title = "$i $j")
end

function outer_bbox(ax; padding)
    sbb = ax.layoutobservables.suggestedbbox[]
    prot = ax.layoutobservables.protrusions[]
    o = sbb.origin .- (prot.left, prot.bottom) .- padding
    w = sbb.widths .+ (prot.left + prot.right, prot.bottom + prot.top) .+ 2 * padding
    Rect2f(o, w)
end

function save_cropped_svg(file, scene, bbox)
    sw, sh = scene.viewport[].widths
    ox, oy = bbox.origin
    w, h = bbox.widths
    svg = repr(MIME"image/svg+xml"(), scene)
    svg = replace(
        svg,
        r"viewBox=\".*?\"" => "viewBox=\"$ox $(sh - oy - h) $w $h\"",
        r"width=\".*?\"" => "width=\"$w\"",
        r"height=\".*?\"" => "height=\"$h\"",
        count = 3,
    )
    open(file, "w") do io
        write(io, svg)
    end
    return
end

ax = current_axis()

save_cropped_svg("test.svg", ax.blockscene, outer_bbox(ax; padding = 10))

Thanks Jules, any idea what I might be doing wrong? I have updated all packages and that does not resolve this issue.

When I use GLMakie I get the following error, when I use GLMakie and CairoMakie my app won’t run, I get no errors but a blank display.png file opens.

MethodError: no method matching backend_show(::GLMakie.Screen{GLFW.Window}, ::IOBuffer, ::MIME{Symbol("image/svg+xml")}, ::Scene)
The function `backend_show` exists, but no method is defined for this 
combination of argument types.

Closest candidates are:
  backend_show(::MakieScreen, ::IO, ::Union{MIME{Symbol("text/html")}, MIME{Symbol("application/vnd.webio.application+html")}, MIME{Symbol("application/prs.juno.plotpane+html")}, MIME{Symbol("juliavscode/html")}}, ::Scene)
   @ Makie C:\Users\steve\.julia\packages\Makie\pFPBw\src\display.jl:497
  backend_show(::MakieScreen, ::IO, ::MIME{Symbol("image/jpeg")}, ::Scene)
   @ Makie C:\Users\steve\.julia\packages\Makie\pFPBw\src\display.jl:491
  backend_show(::MakieScreen, ::IO, ::MIME{Symbol("image/png")}, ::Scene)
   @ Makie C:\Users\steve\.julia\packages\Makie\pFPBw\src\display.jl:485
  ...

Stacktrace:
  [1] show(io::IOBuffer, m::MIME{Symbol("image/svg+xml")}, figlike::Scene)
    @ Makie C:\Users\steve\.julia\packages\Makie\pFPBw\src\display.jl:259
  [2] __binrepr(m::MIME{Symbol("image/svg+xml")}, x::Scene, context::Nothing)
    @ Base.Multimedia .\multimedia.jl:171
  [3] _textrepr(m::MIME{Symbol("image/svg+xml")}, x::Scene, context::Nothing)
    @ Base.Multimedia .\multimedia.jl:163
  [4] repr(m::MIME{Symbol("image/svg+xml")}, x::Scene; context::Nothing)
    @ Base.Multimedia .\multimedia.jl:159
  [5] save_cropped_svg(file::String, scene::Scene, bbox::GeometryBasics.HyperRectangle{2, Float32})
    @ Main c:\Source\Julia\shares\julia_helpers.jl:39
  [6] (::var"#113#115"{Main.mPortfolio.AllPortfolios, String, Date, Int64, Int64})(click::Int64)
    @ Main c:\Source\Julia\shares\shares_events.jl:245```

GLMakie doesn’t do svg, so you have to use CairoMakie. And repr doesn’t have a way to specify a different backend to use. So one has to take a slight detour through save:

using CairoMakie
using GLMakie
GLMakie.activate!()

f = Figure()
for i in 1:3, j in 1:3
    Axis(f[i, j], title = "$i $j")
end
display(f)

function outer_bbox(ax; padding)
    sbb = ax.layoutobservables.suggestedbbox[]
    prot = ax.layoutobservables.protrusions[]
    o = sbb.origin .- (prot.left, prot.bottom) .- padding
    w = sbb.widths .+ (prot.left + prot.right, prot.bottom + prot.top) .+ 2 * padding
    Rect2f(o, w)
end

function save_cropped_svg(file, scene, bbox)
    sw, sh = scene.viewport[].widths
    ox, oy = bbox.origin
    w, h = bbox.widths
    svg = mktempdir() do dir
        save(joinpath(dir, "output.svg"), scene; backend = CairoMakie)
        read(joinpath(dir, "output.svg"), String)
    end
    svg = replace(
        svg,
        r"viewBox=\".*?\"" => "viewBox=\"$ox $(sh - oy - h) $w $h\"",
        r"width=\".*?\"" => "width=\"$w\"",
        r"height=\".*?\"" => "height=\"$h\"",
        count = 3,
    )
    open(file, "w") do io
        write(io, svg)
    end
    return
end

ax = current_axis()

save_cropped_svg("test.svg", ax.blockscene, outer_bbox(ax; padding = 10))
1 Like

Thanks Jules, that’s a perfect solution. and thanks to you and the whole team for such an amazing package.

Steve

2 Likes