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") + } }
