Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

modernize API somewhat #2

Merged
merged 3 commits into from
Apr 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,20 @@ see the [matplotlib.pyplot documentation for more
information](http://matplotlib.org/api/pyplot_api.html). The Matplotlib
version number is returned by `PythonPlot.version`.

### Differences from PyPlot

Compared to the PyPlot package, there are a few differences in the API.

* To avoid type piracy, the functions `show`, `close`, `step`, and `fill` are renamed to `pltshow`, `pltclose`, `pltstep`, and `pltfill`, respectively. (You can also access them as `PythonPlot.show` etcetera.)
* The `matplotlibl.pyplot` module is exported as `pyplot` rather than as `plt`.
* The PythonCall package performs many fewer automatic conversions from Python types to Julia types (in comparison to PyCall). If you need to convert Matplotlib return values to native Julia objects, you'll need to do `using PythonCall` and call its `pyconvert(T, o)` or other conversion functions.

### Exported functions

Only the currently documented `matplotlib.pyplot` API is exported. To use
other functions in the module, you can also call `matplotlib.pyplot.foo(...)`
as `plt.foo(...)`. For example, `plt.plot(x, y)` also works. (And
the raw `Py` object for the `matplotlib` modules is also accessible
as `pyplot.foo(...)`. For example, `pyplot.plot(x, y)` also works. (And
the raw `Py` object for the `matplotlib` module itself is also accessible
as `PythonPlot.matplotlib`.)

Matplotlib is somewhat inconsistent about capitalization: it has
Expand All @@ -71,8 +79,8 @@ must be used to access `matplotlib.pyplot.xcorr`
etcetera.

If you wish to access *all* of the PyPlot functions exclusively
through `plt.somefunction(...)`, as is conventional in Python, you can
do `import PythonPlot as plt` instead of `using PythonPlot`.
through `pyplot.somefunction(...)`, as is conventional in Python, you can
do `import PythonPlot as pyplot` instead of `using PythonPlot`.

### Figure objects

Expand All @@ -96,7 +104,7 @@ function (`plot` etc.) is evaluated.
However, if you use PythonPlot from a Julia script that is run non-interactively
(e.g. `julia myscript.jl`), then Matplotlib is executed in
[non-interactive mode](http://matplotlib.org/faq/usage_faq.html#what-is-interactive-mode):
a plot window is not opened until you run `show()` (equivalent to `plt.show()`
a plot window is not opened until you run `pyshow()` (equivalent to `pyplot.show()`
in the Python examples).

## Interactive versus Julia graphics
Expand Down
72 changes: 38 additions & 34 deletions src/PythonPlot.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
"""
PythonPlot allows Julia to interface with the Matplotlib library in Python, specifically the matplotlib.pyplot module, so you can create beautiful plots in Julia with your favorite Python package.

Only the currently documented matplotlib.pyplot API is exported. To use other functions in the module, you can also call matplotlib.pyplot.foo(...) as plt.foo(...).
For example, plt.plot(x, y) also works. (And the raw Py object for the matplotlib modules is also accessible as PythonPlot.matplotlib.)

In general, all the arguments are the same as in Python.

Here's a brief demo of a simple plot in Julia:
Expand All @@ -19,8 +16,7 @@ For more information on API, see the matplotlib.pyplot documentation and the Pyt
module PythonPlot

using PythonCall
import Base: convert, ==, isequal, hash, getindex, setindex!, haskey, keys, show
export Figure, plt, matplotlib, pygui, withfig
export Figure, matplotlib, pyplot, pygui, withfig, pltshow, pltstep, pltclose

###########################################################################
# Define a documentation object
Expand All @@ -46,11 +42,11 @@ function Base.show(io::IO, ::MIME"text/plain", h::LazyHelp)
print(io, "no Python docstring found for ", o)
end
end
Base.show(io::IO, h::LazyHelp) = show(io, "text/plain", h)
Base.show(io::IO, h::LazyHelp) = Base.show(io, "text/plain", h)
function Base.Docs.catdoc(hs::LazyHelp...)
Base.Docs.Text() do io
for h in hs
show(io, MIME"text/plain"(), h)
Base.show(io, MIME"text/plain"(), h)
end
end
end
Expand All @@ -68,10 +64,9 @@ mutable struct Figure
end
PythonCall.Py(f::Figure) = getfield(f, :o)
PythonCall.pyconvert(::Type{Figure}, o::Py) = Figure(o)
==(f::Figure, g::Figure) = Py(f) == Py(g)
==(f::Figure, g::Py) = Py(f) == g
==(f::Py, g::Figure) = f == Py(g)
hash(f::Figure) = hash(Py(f))
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)
PythonCall.pycall(f::Figure, args...; kws...) = pycall(Py(f), args...; kws...)
(f::Figure)(args...; kws...) = pycall(Py(f), PyAny, args...; kws...)
Base.Docs.doc(f::Figure) = Base.Docs.Text(pyconvert(String, Py(f).__doc__))
Expand All @@ -88,7 +83,7 @@ for (mime,fmt) in aggformats
@eval _showable(::MIME{Symbol($mime)}, f::Figure) = !isempty(f) && haskey(PyDict{Any,Any}(f.canvas.get_supported_filetypes()), $fmt)
@eval function Base.show(io::IO, m::MIME{Symbol($mime)}, f::Figure)
if !_showable(m, f)
throw(MethodError(show, (io, m, f)))
throw(MethodError(Base.show, (io, m, f)))
end
f.canvas.print_figure(io, format=$fmt, bbox_inches="tight")
end
Expand Down Expand Up @@ -127,7 +122,7 @@ function display_figs() # called after IJulia cell executes
if pyconvert(Int, f.number) ∉ withfig_fignums
fig = Figure(f)
isempty(fig) || display(fig)
plt.close(f)
pyplot.close(f)
end
end
end
Expand All @@ -138,7 +133,7 @@ function close_figs() # called after error in IJulia cell
for manager in Gcf.get_all_fig_managers()
f = manager.canvas.figure
if pyconvert(Int, f.number) ∉ withfig_fignums
plt.close(f)
pyplot.close(f)
end
end
end
Expand Down Expand Up @@ -168,47 +163,56 @@ end
###########################################################################

# export documented pyplot API (http://matplotlib.org/api/pyplot_api.html)
export acorr,annotate,arrow,autoscale,autumn,axhline,axhspan,axis,axline,axvline,axvspan,bar,barbs,barh,bone,box,boxplot,broken_barh,cla,clabel,clf,clim,cohere,colorbar,colors,contour,contourf,cool,copper,csd,delaxes,disconnect,draw,errorbar,eventplot,figaspect,figimage,figlegend,figtext,figure,fill_between,fill_betweenx,findobj,flag,gca,gcf,gci,get_current_fig_manager,get_figlabels,get_fignums,get_plot_commands,ginput,gray,grid,hexbin,hist2D,hlines,hold,hot,hsv,imread,imsave,imshow,ioff,ion,ishold,jet,legend,locator_params,loglog,margins,matshow,minorticks_off,minorticks_on,over,pause,pcolor,pcolormesh,pie,pink,plot,plot_date,plotfile,polar,prism,psd,quiver,quiverkey,rc,rc_context,rcdefaults,rgrids,savefig,sca,scatter,sci,semilogx,semilogy,set_cmap,setp,show,specgram,spectral,spring,spy,stackplot,stem,step,streamplot,subplot,subplot2grid,subplot_tool,subplots,subplots_adjust,summer,suptitle,table,text,thetagrids,tick_params,ticklabel_format,tight_layout,title,tricontour,tricontourf,tripcolor,triplot,twinx,twiny,vlines,waitforbuttonpress,winter,xkcd,xlabel,xlim,xscale,xticks,ylabel,ylim,yscale,yticks,hist
export acorr,annotate,arrow,autoscale,autumn,axhline,axhspan,axis,axline,axvline,axvspan,bar,barbs,barh,bone,box,boxplot,broken_barh,cla,clabel,clf,clim,cohere,colorbar,colors,contour,contourf,cool,copper,csd,delaxes,disconnect,draw,errorbar,eventplot,figaspect,figimage,figlegend,figtext,figure,fill_between,fill_betweenx,findobj,flag,gca,gcf,gci,get_current_fig_manager,get_figlabels,get_fignums,get_plot_commands,ginput,gray,grid,hexbin,hist2D,hlines,hold,hot,hsv,imread,imsave,imshow,ioff,ion,ishold,jet,legend,locator_params,loglog,margins,matshow,minorticks_off,minorticks_on,over,pause,pcolor,pcolormesh,pie,pink,plot,plot_date,plotfile,polar,prism,psd,quiver,quiverkey,rc,rc_context,rcdefaults,rgrids,savefig,sca,scatter,sci,semilogx,semilogy,set_cmap,setp,specgram,spectral,spring,spy,stackplot,stem,step,streamplot,subplot,subplot2grid,subplot_tool,subplots,subplots_adjust,summer,suptitle,table,text,thetagrids,tick_params,ticklabel_format,tight_layout,title,tricontour,tricontourf,tripcolor,triplot,twinx,twiny,vlines,waitforbuttonpress,winter,xkcd,xlabel,xlim,xscale,xticks,ylabel,ylim,yscale,yticks,hist

# The following pyplot functions must be handled specially since they
# overlap with standard Julia functions:
# close, fill, show, step
# … as in PyPlot.jl, we commit some type piracy here.
# … unlike PyPlot.jl, we'll avoid type piracy by renaming / not exporting.

const plt_funcs = (:acorr,:annotate,:arrow,:autoscale,:autumn,:axes,:axhline,:axhspan,:axis,:axline,:axvline,:axvspan,:bar,:barbs,:barh,:bone,:box,:boxplot,:broken_barh,:cla,:clabel,:clf,:clim,:cohere,:colorbar,:colors,:connect,:contour,:contourf,:cool,:copper,:csd,:delaxes,:disconnect,:draw,:errorbar,:eventplot,:figaspect,:figimage,:figlegend,:figtext,:fill_between,:fill_betweenx,:findobj,:flag,:gca,:gci,:get_current_fig_manager,:get_figlabels,:get_fignums,:get_plot_commands,:ginput,:gray,:grid,:hexbin,:hlines,:hold,:hot,:hsv,:imread,:imsave,:imshow,:ioff,:ion,:ishold,:jet,:legend,:locator_params,:loglog,:margins,:matshow,:minorticks_off,:minorticks_on,:over,:pause,:pcolor,:pcolormesh,:pie,:pink,:plot,:plot_date,:plotfile,:polar,:prism,:psd,:quiver,:quiverkey,:rc,:rc_context,:rcdefaults,:rgrids,:savefig,:sca,:scatter,:sci,:semilogx,:semilogy,:set_cmap,:setp,:specgram,:spectral,:spring,:spy,:stackplot,:stem,:streamplot,:subplot,:subplot2grid,:subplot_tool,:subplots,:subplots_adjust,:summer,:suptitle,:table,:text,:thetagrids,:tick_params,:ticklabel_format,:tight_layout,:title,:tricontour,:tricontourf,:tripcolor,:triplot,:twinx,:twiny,:vlines,:waitforbuttonpress,:winter,:xkcd,:xlabel,:xlim,:xscale,:xticks,:ylabel,:ylim,:yscale,:yticks,:hist,:xcorr,:isinteractive)

for f in plt_funcs
sf = string(f)
@eval @doc LazyHelp(plt,$sf) function $f(args...; kws...)
if !hasproperty(plt, $(QuoteNode(f)))
@eval @doc LazyHelp(pyplot,$sf) function $f(args...; kws...)
if !hasproperty(pyplot, $(QuoteNode(f)))
error("matplotlib ", version, " does not have pyplot.", $sf)
end
return pycall(plt.$f, args...; kws...)
return pycall(pyplot.$f, args...; kws...)
end
end

# type piracy as in PyPlot.jl:
@doc LazyHelp(plt,"step") Base.step(x, y; kws...) = pycall(plt.step, x, y; kws...)
# rename to avoid type piracy:
@doc LazyHelp(pyplot,"step") pltstep(x, y; kws...) = pycall(pyplot.step, x, y; kws...)

# type piracy as in PyPlot.jl:
Base.show(; kws...) = begin pycall(plt.show; kws...); nothing; end
# rename to avoid type piracy:
pltshow(; kws...) = begin pycall(pyplot.show; kws...); nothing; end

Base.close(f::Figure) = close(f.number)
Base.close(f::Figure) = pltclose(f)

# type piracy as in PyPlot.jl:
function Base.close(f::Integer)
# rename to avoid type piracy:
@doc LazyHelp(pyplot,"close") pltclose() = pyplot.close()
pltclose(f::Figure) = pyconvert(Int, pltclose(f.number))
function pltclose(f::Integer)
pop!(withfig_fignums, f, f)
plt.close(f)
pyplot.close(f)
end
Base.close(f::Union{AbstractString,Symbol}) = plt.close(f)
@doc LazyHelp(plt,"close") Base.close() = plt.close()
pltclose(f::AbstractString) = pyplot.close(f)

# type piracy as in PyPlot.jl:
@doc LazyHelp(plt,"fill") Base.fill(x::AbstractArray,y::AbstractArray, args...; kws...) =
pycall(plt."fill", PyAny, x, y, args...; kws...)
# rename to avoid type piracy:
@doc LazyHelp(pyplot,"fill") pltfill(x::AbstractArray,y::AbstractArray, args...; kws...) =
pycall(pyplot.fill, PyAny, x, y, args...; kws...)

# consistent capitalization with mplot3d
@doc LazyHelp(plt,"hist2d") hist2D(args...; kws...) = pycall(plt.hist2d, args...; kws...)
@doc LazyHelp(pyplot,"hist2d") hist2D(args...; kws...) = pycall(pyplot.hist2d, args...; kws...)

# allow them to be accessed via their original names foo
# as PythonPlot.foo … this also means that we must be careful
# to use them as Base.foo in this module as needed!
const close = pltclose
const fill = pltfill
const show = pltshow
const step = pltstep

include("colormaps.jl")

Expand Down Expand Up @@ -287,7 +291,7 @@ function withfig(actions::Function, f::Figure; clear=true)
ax_save = gca()
push!(withfig_fignums, f.number)
figure(f.number)
finalizer(close, f)
finalizer(pltclose, f)
try
if clear && !isempty(f)
clf()
Expand Down
11 changes: 5 additions & 6 deletions src/colormaps.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@ end

PythonCall.Py(c::ColorMap) = getfield(c, :o)
PythonCall.pyconvert(::Type{ColorMap}, o::Py) = ColorMap(o)
==(c::ColorMap, g::ColorMap) = Py(c) == Py(g)
==(c::Py, g::ColorMap) = c == Py(g)
==(c::ColorMap, g::Py) = Py(c) == g
hash(c::ColorMap) = hash(Py(c))
Base.:(==)(c::ColorMap, g::ColorMap) = pyconvert(Bool, Py(c) == Py(g))
Base.isequal(c::ColorMap, g::ColorMap) = isequal(Py(c), Py(g))
Base.hash(c::ColorMap, h::UInt) = hash(Py(c), h)
PythonCall.pycall(c::ColorMap, args...; kws...) = pycall(Py(c), args...; kws...)
(c::ColorMap)(args...; kws...) = pycall(Py(c), args...; kws...)
Base.Docs.doc(c::ColorMap) = Base.Docs.Text(pyconvert(String, Py(c).__doc__))
Expand All @@ -29,7 +28,7 @@ Base.setproperty!(c::ColorMap, s::AbstractString, x) = setproperty!(Py(c), Symbo
Base.propertynames(c::ColorMap) = propertynames(Py(c))
Base.hasproperty(c::ColorMap, s::Union{Symbol,AbstractString}) = hasproperty(Py(c), s)

function show(io::IO, c::ColorMap)
function Base.show(io::IO, c::ColorMap)
print(io, "ColorMap \"$(pyconvert(String, c.name))\"")
end

Expand Down Expand Up @@ -187,7 +186,7 @@ function Base.show(io::IO, ::MIME"image/svg+xml", cs::AbstractVector{ColorMap})
end

function Base.show(io::IO, m::MIME"image/svg+xml", c::ColorMap)
show(io, m, [c])
Base.show(io, m, [c])
end

########################################################################
22 changes: 11 additions & 11 deletions src/init.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ using VersionParsing
# so that their type is known at compile-time.

const matplotlib = PythonCall.pynew()
const plt = PythonCall.pynew()
const pyplot = PythonCall.pynew()
const Gcf = PythonCall.pynew()
const orig_draw = PythonCall.pynew()
const orig_gcf = PythonCall.pynew()
Expand Down Expand Up @@ -158,12 +158,12 @@ function __init__()
global backend = backend_gui[1]
global gui = backend_gui[2]

PythonCall.pycopy!(plt, pyimport("matplotlib.pyplot")) # raw Python module
PythonCall.pycopy!(pyplot, pyimport("matplotlib.pyplot")) # raw Python module
PythonCall.pycopy!(Gcf, pyimport("matplotlib._pylab_helpers").Gcf)
PythonCall.pycopy!(orig_gcf, plt.gcf)
PythonCall.pycopy!(orig_figure, plt.figure)
plt.gcf = gcf
plt.figure = figure
PythonCall.pycopy!(orig_gcf, pyplot.gcf)
PythonCall.pycopy!(orig_figure, pyplot.figure)
pyplot.gcf = gcf
pyplot.figure = figure

if isdefined(Main, :IJulia) && Main.IJulia.inited
Main.IJulia.push_preexecute_hook(force_new_fig)
Expand All @@ -172,8 +172,8 @@ function __init__()
end

if isjulia_display[] && gui != :gr && backend != "Agg"
plt.switch_backend("Agg")
plt.ioff()
pyplot.switch_backend("Agg")
pyplot.ioff()
end

init_colormaps()
Expand All @@ -182,12 +182,12 @@ end
function pygui(b::Bool)
if !b != isjulia_display[]
if backend != "Agg"
plt.switch_backend(b ? backend : "Agg")
pyplot.switch_backend(b ? backend : "Agg")
if b
pygui_start(gui) # make sure event loop is started
Base.isinteractive() && plt.ion()
Base.isinteractive() && pyplot.ion()
else
plt.ioff()
pyplot.ioff()
end
elseif b
error("No working GUI backend found for matplotlib.")
Expand Down
4 changes: 2 additions & 2 deletions src/plot3d.jl
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ for f in mplot3d_funcs
fs = string(f)
@eval @doc LazyHelp(axes3D,"Axes3D", $fs) function $f(args...; kws...)
using3D() # make sure mplot3d is loaded
ax = version <= v"3.4" ? gca(projection="3d") : plt.subplot(projection="3d")
ax = version <= v"3.4" ? gca(projection="3d") : pyplot.subplot(projection="3d")
pycall(ax.$fs, args...; kws...)
end
end
Expand All @@ -62,7 +62,7 @@ for f in zlabel_funcs
fs = string("set_", f)
@eval @doc LazyHelp(axes3D,"Axes3D", $fs) function $f(args...; kws...)
using3D() # make sure mplot3d is loaded
ax = version <= v"3.4" ? gca(projection="3d") : plt.subplot(projection="3d")
ax = version <= v"3.4" ? gca(projection="3d") : pyplot.subplot(projection="3d")
pycall(ax.$fs, args...; kws...)
end
end
Expand Down