From 2959624af3a718414c48cd6028b7100c2cac71b7 Mon Sep 17 00:00:00 2001
From: Elliot <36275109+Falcons-Royale@users.noreply.github.com>
Date: Fri, 6 Feb 2026 13:09:26 -0800
Subject: [PATCH 1/5] added ternary contour op
---
.../texera/amber/operator/LogicalOp.scala | 2 +
.../ternaryContour/TernaryContourOpDesc.scala | 147 ++++++++++++++++++
.../assets/operator_images/TernaryContour.png | Bin 0 -> 6374 bytes
3 files changed, 149 insertions(+)
create mode 100644 common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/ternaryContour/TernaryContourOpDesc.scala
create mode 100644 frontend/src/assets/operator_images/TernaryContour.png
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 a575d5b0188..2b190eab368 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
@@ -129,6 +129,7 @@ import org.apache.texera.amber.operator.visualization.sankeyDiagram.SankeyDiagra
import org.apache.texera.amber.operator.visualization.scatter3DChart.Scatter3dChartOpDesc
import org.apache.texera.amber.operator.visualization.scatterplot.ScatterplotOpDesc
import org.apache.texera.amber.operator.visualization.tablesChart.TablesPlotOpDesc
+import org.apache.texera.amber.operator.visualization.ternaryContour.TernaryContourOpDesc
import org.apache.texera.amber.operator.visualization.ternaryPlot.TernaryPlotOpDesc
import org.apache.texera.amber.operator.visualization.timeSeriesplot.TimeSeriesOpDesc
import org.apache.texera.amber.operator.visualization.treeplot.TreePlotOpDesc
@@ -243,6 +244,7 @@ trait StateTransferFunc
new Type(value = classOf[TablesPlotOpDesc], name = "TablesPlot"),
new Type(value = classOf[ContinuousErrorBandsOpDesc], name = "ContinuousErrorBands"),
new Type(value = classOf[FigureFactoryTableOpDesc], name = "FigureFactoryTable"),
+ new Type(value = classOf[TernaryContourOpDesc], name = "TernaryContour"),
new Type(value = classOf[TernaryPlotOpDesc], name = "TernaryPlot"),
new Type(value = classOf[DendrogramOpDesc], name = "Dendrogram"),
new Type(value = classOf[NestedTableOpDesc], name = "NestedTable"),
diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/ternaryContour/TernaryContourOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/ternaryContour/TernaryContourOpDesc.scala
new file mode 100644
index 00000000000..2e9bde676aa
--- /dev/null
+++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/ternaryContour/TernaryContourOpDesc.scala
@@ -0,0 +1,147 @@
+/*
+ * 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.ternaryContour
+
+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.core.workflow.OutputPort.OutputMode
+import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, 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}
+
+/**
+ * Visualization Operator for Ternary Plots.
+ *
+ * This operator uses three data fields to construct a ternary plot.
+ * The points can optionally be color coded using a data field.
+ */
+
+class TernaryContourOpDesc extends PythonOperatorDescriptor {
+
+ // Add annotations for the first variable
+ @JsonProperty(value = "firstVariable", required = true)
+ @JsonSchemaTitle("Variable 1")
+ @JsonPropertyDescription("First variable data field")
+ @AutofillAttributeName var firstVariable: String = ""
+
+ // Add annotations for the second variable
+ @JsonProperty(value = "secondVariable", required = true)
+ @JsonSchemaTitle("Variable 2")
+ @JsonPropertyDescription("Second variable data field")
+ @AutofillAttributeName var secondVariable: String = ""
+
+ // Add annotations for the third variable
+ @JsonProperty(value = "thirdVariable", required = true)
+ @JsonSchemaTitle("Variable 3")
+ @JsonPropertyDescription("Third variable data field")
+ @AutofillAttributeName var thirdVariable: String = ""
+
+ // Add annotations for the fourth variable
+ @JsonProperty(value = "fourthVariable", required = true)
+ @JsonSchemaTitle("Variable 4")
+ @JsonPropertyDescription("Fourth variable data field")
+ @AutofillAttributeName var fourthVariable: String = ""
+
+ // OperatorInfo instance describing ternary plot
+ override def operatorInfo: OperatorInfo =
+ OperatorInfo(
+ userFriendlyName = "Ternary Contour",
+ operatorDescription = "A ternary contour plot shows how a measured value changes across all mixtures of three components that always sum to a constant (usually 100%).",
+ operatorGroupName = OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP,
+ inputPorts = List(InputPort()),
+ outputPorts = List(OutputPort(mode = OutputMode.SINGLE_SNAPSHOT))
+ )
+
+ 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)
+ }
+
+ /** Returns a Python string that drops any tuples with missing values */
+ def manipulateTable(): String = {
+ // Check for any empty data field names
+ assert(firstVariable.nonEmpty && secondVariable.nonEmpty && thirdVariable.nonEmpty)
+ s"""
+ | # Remove any tuples that contain missing values
+ | table.dropna(subset=['$firstVariable', '$secondVariable', '$thirdVariable', '$fourthVariable'], inplace = True)
+ |
+ | #Remove rows where any of the first three variables are negative
+ | table = table[(table[['$firstVariable', '$secondVariable', '$thirdVariable']] >= 0).all(axis=1)]
+ |
+ | #Remove zero-sum rows
+ | s = table['$firstVariable'] + table['$secondVariable'] + table['$thirdVariable']
+ | table = table[s > 0]
+ |""".stripMargin
+ }
+
+ /** Returns a Python string that creates the ternary contour plot figure */
+ def createPlotlyFigure(): String = {
+ s"""
+ | A = table['$firstVariable'].to_numpy()
+ | B = table['$secondVariable'].to_numpy()
+ | C = table['$thirdVariable'].to_numpy()
+ | Z = table['$fourthVariable'].to_numpy()
+ | fig = ff.create_ternary_contour(np.array([A,B,C]), Z, pole_labels=['$firstVariable', '$secondVariable', '$thirdVariable'], interp_mode='cartesian')
+ |""".stripMargin
+ }
+
+ /** Returns a Python string that yields the html content of the ternary contour plot */
+ override def generatePythonCode(): String = {
+ val finalCode =
+ s"""
+ |from pytexera import *
+ |
+ |import plotly.express as px
+ |import plotly.io
+ |import plotly.figure_factory as ff
+ |import numpy as np
+ |
+ |class ProcessTableOperator(UDFTableOperator):
+ |
+ | # Generate custom error message as html string
+ | def render_error(self, error_msg):
+ | return '''
TernaryContour is not available.
+ | Reasons are: {}
+ | '''.format(error_msg)
+ |
+ | @overrides
+ | def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:
+ | if table.empty:
+ | yield {'html-content': self.render_error("Input table is empty.")}
+ | return
+ | ${manipulateTable()}
+ | if table.empty:
+ | yield {'html-content': self.render_error("No valid rows left (every row has at least 1 missing value).")}
+ | return
+ | ${createPlotlyFigure()}
+ | # Convert fig to html content
+ | html = plotly.io.to_html(fig, include_plotlyjs = 'cdn', auto_play = False)
+ | yield {'html-content':html}
+ |""".stripMargin
+ finalCode
+ }
+
+}
diff --git a/frontend/src/assets/operator_images/TernaryContour.png b/frontend/src/assets/operator_images/TernaryContour.png
new file mode 100644
index 0000000000000000000000000000000000000000..ba0e8c3ec23d6dc2260b628e5aee5b672bbd2bf8
GIT binary patch
literal 6374
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
literal 0
HcmV?d00001
From 0717639a29305e74503812e31726038d007b9557 Mon Sep 17 00:00:00 2001
From: Elliot <36275109+Falcons-Royale@users.noreply.github.com>
Date: Fri, 6 Feb 2026 13:50:35 -0800
Subject: [PATCH 2/5] scala format fix
---
.../visualization/ternaryContour/TernaryContourOpDesc.scala | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/ternaryContour/TernaryContourOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/ternaryContour/TernaryContourOpDesc.scala
index 2e9bde676aa..1b5b39a293f 100644
--- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/ternaryContour/TernaryContourOpDesc.scala
+++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/ternaryContour/TernaryContourOpDesc.scala
@@ -65,7 +65,8 @@ class TernaryContourOpDesc extends PythonOperatorDescriptor {
override def operatorInfo: OperatorInfo =
OperatorInfo(
userFriendlyName = "Ternary Contour",
- operatorDescription = "A ternary contour plot shows how a measured value changes across all mixtures of three components that always sum to a constant (usually 100%).",
+ operatorDescription =
+ "A ternary contour plot shows how a measured value changes across all mixtures of three components that always sum to a constant (usually 100%).",
operatorGroupName = OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP,
inputPorts = List(InputPort()),
outputPorts = List(OutputPort(mode = OutputMode.SINGLE_SNAPSHOT))
From 44d0c9c343c544244774ada6f34ce4cbded05ca5 Mon Sep 17 00:00:00 2001
From: Elliot <36275109+Falcons-Royale@users.noreply.github.com>
Date: Mon, 9 Feb 2026 14:22:47 -0800
Subject: [PATCH 3/5] reconfigured ternary contour op to most recent PR merge
---
.../ternaryContour/TernaryContourOpDesc.scala | 53 ++++++++++--------
.../assets/operator_images/TernaryContour.png | Bin 6374 -> 167675 bytes
2 files changed, 29 insertions(+), 24 deletions(-)
diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/ternaryContour/TernaryContourOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/ternaryContour/TernaryContourOpDesc.scala
index 1b5b39a293f..37ed50bc7d8 100644
--- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/ternaryContour/TernaryContourOpDesc.scala
+++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/ternaryContour/TernaryContourOpDesc.scala
@@ -23,10 +23,13 @@ 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.core.workflow.OutputPort.OutputMode
+import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext
+import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString
import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, 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 org.apache.texera.amber.pybuilder.PythonTemplateBuilder
/**
* Visualization Operator for Ternary Plots.
@@ -41,25 +44,25 @@ class TernaryContourOpDesc extends PythonOperatorDescriptor {
@JsonProperty(value = "firstVariable", required = true)
@JsonSchemaTitle("Variable 1")
@JsonPropertyDescription("First variable data field")
- @AutofillAttributeName var firstVariable: String = ""
+ @AutofillAttributeName var firstVariable: EncodableString = ""
// Add annotations for the second variable
@JsonProperty(value = "secondVariable", required = true)
@JsonSchemaTitle("Variable 2")
@JsonPropertyDescription("Second variable data field")
- @AutofillAttributeName var secondVariable: String = ""
+ @AutofillAttributeName var secondVariable: EncodableString = ""
// Add annotations for the third variable
@JsonProperty(value = "thirdVariable", required = true)
@JsonSchemaTitle("Variable 3")
@JsonPropertyDescription("Third variable data field")
- @AutofillAttributeName var thirdVariable: String = ""
+ @AutofillAttributeName var thirdVariable: EncodableString = ""
// Add annotations for the fourth variable
@JsonProperty(value = "fourthVariable", required = true)
- @JsonSchemaTitle("Variable 4")
- @JsonPropertyDescription("Fourth variable data field")
- @AutofillAttributeName var fourthVariable: String = ""
+ @JsonSchemaTitle("Measured Value")
+ @JsonPropertyDescription("Measured value data field")
+ @AutofillAttributeName var fourthVariable: EncodableString = ""
// OperatorInfo instance describing ternary plot
override def operatorInfo: OperatorInfo =
@@ -82,37 +85,39 @@ class TernaryContourOpDesc extends PythonOperatorDescriptor {
}
/** Returns a Python string that drops any tuples with missing values */
- def manipulateTable(): String = {
+ def manipulateTable(): PythonTemplateBuilder = {
// Check for any empty data field names
- assert(firstVariable.nonEmpty && secondVariable.nonEmpty && thirdVariable.nonEmpty)
- s"""
+ assert(
+ firstVariable.nonEmpty && secondVariable.nonEmpty && thirdVariable.nonEmpty && fourthVariable.nonEmpty
+ )
+ pyb"""
| # Remove any tuples that contain missing values
- | table.dropna(subset=['$firstVariable', '$secondVariable', '$thirdVariable', '$fourthVariable'], inplace = True)
+ | table.dropna(subset=[$firstVariable, $secondVariable, $thirdVariable, $fourthVariable], inplace = True)
|
| #Remove rows where any of the first three variables are negative
- | table = table[(table[['$firstVariable', '$secondVariable', '$thirdVariable']] >= 0).all(axis=1)]
+ | table = table[(table[[$firstVariable, $secondVariable, $thirdVariable]] >= 0).all(axis=1)]
|
| #Remove zero-sum rows
- | s = table['$firstVariable'] + table['$secondVariable'] + table['$thirdVariable']
+ | s = table[$firstVariable] + table[$secondVariable] + table[$thirdVariable]
| table = table[s > 0]
- |""".stripMargin
+ |"""
}
/** Returns a Python string that creates the ternary contour plot figure */
- def createPlotlyFigure(): String = {
- s"""
- | A = table['$firstVariable'].to_numpy()
- | B = table['$secondVariable'].to_numpy()
- | C = table['$thirdVariable'].to_numpy()
- | Z = table['$fourthVariable'].to_numpy()
- | fig = ff.create_ternary_contour(np.array([A,B,C]), Z, pole_labels=['$firstVariable', '$secondVariable', '$thirdVariable'], interp_mode='cartesian')
- |""".stripMargin
+ def createPlotlyFigure(): PythonTemplateBuilder = {
+ pyb"""
+ | A = table[$firstVariable].to_numpy()
+ | B = table[$secondVariable].to_numpy()
+ | C = table[$thirdVariable].to_numpy()
+ | Z = table[$fourthVariable].to_numpy()
+ | fig = ff.create_ternary_contour(np.array([A,B,C]), Z, pole_labels=[$firstVariable, $secondVariable, $thirdVariable], interp_mode='cartesian')
+ |"""
}
/** Returns a Python string that yields the html content of the ternary contour plot */
override def generatePythonCode(): String = {
val finalCode =
- s"""
+ pyb"""
|from pytexera import *
|
|import plotly.express as px
@@ -141,8 +146,8 @@ class TernaryContourOpDesc extends PythonOperatorDescriptor {
| # Convert fig to html content
| html = plotly.io.to_html(fig, include_plotlyjs = 'cdn', auto_play = False)
| yield {'html-content':html}
- |""".stripMargin
- finalCode
+ |"""
+ finalCode.encode
}
}
diff --git a/frontend/src/assets/operator_images/TernaryContour.png b/frontend/src/assets/operator_images/TernaryContour.png
index ba0e8c3ec23d6dc2260b628e5aee5b672bbd2bf8..6526cb3bbdf3c3b9cf7402ff6f512b5a32b939ef 100644
GIT binary patch
literal 167675
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