This is an automated email from the ASF dual-hosted git repository.

mengw15 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 0485b4d6d8 feat(operator): add carpet plot operator (#4398)
0485b4d6d8 is described below

commit 0485b4d6d8d84915843626bbfc1fe51e1bc7da25
Author: Nicole Ying <[email protected]>
AuthorDate: Sun Apr 19 14:21:11 2026 -0700

    feat(operator): add carpet plot operator (#4398)
    
    ### What changes were proposed in this PR?
    This PR introduces a new scientific visualization operator, Carpet Plot,
    which visualizes data over two axes with a corresponding value to
    generate a carpet plot representation.
    
    The operator takes three required attributes:
    - Axis A: first dimension of the plot
    - Axis B: second dimension of the plot
    - Value (Y): numeric value used to generate the visualization
    Based on these inputs, the operator generates a carpet plot
    visualization.
    
    File Changes:
    - Added CarpetPlot.png to
    frontend/src/assets/operator_images/CarpetPlot.png
    - Added CarpetPlotOpDesc to
    
common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/carpetPlot/CarpetPlotOpDesc.scala
    - Added lines to import CarpetPlot operator to
    
common/workflow-operator/src/main/scala/edu/uci/ics/amber/operator/LogicalOp.scala
    
    Testing CSV file:
    
    
[carpet_test.csv](https://github.com/user-attachments/files/26803094/texera_carpet_test.csv)
    
    Sample Output:
    <img width="1512" height="862" alt="CarpetSample"
    
src="https://github.com/user-attachments/assets/f6230d71-e1ac-4559-ac30-48c0ec87ed7f";
    />
    
    ### Any related issues, documentation, discussions?
    
    No
    
    ### How was this PR tested?
    This PR was tested both manually and using existing backend test suites.
    Backend Tests:
       - Ran `sbt test` from the root directory.
    - All operator validation and compilation checks passed successfully,
    including the newly added CarpetPlot operator.
    
    Manual UI Testing:
       - Started Texera frontend and backend locally.
    - Verified that the Carpet Plot operator appears in the operator panel.
       - Verified the generated result.
       - Confirmed that the custom icon is displayed correctly.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    No
---
 .../apache/texera/amber/operator/LogicalOp.scala   |   2 +
 .../carpetPlot/CarpetPlotOpDesc.scala              | 124 +++++++++++++++++++++
 frontend/src/assets/operator_images/CarpetPlot.png | Bin 0 -> 17941 bytes
 3 files changed, 126 insertions(+)

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 391490a67c..d9b9cd9f10 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
@@ -144,6 +144,7 @@ import org.apache.commons.lang3.builder.{EqualsBuilder, 
HashCodeBuilder, ToStrin
 import org.apache.texera.amber.operator.sklearn.testing.SklearnTestingOpDesc
 import org.apache.texera.amber.operator.source.scan.file.{FileScanOpDesc, 
FileScanSourceOpDesc}
 import 
org.apache.texera.amber.operator.visualization.stripChart.StripChartOpDesc
+import 
org.apache.texera.amber.operator.visualization.carpetPlot.CarpetPlotOpDesc
 
 import java.util.UUID
 import scala.util.Try
@@ -233,6 +234,7 @@ trait StateTransferFunc
     new Type(value = classOf[VolcanoPlotOpDesc], name = "VolcanoPlot"),
     new Type(value = classOf[CartesianProductOpDesc], name = 
"CartesianProduct"),
     new Type(value = classOf[FilledAreaPlotOpDesc], name = "FilledAreaPlot"),
+    new Type(value = classOf[CarpetPlotOpDesc], name = "CarpetPlot"),
     new Type(value = classOf[DotPlotOpDesc], name = "DotPlot"),
     new Type(value = classOf[TreePlotOpDesc], name = "TreePlot"),
     new Type(value = classOf[BubbleChartOpDesc], name = "BubbleChart"),
diff --git 
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/carpetPlot/CarpetPlotOpDesc.scala
 
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/carpetPlot/CarpetPlotOpDesc.scala
new file mode 100644
index 0000000000..1429f2242c
--- /dev/null
+++ 
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/carpetPlot/CarpetPlotOpDesc.scala
@@ -0,0 +1,124 @@
+/*
+ * 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.carpetPlot
+
+import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}
+import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle
+import org.apache.texera.amber.core.tuple.{AttributeType, Schema}
+import 
org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext
+import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString
+import org.apache.texera.amber.core.workflow.PortIdentity
+import org.apache.texera.amber.operator.PythonOperatorDescriptor
+import 
org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName
+import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, 
OperatorInfo}
+import javax.validation.constraints.NotNull
+
+class CarpetPlotOpDesc extends PythonOperatorDescriptor {
+
+  @JsonProperty(value = "a", required = true)
+  @NotNull(message = "A-axis Attribute cannot be empty")
+  @JsonSchemaTitle("First Parameter Axis Column")
+  @JsonPropertyDescription("Column representing the first parameter axis (a)")
+  @AutofillAttributeName
+  var a: EncodableString = ""
+
+  @JsonProperty(value = "b", required = true)
+  @NotNull(message = "B-axis Attribute cannot be empty")
+  @JsonSchemaTitle("Second Parameter Axis Column")
+  @JsonPropertyDescription("Column representing the second parameter axis (b)")
+  @AutofillAttributeName
+  var b: EncodableString = ""
+
+  @JsonProperty(value = "y", required = true)
+  @NotNull(message = "Y Value cannot be empty")
+  @JsonSchemaTitle("Value Column")
+  @JsonPropertyDescription("Column representing the value at each (a, b) 
coordinate")
+  @AutofillAttributeName
+  var y: EncodableString = ""
+
+  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(
+      "Carpet Plot",
+      "Visualize data in a Carpet Plot",
+      OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP
+    )
+
+  override def generatePythonCode(): String = {
+    val finalCode =
+      pyb"""
+           |from pytexera import *
+           |import plotly.graph_objects as go
+           |import plotly.io as pio
+           |
+           |class ProcessTableOperator(UDFTableOperator):
+           |
+           |    @overrides
+           |    def process_table(self, table: Table, port: int) -> 
Iterator[Optional[TableLike]]:
+           |
+           |        if table.empty:
+           |            yield {"html-content": "<h3>Input table is empty</h3>"}
+           |            return
+           |
+           |        a_col = $a
+           |        b_col = $b
+           |        y_col = $y
+           |
+           |        for col in [a_col, b_col, y_col]:
+           |            if col not in table.columns:
+           |                yield {"html-content": f"<h3>Column '{col}' not 
found</h3>"}
+           |                return
+           |
+           |        table = table.dropna(subset=[a_col, b_col, y_col])
+           |
+           |        if table.empty:
+           |            yield {"html-content": "<h3>No valid rows after 
removing nulls</h3>"}
+           |            return
+           |
+           |        try:
+           |            table[a_col] = table[a_col].astype(float)
+           |            table[b_col] = table[b_col].astype(float)
+           |            table[y_col] = table[y_col].astype(float)
+           |        except Exception as e:
+           |            yield {"html-content": f"<h3>Error converting input 
columns to numeric values: {str(e)}</h3>"}
+           |            return
+           |
+           |        try:
+           |            fig = go.Figure(go.Carpet(
+           |                a=table[a_col],
+           |                b=table[b_col],
+           |                y=table[y_col]
+           |            ))
+           |            html = pio.to_html(fig, include_plotlyjs='cdn', 
auto_play=False)
+           |            yield {"html-content": html}
+           |        except Exception as e:
+           |            yield {"html-content": f"<h3>Error generating carpet 
plot: {str(e)}</h3>"}
+           |"""
+    finalCode.encode
+  }
+
+}
diff --git a/frontend/src/assets/operator_images/CarpetPlot.png 
b/frontend/src/assets/operator_images/CarpetPlot.png
new file mode 100644
index 0000000000..2ef1301a2f
Binary files /dev/null and b/frontend/src/assets/operator_images/CarpetPlot.png 
differ

Reply via email to