Skip to content

Commit edf1988

Browse files
committed
added ternary contour op
1 parent cfdad43 commit edf1988

3 files changed

Lines changed: 149 additions & 0 deletions

File tree

common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/LogicalOp.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ import org.apache.texera.amber.operator.visualization.sankeyDiagram.SankeyDiagra
129129
import org.apache.texera.amber.operator.visualization.scatter3DChart.Scatter3dChartOpDesc
130130
import org.apache.texera.amber.operator.visualization.scatterplot.ScatterplotOpDesc
131131
import org.apache.texera.amber.operator.visualization.tablesChart.TablesPlotOpDesc
132+
import org.apache.texera.amber.operator.visualization.ternaryContour.TernaryContourOpDesc
132133
import org.apache.texera.amber.operator.visualization.ternaryPlot.TernaryPlotOpDesc
133134
import org.apache.texera.amber.operator.visualization.timeSeriesplot.TimeSeriesOpDesc
134135
import org.apache.texera.amber.operator.visualization.treeplot.TreePlotOpDesc
@@ -242,6 +243,7 @@ trait StateTransferFunc
242243
new Type(value = classOf[TablesPlotOpDesc], name = "TablesPlot"),
243244
new Type(value = classOf[ContinuousErrorBandsOpDesc], name = "ContinuousErrorBands"),
244245
new Type(value = classOf[FigureFactoryTableOpDesc], name = "FigureFactoryTable"),
246+
new Type(value = classOf[TernaryContourOpDesc], name = "TernaryContour"),
245247
new Type(value = classOf[TernaryPlotOpDesc], name = "TernaryPlot"),
246248
new Type(value = classOf[DendrogramOpDesc], name = "Dendrogram"),
247249
new Type(value = classOf[NestedTableOpDesc], name = "NestedTable"),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.texera.amber.operator.visualization.ternaryContour
21+
22+
import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}
23+
import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle
24+
import org.apache.texera.amber.core.tuple.{AttributeType, Schema}
25+
import org.apache.texera.amber.core.workflow.OutputPort.OutputMode
26+
import org.apache.texera.amber.core.workflow.{InputPort, OutputPort, PortIdentity}
27+
import org.apache.texera.amber.operator.PythonOperatorDescriptor
28+
import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName
29+
import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}
30+
31+
/**
32+
* Visualization Operator for Ternary Plots.
33+
*
34+
* This operator uses three data fields to construct a ternary plot.
35+
* The points can optionally be color coded using a data field.
36+
*/
37+
38+
class TernaryContourOpDesc extends PythonOperatorDescriptor {
39+
40+
// Add annotations for the first variable
41+
@JsonProperty(value = "firstVariable", required = true)
42+
@JsonSchemaTitle("Variable 1")
43+
@JsonPropertyDescription("First variable data field")
44+
@AutofillAttributeName var firstVariable: String = ""
45+
46+
// Add annotations for the second variable
47+
@JsonProperty(value = "secondVariable", required = true)
48+
@JsonSchemaTitle("Variable 2")
49+
@JsonPropertyDescription("Second variable data field")
50+
@AutofillAttributeName var secondVariable: String = ""
51+
52+
// Add annotations for the third variable
53+
@JsonProperty(value = "thirdVariable", required = true)
54+
@JsonSchemaTitle("Variable 3")
55+
@JsonPropertyDescription("Third variable data field")
56+
@AutofillAttributeName var thirdVariable: String = ""
57+
58+
// Add annotations for the fourth variable
59+
@JsonProperty(value = "fourthVariable", required = true)
60+
@JsonSchemaTitle("Variable 4")
61+
@JsonPropertyDescription("Fourth variable data field")
62+
@AutofillAttributeName var fourthVariable: String = ""
63+
64+
// OperatorInfo instance describing ternary plot
65+
override def operatorInfo: OperatorInfo =
66+
OperatorInfo(
67+
userFriendlyName = "Ternary Contour",
68+
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%).",
69+
operatorGroupName = OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP,
70+
inputPorts = List(InputPort()),
71+
outputPorts = List(OutputPort(mode = OutputMode.SINGLE_SNAPSHOT))
72+
)
73+
74+
override def getOutputSchemas(
75+
inputSchemas: Map[PortIdentity, Schema]
76+
): Map[PortIdentity, Schema] = {
77+
val outputSchema = Schema()
78+
.add("html-content", AttributeType.STRING)
79+
Map(operatorInfo.outputPorts.head.id -> outputSchema)
80+
Map(operatorInfo.outputPorts.head.id -> outputSchema)
81+
}
82+
83+
/** Returns a Python string that drops any tuples with missing values */
84+
def manipulateTable(): String = {
85+
// Check for any empty data field names
86+
assert(firstVariable.nonEmpty && secondVariable.nonEmpty && thirdVariable.nonEmpty)
87+
s"""
88+
| # Remove any tuples that contain missing values
89+
| table.dropna(subset=['$firstVariable', '$secondVariable', '$thirdVariable', '$fourthVariable'], inplace = True)
90+
|
91+
| #Remove rows where any of the first three variables are negative
92+
| table = table[(table[['$firstVariable', '$secondVariable', '$thirdVariable']] >= 0).all(axis=1)]
93+
|
94+
| #Remove zero-sum rows
95+
| s = table['$firstVariable'] + table['$secondVariable'] + table['$thirdVariable']
96+
| table = table[s > 0]
97+
|""".stripMargin
98+
}
99+
100+
/** Returns a Python string that creates the ternary contour plot figure */
101+
def createPlotlyFigure(): String = {
102+
s"""
103+
| A = table['$firstVariable'].to_numpy()
104+
| B = table['$secondVariable'].to_numpy()
105+
| C = table['$thirdVariable'].to_numpy()
106+
| Z = table['$fourthVariable'].to_numpy()
107+
| fig = ff.create_ternary_contour(np.array([A,B,C]), Z, pole_labels=['$firstVariable', '$secondVariable', '$thirdVariable'], interp_mode='cartesian')
108+
|""".stripMargin
109+
}
110+
111+
/** Returns a Python string that yields the html content of the ternary contour plot */
112+
override def generatePythonCode(): String = {
113+
val finalCode =
114+
s"""
115+
|from pytexera import *
116+
|
117+
|import plotly.express as px
118+
|import plotly.io
119+
|import plotly.figure_factory as ff
120+
|import numpy as np
121+
|
122+
|class ProcessTableOperator(UDFTableOperator):
123+
|
124+
| # Generate custom error message as html string
125+
| def render_error(self, error_msg):
126+
| return '''<h1>TernaryContour is not available.</h1>
127+
| <p>Reasons are: {} </p>
128+
| '''.format(error_msg)
129+
|
130+
| @overrides
131+
| def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:
132+
| if table.empty:
133+
| yield {'html-content': self.render_error("Input table is empty.")}
134+
| return
135+
| ${manipulateTable()}
136+
| if table.empty:
137+
| yield {'html-content': self.render_error("No valid rows left (every row has at least 1 missing value).")}
138+
| return
139+
| ${createPlotlyFigure()}
140+
| # Convert fig to html content
141+
| html = plotly.io.to_html(fig, include_plotlyjs = 'cdn', auto_play = False)
142+
| yield {'html-content':html}
143+
|""".stripMargin
144+
finalCode
145+
}
146+
147+
}
6.22 KB
Loading

0 commit comments

Comments
 (0)