From 4096382d69a432b738a4dcc38601057fefe4a901 Mon Sep 17 00:00:00 2001 From: kary zheng Date: Tue, 4 Aug 2026 22:43:59 -0700 Subject: [PATCH 1/2] fix(visualization): take the union of Network Graph's two node columns The node set came from set(sources + destinations). On two pandas Series + is element-wise, so the set held each source glued to its destination rather than the union of the columns, and those glued values were added to the graph as nodes; the genuine nodes only arrived afterwards, with the edges. Every graph therefore carried one unconnected dot per distinct source-destination pair, reporting zero connections and indistinguishable from real data, and a pair of columns with different types aborted the run outright. Take the union instead, in first-appearance order: a set iterates strings in an order that varies between processes, so the node sequence would shift from run to run. Closes #7325 Co-Authored-By: Claude Opus 5 (1M context) --- .../visualization/networkGraph/NetworkGraphOpDesc.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/networkGraph/NetworkGraphOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/networkGraph/NetworkGraphOpDesc.scala index ccbf51c7377..789a6b5bb3c 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/networkGraph/NetworkGraphOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/networkGraph/NetworkGraphOpDesc.scala @@ -98,7 +98,9 @@ class NetworkGraphOpDesc extends PythonOperatorDescriptor { | if not table.empty: | sources = table[$source] | destinations = table[$destination] - | nodes = set(sources + destinations) + | # Union of the two columns, in first-appearance order. Adding the + | # Series pairs them off element-wise; a set reorders per run. + | nodes = list(dict.fromkeys(pd.concat([sources, destinations]).tolist())) | G = nx.Graph() | for node in nodes: | G.add_node(node) From f4f33f52cd1e03b765e1f48aeec06f4a18a1ce95 Mon Sep 17 00:00:00 2001 From: kary zheng Date: Wed, 5 Aug 2026 21:10:16 -0700 Subject: [PATCH 2/2] test(visualization): pin Network Graph's node set to the union Reverting the one-line change leaves this the only failing case, so the expression is held rather than merely written once. It also pins the ordered de-duplication: a set would satisfy "union" while reordering the nodes between processes, which is not what the operator needs. Co-Authored-By: Claude Opus 5 (1M context) --- .../networkGraph/NetworkGraphOpDescSpec.scala | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/networkGraph/NetworkGraphOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/networkGraph/NetworkGraphOpDescSpec.scala index 840b62b6431..fc666c61abd 100644 --- a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/networkGraph/NetworkGraphOpDescSpec.scala +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/networkGraph/NetworkGraphOpDescSpec.scala @@ -85,4 +85,19 @@ class NetworkGraphOpDescSpec extends AnyFlatSpec with BeforeAndAfter with Matche assert(carries(code, "My Graph")) code should include("class ProcessTableOperator(UDFTableOperator)") } + + it should "build the node set as a union rather than by adding the two columns" in { + opDesc.source = "from_node" + opDesc.destination = "to_node" + val code = opDesc.generatePythonCode() + + // `sources + destinations` is element-wise on two Series, so it glued each + // source to its destination and those strings entered the graph as nodes. + code should not include "set(sources + destinations)" + code should include("pd.concat([sources, destinations])") + + // Ordered de-duplication, not a set: a set iterates strings in an order that + // varies between processes, which would move the nodes from run to run. + code should include("dict.fromkeys") + } }