2023-08-22__minimal_diy_ijulia_matplotlib

2023-08-22__minimal_diy_ijulia_matplotlib

Both PyPlot.jl and PythonPlot.jl workflows were too slow,
with our Sciplotlib.jl code.

So we’ll have all our custom plot code in .py files.\

And directly call that, via PythonCall.jl

Hopefully this is faster.

We’ll have to extract some functionality from PythonPlot.jl, for the figures to show up in an IJulia notebook.

(We don’t need to bother with any GUI – just agg i.e. static, png img display)

1
1
ENV["JULIA_CONDAPKG_BACKEND"] = "Null"
ENV["JULIA_PYTHONCALL_EXE"] = "C:\\Users\\tfiers\\mambaforge\\python";
@time using PythonCall
  2.046666 seconds (2.20 M allocations: 141.940 MiB, 3.40% gc time)
@time mpl = pyimport("matplotlib")
  1.174725 seconds (2 allocations: 80 bytes)
Python: <module 'matplotlib' from 'C:\\Users\\tfiers\\mambaforge\\Lib\\site-packages\\matplotlib\\__init__.py'>
@time plt = pyimport("matplotlib.pyplot")
  0.752527 seconds (3 allocations: 96 bytes)
Python: <module 'matplotlib.pyplot' from 'C:\\Users\\tfiers\\mambaforge\\Lib\\site-packages\\matplotlib\\pyplot.py'>
@time fig, ax = plt.subplots()
  0.550402 seconds (225.52 k allocations: 15.405 MiB)
Python: (<Figure size 640x480 with 1 Axes>, <Axes: >)
@time ax.plot([1,3,2])
  0.368862 seconds (554.24 k allocations: 36.645 MiB, 3.08% gc time)
Python: [<matplotlib.lines.Line2D object at 0x000001E04F66D050>]
fig
../_images/2023-08-22__minimal_diy_ijulia_matplotlib_9_0.svg

Huh! So we don’t even need any extra show/display logic, no struct Figure wrapper.
Works out of the box..
(if it’s last thing in cell ye).

display(fig)
1
../_images/2023-08-22__minimal_diy_ijulia_matplotlib_11_0.svg
1

Amazing. We indeed need no wrapper. Can just add to postexec hook ye.

(It’s indeed what a default python notebook does. No display calls or ending-cell-with-fig necessary).

Now, for our custom .py code..

# pyimport("./lib/plot.py")

That no work. So we used a global conda env (env vars above), and manually pip install -e’d our plotlib into it.

@time plotlib = pyimport("plotlib")
  0.000043 seconds (2 allocations: 32 bytes)
Python: <module 'plotlib' from 'c:\\users\\tfiers\\phd\\pkg\\plotlib\\src\\plotlib\\__init__.py'>
plotlib.plot([1,2,1])
Python: <Axes: >
Gcf = pyimport("matplotlib._pylab_helpers").Gcf

function display_figs()
    for manager in Gcf.get_all_fig_managers()
        fig = manager.canvas.figure
        if !isempty(fig.get_axes())
            display(fig)
        end
        plt.close(fig)
    end
end

function close_figs()
    for manager in Gcf.get_all_fig_managers()
        fig = manager.canvas.figure
        plt.close(fig)
    end
end

IJulia.push_postexecute_hook(display_figs)
IJulia.push_posterror_hook(close_figs);
plotlib.plot([1,2,1])
../_images/2023-08-22__minimal_diy_ijulia_matplotlib_20_0.svg
Python: <Axes: >

😁😁😁😁😁😁😁😁


Deprecated (no wrapper needed):

Copying and simplifying from PythonPlot.jl:

@time Gcf = pyimport("matplotlib._pylab_helpers").Gcf
  0.000076 seconds (6 allocations: 152 bytes)
Python: <class 'matplotlib._pylab_helpers.Gcf'>
function display_figs()
    for manager in Gcf.get_all_fig_managers()
        f = manager.canvas.figure
        fig = Figure(f)
        isempty(fig) || display(fig)
        plt.close(f)
    end
end

function close_figs() # called after error in IJulia cell
    for manager in Gcf.get_all_fig_managers()
        f = manager.canvas.figure
        plt.close(f)
    end
end;

# The 'force new figs' thing seems to be kinda dead code? (Even in PyPlot.jl). So, not including.

Do we even need the ‘fig’ wrapper.
Yes, cause we don’t know what io is in f.canvas.print_figure(io, …).

mutable struct Figure
    o::Py
end

PythonCall.Py(f::Figure) = getfield(f, :o)
PythonCall.pyconvert(::Type{Figure}, o::Py) = Figure(o)
Base.:(==)(f::Figure, g::Figure) = pyconvert(Bool, Py(f) == Py(g))
Base.isequal(f::Figure, g::Figure) = isequal(Py(f), Py(g))
Base.hash(f::Figure, h::UInt) = hash(Py(f), h)
Base.Docs.doc(f::Figure) = Base.Docs.Text(pyconvert(String, Py(f).__doc__))

# Note: using `Union{Symbol,String}` produces ambiguity.
Base.getproperty(f::Figure, s::Symbol) = getproperty(Py(f), s)
Base.getproperty(f::Figure, s::AbstractString) = getproperty(f, Symbol(s))
Base.setproperty!(f::Figure, s::Symbol, x) = setproperty!(Py(f), s, x)
Base.setproperty!(f::Figure, s::AbstractString, x) = setproperty!(f, Symbol(s), x)
Base.hasproperty(f::Figure, s::Symbol) = pyhasattr(Py(f), s)
Base.propertynames(f::Figure) = propertynames(Py(f))

Base.isempty(f::Figure) = isempty(f.get_axes())

Base.show(io::IO, m::MIME"image/png", f::Figure) =
    f.canvas.print_figure(io, format="png", bbox_inches="tight")
    # We omit the `_showable(m, f)` check.

Base.showable(m::MIME"image/png", f::Figure) = !isempty(f)
# We omit the `&& haskey(PyDict{Any,Any}(f.canvas.get_supported_filetypes()), "png")` check.
typeof(fig)
Py
(fig, ax)
(<py Figure size 640x480 with 1 Axes>, <py Axes: >)
fig
../_images/2023-08-22__minimal_diy_ijulia_matplotlib_30_0.svg