lib/matplotlib/iruby.rb in matplotlib-0.1.0.alpha.20170302 vs lib/matplotlib/iruby.rb in matplotlib-0.1.0.alpha.20170307

- old
+ new

@@ -1,17 +1,90 @@ require 'pycall' module Matplotlib module IRuby + module HookExtension + def self.extended(obj) + @event_registry ||= {} + @event_registry[obj] = {} + end + + def self.register_event(target, event, hook) + @event_registry[target][event] ||= [] + @event_registry[target][event] << hook + end + + def register_event(event, hook=nil, &block) + HookExtension.register_event(self, event, [hook, block].compact) + end + + def self.unregister_event(target, event, hook) + return unless @event_registry[target] + return unless @event_registry[target][event] + @event_registry[target][event].delete(hook) + end + + def unregister_event(event, hook) + HookExtension.unregister_event(self, event, hook) + end + + def self.trigger_event(target, event) + return unless @event_registry[target][event] + @event_registry[target][event].each do |hooks| + hooks.to_a.each do |hook| + hook.call if hook + end + end + rescue Exception + $stderr.puts "Error occurred in triggerred event: target=#{target} event=#{event}", $!.to_s, *$!.backtrace + end + + def trigger_event(event) + HookExtension.trigger_event(self, event) + end + + def execute_request(msg) + code = msg[:content]['code'] + @execution_count += 1 if msg[:content]['store_history'] + @session.send(:publish, :execute_input, code: code, execution_count: @execution_count) + + trigger_event(:pre_execute) + + content = { + status: :ok, + payload: [], + user_expressions: {}, + execution_count: @execution_count + } + result = nil + begin + result = @backend.eval(code, msg[:content]['store_history']) + rescue SystemExit + content[:payload] << { source: :ask_exit } + rescue Exception => e + content = error_message(e) + @session.send(:publish, :error, content) + end + + trigger_event(:post_execute) + + @session.send(:reply, :execute_reply, content) + @session.send(:publish, :execute_result, + data: ::IRuby::Display.display(result), + metadata: {}, + execution_count: @execution_count) unless result.nil? || msg[:content]['silent'] + end + end + AGG_FORMATS = { "image/png" => "png", "application/pdf" => "pdf", "application/eps" => "eps", "image/eps" => "eps", "application/postscript" => "ps", "image/svg+xml" => "svg" - } + }.freeze module Helper def register_formats bytes_io = PyCall.import_module('io').BytesIO type { Figure } @@ -29,10 +102,155 @@ end class << self # NOTE: This method is translated from `IPython.core.activate_matplotlib` function. def activate(gui=:inline) + enable_matplotlib(gui) + end + + GUI_BACKEND_MAP = { + tk: :TkAgg, + gtk: :GTKAgg, + gtk3: :GTK3Agg, + wx: :WXAgg, + qt: :Qt4Agg, + qt4: :Qt4Agg, + qt5: :Qt5Agg, + osx: :MacOSX, + nbagg: :nbAgg, + notebook: :nbAgg, + agg: :agg, + inline: 'module://ruby.matplotlib.backend_inline', + }.freeze + + BACKEND_GUI_MAP = Hash[GUI_BACKEND_MAP.select {|k, v| v }].freeze + + private_constant :GUI_BACKEND_MAP, :BACKEND_GUI_MAP + + def available_gui_names + GUI_BACKEND_MAP.keys + end + + private + + # This method is based on IPython.core.interactiveshell.InteractiveShell.enable_matplotlib function. + def enable_matplotlib(gui=nil) + gui, backend = find_gui_and_backend(gui, @gui_select) + + if gui != :inline + if @gui_select.nil? + @gui_select = gui + elsif gui != @gui_select + $stderr.puts "Warning: Cannot change to a different GUI toolkit: #{gui}. Using #{@gui_select} instead." + gui, backend = find_gui_and_backend(@gui_select) + end + end + + activate_matplotlib(backend) + configure_inline_support(backend) + # self.enable_gui(gui) + # register matplotlib-aware execution runner for ExecutionMagics + + [gui, backend] + end + + # Given a gui string return the gui and matplotlib backend. + # This method is based on IPython.core.pylabtools.find_gui_and_backend function. + # + # @param [String, Symbol, nil] gui can be one of (:tk, :gtk, :wx, :qt, :qt4, :inline, :agg). + # @param [String, Symbol, nil] gui_select can be one of (:tk, :gtk, :wx, :qt, :qt4, :inline, :agg). + # + # @return A pair of (gui, backend) where backend is one of (:TkAgg, :GTKAgg, :WXAgg, :Qt4Agg, :agg). + def find_gui_and_backend(gui=nil, gui_select=nil) + gui = gui.to_sym if gui.kind_of? String + + if gui && gui != :auto + # select backend based on requested gui + backend = GUI_BACKEND_MAP[gui] + gui = nil if gui == :agg + return [gui, backend] + end + + backend = Matplotlib.rcParamsOrig['backend']&.to_sym + gui = BACKEND_GUI_MAP[backend] + + # If we have already had a gui active, we need it and inline are the ones allowed. + if gui_select && gui != gui_select + gui = gui_select + backend = backend[gui] + end + + [gui, backend] + end + + # Activate the given backend and set interactive to true. + # This method is based on IPython.core.pylabtools.activate_matplotlib function. + # + # @param [String, Symbol] backend a name of matplotlib backend + def activate_matplotlib(backend) + require 'matplotlib' + Matplotlib.interactive(true) + + backend = backend.to_s + Matplotlib.rcParams['backend'] = backend + require 'matplotlib/pyplot' + Matplotlib::Pyplot.switch_backend(backend) + + # TODO: should support wrapping python function + # plt = Matplotlib::Pyplot + # plt.__pyobj__.show._needmain = false + # plt.__pyobj__.draw_if_interactive = flag_calls(plt.__pyobj__.draw_if_interactive) + end + + # This method is based on IPython.core.pylabtools.configure_inline_support function. + # + # @param shell an instance of IRuby shell + # @param backend a name of matplotlib backend + def configure_inline_support(backend) + # Temporally monky-patching IRuby kernel to enable flushing and closing figures. + # TODO: Make this feature a pull-request for sciruby/iruby. + kernel = ::IRuby::Kernel.instance + kernel.extend HookExtension + if backend == GUI_BACKEND_MAP[:inline] + kernel.register_event(:post_execute, method(:flush_figures)) + # TODO: save original rcParams and overwrite rcParams with IRuby-specific configuration + new_backend_name = :inline + else + kernel.unregister_event(:post_execute, method(:flush_figures)) + # TODO: restore saved original rcParams + new_backend_name = :not_inline + end + if new_backend_name != @current_backend + # TODO: select figure formats + @current_backend = new_backend_name + end + end + + # This method is based on ipykernel.pylab.backend_inline.flush_figures function. + def flush_figures + # TODO: I want to allow users to turn on/off automatic figure closing. + show_figures(true) + end + + # This method is based on ipykernel.pylab.backend_inline.show function. + # + # @param [true, false] close If true, a `plt.close('all')` call is automatically issued after sending all the figures. + def show_figures(close=false) + _pylab_helpers = PyCall.import_module('matplotlib._pylab_helpers') + gcf = PyCall.getattr(_pylab_helpers, :Gcf) + kernel = ::IRuby::Kernel.instance + gcf.get_all_fig_managers.().each do |fig_manager| + data = ::IRuby::Display.display(fig_manager.canvas.figure) + kernel.session.send(:publish, :execute_result, + data: data, + metadata: {}, + execution_count: kernel.instance_variable_get(:@execution_count)) + end + ensure + unless gcf.get_all_fig_managers.().none? + Matplotlib::Pyplot.close('all') + end end end end end