From 9406af8cab10405b86e570e47687454d9f1bfea1 Mon Sep 17 00:00:00 2001 From: Julie Cao Date: Mon, 16 Feb 2026 18:09:53 -0800 Subject: [PATCH 1/3] feat: add wind rose chart operator --- .../texera/amber/operator/LogicalOp.scala | 2 + .../windRoseChart/WindRoseChartOpDesc.scala | 120 ++++++++++++++++++ .../assets/operator_images/WindRoseChart.png | Bin 0 -> 24191 bytes 3 files changed, 122 insertions(+) create mode 100644 common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/windRoseChart/WindRoseChartOpDesc.scala create mode 100644 frontend/src/assets/operator_images/WindRoseChart.png diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/LogicalOp.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/LogicalOp.scala index eb319a82d1d..64ea1d813fd 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/LogicalOp.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/LogicalOp.scala @@ -135,6 +135,7 @@ import org.apache.texera.amber.operator.visualization.treeplot.TreePlotOpDesc import org.apache.texera.amber.operator.visualization.urlviz.UrlVizOpDesc import org.apache.texera.amber.operator.visualization.volcanoPlot.VolcanoPlotOpDesc import org.apache.texera.amber.operator.visualization.waterfallChart.WaterfallChartOpDesc +import org.apache.texera.amber.operator.visualization.windRoseChart.WindRoseChartOpDesc import org.apache.texera.amber.operator.visualization.wordCloud.WordCloudOpDesc import org.apache.commons.lang3.builder.{EqualsBuilder, HashCodeBuilder, ToStringBuilder} import org.apache.texera.amber.operator.visualization.stripChart.StripChartOpDesc @@ -185,6 +186,7 @@ trait StateTransferFunc new Type(value = classOf[AggregateOpDesc], name = "Aggregate"), new Type(value = classOf[LineChartOpDesc], name = "LineChart"), new Type(value = classOf[WaterfallChartOpDesc], name = "WaterfallChart"), + new Type(value = classOf[WindRoseChartOpDesc], name = "WindRoseChart"), new Type(value = classOf[BarChartOpDesc], name = "BarChart"), new Type(value = classOf[RangeSliderOpDesc], name = "RangeSlider"), new Type(value = classOf[PieChartOpDesc], name = "PieChart"), 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 new file mode 100644 index 00000000000..b814ad69501 --- /dev/null +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/windRoseChart/WindRoseChartOpDesc.scala @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.texera.amber.operator.visualization.windRoseChart + +import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} +import com.kjetland.jackson.jsonSchema.annotations.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 +import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString +import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity} +import org.apache.texera.amber.operator.PythonOperatorDescriptor +import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName +import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} +import org.apache.texera.amber.pybuilder.PythonTemplateBuilder + +class WindRoseChartOpDesc extends PythonOperatorDescriptor { + + @JsonProperty(value = "rColumn", required = true) + @JsonSchemaTitle("Radial Values (r)") + @JsonPropertyDescription("Numeric values representing magnitude (e.g., frequency)") + @AutofillAttributeName + var rColumn: EncodableString = _ + + @JsonProperty(value = "thetaColumn", required = true) + @JsonSchemaTitle("Angular Values (θ)") + @JsonPropertyDescription("Direction or angle categories (e.g., N, NE, E)") + @AutofillAttributeName + var thetaColumn: EncodableString = _ + + @JsonProperty(value = "colorColumn", required = false) + @JsonSchemaTitle("Color Group") + @JsonPropertyDescription("Optional grouping column (e.g., wind strength)") + @AutofillAttributeName + var colorColumn: EncodableString = _ + + override def operatorInfo: OperatorInfo = + OperatorInfo( + userFriendlyName = "Wind Rose Chart", + operatorDescription = "Displays wind distribution using a polar bar chart", + operatorGroupName = OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP, + inputPorts = List(InputPort()), + outputPorts = List(OutputPort(mode = OutputMode.SINGLE_SNAPSHOT)) + ) + + override def getOutputSchemas( + inputSchemas: Map[PortIdentity, Schema] + ): Map[PortIdentity, Schema] = { + val outputSchema = Schema() + .add("html-content", AttributeType.STRING) + Map(operatorInfo.outputPorts.head.id -> outputSchema) + } + + def createPlotlyFigure(): PythonTemplateBuilder = { + val colorArg = + if (colorColumn != null) + pyb""" + | color=$colorColumn, + |""" + else + pyb"" + + pyb""" + | fig = px.bar_polar( + | table, + | r=$rColumn, + | theta=$thetaColumn, + |$colorArg + | color_discrete_sequence=px.colors.sequential.Plasma_r + | ) + |""" + } + + override def generatePythonCode(): String = { + val finalCode = + pyb""" + |from pytexera import * + | + |import plotly.graph_objects as go + |import plotly.io + |import plotly.express as px + | + |class ProcessTableOperator(UDFTableOperator): + | + | # Generate custom error message as html string + | def render_error(self, error_msg) -> str: + | return '''

Wind Rose chart is not available.

+ |

Reason is: {}

+ | '''.format(error_msg) + | + | @overrides + | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]: + | if table.empty: + | yield {'html-content': self.render_error("input table is empty.")} + | return + | ${createPlotlyFigure()} + | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) + | yield {'html-content': html} + |""" + finalCode.encode + } + +} \ No newline at end of file diff --git a/frontend/src/assets/operator_images/WindRoseChart.png b/frontend/src/assets/operator_images/WindRoseChart.png new file mode 100644 index 0000000000000000000000000000000000000000..f63c8e85bd15923fd6ad13cf16049b27c4065f4f GIT binary patch literal 24191 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 P^PIQ3aHh_}Kk5GgbnKYe literal 0 HcmV?d00001 From 8de535a4929233bd9b05aeeb7e41c96b2aab77fd Mon Sep 17 00:00:00 2001 From: Julie Cao Date: Tue, 17 Feb 2026 20:11:15 -0800 Subject: [PATCH 2/3] chore: fix format based on scalafmt check --- .../visualization/windRoseChart/WindRoseChartOpDesc.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b814ad69501..3fb65bbb461 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 @@ -117,4 +117,4 @@ class WindRoseChartOpDesc extends PythonOperatorDescriptor { finalCode.encode } -} \ No newline at end of file +} From 8322e50d091d3825be994e1390aebcc595a195d3 Mon Sep 17 00:00:00 2001 From: Julie Cao Date: Mon, 23 Feb 2026 14:34:08 -0800 Subject: [PATCH 3/3] refactor: add validation and defensive checks for windrose operator --- .../windRoseChart/WindRoseChartOpDesc.scala | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 3fb65bbb461..4b930d541f7 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 @@ -30,6 +30,7 @@ import org.apache.texera.amber.operator.PythonOperatorDescriptor import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.pybuilder.PythonTemplateBuilder +import javax.validation.constraints.NotNull class WindRoseChartOpDesc extends PythonOperatorDescriptor { @@ -37,12 +38,14 @@ class WindRoseChartOpDesc extends PythonOperatorDescriptor { @JsonSchemaTitle("Radial Values (r)") @JsonPropertyDescription("Numeric values representing magnitude (e.g., frequency)") @AutofillAttributeName + @NotNull(message = "Radial Values (r) column must be selected.") var rColumn: EncodableString = _ @JsonProperty(value = "thetaColumn", required = true) @JsonSchemaTitle("Angular Values (θ)") @JsonPropertyDescription("Direction or angle categories (e.g., N, NE, E)") @AutofillAttributeName + @NotNull(message = "Angular Values (θ) column must be selected.") var thetaColumn: EncodableString = _ @JsonProperty(value = "colorColumn", required = false) @@ -70,7 +73,7 @@ class WindRoseChartOpDesc extends PythonOperatorDescriptor { def createPlotlyFigure(): PythonTemplateBuilder = { val colorArg = - if (colorColumn != null) + if (colorColumn != null && colorColumn.nonEmpty) pyb""" | color=$colorColumn, |""" @@ -110,6 +113,11 @@ class WindRoseChartOpDesc extends PythonOperatorDescriptor { | if table.empty: | yield {'html-content': self.render_error("input table is empty.")} | return + | if table[$rColumn].dtype.kind not in ["i", "u", "f"]: + | yield {'html-content': self.render_error( + | "Radial column must be numeric (int, float, or double)." + | )} + | return | ${createPlotlyFigure()} | html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False) | yield {'html-content': html}