This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-8080-b59c7406d0a7d549a50c1f86888e85563f2d3446 in repository https://gitbox.apache.org/repos/asf/texera.git
commit d9c0e19aeed027c4a61d746d875a8b46dd1db568 Author: Kary Zheng <[email protected]> AuthorDate: Sat Aug 29 00:00:53 2026 +0000 feat(dendrogram): say what it cannot cluster instead of ending the run (#8080) ### What changes were proposed in this PR? The operator answered an empty table with an error page and then handed the two configured columns to `ff.create_dendrogram` as they arrived. Two tables that pass that guard ended the run anyway: one with a blank in either coordinate, which scipy refuses with "The condensed distance matrix must contain only finite values", and one with a single row, which leaves it an empty distance matrix. It now drops the rows missing either coordinate and reports an emptied table with the wording Dumbbell Plot already uses, then answers a table with fewer than two rows. Only the coordinates are dropped on. A NaN anywhere in the point matrix makes scipy refuse the whole thing, while a blank label is just a blank tick on the axis and is no reason to lose the row. The row-count check reads after the drop, so a table the drop reduces to one row lands there rather than back in scipy. ### Any related issues, documentation, discussions? Closes #8079 ### How was this PR tested? Two cases added to DendrogramOpDescSpec, over the emitted code. I also ran the guarded body against each table in scipy: an empty table, a single row, three rows with one blank coordinate, a table the drop reduces to one row, a wholly blank coordinate column, two ordinary rows, and a row with a blank label. Each reaches the intended page, and the last two still draw. ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Claude Opus 5) Co-authored-by: Claude Opus 5 (1M context) <[email protected]> --- .../dendrogram/DendrogramOpDesc.scala | 11 ++++++++++ .../dendrogram/DendrogramOpDescSpec.scala | 25 ++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/dendrogram/DendrogramOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/dendrogram/DendrogramOpDesc.scala index f80d7acb5b..826cce9061 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/dendrogram/DendrogramOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/dendrogram/DendrogramOpDesc.scala @@ -127,6 +127,17 @@ class DendrogramOpDesc extends PythonOperatorDescriptor { | if table.empty: | yield {'html-content': self.render_error("input table is empty.")} | return + | # A row missing either coordinate has no position to cluster from, and + | # scipy refuses a NaN anywhere in the distance matrix. + | table = table.dropna(subset=[$xVal, $yVal]) #remove missing values + | if table.empty: + | yield {'html-content': self.render_error("input table has no rows with all of the configured columns filled in.")} + | return + | # Clustering starts from the distances between rows, so a single row + | # leaves scipy an empty distance matrix and it raises rather than draws. + | if len(table) < 2: + | yield {'html-content': self.render_error("input table has fewer than two rows to cluster.")} + | return | ${createDendrogram()} | # convert fig to html content | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/dendrogram/DendrogramOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/dendrogram/DendrogramOpDescSpec.scala index b56ffae273..9851e27596 100644 --- a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/dendrogram/DendrogramOpDescSpec.scala +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/dendrogram/DendrogramOpDescSpec.scala @@ -96,6 +96,31 @@ class DendrogramOpDescSpec extends AnyFlatSpec with BeforeAndAfter with Matchers code should not include "color_threshold=None" } + it should "drop the rows a blank coordinate leaves unusable, and answer an emptied table" in { + // The subset names both coordinates, and only those: a NaN anywhere in the + // point matrix makes scipy refuse the whole distance matrix, while a blank + // label is only a blank tick on the axis. + opDesc.xVal = "coord_a" + opDesc.yVal = "coord_b" + opDesc.labels = "label_col" + val code = opDesc.generatePythonCode() + code should include("table = table.dropna(subset=[") + assert(carries(code, "coord_a")) + assert(carries(code, "coord_b")) + code should include("input table has no rows with all of the configured columns filled in.") + } + + it should "answer a table with too few rows to cluster" in { + // One row leaves an empty distance matrix, which scipy raises on rather than + // drawing an empty picture. + opDesc.xVal = "coord_a" + opDesc.yVal = "coord_b" + opDesc.labels = "label_col" + val code = opDesc.generatePythonCode() + code should include("if len(table) < 2:") + code should include("fewer than two rows to cluster") + } + /** Reads the shapes a stored workflow can hold. Without `contentAs` a JSON string * stays unconverted inside the Option and the first use throws. */
