Skip to content

Commit f761a98

Browse files
authored
Merge pull request #9 from ytlee93/master
change: lineage query visualization experience enhancement
2 parents 169dd30 + e119009 commit f761a98

File tree

3 files changed

+119
-15
lines changed

3 files changed

+119
-15
lines changed

src/sagemaker/lineage/query.py

Lines changed: 85 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from datetime import datetime
1717
from enum import Enum
1818
from typing import Optional, Union, List, Dict
19+
import re
1920

2021
from sagemaker.lineage._utils import get_resource_name_from_arn, get_module
2122

@@ -260,6 +261,8 @@ def __init__(self, graph_styles):
260261
(
261262
self.Network,
262263
self.Options,
264+
self.IFrame,
265+
self.BeautifulSoup,
263266
) = self._import_visual_modules()
264267

265268
self.graph_styles = graph_styles
@@ -300,13 +303,60 @@ def _import_visual_modules(self):
300303
get_module("pyvis")
301304
from pyvis.network import Network
302305
from pyvis.options import Options
306+
from IPython.display import IFrame
303307

304-
return Network, Options
308+
get_module("bs4")
309+
from bs4 import BeautifulSoup
310+
311+
return Network, Options, IFrame, BeautifulSoup
305312

306313
def _node_color(self, entity):
307314
"""Return node color by background-color specified in graph styles."""
308315
return self.graph_styles[entity]["style"]["background-color"]
309316

317+
def _get_legend_line(self, component_name):
318+
"""Generate lengend div line for each graph component in graph_styles."""
319+
if self.graph_styles[component_name]["isShape"] == "False":
320+
return '<div><div style="background-color: {color}; width: 1.6vw; height: 1.6vw;\
321+
display: inline-block; font-size: 1.5vw; vertical-align: -0.2em;"></div>\
322+
<div style="width: 0.3vw; height: 1.5vw; display: inline-block;"></div>\
323+
<div style="display: inline-block; font-size: 1.5vw;">{name}</div></div>'.format(
324+
color=self.graph_styles[component_name]["style"]["background-color"],
325+
name=self.graph_styles[component_name]["name"],
326+
)
327+
else:
328+
return '<div style="background-color: #ffffff; width: 1.6vw; height: 1.6vw;\
329+
display: inline-block; font-size: 0.9vw; vertical-align: -0.2em;">{shape}</div>\
330+
<div style="width: 0.3vw; height: 1.5vw; display: inline-block;"></div>\
331+
<div style="display: inline-block; font-size: 1.5vw;">{name}</div></div>'.format(
332+
shape=self.graph_styles[component_name]["style"]["shape"],
333+
name=self.graph_styles[component_name]["name"],
334+
)
335+
336+
def _add_legend(self, path):
337+
"""Embed legend to html file generated by pyvis."""
338+
f = open(path, "r")
339+
content = self.BeautifulSoup(f, "html.parser")
340+
341+
legend = """
342+
<div style="display: inline-block; font-size: 1vw; font-family: verdana;
343+
vertical-align: top; padding: 1vw;">
344+
"""
345+
# iterate through graph styles to get legend
346+
for component in self.graph_styles.keys():
347+
legend += self._get_legend_line(component_name=component)
348+
349+
legend += "</div>"
350+
351+
legend_div = self.BeautifulSoup(legend, "html.parser")
352+
353+
content.div.insert_after(legend_div)
354+
355+
html = content.prettify()
356+
357+
with open(path, "w", encoding="utf8") as file:
358+
file.write(html)
359+
310360
def render(self, elements, path="pyvisExample.html"):
311361
"""Render graph for lineage query result.
312362
@@ -325,23 +375,51 @@ def render(self, elements, path="pyvisExample.html"):
325375
display graph: The interactive visualization is presented as a static HTML file.
326376
327377
"""
328-
net = self.Network(height="500px", width="100%", notebook=True, directed=True)
378+
net = self.Network(height="600px", width="82%", notebook=True, directed=True)
329379
net.set_options(self._options)
330380

331381
# add nodes to graph
332382
for arn, source, entity, is_start_arn in elements["nodes"]:
383+
entity_text = re.sub(r"(\w)([A-Z])", r"\1 \2", entity)
384+
source = re.sub(r"(\w)([A-Z])", r"\1 \2", source)
385+
account_id = re.search(r":\d{12}:", arn)
386+
name = re.search(r"\/.*", arn)
387+
node_info = (
388+
"Entity: "
389+
+ entity_text
390+
+ "\nType: "
391+
+ source
392+
+ "\nAccount ID: "
393+
+ str(account_id.group()[1:-1])
394+
+ "\nName: "
395+
+ str(name.group()[1:])
396+
)
333397
if is_start_arn: # startarn
334398
net.add_node(
335-
arn, label=source, title=entity, color=self._node_color(entity), shape="star"
399+
arn,
400+
label=source,
401+
title=node_info,
402+
color=self._node_color(entity),
403+
shape="star",
404+
borderWidth=3,
336405
)
337406
else:
338-
net.add_node(arn, label=source, title=entity, color=self._node_color(entity))
407+
net.add_node(
408+
arn,
409+
label=source,
410+
title=node_info,
411+
color=self._node_color(entity),
412+
borderWidth=3,
413+
)
339414

340415
# add edges to graph
341416
for src, dest, asso_type in elements["edges"]:
342-
net.add_edge(src, dest, title=asso_type)
417+
net.add_edge(src, dest, title=asso_type, width=2)
418+
419+
net.write_html(path)
420+
self._add_legend(path)
343421

344-
return net.show(path)
422+
return self.IFrame(path, width="100%", height="600px")
345423

346424

347425
class LineageQueryResult(object):
@@ -391,7 +469,7 @@ def __str__(self):
391469
392470
"""
393471
return (
394-
"{\n"
472+
"{"
395473
+ "\n\n".join("'{}': {},".format(key, val) for key, val in self.__dict__.items())
396474
+ "\n}"
397475
)

tests/integ/sagemaker/lineage/test_lineage_visualize.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from __future__ import absolute_import
1515
import time
1616
import os
17+
import re
1718

1819
import pytest
1920

@@ -160,31 +161,56 @@ def test_graph_visualize(sagemaker_session, extract_data_from_html):
160161
"color": "#146eb4",
161162
"label": "Model",
162163
"shape": "star",
163-
"title": "Artifact",
164+
"title": "Entity: Artifact"
165+
+ "\nType: Model"
166+
+ "\nAccount ID: "
167+
+ str(re.search(r":\d{12}:", graph_startarn).group()[1:-1])
168+
+ "\nName: "
169+
+ str(re.search(r"\/.*", graph_startarn).group()[1:]),
164170
},
165171
image_artifact: {
166172
"color": "#146eb4",
167173
"label": "Image",
168174
"shape": "dot",
169-
"title": "Artifact",
175+
"title": "Entity: Artifact"
176+
+ "\nType: Image"
177+
+ "\nAccount ID: "
178+
+ str(re.search(r":\d{12}:", image_artifact).group()[1:-1])
179+
+ "\nName: "
180+
+ str(re.search(r"\/.*", image_artifact).group()[1:]),
170181
},
171182
dataset_artifact: {
172183
"color": "#146eb4",
173-
"label": "DataSet",
184+
"label": "Data Set",
174185
"shape": "dot",
175-
"title": "Artifact",
186+
"title": "Entity: Artifact"
187+
+ "\nType: Data Set"
188+
+ "\nAccount ID: "
189+
+ str(re.search(r":\d{12}:", dataset_artifact).group()[1:-1])
190+
+ "\nName: "
191+
+ str(re.search(r"\/.*", dataset_artifact).group()[1:]),
176192
},
177193
modeldeploy_action: {
178194
"color": "#88c396",
179-
"label": "ModelDeploy",
195+
"label": "Model Deploy",
180196
"shape": "dot",
181-
"title": "Action",
197+
"title": "Entity: Action"
198+
+ "\nType: Model Deploy"
199+
+ "\nAccount ID: "
200+
+ str(re.search(r":\d{12}:", modeldeploy_action).group()[1:-1])
201+
+ "\nName: "
202+
+ str(re.search(r"\/.*", modeldeploy_action).group()[1:]),
182203
},
183204
endpoint_context: {
184205
"color": "#ff9900",
185206
"label": "Endpoint",
186207
"shape": "dot",
187-
"title": "Context",
208+
"title": "Entity: Context"
209+
+ "\nType: Endpoint"
210+
+ "\nAccount ID: "
211+
+ str(re.search(r":\d{12}:", endpoint_context).group()[1:-1])
212+
+ "\nName: "
213+
+ str(re.search(r"\/.*", endpoint_context).group()[1:]),
188214
},
189215
}
190216

tests/unit/sagemaker/lineage/test_query.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,7 @@ def test_query_lineage_result_str(sagemaker_session):
580580

581581
assert (
582582
response_str
583-
== "{\n'edges': [\n\t{'source_arn': 'arn1', 'destination_arn': 'arn2', 'association_type': 'Produced'}],"
583+
== "{'edges': [\n\t{'source_arn': 'arn1', 'destination_arn': 'arn2', 'association_type': 'Produced'}],"
584584
+ "\n\n'vertices': [\n\t{'arn': 'arn1', 'lineage_entity': 'Artifact', 'lineage_source': 'Endpoint', "
585585
+ "'_session': <Mock id=''>}, \n\t{'arn': 'arn2', 'lineage_entity': 'Context', 'lineage_source': "
586586
+ "'Model', '_session': <Mock id=''>}],\n\n'startarn': "

0 commit comments

Comments
 (0)