comparison env/lib/python3.7/site-packages/prov/dot.py @ 5:9b1c78e6ba9c draft default tip

"planemo upload commit 6c0a8142489327ece472c84e558c47da711a9142"
author shellac
date Mon, 01 Jun 2020 08:59:25 -0400
parents 79f47841a781
children
comparison
equal deleted inserted replaced
4:79f47841a781 5:9b1c78e6ba9c
1 """Graphical visualisation support for prov.model.
2
3 This module produces graphical visualisation for provenanve graphs.
4 Requires pydot module and Graphviz.
5
6 References:
7
8 * pydot homepage: https://github.com/erocarrera/pydot
9 * Graphviz: http://www.graphviz.org/
10 * DOT Language: http://www.graphviz.org/doc/info/lang.html
11
12 .. moduleauthor:: Trung Dong Huynh <trungdong@donggiang.com>
13 """
14 from __future__ import (absolute_import, division, print_function,
15 unicode_literals)
16
17 try:
18 from html import escape
19 except ImportError:
20 from cgi import escape
21 from datetime import datetime
22 import pydot
23 import six
24
25 from prov.model import (
26 PROV_ACTIVITY, PROV_AGENT, PROV_ALTERNATE, PROV_ASSOCIATION,
27 PROV_ATTRIBUTION, PROV_BUNDLE, PROV_COMMUNICATION, PROV_DERIVATION,
28 PROV_DELEGATION, PROV_ENTITY, PROV_GENERATION, PROV_INFLUENCE,
29 PROV_INVALIDATION, PROV_END, PROV_MEMBERSHIP, PROV_MENTION,
30 PROV_SPECIALIZATION, PROV_START, PROV_USAGE, Identifier,
31 PROV_ATTRIBUTE_QNAMES, sorted_attributes, ProvException
32 )
33
34 __author__ = 'Trung Dong Huynh'
35 __email__ = 'trungdong@donggiang.com'
36
37
38 # Visual styles for various elements (nodes) and relations (edges)
39 # see http://graphviz.org/content/attrs
40 DOT_PROV_STYLE = {
41 # Generic node
42 0: {
43 'shape': 'oval', 'style': 'filled',
44 'fillcolor': 'lightgray', 'color': 'dimgray'
45 },
46 # Elements
47 PROV_ENTITY: {
48 'shape': 'oval', 'style': 'filled',
49 'fillcolor': '#FFFC87', 'color': '#808080'
50 },
51 PROV_ACTIVITY: {
52 'shape': 'box', 'style': 'filled',
53 'fillcolor': '#9FB1FC', 'color': '#0000FF'
54 },
55 PROV_AGENT: {
56 'shape': 'house', 'style': 'filled',
57 'fillcolor': '#FED37F'
58 },
59 PROV_BUNDLE: {
60 'shape': 'folder', 'style': 'filled',
61 'fillcolor': 'aliceblue'
62 },
63 # Relations
64 PROV_GENERATION: {
65 'label': 'wasGeneratedBy', 'fontsize': '10.0',
66 'color': 'darkgreen', 'fontcolor': 'darkgreen'
67 },
68 PROV_USAGE: {
69 'label': 'used', 'fontsize': '10.0',
70 'color': 'red4', 'fontcolor': 'red'
71 },
72 PROV_COMMUNICATION: {
73 'label': 'wasInformedBy', 'fontsize': '10.0'
74 },
75 PROV_START: {
76 'label': 'wasStartedBy', 'fontsize': '10.0'
77 },
78 PROV_END: {
79 'label': 'wasEndedBy', 'fontsize': '10.0'
80 },
81 PROV_INVALIDATION: {
82 'label': 'wasInvalidatedBy', 'fontsize': '10.0'
83 },
84 PROV_DERIVATION: {
85 'label': 'wasDerivedFrom', 'fontsize': '10.0'
86 },
87 PROV_ATTRIBUTION: {
88 'label': 'wasAttributedTo', 'fontsize': '10.0',
89 'color': '#FED37F'
90 },
91 PROV_ASSOCIATION: {
92 'label': 'wasAssociatedWith', 'fontsize': '10.0',
93 'color': '#FED37F'
94 },
95 PROV_DELEGATION: {
96 'label': 'actedOnBehalfOf', 'fontsize': '10.0',
97 'color': '#FED37F'
98 },
99 PROV_INFLUENCE: {
100 'label': 'wasInfluencedBy', 'fontsize': '10.0',
101 'color': 'grey'
102 },
103 PROV_ALTERNATE: {
104 'label': 'alternateOf', 'fontsize': '10.0'
105 },
106 PROV_SPECIALIZATION: {
107 'label': 'specializationOf', 'fontsize': '10.0'
108 },
109 PROV_MENTION: {
110 'label': 'mentionOf', 'fontsize': '10.0'
111 },
112 PROV_MEMBERSHIP: {
113 'label': 'hadMember', 'fontsize': '10.0'
114 },
115 }
116
117 ANNOTATION_STYLE = {
118 'shape': 'note', 'color': 'gray',
119 'fontcolor': 'black', 'fontsize': '10'
120 }
121 ANNOTATION_LINK_STYLE = {
122 'arrowhead': 'none', 'style': 'dashed',
123 'color': 'gray'
124 }
125 ANNOTATION_START_ROW = '<<TABLE cellpadding=\"0\" border=\"0\">'
126 ANNOTATION_ROW_TEMPLATE = """ <TR>
127 <TD align=\"left\" href=\"%s\">%s</TD>
128 <TD align=\"left\"%s>%s</TD>
129 </TR>"""
130 ANNOTATION_END_ROW = ' </TABLE>>'
131
132
133 def htlm_link_if_uri(value):
134 try:
135 uri = value.uri
136 return '<a href="%s">%s</a>' % (uri, six.text_type(value))
137 except AttributeError:
138 return six.text_type(value)
139
140
141 def prov_to_dot(bundle, show_nary=True, use_labels=False,
142 direction='BT',
143 show_element_attributes=True, show_relation_attributes=True):
144 """
145 Convert a provenance bundle/document into a DOT graphical representation.
146
147 :param bundle: The provenance bundle/document to be converted.
148 :type bundle: :class:`ProvBundle`
149 :param show_nary: shows all elements in n-ary relations.
150 :type show_nary: bool
151 :param use_labels: uses the prov:label property of an element as its name (instead of its identifier).
152 :type use_labels: bool
153 :param direction: specifies the direction of the graph. Valid values are "BT" (default), "TB", "LR", "RL".
154 :param show_element_attributes: shows attributes of elements.
155 :type show_element_attributes: bool
156 :param show_relation_attributes: shows attributes of relations.
157 :type show_relation_attributes: bool
158 :returns: :class:`pydot.Dot` -- the Dot object.
159 """
160 if direction not in {'BT', 'TB', 'LR', 'RL'}:
161 # Invalid direction is provided
162 direction = 'BT' # reset it to the default value
163 maindot = pydot.Dot(graph_type='digraph', rankdir=direction, charset='utf-8')
164
165 node_map = {}
166 count = [0, 0, 0, 0] # counters for node ids
167
168 def _bundle_to_dot(dot, bundle):
169 def _attach_attribute_annotation(node, record):
170 # Adding a node to show all attributes
171 attributes = list(
172 (attr_name, value) for attr_name, value in record.attributes
173 if attr_name not in PROV_ATTRIBUTE_QNAMES
174 )
175
176 if not attributes:
177 return # No attribute to display
178
179 # Sort the attributes.
180 attributes = sorted_attributes(record.get_type(), attributes)
181
182 ann_rows = [ANNOTATION_START_ROW]
183 ann_rows.extend(
184 ANNOTATION_ROW_TEMPLATE % (
185 attr.uri, escape(six.text_type(attr)),
186 ' href=\"%s\"' % value.uri if isinstance(value, Identifier)
187 else '',
188 escape(six.text_type(value)
189 if not isinstance(value, datetime) else
190 six.text_type(value.isoformat())))
191 for attr, value in attributes
192 )
193 ann_rows.append(ANNOTATION_END_ROW)
194 count[3] += 1
195 annotations = pydot.Node(
196 'ann%d' % count[3], label='\n'.join(ann_rows),
197 **ANNOTATION_STYLE
198 )
199 dot.add_node(annotations)
200 dot.add_edge(pydot.Edge(annotations, node, **ANNOTATION_LINK_STYLE))
201
202 def _add_bundle(bundle):
203 count[2] += 1
204 subdot = pydot.Cluster(
205 graph_name='c%d' % count[2], URL='"%s"' % bundle.identifier.uri
206 )
207 if use_labels:
208 if bundle.label == bundle.identifier:
209 bundle_label = '"%s"' % six.text_type(bundle.label)
210 else:
211 # Fancier label if both are different. The label will be
212 # the main node text, whereas the identifier will be a
213 # kind of subtitle.
214 bundle_label = ('<%s<br />'
215 '<font color="#333333" point-size="10">'
216 '%s</font>>')
217 bundle_label = bundle_label % (
218 six.text_type(bundle.label),
219 six.text_type(bundle.identifier)
220 )
221 subdot.set_label('"%s"' % six.text_type(bundle_label))
222 else:
223 subdot.set_label('"%s"' % six.text_type(bundle.identifier))
224 _bundle_to_dot(subdot, bundle)
225 dot.add_subgraph(subdot)
226 return subdot
227
228 def _add_node(record):
229 count[0] += 1
230 node_id = 'n%d' % count[0]
231 if use_labels:
232 if record.label == record.identifier:
233 node_label = '"%s"' % six.text_type(record.label)
234 else:
235 # Fancier label if both are different. The label will be
236 # the main node text, whereas the identifier will be a
237 # kind of subtitle.
238 node_label = ('<%s<br />'
239 '<font color="#333333" point-size="10">'
240 '%s</font>>')
241 node_label = node_label % (six.text_type(record.label),
242 six.text_type(record.identifier))
243 else:
244 node_label = '"%s"' % six.text_type(record.identifier)
245
246 uri = record.identifier.uri
247 style = DOT_PROV_STYLE[record.get_type()]
248 node = pydot.Node(
249 node_id, label=node_label, URL='"%s"' % uri, **style
250 )
251 node_map[uri] = node
252 dot.add_node(node)
253
254 if show_element_attributes:
255 _attach_attribute_annotation(node, rec)
256 return node
257
258 def _add_generic_node(qname):
259 count[0] += 1
260 node_id = 'n%d' % count[0]
261 node_label = '"%s"' % six.text_type(qname)
262
263 uri = qname.uri
264 style = DOT_PROV_STYLE[0]
265 node = pydot.Node(
266 node_id, label=node_label, URL='"%s"' % uri, **style
267 )
268 node_map[uri] = node
269 dot.add_node(node)
270 return node
271
272 def _get_bnode():
273 count[1] += 1
274 bnode_id = 'b%d' % count[1]
275 bnode = pydot.Node(
276 bnode_id, label='""', shape='point', color='gray'
277 )
278 dot.add_node(bnode)
279 return bnode
280
281 def _get_node(qname):
282 if qname is None:
283 return _get_bnode()
284 uri = qname.uri
285 if uri not in node_map:
286 _add_generic_node(qname)
287 return node_map[uri]
288
289 records = bundle.get_records()
290 relations = []
291 for rec in records:
292 if rec.is_element():
293 _add_node(rec)
294 else:
295 # Saving the relations for later processing
296 relations.append(rec)
297
298 if not bundle.is_bundle():
299 for bundle in bundle.bundles:
300 _add_bundle(bundle)
301
302 for rec in relations:
303 args = rec.args
304 # skipping empty records
305 if not args:
306 continue
307 # picking element nodes
308 nodes = [
309 value for attr_name, value in rec.formal_attributes
310 if attr_name in PROV_ATTRIBUTE_QNAMES
311 ]
312 other_attributes = [
313 (attr_name, value) for attr_name, value in rec.attributes
314 if attr_name not in PROV_ATTRIBUTE_QNAMES
315 ]
316 add_attribute_annotation = (
317 show_relation_attributes and other_attributes
318 )
319 add_nary_elements = len(nodes) > 2 and show_nary
320 style = DOT_PROV_STYLE[rec.get_type()]
321 if len(nodes) < 2: # too few elements for a relation?
322 continue # cannot draw this
323
324 if add_nary_elements or add_attribute_annotation:
325 # a blank node for n-ary relations or the attribute annotation
326 bnode = _get_bnode()
327
328 # the first segment
329 dot.add_edge(
330 pydot.Edge(
331 _get_node(nodes[0]), bnode, arrowhead='none', **style
332 )
333 )
334 style = dict(style) # copy the style
335 del style['label'] # not showing label in the second segment
336 # the second segment
337 dot.add_edge(pydot.Edge(bnode, _get_node(nodes[1]), **style))
338 if add_nary_elements:
339 style['color'] = 'gray' # all remaining segment to be gray
340 for node in nodes[2:]:
341 if node is not None:
342 dot.add_edge(
343 pydot.Edge(bnode, _get_node(node), **style)
344 )
345 if add_attribute_annotation:
346 _attach_attribute_annotation(bnode, rec)
347 else:
348 # show a simple binary relations with no annotation
349 dot.add_edge(
350 pydot.Edge(
351 _get_node(nodes[0]), _get_node(nodes[1]), **style
352 )
353 )
354
355 try:
356 unified = bundle.unified()
357 except ProvException:
358 # Could not unify this bundle
359 # try the original document anyway
360 unified = bundle
361
362 _bundle_to_dot(maindot, unified)
363 return maindot