This is an automated email from the ASF dual-hosted git repository.
linxinyuan pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git
The following commit(s) were added to refs/heads/main by this push:
new d868b85225 feat: add Radar Plot Visualization Operator (#3500)
d868b85225 is described below
commit d868b85225fd479b9d453a047decb3da1cb59619
Author: Madison Lin <[email protected]>
AuthorDate: Fri Apr 17 00:02:02 2026 -0700
feat: add Radar Plot Visualization Operator (#3500)
This PR introduces a new visualization operator for a Radar Plot (also
known as a spider plot). Radar plots are used to visualize multivariate
data, allowing for comparison of different entities across the multiple
variables. Typically, they are used to determine strengths and
weaknesses of entities compared to one another (e.g. how our product
compares to a competitor’s). They are also effective in showing outliers
and commonalities across the entities. This Radar Plot visualization
operator allows the user to select which numeric columns will be used as
the radar axes, and optionally select a column whose value will be used
to determine the name or color for the corresponding radar trace (the
visual representation of that entity in the plot). Users can also toggle
other customization features, such as to normalize the values for each
radar axis, in order to prevent a representation that skews towards axes
with higher values.
Changed files:
- Created RadarPlot.png in core/gui/src/assets/operator_images
- Created radarPlot/RadarPlotOpDesc.scala in
core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization
- Created radarPlot/RadarPlotLinePattern.java in
core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization
- Updated LogicalOp.scala in
core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator
Property Selection:

Normalized Result:

Non-Normalized Result:

Additional Example:

Testing Datasets:
[diabetes_mini.csv](https://github.com/user-attachments/files/20912990/diabetes_mini.csv)
[diabetes_mini_missing_column.csv](https://github.com/user-attachments/files/20912991/diabetes_mini_missing_column.csv)
[diabetes_mini_missing_values.csv](https://github.com/user-attachments/files/20912994/diabetes_mini_missing_values.csv)
[product_data.csv](https://github.com/user-attachments/files/20912995/product_data.csv)
---------
Co-authored-by: Xinyuan Lin <[email protected]>
---
.../apache/texera/amber/operator/LogicalOp.scala | 2 +
.../radarChart/RadarChartOpDesc.scala | 3 +-
.../radarPlot/RadarPlotLinePattern.java | 37 ++++
.../visualization/radarPlot/RadarPlotOpDesc.scala | 230 +++++++++++++++++++++
frontend/src/assets/operator_images/RadarPlot.png | Bin 0 -> 48432 bytes
5 files changed, 270 insertions(+), 2 deletions(-)
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 2d2bd24e8a..7671555d0e 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
@@ -124,6 +124,7 @@ import
org.apache.texera.amber.operator.visualization.nestedTable.NestedTableOpD
import
org.apache.texera.amber.operator.visualization.networkGraph.NetworkGraphOpDesc
import org.apache.texera.amber.operator.visualization.pieChart.PieChartOpDesc
import
org.apache.texera.amber.operator.visualization.quiverPlot.QuiverPlotOpDesc
+import org.apache.texera.amber.operator.visualization.radarPlot.RadarPlotOpDesc
import
org.apache.texera.amber.operator.visualization.radarChart.RadarChartOpDesc
import
org.apache.texera.amber.operator.visualization.rangeSlider.RangeSliderOpDesc
import
org.apache.texera.amber.operator.visualization.sankeyDiagram.SankeyDiagramOpDesc
@@ -196,6 +197,7 @@ trait StateTransferFunc
new Type(value = classOf[RangeSliderOpDesc], name = "RangeSlider"),
new Type(value = classOf[PieChartOpDesc], name = "PieChart"),
new Type(value = classOf[QuiverPlotOpDesc], name = "QuiverPlot"),
+ new Type(value = classOf[RadarPlotOpDesc], name = "RadarPlot"),
new Type(value = classOf[RadarChartOpDesc], name = "RadarChart"),
new Type(value = classOf[WordCloudOpDesc], name = "WordCloud"),
new Type(value = classOf[HtmlVizOpDesc], name = "HTMLVisualizer"),
diff --git
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/radarChart/RadarChartOpDesc.scala
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/radarChart/RadarChartOpDesc.scala
index 8454729657..31f86d302a 100644
---
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/radarChart/RadarChartOpDesc.scala
+++
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/radarChart/RadarChartOpDesc.scala
@@ -22,8 +22,7 @@ package
org.apache.texera.amber.operator.visualization.radarChart
import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}
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.core.workflow.{InputPort, OutputPort,
PortIdentity}
+import org.apache.texera.amber.core.workflow.PortIdentity
import org.apache.texera.amber.operator.PythonOperatorDescriptor
import org.apache.texera.amber.operator.metadata.annotations.{
AutofillAttributeName,
diff --git
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/radarPlot/RadarPlotLinePattern.java
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/radarPlot/RadarPlotLinePattern.java
new file mode 100644
index 0000000000..a37838f9d0
--- /dev/null
+++
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/radarPlot/RadarPlotLinePattern.java
@@ -0,0 +1,37 @@
+/*
+ * 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.radarPlot;
+
+import com.fasterxml.jackson.annotation.JsonValue;
+
+public enum RadarPlotLinePattern {
+ SOLID("solid"),
+ DASH("dash"),
+ DOT("dot");
+ private final String linePattern;
+
+ RadarPlotLinePattern(String linePattern) {
+ this.linePattern = linePattern;
+ }
+
+ @JsonValue
+ public String getLinePattern() {
+ return this.linePattern;
+ }
+}
diff --git
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala
new file mode 100644
index 0000000000..0659c3ceeb
--- /dev/null
+++
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala
@@ -0,0 +1,230 @@
+/*
+ * 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.radarPlot
+
+import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}
+import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject,
JsonSchemaTitle}
+import org.apache.texera.amber.core.tuple.{AttributeType, Schema}
+import org.apache.texera.amber.operator.metadata.annotations.{
+ AutofillAttributeName,
+ AutofillAttributeNameList
+}
+import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants,
OperatorInfo}
+import org.apache.texera.amber.operator.PythonOperatorDescriptor
+import org.apache.texera.amber.core.workflow.PortIdentity
+import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString
+import org.apache.texera.amber.pybuilder.PythonTemplateBuilder
+import
org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext
+
+@JsonSchemaInject(json = """
+{
+ "attributeTypeRules": {
+ "selectedAttributes": {
+ "enum": ["integer", "long", "double"]
+ }
+ }
+}
+""")
+class RadarPlotOpDesc extends PythonOperatorDescriptor {
+ @JsonProperty(value = "selectedAttributes", required = true)
+ @JsonSchemaTitle("Axes")
+ @JsonPropertyDescription("Numeric columns to use as radar axes")
+ @AutofillAttributeNameList
+ var selectedAttributes: List[EncodableString] = _
+
+ @JsonProperty(value = "traceNameAttribute", defaultValue = "No Selection",
required = false)
+ @JsonSchemaTitle("Trace Name Column")
+ @JsonPropertyDescription("Optional - Select a column to use for naming each
radar trace")
+ @AutofillAttributeName
+ var traceNameAttribute: EncodableString = ""
+
+ @JsonProperty(
+ value = "traceColorAttribute",
+ defaultValue = "No Selection",
+ required = false
+ )
+ @JsonSchemaTitle("Trace Color Column")
+ @JsonPropertyDescription(
+ "Optional - Select a column to use for coloring each radar trace (note: if
there are too many traces with distinct coloring values, colors may repeat)"
+ )
+ @AutofillAttributeName
+ var traceColorAttribute: EncodableString = ""
+
+ @JsonProperty(value = "linePattern", defaultValue = "solid", required = true)
+ @JsonPropertyDescription("Pattern of the lines connecting points on the
radar plot")
+ var linePattern: RadarPlotLinePattern = _
+
+ @JsonProperty(value = "maxNormalize", defaultValue = "true", required = true)
+ @JsonSchemaTitle("Max Normalize")
+ @JsonPropertyDescription(
+ "Normalize radar plot values by scaling them relative to the maximum value
on their respective axes"
+ )
+ var maxNormalize: Boolean = true
+
+ @JsonProperty(value = "fillTrace", defaultValue = "true", required = true)
+ @JsonSchemaTitle("Fill Trace")
+ @JsonPropertyDescription("Fill the area within each radar trace")
+ var fillTrace: Boolean = true
+
+ @JsonProperty(value = "showMarkers", defaultValue = "true", required = true)
+ @JsonSchemaTitle("Show Point Markers")
+ @JsonPropertyDescription("Display point markers on the radar plot")
+ var showMarkers: Boolean = true
+
+ @JsonProperty(value = "showLegend", defaultValue = "true", required = false)
+ @JsonSchemaTitle("Show Legend")
+ @JsonPropertyDescription(
+ "Display the legend (note: without the legend, you are unable to
selectively hide or show traces in the plot)"
+ )
+ var showLegend: Boolean = true
+
+ override def getOutputSchemas(
+ inputSchemas: Map[PortIdentity, Schema]
+ ): Map[PortIdentity, Schema] = {
+ val outputSchema = Schema()
+ .add("html-content", AttributeType.STRING)
+ Map(operatorInfo.outputPorts.head.id -> outputSchema)
+ }
+
+ override def operatorInfo: OperatorInfo =
+ OperatorInfo.forVisualization(
+ "Radar Plot",
+ "View the result in a radar plot.",
+ OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP
+ )
+
+ private def toPythonBool(value: Boolean): String = if (value) "True" else
"False"
+
+ private def optionalColumnExpr(column: EncodableString):
PythonTemplateBuilder =
+ Option(column).filterNot(col => col.isEmpty || col == "No Selection")
match {
+ case Some(col) => pyb"$col"
+ case None => pyb"None"
+ }
+
+ def generateRadarPlotCode(): PythonTemplateBuilder = {
+ val attributes = Option(selectedAttributes).getOrElse(Nil)
+ val attrList = attributes.map(attr => pyb"$attr").mkString(", ")
+ val traceNameCol = optionalColumnExpr(traceNameAttribute)
+ val traceColorCol = optionalColumnExpr(traceColorAttribute)
+
+ pyb"""
+ | categories = [$attrList]
+ | if not categories:
+ | yield {'html-content': self.render_error("No columns
selected as axes.")}
+ | return
+ |
+ | trace_name_col = $traceNameCol
+ | trace_color_col = $traceColorCol
+ | line_pattern = "${linePattern.getLinePattern}"
+ | max_normalize = ${toPythonBool(maxNormalize)}
+ | fill_trace = ${toPythonBool(fillTrace)}
+ | show_markers = ${toPythonBool(showMarkers)}
+ | show_legend = ${toPythonBool(showLegend)}
+ |
+ | selected_table_df = table[categories].astype(float)
+ | selected_table = selected_table_df.values
+ |
+ | trace_names = (
+ | table[trace_name_col].values if trace_name_col
+ | else np.full(len(table), "", dtype=object)
+ | )
+ |
+ | trace_colors = [None] * len(table)
+ | if trace_color_col:
+ | unique_vals = table[trace_color_col].unique()
+ | color_map = {val: px.colors.qualitative.Plotly[idx %
len(px.colors.qualitative.Plotly)]
+ | for idx, val in enumerate(unique_vals)}
+ | nan_color = '#000000'
+ | trace_colors =
table[trace_color_col].map(color_map).fillna(nan_color).values
+ |
+ | hover_texts = []
+ | for idx, row in enumerate(selected_table):
+ | name_prefix = str(trace_names[idx]) + "<br>" if
trace_names[idx] else ""
+ | row_hover_texts = []
+ | for attr, value in zip(categories, row):
+ | row_hover_texts.append(name_prefix + attr + ": " +
str(value))
+ | hover_texts.append(row_hover_texts)
+ |
+ | if max_normalize:
+ | max_vals = selected_table_df.max().values
+ | max_vals[max_vals == 0] = 1
+ | selected_table = selected_table / max_vals
+ |
+ | selected_table = np.nan_to_num(selected_table)
+ |
+ | fig = go.Figure()
+ |
+ | for idx, row in enumerate(selected_table):
+ | # To connect ensure all points in the radar trace are
connected
+ | closed_row = row.tolist() + [row[0]]
+ | closed_categories = categories + [categories[0]]
+ | closed_hover_texts = hover_texts[idx] +
[hover_texts[idx][0]]
+ |
+ | fig.add_trace(go.Scatterpolar(
+ | r=closed_row,
+ | theta=closed_categories,
+ | fill='toself' if fill_trace else 'none',
+ | name=str(trace_names[idx]) if trace_names[idx] else "",
+ | text=closed_hover_texts,
+ | hoverinfo="text",
+ | mode="lines+markers" if show_markers else "lines",
+ | line=dict(dash=line_pattern, color=trace_colors[idx]
if trace_colors[idx] else None),
+ | marker=dict(color=trace_colors[idx]) if
trace_colors[idx] else {}
+ | ))
+ |
+ | fig.update_layout(
+ | polar=dict(radialaxis=dict(visible=True)),
+ | showlegend=show_legend,
+ | width=600,
+ | height=600
+ | )
+ |"""
+ }
+
+ override def generatePythonCode(): String = {
+ val finalCode =
+ pyb"""
+ |from pytexera import *
+ |import numpy as np
+ |import plotly.graph_objects as go
+ |import plotly.express as px
+ |import plotly.io
+ |
+ |class ProcessTableOperator(UDFTableOperator):
+ |
+ | def render_error(self, error_msg):
+ | return '''<h1>Radar Plot is not available.</h1>
+ | <p>Reason is: {} </p>
+ | '''.format(error_msg)
+ |
+ | @overrides
+ | def process_table(self, table: Table, port: int):
+ | if table.empty:
+ | yield {'html-content': self.render_error("Input table is
empty.")}
+ | return
+ |
+ | ${generateRadarPlotCode()}
+ |
+ | html = plotly.io.to_html(fig, include_plotlyjs='cdn',
auto_play=False, config={'responsive': True})
+ | yield {'html-content': html}
+ |"""
+ finalCode.encode
+ }
+}
diff --git a/frontend/src/assets/operator_images/RadarPlot.png
b/frontend/src/assets/operator_images/RadarPlot.png
new file mode 100644
index 0000000000..374a6e7773
Binary files /dev/null and b/frontend/src/assets/operator_images/RadarPlot.png
differ