Skip to content

Commit 4075a76

Browse files
authored
Visualizer changes (#20)
1 parent 4b647c8 commit 4075a76

File tree

3 files changed

+282
-196
lines changed

3 files changed

+282
-196
lines changed
Lines changed: 151 additions & 196 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
import base64
2+
3+
from sio3pack.visualizer import cytoscope
4+
15
try:
26
import dash
37
import dash_cytoscape as cyto
4-
from dash import Input, Output, html
8+
from dash import Input, Output, State, dcc, html
59
except ImportError:
610
raise ImportError("Please install the 'dash' and 'dash-cytoscape' packages to use the visualizer.")
711

@@ -11,216 +15,167 @@
1115

1216

1317
def main():
14-
if len(sys.argv) != 2:
15-
print("Usage: python -m sio3pack.visualizer <workflow file>")
16-
sys.exit(1)
17-
file_path = sys.argv[1]
18-
if not file_path.endswith(".json"):
19-
print("The file must be a JSON file.")
20-
sys.exit(1)
21-
if not os.path.isfile(file_path):
22-
print("The file does not exist.")
23-
sys.exit(1)
24-
25-
graph = json.load(open(file_path))
26-
elements = []
27-
ins = {}
28-
rendered_registers = set()
29-
30-
# Create nodes for observable registers.
31-
for register in range(graph["observable_registers"]):
32-
elements.append(
33-
{
34-
"data": {
35-
"id": f"obs_register_{register}",
36-
"label": f"Observable register {register}",
37-
"info": "This is an observable register. It's an output of a workflow.",
38-
},
39-
"classes": "register",
40-
}
41-
)
42-
ins[register] = [f"obs_register_{register}"]
43-
rendered_registers.add(register)
44-
45-
script_i = 0
46-
execution_i = 0
47-
# First pass to create nodes and mark input registers.
48-
for task in graph["tasks"]:
49-
if task["type"] == "script":
50-
id = f"script_{script_i}"
51-
elements.append(
52-
{"data": {"id": id, "label": task.get("name", f"Script {script_i}"), "info": task}, "classes": "script"}
53-
)
54-
if task["reactive"]:
55-
elements[-1]["classes"] += " reactive"
56-
script_i += 1
57-
for register in task["input_registers"]:
58-
if register not in ins:
59-
ins[register] = []
60-
ins[register].append(id)
61-
elif task["type"] == "execution":
62-
id = f"execution_{execution_i}"
63-
elements.append(
64-
{
65-
"data": {"id": id, "label": task.get("name", f"Execution {execution_i}"), "info": task},
66-
"classes": "execution",
67-
}
68-
)
69-
if task["exclusive"]:
70-
elements[-1]["classes"] += " exclusive"
71-
72-
# To delete, final spec is different
73-
if "input_register" in task:
74-
register = task["input_register"]
75-
if register not in ins:
76-
ins[register] = []
77-
ins[register].append(id)
78-
execution_i += 1
79-
80-
# Second pass to create edges.
81-
script_i = 0
82-
execution_i = 0
83-
for task in graph["tasks"]:
84-
if task["type"] == "script":
85-
registers = task["output_registers"]
86-
elif task["type"] == "execution":
87-
registers = [task["output_register"]]
88-
else:
89-
raise
90-
91-
for register in registers:
92-
if register not in ins:
93-
elements.append(
94-
{
95-
"data": {
96-
"id": f"register_{register}",
97-
"label": f"Register {register}",
98-
"info": f"This is a register. It's an intermediate value in a workflow.",
99-
},
100-
"classes": "register",
101-
}
102-
)
103-
ins[register] = [f"register_{register}"]
104-
rendered_registers.add(register)
105-
for id in ins[register]:
106-
if task["type"] == "script":
107-
elements.append(
108-
{
109-
"data": {
110-
"source": f"script_{script_i}",
111-
"target": id,
112-
}
113-
}
114-
)
115-
elif task["type"] == "execution":
116-
elements.append(
117-
{
118-
"data": {
119-
"source": f"execution_{execution_i}",
120-
"target": id,
121-
}
122-
}
123-
)
124-
if register not in rendered_registers:
125-
elements[-1]["data"]["label"] = f"via register {register}"
126-
127-
if task["type"] == "script":
128-
script_i += 1
129-
elif task["type"] == "execution":
130-
execution_i += 1
131-
13218
app = dash.Dash(__name__)
13319
app.layout = html.Div(
13420
[
13521
html.Div(
13622
[
137-
cyto.Cytoscape(
138-
id="cytoscape",
139-
layout={"name": "breadthfirst", "directed": True},
140-
style={"width": "100%", "height": "100vh"},
141-
elements=elements,
142-
stylesheet=[
143-
{
144-
"selector": "node",
145-
"style": {
146-
"label": "data(label)",
147-
"text-valign": "center",
148-
"text-margin-y": "-20px",
149-
},
150-
},
151-
{
152-
"selector": "edge",
153-
"style": {
154-
"curve-style": "bezier", # Makes edges curved for better readability
155-
"target-arrow-shape": "triangle", # Adds an arrowhead to indicate direction
156-
"arrow-scale": 1.5, # Makes the arrow larger
157-
"line-color": "#0074D9", # Edge color
158-
"target-arrow-color": "#0074D9", # Arrow color
159-
"width": 2, # Line thickness
160-
"content": "data(label)", # Show edge label on hover
161-
"font-size": "12px",
162-
"color": "#ff4136",
163-
"text-background-opacity": 1,
164-
"text-background-color": "white",
165-
"text-background-shape": "roundrectangle",
166-
"text-border-opacity": 1,
167-
"text-border-width": 1,
168-
"text-border-color": "#ff4136",
169-
},
170-
},
171-
{
172-
"selector": ".register",
173-
"style": {
174-
"shape": "rectangle",
175-
},
176-
},
177-
{
178-
"selector": ".script",
179-
"style": {
180-
"shape": "roundrectangle",
181-
},
182-
},
183-
{
184-
"selector": ".execution",
185-
"style": {
186-
"shape": "ellipse",
187-
},
188-
},
189-
{
190-
"selector": ".reactive",
191-
"style": {
192-
"background-color": "#ff851b",
193-
},
194-
},
195-
{
196-
"selector": ".exclusive",
197-
"style": {
198-
"background-color": "#ff4136",
23+
html.Div(
24+
[],
25+
style={"flex": "3", "height": "100vh"},
26+
id="graph-div",
27+
),
28+
html.Div(
29+
[
30+
html.Pre(
31+
id="node-data",
32+
style={
33+
"padding": "10px",
34+
"whiteSpace": "pre",
35+
"overflow": "auto",
36+
"maxHeight": "95vh",
37+
"maxWidth": "100%",
19938
},
200-
},
39+
)
20140
],
41+
style={"flex": "1", "height": "100vh", "backgroundColor": "#f7f7f7"},
20242
),
20343
],
204-
style={"flex": "3", "height": "100vh"},
44+
id="graph",
45+
style={"display": "flex", "flexDirection": "row", "height": "100vh"},
20546
),
20647
html.Div(
20748
[
208-
html.Pre(
209-
id="node-data",
210-
style={
211-
"padding": "10px",
212-
"white-space": "pre",
213-
"overflow": "auto",
214-
"max-height": "95vh",
215-
"max-width": "100%",
216-
},
217-
)
49+
html.Div(
50+
[
51+
html.H1("SIO3Worker Visualizer"),
52+
html.P(
53+
"This is a visualizer for SIO3Worker's graph representation. <br>"
54+
"Paste a JSON representation of the workflow in the text area below or upload a file."
55+
),
56+
],
57+
style={"padding": "10px", "backgroundColor": "#f7f7f7"},
58+
),
59+
html.Div(
60+
[
61+
dcc.Textarea(id="graph-input", placeholder="JSON description of the workflow"),
62+
dcc.Upload(
63+
id="graph-file",
64+
children=html.Button("Upload File"),
65+
multiple=False,
66+
),
67+
html.Button("Load", id="load-button", n_clicks=0),
68+
]
69+
),
21870
],
219-
style={"flex": "1", "height": "100vh", "background-color": "#f7f7f7"},
71+
id="input-container",
22072
),
73+
]
74+
)
75+
76+
@app.callback(
77+
[
78+
Output("graph", "style"),
79+
Output("graph-div", "children"),
80+
Output("input-container", "style"),
81+
],
82+
Input("load-button", "n_clicks"),
83+
[
84+
State("graph-input", "value"),
85+
State("graph-file", "contents"),
22186
],
222-
style={"display": "flex", "flex-direction": "row", "height": "100vh"},
22387
)
88+
def show_graph(n_clicks, value, contents):
89+
if n_clicks > 0:
90+
if not value and not contents:
91+
return {"display": "flex"}, [], {"display": "block"}
92+
if value:
93+
file_content = value
94+
else:
95+
try:
96+
content_type, content_string = contents.split(",")
97+
file_content = base64.b64decode(content_string).decode("utf-8")
98+
except Exception as e:
99+
print(e)
100+
return {"display": "flex"}, [], {"display": "block"}
101+
graph = json.loads(file_content)
102+
elements = cytoscope.get_elements(graph)
103+
instance = cyto.Cytoscape(
104+
id="cytoscape",
105+
layout={"name": "breadthfirst", "directed": True},
106+
style={"width": "100%", "height": "100vh"},
107+
elements=elements,
108+
stylesheet=[
109+
{
110+
"selector": "node",
111+
"style": {
112+
"label": "data(label)",
113+
"text-valign": "center",
114+
"text-margin-y": "-20px",
115+
},
116+
},
117+
{
118+
"selector": "edge",
119+
"style": {
120+
"curve-style": "bezier", # Makes edges curved for better readability
121+
"target-arrow-shape": "triangle", # Adds an arrowhead to indicate direction
122+
"arrow-scale": 1.5, # Makes the arrow larger
123+
"line-color": "#0074D9", # Edge color
124+
"target-arrow-color": "#0074D9", # Arrow color
125+
"width": 2, # Line thickness
126+
"content": "data(label)", # Show edge label on hover
127+
"font-size": "12px",
128+
"color": "#ff4136",
129+
"text-background-opacity": 1,
130+
"text-background-color": "white",
131+
"text-background-shape": "roundrectangle",
132+
"text-border-opacity": 1,
133+
"text-border-width": 1,
134+
"text-border-color": "#ff4136",
135+
},
136+
},
137+
{
138+
"selector": ".register",
139+
"style": {
140+
"shape": "rectangle",
141+
},
142+
},
143+
{
144+
"selector": ".script",
145+
"style": {
146+
"shape": "roundrectangle",
147+
},
148+
},
149+
{
150+
"selector": ".execution",
151+
"style": {
152+
"shape": "ellipse",
153+
},
154+
},
155+
{
156+
"selector": ".reactive",
157+
"style": {
158+
"background-color": "#ff851b",
159+
},
160+
},
161+
{
162+
"selector": ".exclusive",
163+
"style": {
164+
"background-color": "#ff4136",
165+
},
166+
},
167+
],
168+
)
169+
return (
170+
{"display": "flex", "flex-direction": "row", "height": "100vh"},
171+
instance,
172+
{"display": "none"},
173+
)
174+
return (
175+
{"display": "none"},
176+
None,
177+
{"display": "block"},
178+
)
224179

225180
@app.callback(Output("node-data", "children"), Input("cytoscape", "tapNodeData"))
226181
def display_task_info(data):
@@ -230,4 +185,4 @@ def display_task_info(data):
230185
return json.dumps(data["info"], indent=4)
231186
return data["info"]
232187

233-
app.run_server(debug=True)
188+
app.run()

0 commit comments

Comments
 (0)