require 'matplotlib' 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 BytesIO = PyCall.import_module('io').BytesIO def register_formats type { Figure } AGG_FORMATS.each do |mime, format| format mime do |fig| unless fig.canvas.get_supported_filetypes.has_key?(format) raise Error, "Unable to display a figure in #{format} format" end io = BytesIO.new fig.canvas.print_figure(io, format: format, bbox_inches: 'tight') io.getvalue end end end 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 = _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.nil? Matplotlib::Pyplot.close('all') end end end end end ::IRuby::Display::Registry.module_eval do extend Matplotlib::IRuby::Helper register_formats end