visualization.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. # Copyright 2017 Google Inc. All Rights Reserved.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. # ==============================================================================
  15. """Helper library for visualizations.
  16. TODO(googleuser): Find a more reliable way to serve stuff from IPython
  17. notebooks (e.g. determining where the root notebook directory is).
  18. """
  19. from __future__ import absolute_import
  20. from __future__ import division
  21. from __future__ import print_function
  22. import gzip
  23. import os
  24. import uuid
  25. from google.protobuf import json_format
  26. from dragnn.protos import trace_pb2
  27. # Make a guess about where the IPython kernel root is.
  28. _IPYTHON_KERNEL_PATH = os.path.realpath(os.getcwd())
  29. # Bazel uses the 'data' attribute for this library to ensure viz.min.js.gz is
  30. # packaged.
  31. module_path = os.path.dirname(os.path.abspath(__file__))
  32. viz_script = os.path.join(os.path.dirname(module_path), 'viz', 'viz.min.js.gz')
  33. def _load_viz_script():
  34. """Reads the bundled visualization script.
  35. Raises:
  36. EnvironmentError: If the visualization script could not be found.
  37. Returns:
  38. str JavaScript source code.
  39. """
  40. if not os.path.isfile(viz_script):
  41. raise EnvironmentError(
  42. 'Visualization script should be built into {}'.format(viz_script))
  43. with gzip.GzipFile(viz_script) as f:
  44. return f.read()
  45. def parse_trace_json(trace):
  46. """Converts a binary-encoded MasterTrace proto to a JSON parser trace.
  47. Args:
  48. trace: Binary string containing a MasterTrace.
  49. Returns:
  50. JSON str, as expected by visualization tools.
  51. """
  52. as_proto = trace_pb2.MasterTrace.FromString(trace)
  53. as_json = json_format.MessageToJson(
  54. as_proto, preserving_proto_field_name=True)
  55. return as_json
  56. def _optional_master_spec_json(master_spec):
  57. """Helper function to return 'null' or a master spec JSON string."""
  58. if master_spec is None:
  59. return 'null'
  60. else:
  61. return json_format.MessageToJson(
  62. master_spec, preserving_proto_field_name=True)
  63. def _container_div(height='700px', contents=''):
  64. elt_id = str(uuid.uuid4())
  65. html = """
  66. <div id="{elt_id}" style="width: 100%; min-width: 200px; height: {height};">
  67. {contents}</div>
  68. """.format(
  69. elt_id=elt_id, height=height, contents=contents)
  70. return elt_id, html
  71. def trace_html(trace,
  72. convert_to_unicode=True,
  73. height='700px',
  74. script=None,
  75. master_spec=None):
  76. """Generates HTML that will render a master trace.
  77. This will result in a self-contained "div" element.
  78. Args:
  79. trace: binary-encoded MasterTrace string.
  80. convert_to_unicode: Whether to convert the output to unicode. Defaults to
  81. True because IPython.display.HTML expects unicode, and we expect users to
  82. often pass the output of this function to IPython.display.HTML.
  83. height: CSS string representing the height of the element, default '700px'.
  84. script: Visualization script contents, if the defaults are unacceptable.
  85. master_spec: Master spec proto (parsed), which can improve the layout. May
  86. be required in future versions.
  87. Returns:
  88. unicode or str with HTML contents.
  89. """
  90. if script is None:
  91. script = _load_viz_script()
  92. json_trace = parse_trace_json(trace)
  93. elt_id, div_html = _container_div(height=height)
  94. as_str = """
  95. <meta charset="utf-8"/>
  96. {div_html}
  97. <script type='text/javascript'>
  98. {script}
  99. visualizeToDiv({json}, "{elt_id}", {master_spec_json});
  100. </script>
  101. """.format(
  102. script=script,
  103. json=json_trace,
  104. master_spec_json=_optional_master_spec_json(master_spec),
  105. elt_id=elt_id,
  106. div_html=div_html)
  107. return unicode(as_str, 'utf-8') if convert_to_unicode else as_str
  108. def open_in_new_window(html, notebook_html_fcn=None, temp_file_basename=None):
  109. """Opens an HTML visualization in a new window.
  110. This function assumes that the module was loaded when the current working
  111. directory is the IPython/Jupyter notebook root directory. Then it writes a
  112. file ./tmp/_new_window_html/<random-uuid>.html, and returns an HTML display
  113. element, which will call `window.open("/files/<filename>")`. This works
  114. because IPython serves files from the /files root.
  115. Args:
  116. html: HTML to write to a file.
  117. notebook_html_fcn: Function to generate an HTML element; defaults to
  118. IPython.display.HTML (lazily imported).
  119. temp_file_basename: File name to write (defaults to <random-uuid>.html).
  120. Returns:
  121. HTML notebook element, which will trigger the browser to open a new window.
  122. """
  123. if isinstance(html, unicode):
  124. html = html.encode('utf-8')
  125. if notebook_html_fcn is None:
  126. from IPython import display
  127. notebook_html_fcn = display.HTML
  128. if temp_file_basename is None:
  129. temp_file_basename = '{}.html'.format(str(uuid.uuid4()))
  130. rel_path = os.path.join('tmp', '_new_window_html', temp_file_basename)
  131. abs_path = os.path.join(_IPYTHON_KERNEL_PATH, rel_path)
  132. # Write the file, creating the directory if it doesn't exist.
  133. if not os.path.isdir(os.path.dirname(abs_path)):
  134. os.makedirs(os.path.dirname(abs_path))
  135. with open(abs_path, 'w') as f:
  136. f.write(html)
  137. return notebook_html_fcn("""
  138. <script type='text/javascript'>
  139. window.open("/files/{}");
  140. </script>
  141. """.format(rel_path))
  142. class InteractiveVisualization(object):
  143. """Helper class for displaying visualizations interactively.
  144. See usage in examples/dragnn/interactive_text_analyzer.ipynb.
  145. """
  146. def initial_html(self, height='700px', script=None, init_message=None):
  147. """Returns HTML for a container, which will be populated later.
  148. Args:
  149. height: CSS string representing the height of the element, default
  150. '700px'.
  151. script: Visualization script contents, if the defaults are unacceptable.
  152. init_message: Initial message to display.
  153. Returns:
  154. unicode with HTML contents.
  155. """
  156. if script is None:
  157. script = _load_viz_script()
  158. if init_message is None:
  159. init_message = 'Type a sentence and press (enter) to see the trace.'
  160. self.elt_id, div_html = _container_div(
  161. height=height, contents='<strong>{}</strong>'.format(init_message))
  162. html = """
  163. <meta charset="utf-8"/>
  164. {div_html}
  165. <script type='text/javascript'>
  166. {script}
  167. </script>
  168. """.format(
  169. script=script, div_html=div_html)
  170. return unicode(html, 'utf-8') # IPython expects unicode.
  171. def show_trace(self, trace, master_spec=None):
  172. """Returns a JS script HTML fragment, which will populate the container.
  173. Args:
  174. trace: binary-encoded MasterTrace string.
  175. master_spec: Master spec proto (parsed), which can improve the layout. May
  176. be required in future versions.
  177. Returns:
  178. unicode with HTML contents.
  179. """
  180. html = """
  181. <meta charset="utf-8"/>
  182. <script type='text/javascript'>
  183. document.getElementById("{elt_id}").innerHTML = ""; // Clear previous.
  184. visualizeToDiv({json}, "{elt_id}", {master_spec_json});
  185. </script>
  186. """.format(
  187. json=parse_trace_json(trace),
  188. master_spec_json=_optional_master_spec_json(master_spec),
  189. elt_id=self.elt_id)
  190. return unicode(html, 'utf-8') # IPython expects unicode.