visualization.py 6.05 KB
Newer Older
Ivan Bogatyy's avatar
Ivan Bogatyy committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
"""Helper library for visualizations.

TODO(googleuser): Find a more reliable way to serve stuff from IPython
notebooks (e.g. determining where the root notebook directory is).
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import gzip
import os
import uuid

from google.protobuf import json_format
from dragnn.protos import trace_pb2

# Make a guess about where the IPython kernel root is.
_IPYTHON_KERNEL_PATH = os.path.realpath(os.getcwd())

# Bazel uses the 'data' attribute for this library to ensure viz.min.js.gz is
# packaged.
module_path = os.path.dirname(os.path.abspath(__file__))
viz_script = os.path.join(os.path.dirname(module_path), 'viz', 'viz.min.js.gz')


def _load_viz_script():
  """Reads the bundled visualization script.

  Raises:
    EnvironmentError: If the visualization script could not be found.

  Returns:
    str JavaScript source code.
  """
  if not os.path.isfile(viz_script):
    raise EnvironmentError(
        'Visualization script should be built into {}'.format(viz_script))
  with gzip.GzipFile(viz_script) as f:
    return f.read()


def parse_trace_json(trace):
  """Converts a binary-encoded MasterTrace proto to a JSON parser trace.

  Args:
    trace: Binary string containing a MasterTrace.

  Returns:
    JSON str, as expected by visualization tools.
  """
  as_proto = trace_pb2.MasterTrace.FromString(trace)
  as_json = json_format.MessageToJson(
      as_proto, preserving_proto_field_name=True)
  return as_json


def _container_div(height='700px', contents=''):
  elt_id = str(uuid.uuid4())
  html = """
  <div id="{elt_id}" style="width: 100%; min-width: 200px; height: {height};">
  {contents}</div>
  """.format(
      elt_id=elt_id, height=height, contents=contents)
  return elt_id, html


def trace_html(trace, convert_to_unicode=True, height='700px', script=None):
  """Generates HTML that will render a master trace.

  This will result in a self-contained "div" element.

  Args:
    trace: binary-encoded MasterTrace string.
    convert_to_unicode: Whether to convert the output to unicode. Defaults to
      True because IPython.display.HTML expects unicode, and we expect users to
      often pass the output of this function to IPython.display.HTML.
    height: CSS string representing the height of the element, default '700px'.
    script: Visualization script contents, if the defaults are unacceptable.

  Returns:
    unicode or str with HTML contents.
  """
  if script is None:
    script = _load_viz_script()
  json_trace = parse_trace_json(trace)
  elt_id, div_html = _container_div(height=height)
  as_str = """
  <meta charset="utf-8"/>
  {div_html}
  <script type='text/javascript'>
  {script}
  visualizeToDiv({json}, "{elt_id}");
  </script>
  """.format(
      script=script, json=json_trace, elt_id=elt_id, div_html=div_html)
  return unicode(as_str, 'utf-8') if convert_to_unicode else as_str


def open_in_new_window(html, notebook_html_fcn=None, temp_file_basename=None):
  """Opens an HTML visualization in a new window.

  This function assumes that the module was loaded when the current working
  directory is the IPython/Jupyter notebook root directory. Then it writes a
  file ./tmp/_new_window_html/<random-uuid>.html, and returns an HTML display
  element, which will call `window.open("/files/<filename>")`. This works
  because IPython serves files from the /files root.

  Args:
    html: HTML to write to a file.
    notebook_html_fcn: Function to generate an HTML element; defaults to
      IPython.display.HTML (lazily imported).
    temp_file_basename: File name to write (defaults to <random-uuid>.html).

  Returns:
    HTML notebook element, which will trigger the browser to open a new window.
  """
  if isinstance(html, unicode):
    html = html.encode('utf-8')

  if notebook_html_fcn is None:
    from IPython import display
    notebook_html_fcn = display.HTML

  if temp_file_basename is None:
    temp_file_basename = '{}.html'.format(str(uuid.uuid4()))

  rel_path = os.path.join('tmp', '_new_window_html', temp_file_basename)
  abs_path = os.path.join(_IPYTHON_KERNEL_PATH, rel_path)

  # Write the file, creating the directory if it doesn't exist.
  if not os.path.isdir(os.path.dirname(abs_path)):
    os.makedirs(os.path.dirname(abs_path))
  with open(abs_path, 'w') as f:
    f.write(html)

  return notebook_html_fcn("""
  <script type='text/javascript'>
  window.open("/files/{}");
  </script>
  """.format(rel_path))


class InteractiveVisualization(object):
  """Helper class for displaying visualizations interactively.

  See usage in examples/dragnn/interactive_text_analyzer.ipynb.
  """

  def initial_html(self, height='700px', script=None, init_message=None):
    """Returns HTML for a container, which will be populated later.

    Args:
      height: CSS string representing the height of the element, default
        '700px'.
      script: Visualization script contents, if the defaults are unacceptable.
      init_message: Initial message to display.

    Returns:
      unicode with HTML contents.
    """
    if script is None:
      script = _load_viz_script()
    if init_message is None:
      init_message = 'Type a sentence and press (enter) to see the trace.'
    self.elt_id, div_html = _container_div(
        height=height, contents='<strong>{}</strong>'.format(init_message))
    html = """
    <meta charset="utf-8"/>
    {div_html}
    <script type='text/javascript'>
    {script}
    </script>
    """.format(
        script=script, div_html=div_html)
    return unicode(html, 'utf-8')  # IPython expects unicode.

  def show_trace(self, trace):
    """Returns a JS script HTML fragment, which will populate the container.

    Args:
      trace: binary-encoded MasterTrace string.

    Returns:
      unicode with HTML contents.
    """
    html = """
    <meta charset="utf-8"/>
    <script type='text/javascript'>
    document.getElementById("{elt_id}").innerHTML = "";  // Clear previous.
    visualizeToDiv({json}, "{elt_id}");
    </script>
    """.format(
        json=parse_trace_json(trace), elt_id=self.elt_id)
    return unicode(html, 'utf-8')  # IPython expects unicode.