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-7341-d2fe4ba34e0e76679eb14499c854d487ed953a09 in repository https://gitbox.apache.org/repos/asf/texera.git
commit 1362167975e93322282908bbfbe7ea8ac2c4d2fe Author: Kary Zheng <[email protected]> AuthorDate: Fri Aug 7 18:15:40 2026 -0700 feat(visualization): declare the column types Wind Rose and Bubble Chart require (#7341) ### What changes were proposed in this PR? Wind Rose's radial value and Bubble Chart's z column are both consumed as magnitudes, and neither declared an `attributeTypeRules` entry, so the property form offered every column and accepted a string one. Each now declares `integer`, `long` or `double`, the way Range Slider's y-axis and Radar Chart's value columns already do. <img width="1433" height="925" alt="Screenshot 2026-08-07 at 2 21 49 PM" src="https://github.com/user-attachments/assets/61e45feb-22ca-4b7d-b447-8785a4ad3695" /> <img width="1432" height="920" alt="Screenshot 2026-08-07 at 2 23 46 PM" src="https://github.com/user-attachments/assets/7336c652-869f-4f92-a075-98b2b55a2c52" /> The other pickers are deliberately left alone: Wind Rose's angle is a direction label and Bubble Chart's x and y are positions, all of which take any type the way a scatter plot's axes do. ### Any related issues, documentation, discussions? Closes #7324. Same class of gap as #7250, which covered a different set of operators. ### How was this PR tested? Each spec gains a case that generates its descriptor's schema and asserts the rule on it: keyed to `rColumn` and to `zValue`, each naming a property the schema declares, each allowing exactly `integer`, `long` and `double`, and each stating that `string` is not among them. Reading the annotation text would not have shown this — a key naming no property parses exactly as well and constrains nothing, which is what #7210 collected four instances of. Removing either annotation leaves exactly those two cases failing. Each asserts the whole key set rather than the presence of its own key, so constraining the angle or the two axes later would fail here too — those read as any type on purpose. ``` sbt "WorkflowOperator/testOnly org.apache.texera.amber.operator.visualization.windRoseChart.WindRoseChartOpDescSpec org.apache.texera.amber.operator.visualization.bubbleChart.BubbleChartOpDescSpec" ``` Thirteen cases, all passing. The behaviour each rule prevents was reproduced first: rendering the same three-row frame with a numeric column and with a string one, Wind Rose's `radialaxis.type` comes out `linear` with range 0 to 4.2 for the numeric column and `category` with range -0.11 to 2.11 for the string one — the wedges drawn at ordinal positions rather than lengths, with no error — and this holds even when every value is a number written as text. Bubble Chart raises `TypeError: unsupported operand type(s) for /: 'str' and 'int'` on either. ### 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]> --- .../bubbleChart/BubbleChartOpDesc.scala | 12 +++++++++++- .../windRoseChart/WindRoseChartOpDesc.scala | 12 +++++++++++- .../bubbleChart/BubbleChartOpDescSpec.scala | 20 ++++++++++++++++++++ .../windRoseChart/WindRoseChartOpDescSpec.scala | 20 ++++++++++++++++++++ 4 files changed, 62 insertions(+), 2 deletions(-) diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/bubbleChart/BubbleChartOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/bubbleChart/BubbleChartOpDesc.scala index d876b95769..ad31361064 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/bubbleChart/BubbleChartOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/bubbleChart/BubbleChartOpDesc.scala @@ -20,7 +20,7 @@ package org.apache.texera.amber.operator.visualization.bubbleChart import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} -import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle +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 @@ -39,6 +39,16 @@ import javax.validation.constraints.NotNull */ // type can be numerical only +// The z column is the bubble size, which plotly express divides by a scale +// factor, so text aborts the run. The x and y axes are positions and take any +// type, the way a scatter plot's do. +@JsonSchemaInject(json = """ +{ + "attributeTypeRules": { + "zValue": { "enum": ["integer", "long", "double"] } + } +} +""") class BubbleChartOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "xValue", required = true) diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/windRoseChart/WindRoseChartOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/windRoseChart/WindRoseChartOpDesc.scala index e12011c7d3..9cb36f5882 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/windRoseChart/WindRoseChartOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/windRoseChart/WindRoseChartOpDesc.scala @@ -20,7 +20,7 @@ package org.apache.texera.amber.operator.visualization.windRoseChart import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} -import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle +import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} import org.apache.texera.amber.core.tuple.{AttributeType, Schema} import org.apache.texera.amber.core.workflow.OutputPort.OutputMode import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext @@ -32,6 +32,16 @@ import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, Operat import org.apache.texera.amber.pybuilder.PythonTemplateBuilder import javax.validation.constraints.NotNull +// The radial value is the length of each wedge, so it has to be a number: given +// text, plotly turns the radial axis categorical and the wedges stop meaning +// anything. The angle is a direction label and takes any type. +@JsonSchemaInject(json = """ +{ + "attributeTypeRules": { + "rColumn": { "enum": ["integer", "long", "double"] } + } +} +""") class WindRoseChartOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "rColumn", required = true) diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/bubbleChart/BubbleChartOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/bubbleChart/BubbleChartOpDescSpec.scala index b0945fd770..85fbd1fa3f 100644 --- a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/bubbleChart/BubbleChartOpDescSpec.scala +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/bubbleChart/BubbleChartOpDescSpec.scala @@ -19,10 +19,13 @@ package org.apache.texera.amber.operator.visualization.bubbleChart +import org.apache.texera.amber.operator.metadata.OperatorMetadataGenerator import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers +import scala.jdk.CollectionConverters._ + class BubbleChartOpDescSpec extends AnyFlatSpec with BeforeAndAfter with Matchers { var opDesc: BubbleChartOpDesc = _ @@ -99,4 +102,21 @@ class BubbleChartOpDescSpec extends AnyFlatSpec with BeforeAndAfter with Matcher plain should include("column2") plain should include("column3") } + + "BubbleChartOpDesc's generated schema" should + "constrain the bubble size to numeric and leave the axes unconstrained" in { + // x and y are positions and read as any type, the way a scatter plot's do. + // Only the size is divided by a scale factor and so has to be a number. + val schema = OperatorMetadataGenerator.generateOperatorJsonSchema(classOf[BubbleChartOpDesc]) + val rules = schema.path("attributeTypeRules") + rules.fieldNames().asScala.toSet shouldBe Set("zValue") + + // The key has to name a property the form actually renders; one that names + // nothing parses fine and constrains nothing. + schema.path("properties").has("zValue") shouldBe true + + val allowed = rules.path("zValue").path("enum").elements().asScala.map(_.asText()).toSet + allowed shouldBe Set("integer", "long", "double") + allowed should not contain "string" + } } diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/windRoseChart/WindRoseChartOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/windRoseChart/WindRoseChartOpDescSpec.scala index c25093457e..d8b091eff9 100644 --- a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/windRoseChart/WindRoseChartOpDescSpec.scala +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/windRoseChart/WindRoseChartOpDescSpec.scala @@ -19,6 +19,7 @@ package org.apache.texera.amber.operator.visualization.windRoseChart +import org.apache.texera.amber.operator.metadata.OperatorMetadataGenerator import org.scalatest.BeforeAndAfter import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers @@ -26,6 +27,8 @@ import org.scalatest.matchers.should.Matchers import java.nio.charset.StandardCharsets import java.util.Base64 +import scala.jdk.CollectionConverters._ + class WindRoseChartOpDescSpec extends AnyFlatSpec with BeforeAndAfter with Matchers { var opDesc: WindRoseChartOpDesc = _ @@ -77,4 +80,21 @@ class WindRoseChartOpDescSpec extends AnyFlatSpec with BeforeAndAfter with Match assert(carries(code, "theta_col")) code should include("class ProcessTableOperator(UDFTableOperator)") } + + "WindRoseChartOpDesc's generated schema" should + "constrain the radial values to numeric and leave the angle unconstrained" in { + // The angle is a direction label — "N", "NE" — so a rule there would reject + // the ordinary case. Only the wedge length has to be a number. + val schema = OperatorMetadataGenerator.generateOperatorJsonSchema(classOf[WindRoseChartOpDesc]) + val rules = schema.path("attributeTypeRules") + rules.fieldNames().asScala.toSet shouldBe Set("rColumn") + + // The key has to name a property the form actually renders; one that names + // nothing parses fine and constrains nothing. + schema.path("properties").has("rColumn") shouldBe true + + val allowed = rules.path("rColumn").path("enum").elements().asScala.map(_.asText()).toSet + allowed shouldBe Set("integer", "long", "double") + allowed should not contain "string" + } }
