From 55cbb47398afd40267e2b38d9d0d951c9e2f4165 Mon Sep 17 00:00:00 2001 From: Madison Lin Date: Sun, 22 Jun 2025 22:26:02 -0700 Subject: [PATCH 01/19] Add Radar Plot Visualization Operator --- .../src/assets/operator_images/RadarPlot.png | Bin 0 -> 48432 bytes .../uci/ics/amber/operator/LogicalOp.scala | 2 + .../radarPlot/RadarPlotOpDesc.scala | 127 ++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 core/gui/src/assets/operator_images/RadarPlot.png create mode 100644 core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala diff --git a/core/gui/src/assets/operator_images/RadarPlot.png b/core/gui/src/assets/operator_images/RadarPlot.png new file mode 100644 index 0000000000000000000000000000000000000000..374a6e77731910746834f964c052390b4f545ff4 GIT binary patch literal 48432 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 UpX#cCL%^S=nyzZOvQ^ms0ke70lmGw# literal 0 HcmV?d00001 diff --git a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/LogicalOp.scala b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/LogicalOp.scala index b053ff929a9..cd84afa7b4f 100644 --- a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/LogicalOp.scala +++ b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/LogicalOp.scala @@ -113,6 +113,7 @@ import edu.uci.ics.amber.operator.visualization.lineChart.LineChartOpDesc import edu.uci.ics.amber.operator.visualization.networkGraph.NetworkGraphOpDesc import edu.uci.ics.amber.operator.visualization.pieChart.PieChartOpDesc import edu.uci.ics.amber.operator.visualization.quiverPlot.QuiverPlotOpDesc +import edu.uci.ics.amber.operator.visualization.radarPlot.RadarPlotOpDesc import edu.uci.ics.amber.operator.visualization.rangeSlider.RangeSliderOpDesc import edu.uci.ics.amber.operator.visualization.sankeyDiagram.SankeyDiagramOpDesc import edu.uci.ics.amber.operator.visualization.scatter3DChart.Scatter3dChartOpDesc @@ -173,6 +174,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[WordCloudOpDesc], name = "WordCloud"), new Type(value = classOf[HtmlVizOpDesc], name = "HTMLVisualizer"), new Type(value = classOf[UrlVizOpDesc], name = "URLVisualizer"), diff --git a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala new file mode 100644 index 00000000000..2a650442723 --- /dev/null +++ b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala @@ -0,0 +1,127 @@ +package edu.uci.ics.amber.operator.visualization.radarPlot + +import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} +import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} +import edu.uci.ics.amber.core.tuple.{AttributeType, Schema} +import edu.uci.ics.amber.operator.PythonOperatorDescriptor +import edu.uci.ics.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} +import edu.uci.ics.amber.core.workflow.OutputPort.OutputMode +import edu.uci.ics.amber.core.workflow.{InputPort, OutputPort, PortIdentity} +import edu.uci.ics.amber.operator.metadata.annotations.{AutofillAttributeName, AutofillAttributeNameList} + +@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[String] = _ + + @JsonProperty(value = "traceNameAttribute", required = false) + @JsonSchemaTitle("Trace Name Column") + @JsonPropertyDescription("Optional column to use for naming each radar trace") + @AutofillAttributeName + var traceNameAttribute: String = "" + + override def getOutputSchemas(inputSchemas: Map[PortIdentity, Schema]): Map[PortIdentity, Schema] = { + val outputSchema = Schema() + .add("html-content", AttributeType.STRING) + Map(operatorInfo.outputPorts.head.id -> outputSchema) + Map(operatorInfo.outputPorts.head.id -> outputSchema) + } + + override def operatorInfo: OperatorInfo = + OperatorInfo( + "Radar Plot", + "View the result in radar plot", + OperatorGroupConstants.VISUALIZATION_BASIC_GROUP, + inputPorts = List(InputPort()), + outputPorts = List(OutputPort(mode = OutputMode.SINGLE_SNAPSHOT)) + ) + + def generateRadarPlotCode(): String = { + assert(selectedAttributes.nonEmpty) + + val attrList = selectedAttributes.map(attr => s""""$attr"""").mkString(", ") + val traceNameCol = traceNameAttribute match { + case null | "" => "None" + case col => s"'$col'" + } + + s""" + | categories = [$attrList] + | trace_name_col = $traceNameCol + | max_vals = {attr: float('-inf') for attr in categories} + | + | for _, row in table.iterrows(): + | for attr in categories: + | max_vals[attr] = max(max_vals[attr], row[attr]) + | + | fig = go.Figure() + | + | for _, row in table.iterrows(): + | original_vals = [] + | normalized_vals = [] + | for attr in categories: + | original_vals.append(f"{attr}: {row[attr]}") + | normalized_vals.append( + | row[attr] / max_vals[attr] if max_vals[attr] != 0 else 0 + | ) + | trace_name = row[trace_name_col] if trace_name_col is not None else "" + | + | fig.add_trace(go.Scatterpolar( + | r=normalized_vals, + | theta=categories, + | fill='toself', + | name=str(trace_name) if trace_name is not None else "", + | text=original_vals, + | hoverinfo="text" + | )) + | + | showlegend = any(row.get(trace_name_col, "") for _, row in table.iterrows()) if trace_name_col is not None else False + | + | fig.update_layout( + | polar=dict(radialaxis=dict(visible=True)), + | showlegend=showlegend, + | width=600, + | height=600 + | ) + |""".stripMargin + } + + + + override def generatePythonCode(): String = { + s""" + |from pytexera import * + |import plotly.graph_objects as go + |import plotly.io + | + |class ProcessTableOperator(UDFTableOperator): + | + | def render_error(self, error_msg): + | return '''

Radar Plot is not available.

+ |

Reason is: {}

+ | '''.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} + |""".stripMargin + } +} From 6a4ff79dac867136a5aad2cd25c7d1bec209d22c Mon Sep 17 00:00:00 2001 From: Madison Lin Date: Mon, 23 Jun 2025 10:42:41 -0700 Subject: [PATCH 02/19] Add normalization tick box in Radar Plot visualization operator properties --- .../radarPlot/RadarPlotOpDesc.scala | 58 ++++++++++++------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala index 2a650442723..414f0eda469 100644 --- a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala +++ b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala @@ -31,6 +31,11 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { @AutofillAttributeName var traceNameAttribute: String = "" + @JsonProperty(value = "normalize", required = false) + @JsonSchemaTitle("Normalize Data") + @JsonPropertyDescription("Normalize the radar plot values") + var normalize: Boolean = true + override def getOutputSchemas(inputSchemas: Map[PortIdentity, Schema]): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) @@ -55,38 +60,51 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { case null | "" => "None" case col => s"'$col'" } + val normalizePython = if (normalize) "True" else "False" s""" | categories = [$attrList] | trace_name_col = $traceNameCol + | normalize = $normalizePython | max_vals = {attr: float('-inf') for attr in categories} | - | for _, row in table.iterrows(): - | for attr in categories: - | max_vals[attr] = max(max_vals[attr], row[attr]) - | | fig = go.Figure() | + | if normalize: + | for _, row in table.iterrows(): + | for attr in categories: + | max_vals[attr] = max(max_vals[attr], row[attr]) + | | for _, row in table.iterrows(): - | original_vals = [] - | normalized_vals = [] - | for attr in categories: - | original_vals.append(f"{attr}: {row[attr]}") - | normalized_vals.append( - | row[attr] / max_vals[attr] if max_vals[attr] != 0 else 0 - | ) | trace_name = row[trace_name_col] if trace_name_col is not None else "" + | if normalize: + | original_vals = [] + | normalized_vals = [] + | for attr in categories: + | original_vals.append(f"{attr}: {row[attr]}") + | normalized_vals.append( + | row[attr] / max_vals[attr] if max_vals[attr] != 0 else 0 + | ) | - | fig.add_trace(go.Scatterpolar( - | r=normalized_vals, - | theta=categories, - | fill='toself', - | name=str(trace_name) if trace_name is not None else "", - | text=original_vals, - | hoverinfo="text" - | )) + | fig.add_trace(go.Scatterpolar( + | r=normalized_vals, + | theta=categories, + | fill='toself', + | name=str(trace_name) if trace_name is not None else "", + | text=original_vals, + | hoverinfo="text" + | )) + | else: + | fig.add_trace(go.Scatterpolar( + | r=[row[attr] for attr in categories], + | theta=categories, + | fill='toself', + | name=str(trace_name) if trace_name is not None else "", + | text=[f"{attr}: {row[attr]}" for attr in categories], + | hoverinfo="text" + | )) | - | showlegend = any(row.get(trace_name_col, "") for _, row in table.iterrows()) if trace_name_col is not None else False + | showlegend = any(row.get(trace_name_col) for _, row in table.iterrows()) if trace_name_col is not None else False | | fig.update_layout( | polar=dict(radialaxis=dict(visible=True)), From 8c4d71780b08b2890d0b693351bddf99ae320065 Mon Sep 17 00:00:00 2001 From: Madison Lin Date: Mon, 23 Jun 2025 11:11:07 -0700 Subject: [PATCH 03/19] Add Apache License text to RadarPlotOpDesc --- .../radarPlot/RadarPlotOpDesc.scala | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala index 414f0eda469..1dfd111e642 100644 --- a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala +++ b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala @@ -1,3 +1,22 @@ +/* + * 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 edu.uci.ics.amber.operator.visualization.radarPlot import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} From 36a01f2ddd556861f8824013753e1aabde5a3bf4 Mon Sep 17 00:00:00 2001 From: Madison Lin Date: Sun, 22 Jun 2025 22:26:02 -0700 Subject: [PATCH 04/19] Add Radar Plot Visualization Operator --- .../src/assets/operator_images/RadarPlot.png | Bin 0 -> 48432 bytes .../uci/ics/amber/operator/LogicalOp.scala | 2 + .../radarPlot/RadarPlotOpDesc.scala | 127 ++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 core/gui/src/assets/operator_images/RadarPlot.png create mode 100644 core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala diff --git a/core/gui/src/assets/operator_images/RadarPlot.png b/core/gui/src/assets/operator_images/RadarPlot.png new file mode 100644 index 0000000000000000000000000000000000000000..374a6e77731910746834f964c052390b4f545ff4 GIT binary patch literal 48432 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 UpX#cCL%^S=nyzZOvQ^ms0ke70lmGw# literal 0 HcmV?d00001 diff --git a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/LogicalOp.scala b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/LogicalOp.scala index b053ff929a9..cd84afa7b4f 100644 --- a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/LogicalOp.scala +++ b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/LogicalOp.scala @@ -113,6 +113,7 @@ import edu.uci.ics.amber.operator.visualization.lineChart.LineChartOpDesc import edu.uci.ics.amber.operator.visualization.networkGraph.NetworkGraphOpDesc import edu.uci.ics.amber.operator.visualization.pieChart.PieChartOpDesc import edu.uci.ics.amber.operator.visualization.quiverPlot.QuiverPlotOpDesc +import edu.uci.ics.amber.operator.visualization.radarPlot.RadarPlotOpDesc import edu.uci.ics.amber.operator.visualization.rangeSlider.RangeSliderOpDesc import edu.uci.ics.amber.operator.visualization.sankeyDiagram.SankeyDiagramOpDesc import edu.uci.ics.amber.operator.visualization.scatter3DChart.Scatter3dChartOpDesc @@ -173,6 +174,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[WordCloudOpDesc], name = "WordCloud"), new Type(value = classOf[HtmlVizOpDesc], name = "HTMLVisualizer"), new Type(value = classOf[UrlVizOpDesc], name = "URLVisualizer"), diff --git a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala new file mode 100644 index 00000000000..2a650442723 --- /dev/null +++ b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala @@ -0,0 +1,127 @@ +package edu.uci.ics.amber.operator.visualization.radarPlot + +import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} +import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle} +import edu.uci.ics.amber.core.tuple.{AttributeType, Schema} +import edu.uci.ics.amber.operator.PythonOperatorDescriptor +import edu.uci.ics.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} +import edu.uci.ics.amber.core.workflow.OutputPort.OutputMode +import edu.uci.ics.amber.core.workflow.{InputPort, OutputPort, PortIdentity} +import edu.uci.ics.amber.operator.metadata.annotations.{AutofillAttributeName, AutofillAttributeNameList} + +@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[String] = _ + + @JsonProperty(value = "traceNameAttribute", required = false) + @JsonSchemaTitle("Trace Name Column") + @JsonPropertyDescription("Optional column to use for naming each radar trace") + @AutofillAttributeName + var traceNameAttribute: String = "" + + override def getOutputSchemas(inputSchemas: Map[PortIdentity, Schema]): Map[PortIdentity, Schema] = { + val outputSchema = Schema() + .add("html-content", AttributeType.STRING) + Map(operatorInfo.outputPorts.head.id -> outputSchema) + Map(operatorInfo.outputPorts.head.id -> outputSchema) + } + + override def operatorInfo: OperatorInfo = + OperatorInfo( + "Radar Plot", + "View the result in radar plot", + OperatorGroupConstants.VISUALIZATION_BASIC_GROUP, + inputPorts = List(InputPort()), + outputPorts = List(OutputPort(mode = OutputMode.SINGLE_SNAPSHOT)) + ) + + def generateRadarPlotCode(): String = { + assert(selectedAttributes.nonEmpty) + + val attrList = selectedAttributes.map(attr => s""""$attr"""").mkString(", ") + val traceNameCol = traceNameAttribute match { + case null | "" => "None" + case col => s"'$col'" + } + + s""" + | categories = [$attrList] + | trace_name_col = $traceNameCol + | max_vals = {attr: float('-inf') for attr in categories} + | + | for _, row in table.iterrows(): + | for attr in categories: + | max_vals[attr] = max(max_vals[attr], row[attr]) + | + | fig = go.Figure() + | + | for _, row in table.iterrows(): + | original_vals = [] + | normalized_vals = [] + | for attr in categories: + | original_vals.append(f"{attr}: {row[attr]}") + | normalized_vals.append( + | row[attr] / max_vals[attr] if max_vals[attr] != 0 else 0 + | ) + | trace_name = row[trace_name_col] if trace_name_col is not None else "" + | + | fig.add_trace(go.Scatterpolar( + | r=normalized_vals, + | theta=categories, + | fill='toself', + | name=str(trace_name) if trace_name is not None else "", + | text=original_vals, + | hoverinfo="text" + | )) + | + | showlegend = any(row.get(trace_name_col, "") for _, row in table.iterrows()) if trace_name_col is not None else False + | + | fig.update_layout( + | polar=dict(radialaxis=dict(visible=True)), + | showlegend=showlegend, + | width=600, + | height=600 + | ) + |""".stripMargin + } + + + + override def generatePythonCode(): String = { + s""" + |from pytexera import * + |import plotly.graph_objects as go + |import plotly.io + | + |class ProcessTableOperator(UDFTableOperator): + | + | def render_error(self, error_msg): + | return '''

Radar Plot is not available.

+ |

Reason is: {}

+ | '''.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} + |""".stripMargin + } +} From bb695b905add902af4a097caa6f11a995a05f4be Mon Sep 17 00:00:00 2001 From: Madison Lin Date: Mon, 23 Jun 2025 10:42:41 -0700 Subject: [PATCH 05/19] Add normalization tick box in Radar Plot visualization operator properties --- .../radarPlot/RadarPlotOpDesc.scala | 58 ++++++++++++------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala index 2a650442723..414f0eda469 100644 --- a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala +++ b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala @@ -31,6 +31,11 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { @AutofillAttributeName var traceNameAttribute: String = "" + @JsonProperty(value = "normalize", required = false) + @JsonSchemaTitle("Normalize Data") + @JsonPropertyDescription("Normalize the radar plot values") + var normalize: Boolean = true + override def getOutputSchemas(inputSchemas: Map[PortIdentity, Schema]): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) @@ -55,38 +60,51 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { case null | "" => "None" case col => s"'$col'" } + val normalizePython = if (normalize) "True" else "False" s""" | categories = [$attrList] | trace_name_col = $traceNameCol + | normalize = $normalizePython | max_vals = {attr: float('-inf') for attr in categories} | - | for _, row in table.iterrows(): - | for attr in categories: - | max_vals[attr] = max(max_vals[attr], row[attr]) - | | fig = go.Figure() | + | if normalize: + | for _, row in table.iterrows(): + | for attr in categories: + | max_vals[attr] = max(max_vals[attr], row[attr]) + | | for _, row in table.iterrows(): - | original_vals = [] - | normalized_vals = [] - | for attr in categories: - | original_vals.append(f"{attr}: {row[attr]}") - | normalized_vals.append( - | row[attr] / max_vals[attr] if max_vals[attr] != 0 else 0 - | ) | trace_name = row[trace_name_col] if trace_name_col is not None else "" + | if normalize: + | original_vals = [] + | normalized_vals = [] + | for attr in categories: + | original_vals.append(f"{attr}: {row[attr]}") + | normalized_vals.append( + | row[attr] / max_vals[attr] if max_vals[attr] != 0 else 0 + | ) | - | fig.add_trace(go.Scatterpolar( - | r=normalized_vals, - | theta=categories, - | fill='toself', - | name=str(trace_name) if trace_name is not None else "", - | text=original_vals, - | hoverinfo="text" - | )) + | fig.add_trace(go.Scatterpolar( + | r=normalized_vals, + | theta=categories, + | fill='toself', + | name=str(trace_name) if trace_name is not None else "", + | text=original_vals, + | hoverinfo="text" + | )) + | else: + | fig.add_trace(go.Scatterpolar( + | r=[row[attr] for attr in categories], + | theta=categories, + | fill='toself', + | name=str(trace_name) if trace_name is not None else "", + | text=[f"{attr}: {row[attr]}" for attr in categories], + | hoverinfo="text" + | )) | - | showlegend = any(row.get(trace_name_col, "") for _, row in table.iterrows()) if trace_name_col is not None else False + | showlegend = any(row.get(trace_name_col) for _, row in table.iterrows()) if trace_name_col is not None else False | | fig.update_layout( | polar=dict(radialaxis=dict(visible=True)), From 8f7ebbfb33fb32754a4d5ecdbd39eaaa93f6a66e Mon Sep 17 00:00:00 2001 From: Madison Lin Date: Mon, 23 Jun 2025 11:11:07 -0700 Subject: [PATCH 06/19] Add Apache License text to RadarPlotOpDesc --- .../radarPlot/RadarPlotOpDesc.scala | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala index 414f0eda469..1dfd111e642 100644 --- a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala +++ b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala @@ -1,3 +1,22 @@ +/* + * 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 edu.uci.ics.amber.operator.visualization.radarPlot import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription} From 0214893a190c5a8284a06e5806d0f52c5b4ebc11 Mon Sep 17 00:00:00 2001 From: Madison Lin Date: Mon, 23 Jun 2025 13:05:20 -0700 Subject: [PATCH 07/19] Reformatted RadarPlotOpDesc with scalafmt --- .../visualization/radarPlot/RadarPlotOpDesc.scala | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala index 1dfd111e642..d09d68795d7 100644 --- a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala +++ b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala @@ -26,7 +26,10 @@ import edu.uci.ics.amber.operator.PythonOperatorDescriptor import edu.uci.ics.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import edu.uci.ics.amber.core.workflow.OutputPort.OutputMode import edu.uci.ics.amber.core.workflow.{InputPort, OutputPort, PortIdentity} -import edu.uci.ics.amber.operator.metadata.annotations.{AutofillAttributeName, AutofillAttributeNameList} +import edu.uci.ics.amber.operator.metadata.annotations.{ + AutofillAttributeName, + AutofillAttributeNameList +} @JsonSchemaInject(json = """ { @@ -55,7 +58,9 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { @JsonPropertyDescription("Normalize the radar plot values") var normalize: Boolean = true - override def getOutputSchemas(inputSchemas: Map[PortIdentity, Schema]): Map[PortIdentity, Schema] = { + override def getOutputSchemas( + inputSchemas: Map[PortIdentity, Schema] + ): Map[PortIdentity, Schema] = { val outputSchema = Schema() .add("html-content", AttributeType.STRING) Map(operatorInfo.outputPorts.head.id -> outputSchema) @@ -134,8 +139,6 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { |""".stripMargin } - - override def generatePythonCode(): String = { s""" |from pytexera import * From fbbbebab10fdad69deeed62537fd419ad30123de Mon Sep 17 00:00:00 2001 From: Madison Lin Date: Tue, 24 Jun 2025 11:08:08 -0700 Subject: [PATCH 08/19] Adjust Radar Plot operator attributes and change operator category to 'Scientific' --- .../operator/visualization/radarPlot/RadarPlotOpDesc.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala index d09d68795d7..505a4b7a57b 100644 --- a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala +++ b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala @@ -47,13 +47,13 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { @AutofillAttributeNameList var selectedAttributes: List[String] = _ - @JsonProperty(value = "traceNameAttribute", required = false) + @JsonProperty(value = "traceNameAttribute", defaultValue = "", required = false) @JsonSchemaTitle("Trace Name Column") @JsonPropertyDescription("Optional column to use for naming each radar trace") @AutofillAttributeName var traceNameAttribute: String = "" - @JsonProperty(value = "normalize", required = false) + @JsonProperty(value = "normalize", defaultValue = "true", required = true) @JsonSchemaTitle("Normalize Data") @JsonPropertyDescription("Normalize the radar plot values") var normalize: Boolean = true @@ -71,7 +71,7 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { OperatorInfo( "Radar Plot", "View the result in radar plot", - OperatorGroupConstants.VISUALIZATION_BASIC_GROUP, + OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort(mode = OutputMode.SINGLE_SNAPSHOT)) ) From 2057748cf5f9cabb8642b6b5031a352ecc2a0a37 Mon Sep 17 00:00:00 2001 From: Madison Lin Date: Tue, 24 Jun 2025 11:24:59 -0700 Subject: [PATCH 09/19] Adjust Radar Plot operator description and do error checking for if selectedAttributes is empty --- .../visualization/radarPlot/RadarPlotOpDesc.scala | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala index 505a4b7a57b..cd6f6851368 100644 --- a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala +++ b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala @@ -70,15 +70,13 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { override def operatorInfo: OperatorInfo = OperatorInfo( "Radar Plot", - "View the result in radar plot", + "View the result in a radar plot. A radar plot displays multivariate data on multiple axes arranged in a circular layout, allowing for comparison between different entities.", OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP, inputPorts = List(InputPort()), outputPorts = List(OutputPort(mode = OutputMode.SINGLE_SNAPSHOT)) ) def generateRadarPlotCode(): String = { - assert(selectedAttributes.nonEmpty) - val attrList = selectedAttributes.map(attr => s""""$attr"""").mkString(", ") val traceNameCol = traceNameAttribute match { case null | "" => "None" @@ -88,6 +86,10 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { s""" | categories = [$attrList] + | if len(categories) == 0: + | yield {'html-content': self.render_error("No columns selected as axes.")} + | return + | | trace_name_col = $traceNameCol | normalize = $normalizePython | max_vals = {attr: float('-inf') for attr in categories} @@ -155,7 +157,7 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { | @overrides | def process_table(self, table: Table, port: int): | if table.empty: - | yield {'html-content': self.render_error("input table is empty.")} + | yield {'html-content': self.render_error("Input table is empty.")} | return | | ${generateRadarPlotCode()} From f2e5145d2669d0660a759bf3498b391232658673 Mon Sep 17 00:00:00 2001 From: Madison Lin Date: Tue, 24 Jun 2025 11:41:26 -0700 Subject: [PATCH 10/19] Adjust Radar Plot operator's 'normalize' attribute to highlight use of max normalization specifically --- .../radarPlot/RadarPlotOpDesc.scala | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala index cd6f6851368..16dcb16fbbc 100644 --- a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala +++ b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala @@ -53,10 +53,10 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { @AutofillAttributeName var traceNameAttribute: String = "" - @JsonProperty(value = "normalize", defaultValue = "true", required = true) - @JsonSchemaTitle("Normalize Data") - @JsonPropertyDescription("Normalize the radar plot values") - var normalize: Boolean = true + @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 override def getOutputSchemas( inputSchemas: Map[PortIdentity, Schema] @@ -82,7 +82,7 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { case null | "" => "None" case col => s"'$col'" } - val normalizePython = if (normalize) "True" else "False" + val maxNormalizePython = if (maxNormalize) "True" else "False" s""" | categories = [$attrList] @@ -91,29 +91,29 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { | return | | trace_name_col = $traceNameCol - | normalize = $normalizePython + | max_normalize = $maxNormalizePython | max_vals = {attr: float('-inf') for attr in categories} | | fig = go.Figure() | - | if normalize: + | if max_normalize: | for _, row in table.iterrows(): | for attr in categories: | max_vals[attr] = max(max_vals[attr], row[attr]) | | for _, row in table.iterrows(): | trace_name = row[trace_name_col] if trace_name_col is not None else "" - | if normalize: + | if max_normalize: | original_vals = [] - | normalized_vals = [] + | max_normalized_vals = [] | for attr in categories: | original_vals.append(f"{attr}: {row[attr]}") - | normalized_vals.append( + | max_normalized_vals.append( | row[attr] / max_vals[attr] if max_vals[attr] != 0 else 0 | ) | | fig.add_trace(go.Scatterpolar( - | r=normalized_vals, + | r=max_normalized_vals, | theta=categories, | fill='toself', | name=str(trace_name) if trace_name is not None else "", From 338ec45f6b3198fb9bbe290bd238c4dbbb7c9e55 Mon Sep 17 00:00:00 2001 From: Madison Lin Date: Tue, 24 Jun 2025 21:26:25 -0700 Subject: [PATCH 11/19] Update Radar Plot operator to use pandas vectorized operations and improve readability --- .../radarPlot/RadarPlotOpDesc.scala | 56 +++++++------------ 1 file changed, 19 insertions(+), 37 deletions(-) diff --git a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala index 16dcb16fbbc..ad28c0588aa 100644 --- a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala +++ b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala @@ -86,55 +86,36 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { s""" | categories = [$attrList] - | if len(categories) == 0: + | if not categories: | yield {'html-content': self.render_error("No columns selected as axes.")} | return | | trace_name_col = $traceNameCol | max_normalize = $maxNormalizePython - | max_vals = {attr: float('-inf') for attr in categories} - | - | fig = go.Figure() + | selected_table = table[categories] + | hover_texts = selected_table.apply(lambda row: [f"{attr}: {row[attr]}" for attr in categories], axis=1) + | trace_names = table[trace_name_col] if trace_name_col else pd.Series([""] * len(table)) | | if max_normalize: - | for _, row in table.iterrows(): - | for attr in categories: - | max_vals[attr] = max(max_vals[attr], row[attr]) + | max_vals = selected_table.max() + | selected_table = selected_table.divide(max_vals).fillna(0) | - | for _, row in table.iterrows(): - | trace_name = row[trace_name_col] if trace_name_col is not None else "" - | if max_normalize: - | original_vals = [] - | max_normalized_vals = [] - | for attr in categories: - | original_vals.append(f"{attr}: {row[attr]}") - | max_normalized_vals.append( - | row[attr] / max_vals[attr] if max_vals[attr] != 0 else 0 - | ) - | - | fig.add_trace(go.Scatterpolar( - | r=max_normalized_vals, - | theta=categories, - | fill='toself', - | name=str(trace_name) if trace_name is not None else "", - | text=original_vals, - | hoverinfo="text" - | )) - | else: - | fig.add_trace(go.Scatterpolar( - | r=[row[attr] for attr in categories], - | theta=categories, - | fill='toself', - | name=str(trace_name) if trace_name is not None else "", - | text=[f"{attr}: {row[attr]}" for attr in categories], - | hoverinfo="text" - | )) + | fig = go.Figure() | - | showlegend = any(row.get(trace_name_col) for _, row in table.iterrows()) if trace_name_col is not None else False + | for idx, row in selected_table.iterrows(): + | trace_name = trace_names.iloc[idx] + | fig.add_trace(go.Scatterpolar( + | r=row.tolist(), + | theta=categories, + | fill='toself', + | name=str(trace_name) if trace_name else "", + | text=hover_texts.iloc[idx], + | hoverinfo="text" + | )) | | fig.update_layout( | polar=dict(radialaxis=dict(visible=True)), - | showlegend=showlegend, + | showlegend=bool(table[trace_name_col].notna().any()) if trace_name_col else False, | width=600, | height=600 | ) @@ -144,6 +125,7 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { override def generatePythonCode(): String = { s""" |from pytexera import * + |import pandas as pd |import plotly.graph_objects as go |import plotly.io | From 4c78ce251a6ae93fb190e501d656ac04777d4747 Mon Sep 17 00:00:00 2001 From: Madison Lin Date: Tue, 24 Jun 2025 21:29:45 -0700 Subject: [PATCH 12/19] Reformat RadarPlotOpDesc with scalafmt --- .../operator/visualization/radarPlot/RadarPlotOpDesc.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala index ad28c0588aa..1d8ee82cd41 100644 --- a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala +++ b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala @@ -55,7 +55,9 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { @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") + @JsonPropertyDescription( + "Normalize radar plot values by scaling them relative to the maximum value on their respective axes" + ) var maxNormalize: Boolean = true override def getOutputSchemas( From aa474c65d7bcc51b62b42dadcc006bbdaad034de Mon Sep 17 00:00:00 2001 From: Madison Lin Date: Wed, 25 Jun 2025 14:45:56 -0700 Subject: [PATCH 13/19] Update Radar Plot operator to use Numpy arrays instead of Pandas DataFrame, and handle None values --- .../radarPlot/RadarPlotOpDesc.scala | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala index 1d8ee82cd41..7c9090a47b2 100644 --- a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala +++ b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala @@ -94,30 +94,42 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { | | trace_name_col = $traceNameCol | max_normalize = $maxNormalizePython - | selected_table = table[categories] - | hover_texts = selected_table.apply(lambda row: [f"{attr}: {row[attr]}" for attr in categories], axis=1) - | trace_names = table[trace_name_col] if trace_name_col else pd.Series([""] * len(table)) + | + | 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) + | ) + | + | hover_texts = selected_table_df.apply( + | lambda row: [f"{attr}: {row[attr]}" for attr in categories], axis=1 + | ).tolist() | | if max_normalize: - | max_vals = selected_table.max() - | selected_table = selected_table.divide(max_vals).fillna(0) + | 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 selected_table.iterrows(): - | trace_name = trace_names.iloc[idx] + | for idx, row in enumerate(selected_table): + | trace_name = trace_names[idx] | fig.add_trace(go.Scatterpolar( | r=row.tolist(), | theta=categories, | fill='toself', | name=str(trace_name) if trace_name else "", - | text=hover_texts.iloc[idx], + | text=hover_texts[idx], | hoverinfo="text" | )) | | fig.update_layout( | polar=dict(radialaxis=dict(visible=True)), - | showlegend=bool(table[trace_name_col].notna().any()) if trace_name_col else False, + | showlegend=True, | width=600, | height=600 | ) @@ -127,7 +139,7 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { override def generatePythonCode(): String = { s""" |from pytexera import * - |import pandas as pd + |import numpy as np |import plotly.graph_objects as go |import plotly.io | From f9aec73c8248fb7a8bd1d1ca3588be000735ee0c Mon Sep 17 00:00:00 2001 From: Madison Lin Date: Wed, 25 Jun 2025 15:25:10 -0700 Subject: [PATCH 14/19] Adjust Radar Plot operator to allow user to revert optional property back to None --- .../visualization/radarPlot/RadarPlotOpDesc.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala index 7c9090a47b2..88746311dde 100644 --- a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala +++ b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala @@ -47,9 +47,9 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { @AutofillAttributeNameList var selectedAttributes: List[String] = _ - @JsonProperty(value = "traceNameAttribute", defaultValue = "", required = false) + @JsonProperty(value = "traceNameAttribute", defaultValue = "-- No Selection --", required = false) @JsonSchemaTitle("Trace Name Column") - @JsonPropertyDescription("Optional column to use for naming each radar trace") + @JsonPropertyDescription("Optional - Select a column to use for naming each radar trace") @AutofillAttributeName var traceNameAttribute: String = "" @@ -81,8 +81,8 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { def generateRadarPlotCode(): String = { val attrList = selectedAttributes.map(attr => s""""$attr"""").mkString(", ") val traceNameCol = traceNameAttribute match { - case null | "" => "None" - case col => s"'$col'" + case "" | "-- No Selection --" => "None" + case col => s"'$col'" } val maxNormalizePython = if (maxNormalize) "True" else "False" From 915d875c57b25dd1839e42e0246caba228ade46d Mon Sep 17 00:00:00 2001 From: Madison Lin Date: Mon, 30 Jun 2025 17:46:30 -0700 Subject: [PATCH 15/19] Add more customization options to Radar Plot operator, fix issue of disconnected trace lines --- .../radarPlot/RadarPlotOpDesc.scala | 44 +++++++++++++++---- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala index 88746311dde..175af4deb97 100644 --- a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala +++ b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala @@ -60,13 +60,28 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { ) var maxNormalize: Boolean = true + // fill, line style, line width, show markers, marker size, show legend, hover info, plot background color + @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") + 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) - Map(operatorInfo.outputPorts.head.id -> outputSchema) } override def operatorInfo: OperatorInfo = @@ -85,6 +100,9 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { case col => s"'$col'" } val maxNormalizePython = if (maxNormalize) "True" else "False" + val fillTracePython = if (fillTrace) "True" else "False" + val showMarkersPython = if (showMarkers) "True" else "False" + val showLegendPython = if (showLegend) "True" else "False" s""" | categories = [$attrList] @@ -94,6 +112,9 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { | | trace_name_col = $traceNameCol | max_normalize = $maxNormalizePython + | fill_trace = $fillTracePython + | show_markers = $showMarkersPython + | show_legend = $showLegendPython | | selected_table_df = table[categories].astype(float) | selected_table = selected_table_df.values @@ -117,19 +138,24 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { | fig = go.Figure() | | for idx, row in enumerate(selected_table): - | trace_name = trace_names[idx] + | # 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=row.tolist(), - | theta=categories, - | fill='toself', - | name=str(trace_name) if trace_name else "", - | text=hover_texts[idx], - | hoverinfo="text" + | 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" | )) | | fig.update_layout( | polar=dict(radialaxis=dict(visible=True)), - | showlegend=True, + | showlegend=show_legend, | width=600, | height=600 | ) From d6c01d08dc28aad21d8c7fba62db7b628989d74c Mon Sep 17 00:00:00 2001 From: Madison Lin Date: Mon, 30 Jun 2025 22:19:20 -0700 Subject: [PATCH 16/19] Add trace color column property to Radar Plot operator --- .../radarPlot/RadarPlotOpDesc.scala | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala index 175af4deb97..2a5af9c7330 100644 --- a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala +++ b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala @@ -53,6 +53,12 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { @AutofillAttributeName var traceNameAttribute: String = "" + @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: String = "" + @JsonProperty(value = "maxNormalize", defaultValue = "true", required = true) @JsonSchemaTitle("Max Normalize") @JsonPropertyDescription( @@ -60,7 +66,6 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { ) var maxNormalize: Boolean = true - // fill, line style, line width, show markers, marker size, show legend, hover info, plot background color @JsonProperty(value = "fillTrace", defaultValue = "true", required = true) @JsonSchemaTitle("Fill Trace") @JsonPropertyDescription("Fill the area within each radar trace") @@ -94,15 +99,17 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { ) def generateRadarPlotCode(): String = { + def toPythonBool(value: Boolean): String = if (value) "True" else "False" + val attrList = selectedAttributes.map(attr => s""""$attr"""").mkString(", ") val traceNameCol = traceNameAttribute match { case "" | "-- No Selection --" => "None" case col => s"'$col'" } - val maxNormalizePython = if (maxNormalize) "True" else "False" - val fillTracePython = if (fillTrace) "True" else "False" - val showMarkersPython = if (showMarkers) "True" else "False" - val showLegendPython = if (showLegend) "True" else "False" + val traceColorCol = traceColorAttribute match { + case "" | "-- No Selection --" => "None" + case col => s"'$col'" + } s""" | categories = [$attrList] @@ -111,10 +118,11 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { | return | | trace_name_col = $traceNameCol - | max_normalize = $maxNormalizePython - | fill_trace = $fillTracePython - | show_markers = $showMarkersPython - | show_legend = $showLegendPython + | trace_color_col = $traceColorCol + | 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 @@ -124,9 +132,21 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { | else np.full(len(table), "", dtype=object) | ) | - | hover_texts = selected_table_df.apply( - | lambda row: [f"{attr}: {row[attr]}" for attr in categories], axis=1 - | ).tolist() + | 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]) + "
" 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 @@ -150,7 +170,9 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { | name=str(trace_names[idx]) if trace_names[idx] else "", | text=closed_hover_texts, | hoverinfo="text", - | mode="lines+markers" if show_markers else "lines" + | mode="lines+markers" if show_markers else "lines", + | line=dict(color=trace_colors[idx]) if trace_colors[idx] else {}, + | marker=dict(color=trace_colors[idx]) if trace_colors[idx] else {} | )) | | fig.update_layout( @@ -167,6 +189,7 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { |from pytexera import * |import numpy as np |import plotly.graph_objects as go + |import plotly.express as px |import plotly.io | |class ProcessTableOperator(UDFTableOperator): From 6e767d1ed30a3dd75fdecf3143cf164a9716f2e1 Mon Sep 17 00:00:00 2001 From: Madison Lin Date: Mon, 30 Jun 2025 22:22:00 -0700 Subject: [PATCH 17/19] Reformatted RadarPlotOpDesc with scalafmt and scalafix --- .../visualization/radarPlot/RadarPlotOpDesc.scala | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala index 2a5af9c7330..48b581eb1cc 100644 --- a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala +++ b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala @@ -53,9 +53,15 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { @AutofillAttributeName var traceNameAttribute: String = "" - @JsonProperty(value = "traceColorAttribute", defaultValue = "-- No Selection --", required = false) + @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)") + @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: String = "" From 5f64b57bba4a98195f13e7e650ecf31da44f53d4 Mon Sep 17 00:00:00 2001 From: Madison Lin Date: Tue, 1 Jul 2025 12:03:15 -0700 Subject: [PATCH 18/19] Add property to select line pattern in Radar Plot operator --- .../radarPlot/RadarPlotLinePattern.java | 37 +++++++++++++++++++ .../radarPlot/RadarPlotOpDesc.scala | 9 ++++- 2 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotLinePattern.java diff --git a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotLinePattern.java b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotLinePattern.java new file mode 100644 index 00000000000..ed9a9e54656 --- /dev/null +++ b/core/workflow-operator/src/main/scala/edu/uci/ics/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 edu.uci.ics.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/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala index 48b581eb1cc..c4e9f4cb805 100644 --- a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala +++ b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala @@ -65,6 +65,10 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { @AutofillAttributeName var traceColorAttribute: String = "" + @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( @@ -84,7 +88,7 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { @JsonProperty(value = "showLegend", defaultValue = "true", required = false) @JsonSchemaTitle("Show Legend") - @JsonPropertyDescription("Display the 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( @@ -125,6 +129,7 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { | | trace_name_col = $traceNameCol | trace_color_col = $traceColorCol + | line_pattern = "${linePattern.getLinePattern}" | max_normalize = ${toPythonBool(maxNormalize)} | fill_trace = ${toPythonBool(fillTrace)} | show_markers = ${toPythonBool(showMarkers)} @@ -177,7 +182,7 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { | text=closed_hover_texts, | hoverinfo="text", | mode="lines+markers" if show_markers else "lines", - | line=dict(color=trace_colors[idx]) if trace_colors[idx] else {}, + | 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 {} | )) | From 489444c673927ed8142ebce08817cc5023dee505 Mon Sep 17 00:00:00 2001 From: Madison Lin Date: Tue, 1 Jul 2025 12:08:16 -0700 Subject: [PATCH 19/19] Reformatted RadarPlotOpDesc with scalafmt and scalafix --- .../operator/visualization/radarPlot/RadarPlotOpDesc.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala index c4e9f4cb805..5f66e9f14f5 100644 --- a/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala +++ b/core/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/visualization/radarPlot/RadarPlotOpDesc.scala @@ -88,7 +88,9 @@ class RadarPlotOpDesc extends PythonOperatorDescriptor { @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)") + @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(