visualization.py 7.49 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Copyright 2017 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================

Ivan Bogatyy's avatar
Ivan Bogatyy committed
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
"""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


Ivan Bogatyy's avatar
Ivan Bogatyy committed
72
73
74
75
76
77
78
79
80
def _optional_master_spec_json(master_spec):
  """Helper function to return 'null' or a master spec JSON string."""
  if master_spec is None:
    return 'null'
  else:
    return json_format.MessageToJson(
        master_spec, preserving_proto_field_name=True)


Ivan Bogatyy's avatar
Ivan Bogatyy committed
81
82
83
84
85
86
87
88
89
90
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


Ivan Bogatyy's avatar
Ivan Bogatyy committed
91
92
93
94
95
def trace_html(trace,
               convert_to_unicode=True,
               height='700px',
               script=None,
               master_spec=None):
Ivan Bogatyy's avatar
Ivan Bogatyy committed
96
97
98
99
100
101
102
103
104
105
106
  """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.
Ivan Bogatyy's avatar
Ivan Bogatyy committed
107
108
    master_spec: Master spec proto (parsed), which can improve the layout. May
      be required in future versions.
Ivan Bogatyy's avatar
Ivan Bogatyy committed
109
110
111
112
113
114
115
116
117
118
119
120
121

  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}
Ivan Bogatyy's avatar
Ivan Bogatyy committed
122
  visualizeToDiv({json}, "{elt_id}", {master_spec_json});
Ivan Bogatyy's avatar
Ivan Bogatyy committed
123
124
  </script>
  """.format(
Ivan Bogatyy's avatar
Ivan Bogatyy committed
125
126
127
128
129
      script=script,
      json=json_trace,
      master_spec_json=_optional_master_spec_json(master_spec),
      elt_id=elt_id,
      div_html=div_html)
Ivan Bogatyy's avatar
Ivan Bogatyy committed
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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
  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.

Ivan Bogatyy's avatar
Ivan Bogatyy committed
211
  def show_trace(self, trace, master_spec=None):
Ivan Bogatyy's avatar
Ivan Bogatyy committed
212
213
214
215
    """Returns a JS script HTML fragment, which will populate the container.

    Args:
      trace: binary-encoded MasterTrace string.
Ivan Bogatyy's avatar
Ivan Bogatyy committed
216
217
      master_spec: Master spec proto (parsed), which can improve the layout. May
        be required in future versions.
Ivan Bogatyy's avatar
Ivan Bogatyy committed
218
219
220
221
222
223
224
225

    Returns:
      unicode with HTML contents.
    """
    html = """
    <meta charset="utf-8"/>
    <script type='text/javascript'>
    document.getElementById("{elt_id}").innerHTML = "";  // Clear previous.
Ivan Bogatyy's avatar
Ivan Bogatyy committed
226
    visualizeToDiv({json}, "{elt_id}", {master_spec_json});
Ivan Bogatyy's avatar
Ivan Bogatyy committed
227
228
    </script>
    """.format(
Ivan Bogatyy's avatar
Ivan Bogatyy committed
229
230
231
        json=parse_trace_json(trace),
        master_spec_json=_optional_master_spec_json(master_spec),
        elt_id=self.elt_id)
Ivan Bogatyy's avatar
Ivan Bogatyy committed
232
    return unicode(html, 'utf-8')  # IPython expects unicode.