From 961d15251d6a124730d9c67e2b85a310839af448 Mon Sep 17 00:00:00 2001
From: Yusei Tahara <yusei@nexedi.com>
Date: Sat, 18 Feb 2017 17:10:40 +0900
Subject: [PATCH] Jupyter: Display matplotlib figure automatically like the
 original python kernel.

---
 .../extension.erp5.JupyterCompile.py          | 86 ++++++++++++++++++-
 1 file changed, 82 insertions(+), 4 deletions(-)

diff --git a/bt5/erp5_data_notebook/ExtensionTemplateItem/portal_components/extension.erp5.JupyterCompile.py b/bt5/erp5_data_notebook/ExtensionTemplateItem/portal_components/extension.erp5.JupyterCompile.py
index 6f7151b314..a7bcda0e2d 100644
--- a/bt5/erp5_data_notebook/ExtensionTemplateItem/portal_components/extension.erp5.JupyterCompile.py
+++ b/bt5/erp5_data_notebook/ExtensionTemplateItem/portal_components/extension.erp5.JupyterCompile.py
@@ -15,9 +15,18 @@ import json
 import transaction
 import Acquisition
 import astor
-import importlib
 from Products.ERP5Type.Log import log
 
+# Display matplotlib figure automatically like
+# the original python kernel
+import matplotlib
+import matplotlib.pyplot as plt
+from IPython.core.pylabtools import print_figure
+from IPython.core.display import _pngxy
+from ipykernel.jsonutil import json_clean, encode_images
+import threading
+display_data_wrapper_lock = threading.Lock()
+
 def Base_executeJupyter(self, python_expression=None, reference=None, \
                         title=None, request_reference=False, **kw):
   # Check permissions for current user and display message to non-authorized user 
@@ -69,17 +78,21 @@ def Base_executeJupyter(self, python_expression=None, reference=None, \
   
   # Pass all to code Base_runJupyter external function which would execute the code
   # and returns a dict of result
-  final_result = Base_runJupyterCode(self, python_expression, old_notebook_context)
+  final_result = displayDataWrapper(lambda:Base_runJupyterCode(self, python_expression, old_notebook_context))
     
   new_notebook_context = final_result['notebook_context']
   
   result = {
     u'code_result': final_result['result_string'],
+    u'print_result': final_result['print_result'],
+    u'displayhook_result': final_result['displayhook_result'],
     u'ename': final_result['ename'],
     u'evalue': final_result['evalue'],
     u'traceback': final_result['traceback'],
     u'status': final_result['status'],
-    u'mime_type': final_result['mime_type']}
+    u'mime_type': final_result['mime_type'],
+    u'extra_data_list': final_result['extra_data_list'],
+  }
   
   # Updates the context in the notebook with the resulting context of code 
   # execution.
@@ -106,6 +119,8 @@ def Base_executeJupyter(self, python_expression=None, reference=None, \
   except UnicodeDecodeError:
     result = {
       u'code_result': None,
+      u'print_result': None,
+      u'displayhook_result': None,
       u'ename': u'UnicodeDecodeError',
       u'evalue': None,
       u'traceback': None,
@@ -129,6 +144,62 @@ def mergeTracebackListIntoResultDict(result_dict, error_result_dict_list):
       result_dict['status'] = error_result_dict['status']
   return result_dict
 
+
+def matplotlib_pre_run():
+  matplotlib.interactive(True)
+  rc = {'figure.figsize': (6.0,4.0),
+        'figure.facecolor': (1,1,1,0),
+        'figure.edgecolor': (1,1,1,0),
+        'font.size': 10,
+        'figure.dpi': 36,
+        'figure.subplot.bottom' : .125
+        }
+  for key, value in rc.items():
+    matplotlib.rcParams[key] = value
+  plt.gcf().clear()
+
+def matplotlib_post_run(data_list):
+  png_data = None
+  figure = plt.gcf()
+  # Always try to get the current figure.
+  # This is not efficient, but we can support any libraries
+  # that use matplotlib.
+  png_data = print_figure(figure, fmt='png')
+  figure.clear()
+  if png_data is not None:
+    width, height = _pngxy(png_data)
+    data = encode_images({'image/png':png_data})
+    metadata = {'image/png':dict(width=width, height=height)}
+    data_list.append(json_clean(dict(data=data, metadata=metadata)))
+
+class Displayhook(object):
+  def hook(self, value):
+    if value is not None:
+      self.result = repr(value)
+  def pre_run(self):
+    self.old_hook = sys.displayhook
+    sys.displayhook = self.hook
+    self.result = None
+  def post_run(self):
+    sys.displayhook = self.old_hook
+displayhook = Displayhook()
+
+def displayDataWrapper(function):
+  with display_data_wrapper_lock:
+    # pre run
+    displayhook.pre_run()
+    matplotlib_pre_run()
+    extra_data_list = []
+    try:
+      result = function()
+      extra_data_list = result.get('extra_data_list', [])
+    finally:
+      # post run
+      displayhook.post_run()
+      matplotlib_post_run(extra_data_list)
+  result['extra_data_list'] = extra_data_list
+  return result
+
 def Base_runJupyterCode(self, jupyter_code, old_notebook_context):
   """
     Function to execute jupyter code and update the context dictionary.
@@ -292,6 +363,8 @@ def Base_runJupyterCode(self, jupyter_code, old_notebook_context):
           transaction.abort()
           result = {
             'result_string': "EnvironmentUndefineError: Trying to remove non existing function/variable from environment: '%s'\n" % func_alias,
+            'print_result': {"data":{"text/plain":"EnvironmentUndefineError: Trying to remove non existing function/variable from environment: '%s'\n" % func_alias}, "metadata":{}},
+            'displayhook_result': None,
             'notebook_context': notebook_context,
             'status': 'ok',
             'mime_type': 'text/plain',
@@ -404,9 +477,14 @@ def Base_runJupyterCode(self, jupyter_code, old_notebook_context):
     
     if inject_variable_dict.get('_print') is not None:
       output = inject_variable_dict['_print'].getCapturedOutputString()
-  
+
+  displayhook_result = {"data":{}, "metadata":{}}
+  if displayhook.result is not None:
+    displayhook_result["data"]["text/plain"] = displayhook.result
   result = {
     'result_string': output,
+    'print_result': {"data":{"text/plain":output}, "metadata":{}},
+    'displayhook_result': displayhook_result,
     'notebook_context': notebook_context,
     'status': status,
     'mime_type': mime_type,
-- 
2.30.9