Skip to content

Commit fee6f8c

Browse files
feat: add a new wind rose chart operator (#4224)
### What changes were proposed in this PR? <img width="3023" height="1714" alt="image" src="https://github.com/user-attachments/assets/dd6e94c0-70da-4c5d-bedb-27f78bab403b" /> This change adds a new wind rose chart operator, which visualizes how a magnitude (radial value) is distributed across different directions (angular values) and optionally grouped by a categorical variable (color). In a wind rose chart: - Each angular segment represents a direction (e.g., N, NE, E). - The length of each bar from the center represents the magnitude or frequency for that direction. - Color can optionally represent an additional grouping variable, such as strength or category. This visualization is useful for understanding directional distributions, identifying dominant directions, and comparing patterns across different groups or conditions. The operator takes in 2 or 3 inputs: the radial values, the angular values, and optionally a color/grouping variable. ### Any related issues, documentation, discussions? No related issues, documentation, or discussions. ### How was this PR tested? PR is tested with existing test cases. ### Was this PR authored or co-authored using generative AI tooling? No. --------- Co-authored-by: Xinyuan Lin <xinyual3@uci.edu>
1 parent b3db209 commit fee6f8c

3 files changed

Lines changed: 130 additions & 0 deletions

File tree

common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/LogicalOp.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ import org.apache.texera.amber.operator.visualization.treeplot.TreePlotOpDesc
136136
import org.apache.texera.amber.operator.visualization.urlviz.UrlVizOpDesc
137137
import org.apache.texera.amber.operator.visualization.volcanoPlot.VolcanoPlotOpDesc
138138
import org.apache.texera.amber.operator.visualization.waterfallChart.WaterfallChartOpDesc
139+
import org.apache.texera.amber.operator.visualization.windRoseChart.WindRoseChartOpDesc
139140
import org.apache.texera.amber.operator.visualization.wordCloud.WordCloudOpDesc
140141
import org.apache.commons.lang3.builder.{EqualsBuilder, HashCodeBuilder, ToStringBuilder}
141142
import org.apache.texera.amber.operator.sklearn.testing.SklearnTestingOpDesc
@@ -190,6 +191,7 @@ trait StateTransferFunc
190191
new Type(value = classOf[AggregateOpDesc], name = "Aggregate"),
191192
new Type(value = classOf[LineChartOpDesc], name = "LineChart"),
192193
new Type(value = classOf[WaterfallChartOpDesc], name = "WaterfallChart"),
194+
new Type(value = classOf[WindRoseChartOpDesc], name = "WindRoseChart"),
193195
new Type(value = classOf[BarChartOpDesc], name = "BarChart"),
194196
new Type(value = classOf[RangeSliderOpDesc], name = "RangeSlider"),
195197
new Type(value = classOf[PieChartOpDesc], name = "PieChart"),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.texera.amber.operator.visualization.windRoseChart
21+
22+
import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}
23+
import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle
24+
import org.apache.texera.amber.core.tuple.{AttributeType, Schema}
25+
import org.apache.texera.amber.core.workflow.OutputPort.OutputMode
26+
import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext
27+
import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString
28+
import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity}
29+
import org.apache.texera.amber.operator.PythonOperatorDescriptor
30+
import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName
31+
import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}
32+
import org.apache.texera.amber.pybuilder.PythonTemplateBuilder
33+
import javax.validation.constraints.NotNull
34+
35+
class WindRoseChartOpDesc extends PythonOperatorDescriptor {
36+
37+
@JsonProperty(value = "rColumn", required = true)
38+
@JsonSchemaTitle("Radial Values (r)")
39+
@JsonPropertyDescription("Numeric values representing magnitude (e.g., frequency)")
40+
@AutofillAttributeName
41+
@NotNull(message = "Radial Values (r) column must be selected.")
42+
var rColumn: EncodableString = _
43+
44+
@JsonProperty(value = "thetaColumn", required = true)
45+
@JsonSchemaTitle("Angular Values (θ)")
46+
@JsonPropertyDescription("Direction or angle categories (e.g., N, NE, E)")
47+
@AutofillAttributeName
48+
@NotNull(message = "Angular Values (θ) column must be selected.")
49+
var thetaColumn: EncodableString = _
50+
51+
@JsonProperty(value = "colorColumn", required = false)
52+
@JsonSchemaTitle("Color Group")
53+
@JsonPropertyDescription("Optional grouping column (e.g., wind strength)")
54+
@AutofillAttributeName
55+
var colorColumn: EncodableString = _
56+
57+
override def operatorInfo: OperatorInfo =
58+
OperatorInfo(
59+
userFriendlyName = "Wind Rose Chart",
60+
operatorDescription = "Displays wind distribution using a polar bar chart",
61+
operatorGroupName = OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP,
62+
inputPorts = List(InputPort()),
63+
outputPorts = List(OutputPort(mode = OutputMode.SINGLE_SNAPSHOT))
64+
)
65+
66+
override def getOutputSchemas(
67+
inputSchemas: Map[PortIdentity, Schema]
68+
): Map[PortIdentity, Schema] = {
69+
val outputSchema = Schema()
70+
.add("html-content", AttributeType.STRING)
71+
Map(operatorInfo.outputPorts.head.id -> outputSchema)
72+
}
73+
74+
def createPlotlyFigure(): PythonTemplateBuilder = {
75+
val colorArg =
76+
if (colorColumn != null && colorColumn.nonEmpty)
77+
pyb"""
78+
| color=$colorColumn,
79+
|"""
80+
else
81+
pyb""
82+
83+
pyb"""
84+
| fig = px.bar_polar(
85+
| table,
86+
| r=$rColumn,
87+
| theta=$thetaColumn,
88+
|$colorArg
89+
| color_discrete_sequence=px.colors.sequential.Plasma_r
90+
| )
91+
|"""
92+
}
93+
94+
override def generatePythonCode(): String = {
95+
val finalCode =
96+
pyb"""
97+
|from pytexera import *
98+
|
99+
|import plotly.graph_objects as go
100+
|import plotly.io
101+
|import plotly.express as px
102+
|
103+
|class ProcessTableOperator(UDFTableOperator):
104+
|
105+
| # Generate custom error message as html string
106+
| def render_error(self, error_msg) -> str:
107+
| return '''<h1>Wind Rose chart is not available.</h1>
108+
| <p>Reason is: {} </p>
109+
| '''.format(error_msg)
110+
|
111+
| @overrides
112+
| def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:
113+
| if table.empty:
114+
| yield {'html-content': self.render_error("input table is empty.")}
115+
| return
116+
| if table[$rColumn].dtype.kind not in ["i", "u", "f"]:
117+
| yield {'html-content': self.render_error(
118+
| "Radial column must be numeric (int, float, or double)."
119+
| )}
120+
| return
121+
| ${createPlotlyFigure()}
122+
| html = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)
123+
| yield {'html-content': html}
124+
|"""
125+
finalCode.encode
126+
}
127+
128+
}
23.6 KB
Loading

0 commit comments

Comments
 (0)