Skip to content

Commit d47fa93

Browse files
committed
Support automatic flushing and closing figures in IRuby
1 parent a6beea5 commit d47fa93

File tree

3 files changed

+222
-1
lines changed

3 files changed

+222
-1
lines changed

lib/matplotlib.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,5 @@ class Error < StandardError
3636

3737
require 'matplotlib/axes'
3838
require 'matplotlib/figure'
39+
40+
PyCall.append_sys_path(File.expand_path('../matplotlib/python', __FILE__))

lib/matplotlib/iruby.rb

Lines changed: 219 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,87 @@
22

33
module Matplotlib
44
module IRuby
5+
module HookExtension
6+
def self.extended(obj)
7+
@event_registry ||= {}
8+
@event_registry[obj] = {}
9+
end
10+
11+
def self.register_event(target, event, hook)
12+
@event_registry[target][event] ||= []
13+
@event_registry[target][event] << hook
14+
end
15+
16+
def register_event(event, hook=nil, &block)
17+
HookExtension.register_event(self, event, [hook, block].compact)
18+
end
19+
20+
def self.unregister_event(target, event, hook)
21+
return unless @event_registry[target]
22+
return unless @event_registry[target][event]
23+
@event_registry[target][event].delete(hook)
24+
end
25+
26+
def unregister_event(event, hook)
27+
HookExtension.unregister_event(self, event, hook)
28+
end
29+
30+
def self.trigger_event(target, event)
31+
return unless @event_registry[target][event]
32+
@event_registry[target][event].each do |hooks|
33+
hooks.to_a.each do |hook|
34+
hook.call if hook
35+
end
36+
end
37+
rescue Exception
38+
$stderr.puts "Error occurred in triggerred event: target=#{target} event=#{event}", $!.to_s, *$!.backtrace
39+
end
40+
41+
def trigger_event(event)
42+
HookExtension.trigger_event(self, event)
43+
end
44+
45+
def execute_request(msg)
46+
code = msg[:content]['code']
47+
@execution_count += 1 if msg[:content]['store_history']
48+
@session.send(:publish, :execute_input, code: code, execution_count: @execution_count)
49+
50+
trigger_event(:pre_execute)
51+
52+
content = {
53+
status: :ok,
54+
payload: [],
55+
user_expressions: {},
56+
execution_count: @execution_count
57+
}
58+
result = nil
59+
begin
60+
result = @backend.eval(code, msg[:content]['store_history'])
61+
rescue SystemExit
62+
content[:payload] << { source: :ask_exit }
63+
rescue Exception => e
64+
content = error_message(e)
65+
@session.send(:publish, :error, content)
66+
end
67+
68+
trigger_event(:post_execute)
69+
70+
@session.send(:reply, :execute_reply, content)
71+
@session.send(:publish, :execute_result,
72+
data: ::IRuby::Display.display(result),
73+
metadata: {},
74+
execution_count: @execution_count) unless result.nil? || msg[:content]['silent']
75+
end
76+
end
77+
578
AGG_FORMATS = {
679
"image/png" => "png",
780
"application/pdf" => "pdf",
881
"application/eps" => "eps",
982
"image/eps" => "eps",
1083
"application/postscript" => "ps",
1184
"image/svg+xml" => "svg"
12-
}
85+
}.freeze
1386

1487
module Helper
1588
def register_formats
@@ -31,7 +104,152 @@ def register_formats
31104
class << self
32105
# NOTE: This method is translated from `IPython.core.activate_matplotlib` function.
33106
def activate(gui=:inline)
107+
enable_matplotlib(gui)
108+
end
109+
110+
GUI_BACKEND_MAP = {
111+
tk: :TkAgg,
112+
gtk: :GTKAgg,
113+
gtk3: :GTK3Agg,
114+
wx: :WXAgg,
115+
qt: :Qt4Agg,
116+
qt4: :Qt4Agg,
117+
qt5: :Qt5Agg,
118+
osx: :MacOSX,
119+
nbagg: :nbAgg,
120+
notebook: :nbAgg,
121+
agg: :agg,
122+
inline: 'module://ruby.matplotlib.backend_inline',
123+
}.freeze
124+
125+
BACKEND_GUI_MAP = Hash[GUI_BACKEND_MAP.select {|k, v| v }].freeze
126+
127+
private_constant :GUI_BACKEND_MAP, :BACKEND_GUI_MAP
128+
129+
def available_gui_names
130+
GUI_BACKEND_MAP.keys
131+
end
132+
133+
private
134+
135+
# This method is based on IPython.core.interactiveshell.InteractiveShell.enable_matplotlib function.
136+
def enable_matplotlib(gui=nil)
137+
gui, backend = find_gui_and_backend(gui, @gui_select)
138+
139+
if gui != :inline
140+
if @gui_select.nil?
141+
@gui_select = gui
142+
elsif gui != @gui_select
143+
$stderr.puts "Warning: Cannot change to a different GUI toolkit: #{gui}. Using #{@gui_select} instead."
144+
gui, backend = find_gui_and_backend(@gui_select)
145+
end
146+
end
147+
148+
activate_matplotlib(backend)
149+
configure_inline_support(backend)
150+
# self.enable_gui(gui)
151+
# register matplotlib-aware execution runner for ExecutionMagics
152+
153+
[gui, backend]
154+
end
155+
156+
# Given a gui string return the gui and matplotlib backend.
157+
# This method is based on IPython.core.pylabtools.find_gui_and_backend function.
158+
#
159+
# @param [String, Symbol, nil] gui can be one of (:tk, :gtk, :wx, :qt, :qt4, :inline, :agg).
160+
# @param [String, Symbol, nil] gui_select can be one of (:tk, :gtk, :wx, :qt, :qt4, :inline, :agg).
161+
#
162+
# @return A pair of (gui, backend) where backend is one of (:TkAgg, :GTKAgg, :WXAgg, :Qt4Agg, :agg).
163+
def find_gui_and_backend(gui=nil, gui_select=nil)
164+
gui = gui.to_sym if gui.kind_of? String
165+
166+
if gui && gui != :auto
167+
# select backend based on requested gui
168+
backend = GUI_BACKEND_MAP[gui]
169+
gui = nil if gui == :agg
170+
return [gui, backend]
171+
end
172+
173+
backend = Matplotlib.rcParamsOrig['backend']&.to_sym
174+
gui = BACKEND_GUI_MAP[backend]
175+
176+
# If we have already had a gui active, we need it and inline are the ones allowed.
177+
if gui_select && gui != gui_select
178+
gui = gui_select
179+
backend = backend[gui]
180+
end
181+
182+
[gui, backend]
183+
end
184+
185+
# Activate the given backend and set interactive to true.
186+
# This method is based on IPython.core.pylabtools.activate_matplotlib function.
187+
#
188+
# @param [String, Symbol] backend a name of matplotlib backend
189+
def activate_matplotlib(backend)
190+
require 'matplotlib'
191+
Matplotlib.interactive(true)
192+
193+
backend = backend.to_s
194+
Matplotlib.rcParams['backend'] = backend
195+
34196
require 'matplotlib/pyplot'
197+
Matplotlib::Pyplot.switch_backend(backend)
198+
199+
# TODO: should support wrapping python function
200+
# plt = Matplotlib::Pyplot
201+
# plt.__pyobj__.show._needmain = false
202+
# plt.__pyobj__.draw_if_interactive = flag_calls(plt.__pyobj__.draw_if_interactive)
203+
end
204+
205+
# This method is based on IPython.core.pylabtools.configure_inline_support function.
206+
#
207+
# @param shell an instance of IRuby shell
208+
# @param backend a name of matplotlib backend
209+
def configure_inline_support(backend)
210+
# Temporally monky-patching IRuby kernel to enable flushing and closing figures.
211+
# TODO: Make this feature a pull-request for sciruby/iruby.
212+
kernel = ::IRuby::Kernel.instance
213+
kernel.extend HookExtension
214+
if backend == GUI_BACKEND_MAP[:inline]
215+
kernel.register_event(:post_execute, method(:flush_figures))
216+
# TODO: save original rcParams and overwrite rcParams with IRuby-specific configuration
217+
new_backend_name = :inline
218+
else
219+
kernel.unregister_event(:post_execute, method(:flush_figures))
220+
# TODO: restore saved original rcParams
221+
new_backend_name = :not_inline
222+
end
223+
if new_backend_name != @current_backend
224+
# TODO: select figure formats
225+
@current_backend = new_backend_name
226+
end
227+
end
228+
229+
# This method is based on ipykernel.pylab.backend_inline.flush_figures function.
230+
def flush_figures
231+
# TODO: I want to allow users to turn on/off automatic figure closing.
232+
show_figures(true)
233+
end
234+
235+
# This method is based on ipykernel.pylab.backend_inline.show function.
236+
#
237+
# @param [true, false] close If true, a `plt.close('all')` call is automatically issued after sending all the figures.
238+
def show_figures(close=false)
239+
_pylab_helpers = PyCall.import_module('matplotlib._pylab_helpers')
240+
gcf = PyCall.getattr(_pylab_helpers, :Gcf)
241+
kernel = ::IRuby::Kernel.instance
242+
gcf.get_all_fig_managers.().each do |fig_manager|
243+
data = ::IRuby::Display.display(fig_manager.canvas.figure)
244+
kernel.session.send(:publish, :execute_result,
245+
data: data,
246+
metadata: {},
247+
execution_count: kernel.instance_variable_get(:@execution_count))
248+
end
249+
ensure
250+
unless gcf.get_all_fig_managers.().none?
251+
Matplotlib::Pyplot.close('all')
252+
end
35253
end
36254
end
37255
end
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from matplotlib.backends.backend_agg import new_figure_manager, FigureCanvasAgg

0 commit comments

Comments
 (0)