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-7343-c7e7362a4022d41238227f47ec474dadd225150e
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 10c46f1e4d31893e5f9d12e7c19ff98e79932d04
Author: Kary Zheng <[email protected]>
AuthorDate: Thu Aug 13 05:08:17 2026 +0000

    feat(contour-plot): plot at the default grid size when the field is left 
alone (#7343)
    
    ### What changes were proposed in this PR?
    
    Contour Plot's Grid Size is declared an optional string, but the
    generated code consumes it as `int(<value>)` with no guard, so the only
    content the operator accepts is an integer literal. Typing `2.5` into a
    field described as "Grid resolution of the final image" aborts the run
    with `ValueError: invalid literal for int() with base 10: '2.5'`, and
    the message names no field, so nothing points back to Grid Size. Any
    non-integer text does the same.
    
    The field is now an `Option[Int]` falling back to the documented default
    of 10, and the number is emitted directly rather than wrapped in
    `int()`. The form renders it as a numeric input, so the free text that
    reached `int()` no longer exists.
    
    `contentAs` names the boxed class because `Option` erases its element
    type; without it a blank would read as 0 rather than as absent.
    
    ### Any related issues, documentation, discussions?
    
    Closes #7212.
    
    Split out of #7233, which covers the same numeric-settings gap in Bullet
    Chart and Gauge Chart.
    
    ### How was this PR tested?
    
    `ContourPlotOpDescSpec` covers it: the generated code carries the grid
    size as a number, falling back to 10 when the field is unset, and emits
    an explicit value as itself. It also pins the deserialization — a JSON
    number, the numeric string a workflow saved before the field was
    numeric, and an absent value read as unset rather than as zero.
    
    ```
    sbt "WorkflowOperator/testOnly 
org.apache.texera.amber.operator.visualization.contourPlot.ContourPlotOpDescSpec"
    ```
    
    Nine cases, all passing.
    
    In the UI on main, a Contour Plot fed three numeric columns with Grid
    Size set to `2.5` aborts the run:
    
    <img width="1290" height="918" alt="Screenshot 2026-08-07 at 3 16 14 PM"
    
src="https://github.com/user-attachments/assets/f93c8403-6eac-427c-b16f-9b7af858e786";
    />
    
    With this PR the field only accepts a number and the generated code no
    longer calls `int()` at all.
    
    ### 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]>
---
 .../contourPlot/ContourPlotOpDesc.scala            | 19 +++++++--
 .../contourPlot/ContourPlotOpDescSpec.scala        | 49 ++++++++++++++++++++--
 2 files changed, 61 insertions(+), 7 deletions(-)

diff --git 
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/contourPlot/ContourPlotOpDesc.scala
 
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/contourPlot/ContourPlotOpDesc.scala
index 3ff542e5d0..e548ea389e 100644
--- 
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/contourPlot/ContourPlotOpDesc.scala
+++ 
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/contourPlot/ContourPlotOpDesc.scala
@@ -20,10 +20,11 @@
 package org.apache.texera.amber.operator.visualization.contourPlot
 
 import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize
 import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, 
JsonSchemaTitle}
 import org.apache.texera.amber.core.tuple.{AttributeType, Schema}
 import 
org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext
-import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString
+import org.apache.texera.amber.pybuilder.PyStringTypes.{EncodableString, 
PythonLiteral}
 import org.apache.texera.amber.core.workflow.PortIdentity
 import org.apache.texera.amber.operator.PythonOperatorDescriptor
 import 
org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName
@@ -65,10 +66,13 @@ class ContourPlotOpDesc extends PythonOperatorDescriptor {
   @NotNull(message = "z cannot be empty")
   var z: EncodableString = ""
 
+  // Numeric: only used as int(). contentAs names the boxed class — Option 
erases
+  // its element type, and a blank must not read as 0.
   @JsonProperty(required = false, defaultValue = "10")
   @JsonSchemaTitle("Grid Size")
   @JsonPropertyDescription("Grid resolution of the final image")
-  var gridSize: EncodableString = ""
+  @JsonDeserialize(contentAs = classOf[Integer])
+  var gridSize: Option[Int] = None
 
   @JsonProperty(required = false, defaultValue = "true")
   @JsonSchemaTitle("Connect Gaps")
@@ -98,6 +102,9 @@ class ContourPlotOpDesc extends PythonOperatorDescriptor {
     )
 
   override def generatePythonCode(): String = {
+    // A number in the generated code, so it needs no int() around it.
+    val gridSizeLiteral: PythonLiteral =
+      gridSize.getOrElse(ContourPlotOpDesc.DefaultGridSize).toString
     pyb"""from pytexera import *
        |import numpy as np
        |import plotly.graph_objects as go
@@ -111,7 +118,7 @@ class ContourPlotOpDesc extends PythonOperatorDescriptor {
        |        x = table[$x].values
        |        y = table[$y].values
        |        z = table[$z].values
-       |        grid_size = int($gridSize)
+       |        grid_size = $gridSizeLiteral
        |        connGaps = True if '$connectGaps' == 'true' else False
        |
        |        grid_x, grid_y = np.meshgrid(np.linspace(min(x), max(x), 
grid_size), np.linspace(min(y), max(y), grid_size))
@@ -131,3 +138,9 @@ class ContourPlotOpDesc extends PythonOperatorDescriptor {
        |""".encode
   }
 }
+
+object ContourPlotOpDesc {
+
+  /** Matches the form's `defaultValue`, so an unset Grid Size plots at 10. */
+  private val DefaultGridSize: Int = 10
+}
diff --git 
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/contourPlot/ContourPlotOpDescSpec.scala
 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/contourPlot/ContourPlotOpDescSpec.scala
index 8464b1f520..3a1fa4ba9b 100644
--- 
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/contourPlot/ContourPlotOpDescSpec.scala
+++ 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/contourPlot/ContourPlotOpDescSpec.scala
@@ -38,12 +38,12 @@ class ContourPlotOpDescSpec extends AnyFlatSpec with 
Matchers {
   }
 
   "ContourPlotOpDesc" should
-    "default the x/y/z/gridSize columns to empty and connectGaps to false" in {
+    "default the x/y/z columns to empty, gridSize to unset and connectGaps to 
false" in {
     val d = new ContourPlotOpDesc
     d.x shouldBe ""
     d.y shouldBe ""
     d.z shouldBe ""
-    d.gridSize shouldBe ""
+    d.gridSize shouldBe None
     d.connectGaps shouldBe false
   }
 
@@ -62,7 +62,7 @@ class ContourPlotOpDescSpec extends AnyFlatSpec with Matchers 
{
     d.x = "lon"
     d.y = "lat"
     d.z = "elev"
-    d.gridSize = "20"
+    d.gridSize = Some(20)
     d.connectGaps = true
     val restored = objectMapper.readValue(objectMapper.writeValueAsString(d), 
classOf[LogicalOp])
     restored shouldBe a[ContourPlotOpDesc]
@@ -70,7 +70,48 @@ class ContourPlotOpDescSpec extends AnyFlatSpec with 
Matchers {
     c.x shouldBe "lon"
     c.y shouldBe "lat"
     c.z shouldBe "elev"
-    c.gridSize shouldBe "20"
+    c.gridSize shouldBe Some(20)
     c.connectGaps shouldBe true
   }
+
+  /** Reads the shapes a stored workflow can hold; a round trip cannot cover 
them,
+    * since it writes a number back. See GaugeChartStepsSpec for why 
`contentAs` is
+    * what these pin.
+    */
+  private def readGridSize(json: String): Option[Int] =
+    objectMapper
+      .readValue(s"""{"operatorType":"ContourPlot"$json}""", 
classOf[LogicalOp])
+      .asInstanceOf[ContourPlotOpDesc]
+      .gridSize
+
+  "ContourPlotOpDesc.gridSize" should "deserialize a JSON number" in {
+    readGridSize(""","gridSize":20""") shouldBe Some(20)
+  }
+
+  it should "deserialize the numeric string a workflow saved before the field 
was numeric" in {
+    readGridSize(""","gridSize":"20"""") shouldBe Some(20)
+  }
+
+  it should "read an absent, null or blank value as unset rather than as zero" 
in {
+    readGridSize("") shouldBe None
+    readGridSize(""","gridSize":null""") shouldBe None
+    readGridSize(""","gridSize":""""") shouldBe None
+  }
+
+  it should "hold an Int, not the raw JSON value" in {
+    // The ClassCastException surfaces here, at the first use, not at read 
time.
+    readGridSize(""","gridSize":"20"""").map(_ + 1) shouldBe Some(21)
+  }
+
+  "ContourPlotOpDesc.generatePythonCode" should
+    "assign the grid size as a number and fall back to the form's default" in {
+    val d = new ContourPlotOpDesc
+    d.x = "lon"
+    d.y = "lat"
+    d.z = "elev"
+    d.coloringMethod = ContourPlotColoringFunction.HEATMAP
+    d.generatePythonCode() should include("grid_size = 10")
+    d.gridSize = Some(25)
+    d.generatePythonCode() should include("grid_size = 25")
+  }
 }

Reply via email to