Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
55cbb47
Add Radar Plot Visualization Operator
madisonmlin Jun 23, 2025
6a4ff79
Add normalization tick box in Radar Plot visualization operator prope…
madisonmlin Jun 23, 2025
8c4d717
Add Apache License text to RadarPlotOpDesc
madisonmlin Jun 23, 2025
36a01f2
Add Radar Plot Visualization Operator
madisonmlin Jun 23, 2025
bb695b9
Add normalization tick box in Radar Plot visualization operator prope…
madisonmlin Jun 23, 2025
8f7ebbf
Add Apache License text to RadarPlotOpDesc
madisonmlin Jun 23, 2025
027a4ef
Merge branch 'master' into madison-add-radarplot-operator
madisonmlin Jun 23, 2025
0214893
Reformatted RadarPlotOpDesc with scalafmt
madisonmlin Jun 23, 2025
354cad0
Resolved merge conflicts
madisonmlin Jun 23, 2025
fbbbeba
Adjust Radar Plot operator attributes and change operator category to…
madisonmlin Jun 24, 2025
2057748
Adjust Radar Plot operator description and do error checking for if s…
madisonmlin Jun 24, 2025
f2e5145
Adjust Radar Plot operator's 'normalize' attribute to highlight use o…
madisonmlin Jun 24, 2025
338ec45
Update Radar Plot operator to use pandas vectorized operations and im…
madisonmlin Jun 25, 2025
4c78ce2
Reformat RadarPlotOpDesc with scalafmt
madisonmlin Jun 25, 2025
aa474c6
Update Radar Plot operator to use Numpy arrays instead of Pandas Data…
madisonmlin Jun 25, 2025
f9aec73
Adjust Radar Plot operator to allow user to revert optional property …
madisonmlin Jun 25, 2025
915d875
Add more customization options to Radar Plot operator, fix issue of d…
madisonmlin Jul 1, 2025
d6c01d0
Add trace color column property to Radar Plot operator
madisonmlin Jul 1, 2025
6e767d1
Reformatted RadarPlotOpDesc with scalafmt and scalafix
madisonmlin Jul 1, 2025
5f64b57
Add property to select line pattern in Radar Plot operator
madisonmlin Jul 1, 2025
489444c
Reformatted RadarPlotOpDesc with scalafmt and scalafix
madisonmlin Jul 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added core/gui/src/assets/operator_images/RadarPlot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/*
* 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}
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", defaultValue = "-- No Selection --", required = false)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove "--", just "No Selection"

@JsonSchemaTitle("Trace Name Column")
@JsonPropertyDescription("Optional - Select a column to use for naming each radar trace")
@AutofillAttributeName
var traceNameAttribute: String = ""

@JsonProperty(
value = "traceColorAttribute",
defaultValue = "-- No Selection --",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

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 = "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(
"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.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just checked, I think "View the result in a radar plot." is enough.

OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP,
inputPorts = List(InputPort()),
outputPorts = List(OutputPort(mode = OutputMode.SINGLE_SNAPSHOT))
)

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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

case col => s"'$col'"
}
val traceColorCol = traceColorAttribute match {
case "" | "-- No Selection --" => "None"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

case col => s"'$col'"
}

s"""
| 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
| )
|""".stripMargin
}

override def generatePythonCode(): String = {
s"""
|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}
|""".stripMargin
}
}