Skip to content

Commit 6b2d8d4

Browse files
paulschattTexeraTexeraGspikeHalo
authored andcommitted
Add Network Graph operator for visualizing graph structured data (#3207)
### Purpose Add Network Graph operator to visualize graph structured data and network topologies. The operator gets a table as an input, where one column is specified as source and another as destination. Each entry in a column is a node and the source and destination entry in a row form an edge. ### Changes Added NetworkGraphOpDesc Added Icon for operator CSV file used for testing: [musae_git_edges.csv](https://github.com/user-attachments/files/18388032/musae_git_edges.csv) https://github.com/user-attachments/assets/9a41a7ab-27fa-4f38-98ec-958874ca08fe --------- --------- Co-authored-by: Texera <[email protected]> Co-authored-by: Texera <[email protected]> Co-authored-by: GspikeHalo <[email protected]> Co-authored-by: gspikehalo <[email protected]>
1 parent 36492a8 commit 6b2d8d4

File tree

3 files changed

+177
-0
lines changed

3 files changed

+177
-0
lines changed
16.2 KB
Loading

core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/LogicalOp.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ import edu.uci.ics.amber.operator.visualization.hierarchychart.HierarchyChartOpD
9191
import edu.uci.ics.amber.operator.visualization.histogram.HistogramChartOpDesc
9292
import edu.uci.ics.amber.operator.visualization.htmlviz.HtmlVizOpDesc
9393
import edu.uci.ics.amber.operator.visualization.lineChart.LineChartOpDesc
94+
import edu.uci.ics.amber.operator.visualization.networkGraph.NetworkGraphOpDesc
9495
import edu.uci.ics.amber.operator.visualization.pieChart.PieChartOpDesc
9596
import edu.uci.ics.amber.operator.visualization.quiverPlot.QuiverPlotOpDesc
9697
import edu.uci.ics.amber.operator.visualization.sankeyDiagram.SankeyDiagramOpDesc
@@ -187,6 +188,7 @@ trait StateTransferFunc
187188
new Type(value = classOf[DumbbellPlotOpDesc], name = "DumbbellPlot"),
188189
new Type(value = classOf[DummyOpDesc], name = "Dummy"),
189190
new Type(value = classOf[BoxPlotOpDesc], name = "BoxPlot"),
191+
new Type(value = classOf[NetworkGraphOpDesc], name = "NetworkGraph"),
190192
new Type(value = classOf[HistogramChartOpDesc], name = "Histogram"),
191193
new Type(value = classOf[ScatterMatrixChartOpDesc], name = "ScatterMatrixChart"),
192194
new Type(value = classOf[HeatMapOpDesc], name = "HeatMap"),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package edu.uci.ics.amber.operator.visualization.networkGraph
2+
3+
import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}
4+
import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}
5+
import edu.uci.ics.amber.core.tuple.{AttributeType, Schema}
6+
import edu.uci.ics.amber.core.workflow.OutputPort.OutputMode
7+
import edu.uci.ics.amber.core.workflow.{InputPort, OutputPort, PortIdentity}
8+
import edu.uci.ics.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}
9+
import edu.uci.ics.amber.operator.metadata.annotations.AutofillAttributeName
10+
import edu.uci.ics.amber.operator.PythonOperatorDescriptor
11+
12+
class NetworkGraphOpDesc extends PythonOperatorDescriptor {
13+
@JsonProperty(required = true)
14+
@JsonSchemaTitle("Source Column")
15+
@JsonPropertyDescription("Source node for edge in graph")
16+
@AutofillAttributeName
17+
var source: String = ""
18+
19+
@JsonProperty(required = true)
20+
@JsonSchemaTitle("Destination Column")
21+
@JsonPropertyDescription("Destination node for edge in graph")
22+
@AutofillAttributeName
23+
var destination: String = ""
24+
25+
@JsonProperty(defaultValue = "Network Graph")
26+
@JsonSchemaTitle("Title")
27+
var title: String = ""
28+
29+
override def getOutputSchemas(
30+
inputSchemas: Map[PortIdentity, Schema]
31+
): Map[PortIdentity, Schema] = {
32+
val outputSchema = Schema()
33+
.add("html-content", AttributeType.STRING)
34+
Map(operatorInfo.outputPorts.head.id -> outputSchema)
35+
Map(operatorInfo.outputPorts.head.id -> outputSchema)
36+
}
37+
38+
override def operatorInfo: OperatorInfo =
39+
OperatorInfo(
40+
"Network Graph",
41+
"Visualize data in a network graph",
42+
OperatorGroupConstants.VISUALIZATION_GROUP,
43+
inputPorts = List(InputPort()),
44+
outputPorts = List(OutputPort(mode = OutputMode.SINGLE_SNAPSHOT))
45+
)
46+
def manipulateTable(): String = {
47+
assert(source.nonEmpty)
48+
assert(destination.nonEmpty)
49+
s"""
50+
| table = table.dropna(subset = ['$source']) #remove missing values
51+
| table = table.dropna(subset = ['$destination']) #remove missing values
52+
|""".stripMargin
53+
}
54+
55+
override def generatePythonCode(): String = {
56+
val finalCode =
57+
s"""
58+
|from pytexera import *
59+
|import pandas as pd
60+
|import plotly.graph_objects as go
61+
|import plotly.io
62+
|import json
63+
|import pickle
64+
|import plotly
65+
|import networkx as nx
66+
|
67+
|class ProcessTableOperator(UDFTableOperator):
68+
| def render_error(self, error_msg):
69+
| return '''<h1>Network graph is not available.</h1>
70+
| <p>Reason is: {} </p>
71+
| '''.format(error_msg)
72+
|
73+
| @overrides
74+
| def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:
75+
| if not table.empty:
76+
| sources = table['$source']
77+
| destinations = table['$destination']
78+
| nodes = set(sources + destinations)
79+
| G = nx.Graph()
80+
| for node in nodes:
81+
| G.add_node(node)
82+
| for i, j in table.iterrows():
83+
| G.add_edges_from([(j['$source'], j['$destination'])])
84+
| pos = nx.spring_layout(G, k=0.5, iterations=50)
85+
| for n, p in pos.items():
86+
| G.nodes[n]['pos'] = p
87+
|
88+
| edge_trace = go.Scatter(
89+
| x=[],
90+
| y=[],
91+
| name='Edges',
92+
| line=dict(width=0.5, color='#888'),
93+
| hoverinfo='none',
94+
| mode='lines',
95+
| visible=True
96+
| )
97+
|
98+
| for edge in G.edges():
99+
| x0, y0 = G.nodes[edge[0]]['pos']
100+
| x1, y1 = G.nodes[edge[1]]['pos']
101+
| edge_trace['x'] += tuple([x0, x1, None])
102+
| edge_trace['y'] += tuple([y0, y1, None])
103+
|
104+
| node_trace = go.Scatter(
105+
| x=[],
106+
| y=[],
107+
| name='Nodes',
108+
| text=[],
109+
| mode='markers',
110+
| hoverinfo='text',
111+
| visible=True,
112+
| marker=dict(
113+
| showscale=True,
114+
| colorscale='plasma',
115+
| reversescale=True,
116+
| color=[],
117+
| size=15,
118+
| colorbar=dict(
119+
| thickness=10,
120+
| title='Node Connections',
121+
| xanchor='left',
122+
| titleside='right'
123+
| ),
124+
| line=dict(width=0)
125+
| )
126+
| )
127+
|
128+
| for node in G.nodes():
129+
| x, y = G.nodes[node]['pos']
130+
| node_trace['x'] += tuple([x])
131+
| node_trace['y'] += tuple([y])
132+
|
133+
| for node, adjacencies in enumerate(G.adjacency()):
134+
| node_trace['marker']['color'] += tuple([len(adjacencies[1])])
135+
| node_info = str(adjacencies[0]) + ': ' + str(len(adjacencies[1])) + ' connections.'
136+
| node_trace['text'] += tuple([node_info])
137+
|
138+
| fig = go.Figure(
139+
| data=[edge_trace, node_trace],
140+
| layout=go.Layout(
141+
| title='<br>$title',
142+
| hovermode='closest',
143+
| showlegend=False,
144+
| margin=dict(b=20, l=5, r=5, t=40),
145+
| annotations=[
146+
| dict(
147+
| text='',
148+
| showarrow=False,
149+
| xref="paper",
150+
| yref="paper"
151+
| )
152+
| ],
153+
| xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
154+
| yaxis=dict(showgrid=False, zeroline=False, showticklabels=False)
155+
| )
156+
| )
157+
| fig.update_layout(
158+
| margin=dict(l=0, r=0, t=0, b=0),
159+
| legend=dict(
160+
| itemclick=False,
161+
| itemdoubleclick=False
162+
| )
163+
| )
164+
|
165+
| html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)
166+
| else:
167+
| html = self.render_error('Table should not have any empty/null values or fields.')
168+
|
169+
| yield {'html-content': html}
170+
|
171+
|""".stripMargin
172+
finalCode
173+
}
174+
175+
}

0 commit comments

Comments
 (0)