Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ package org.apache.texera.amber.operator.visualization.IcicleChart
import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}
import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}
import org.apache.texera.amber.core.tuple.{AttributeType, Schema}
import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext
import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.{
PythonTemplateBuilderStringContext,
pyStringLiteral
}
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.{PythonOperatorDescriptor, StandaloneCodeGenerator}
import org.apache.texera.amber.operator.metadata.annotations.AutofillAttributeName
import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}
import org.apache.texera.amber.operator.visualization.hierarchychart.HierarchySection
Expand All @@ -43,7 +46,7 @@ import javax.validation.constraints.{NotEmpty, NotNull}
}
}
""")
class IcicleChartOpDesc extends PythonOperatorDescriptor {
class IcicleChartOpDesc extends PythonOperatorDescriptor with StandaloneCodeGenerator {
@JsonProperty(required = true)
@JsonSchemaTitle("Hierarchy Path")
@JsonPropertyDescription(
Expand Down Expand Up @@ -132,4 +135,43 @@ class IcicleChartOpDesc extends PythonOperatorDescriptor {
finalCode.encode
}

// Output is an HTML chart, not a tabular DataFrame.
// The translator skips it in the leaf-DataFrame print block.
override def producesDataFrame(): Boolean = false

override def generateStandaloneCode(): String = {
val attributes = hierarchy.map(section => pyStringLiteral(section.attributeName)).mkString(", ")
val valueLit = pyStringLiteral(value)
// The error page is written to output.html, the same file a plotted chart lands
// in, so a reason for "no chart" is where the reader looks for the chart —
// printing it to the terminal alone left output.html absent. render_error's
// continuation line keeps the runtime path's indentation, since the HTML is
// triple-quoted and those spaces reach the browser.
s"""def render_error(error_msg):
| return '''<h1>Icicle chart is not available.</h1>
| <p>Reason is: {} </p>
| '''.format(error_msg)
|
|def fail(error_msg):
| with open("output.html", "w", encoding="utf-8") as output:
| output.write(render_error(error_msg))
| print(f"Icicle chart error: {error_msg}")
|
|if in1df.empty:
| fail("input table is empty.")
|else:
| in1df[$valueLit] = in1df[in1df[$valueLit] > 0][$valueLit]
| in1df.dropna(subset=[$attributes], inplace=True)
| if in1df.empty:
| fail("value column contains only non-positive numbers or nulls.")
| else:
| fig = px.icicle(in1df, path=[$attributes], values=$valueLit,
| color=$valueLit, hover_data=[$attributes],
| color_continuous_scale='RdBu')
| fig.update_layout(margin=dict(l=0, r=0, b=0, t=0))
| fig.write_json("output.json")
| fig.write_html("output.html")
| print("Icicle chart saved to output.html")""".stripMargin
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@ import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}
import org.apache.texera.amber.core.tuple.{AttributeType, Schema}
import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext
import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.{
PythonTemplateBuilderStringContext,
pyStringLiteral
}
import org.apache.texera.amber.pybuilder.PyStringTypes.{EncodableString, PythonLiteral}
import org.apache.texera.amber.core.workflow.PortIdentity
import org.apache.texera.amber.operator.PythonOperatorDescriptor
import org.apache.texera.amber.operator.{PythonOperatorDescriptor, StandaloneCodeGenerator}
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
Expand All @@ -43,7 +46,7 @@ import javax.validation.constraints.NotNull
}
}
""")
class DendrogramOpDesc extends PythonOperatorDescriptor {
class DendrogramOpDesc extends PythonOperatorDescriptor with StandaloneCodeGenerator {
@JsonProperty(value = "xVal", required = true)
@JsonSchemaTitle("Value X Column")
@JsonPropertyDescription("The x values of points in dendrogram")
Expand Down Expand Up @@ -88,12 +91,15 @@ class DendrogramOpDesc extends PythonOperatorDescriptor {
OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP
)

/** An unset threshold reaches the generated code as Python's `None`, which is
* scipy's own 0.7 * max distance — what a blank field already meant.
*/
private def thresholdExpr: PythonLiteral = threshold.map(_.toString).getOrElse("None")

private def createDendrogram(): PythonTemplateBuilder = {
assert(xVal.nonEmpty, "Value X Column cannot be empty")
assert(yVal.nonEmpty, "Value Y Column cannot be empty")
assert(labels.nonEmpty, "Labels cannot be empty")
// Unset means None, which is scipy's own 0.7 * max distance.
val thresholdExpr: PythonLiteral = threshold.map(_.toString).getOrElse("None")
pyb"""
| x = np.array(table[$xVal])
| y = np.array(table[$yVal])
Expand Down Expand Up @@ -146,4 +152,47 @@ class DendrogramOpDesc extends PythonOperatorDescriptor {
|"""
finalcode.encode
}

override def producesDataFrame(): Boolean = false

override def generateStandaloneCode(): String = {

// render_error's continuation line keeps the runtime path's indentation — the
// HTML is triple-quoted, so those spaces reach the browser.
s"""import numpy as np
|import plotly.figure_factory as ff
|
|def render_error(error_msg):
| return '''<h1>Dendrogram is not available.</h1>
| <p>Reason is: {} </p>
| '''.format(error_msg)
|
|def _write_error(message):
| with open("output.html", "w", encoding="utf-8") as output:
| output.write(render_error(message))
|
|if in1df.empty:
| _write_error("input table is empty.")
|else:
| # A row missing either coordinate has no position to cluster from, and
| # scipy refuses a NaN anywhere in the distance matrix.
| in1df = in1df.dropna(subset=[${pyStringLiteral(xVal)}, ${pyStringLiteral(yVal)}])
| if in1df.empty:
| _write_error("input table has no rows with all of the configured columns filled in.")
| # Clustering starts from the distances between rows, so a single row
| # leaves scipy an empty distance matrix and it raises rather than draws.
| elif len(in1df) < 2:
| _write_error("input table has fewer than two rows to cluster.")
| else:
| x = np.array(in1df[${pyStringLiteral(xVal)}])
| y = np.array(in1df[${pyStringLiteral(yVal)}])
| data = np.column_stack((x, y))
| labels = in1df[${pyStringLiteral(labels)}].tolist()
| fig = ff.create_dendrogram(data, labels=labels, color_threshold=$thresholdExpr)
| fig.update_layout(yaxis_title="Linkage Distance", margin=dict(l=0, r=0, b=0, t=0))
| fig.write_json("output.json")
| fig.write_html("output.html")
| print("Dendrogram saved to output.html")""".stripMargin
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ package org.apache.texera.amber.operator.visualization.hierarchychart
import com.fasterxml.jackson.annotation.{JsonProperty, JsonPropertyDescription}
import com.kjetland.jackson.jsonSchema.annotations.{JsonSchemaInject, JsonSchemaTitle}
import org.apache.texera.amber.core.tuple.{AttributeType, Schema}
import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext
import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.{
PythonTemplateBuilderStringContext,
pyStringLiteral
}
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.{PythonOperatorDescriptor, StandaloneCodeGenerator}
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
Expand All @@ -42,7 +45,7 @@ import javax.validation.constraints.{NotEmpty, NotNull}
}
}
""")
class HierarchyChartOpDesc extends PythonOperatorDescriptor {
class HierarchyChartOpDesc extends PythonOperatorDescriptor with StandaloneCodeGenerator {
@JsonProperty(required = true)
@JsonSchemaTitle("Chart Type")
@JsonPropertyDescription("Treemap or Sunburst")
Expand Down Expand Up @@ -137,4 +140,43 @@ class HierarchyChartOpDesc extends PythonOperatorDescriptor {
finalCode.encode
}

// Output is an HTML chart, not a tabular DataFrame.
// The translator skips it in the leaf-DataFrame print block.
override def producesDataFrame(): Boolean = false

override def generateStandaloneCode(): String = {
val attributes = hierarchy.map(section => pyStringLiteral(section.attributeName)).mkString(", ")
val valueLit = pyStringLiteral(value)
// The error page is written to output.html, the same file a plotted chart lands
// in, so a reason for "no chart" is where the reader looks for the chart —
// printing it to the terminal alone left output.html absent. render_error's
// continuation line keeps the runtime path's indentation, since the HTML is
// triple-quoted and those spaces reach the browser.
s"""def render_error(error_msg):
| return '''<h1>Hierarchy chart is not available.</h1>
| <p>Reason is: {} </p>
| '''.format(error_msg)
|
|def fail(error_msg):
| with open("output.html", "w", encoding="utf-8") as output:
| output.write(render_error(error_msg))
| print(f"Hierarchy chart error: {error_msg}")
|
|if in1df.empty:
| fail("input table is empty.")
|else:
| in1df[$valueLit] = in1df[in1df[$valueLit] > 0][$valueLit]
| in1df.dropna(subset=[$attributes], inplace=True)
| if in1df.empty:
| fail("value column contains only non-positive numbers or nulls.")
| else:
| fig = px.${hierarchyChartType.getPlotlyExpressApiName}(in1df, path=[$attributes], values=$valueLit,
| color=$valueLit, hover_data=[$attributes],
| color_continuous_scale='RdBu')
| fig.update_layout(margin=dict(l=0, r=0, b=0, t=0))
| fig.write_json("output.json")
| fig.write_html("output.html")
| print("Hierarchy chart saved to output.html")""".stripMargin
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@ 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.{PythonOperatorDescriptor, StandaloneCodeGenerator}
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
import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.pyStringLiteral

import javax.validation.constraints.NotNull

class NetworkGraphOpDesc extends PythonOperatorDescriptor {
class NetworkGraphOpDesc extends PythonOperatorDescriptor with StandaloneCodeGenerator {
@JsonProperty(required = true)
@JsonSchemaTitle("Source Column")
@JsonPropertyDescription("Source node for edge in graph")
Expand Down Expand Up @@ -66,6 +67,10 @@ class NetworkGraphOpDesc extends PythonOperatorDescriptor {
OperatorGroupConstants.VISUALIZATION_SCIENTIFIC_GROUP
)

/** An edge needs both of its ends, and networkx refuses a null as a node
* outright. A row missing either one names no edge, so it goes before the
* graph is built rather than reaching add_node.
*/
def manipulateTable(): PythonTemplateBuilder = {
assert(source.nonEmpty, "Source Column cannot be empty")
assert(destination.nonEmpty, "Destination Column cannot be empty")
Expand Down Expand Up @@ -95,6 +100,7 @@ class NetworkGraphOpDesc extends PythonOperatorDescriptor {
|
| @overrides
| def process_table(self, table: Table, port: int) -> Iterator[Optional[TableLike]]:
|${manipulateTable()}
| if not table.empty:
| sources = table[$source]
| destinations = table[$destination]
Expand Down Expand Up @@ -199,4 +205,121 @@ class NetworkGraphOpDesc extends PythonOperatorDescriptor {
finalCode.encode
}

override def producesDataFrame(): Boolean = false

override def generateStandaloneCode(): String = {
val sourceLit = pyStringLiteral(source)
val destinationLit = pyStringLiteral(destination)
// The <br> is part of the emitted title, so it is escaped with the value
// rather than left outside the literal.
val titleLit = pyStringLiteral(s"<br>$title")
s"""import networkx as nx
|
|def render_error(error_msg):
| return '''<h1>Network graph is not available.</h1>
| <p>Reason is: {} </p>
| '''.format(error_msg)
|
|if in1df.empty:
| with open("output.html", "w", encoding="utf-8") as output:
| output.write(render_error("Table should not have any empty/null values or fields."))
|else:
| table = in1df.dropna(subset=[$sourceLit]).dropna(subset=[$destinationLit]).copy()
| if table.empty:
| with open("output.html", "w", encoding="utf-8") as output:
| output.write(render_error("Table should not have any empty/null values or fields."))
| else:
| sources = table[$sourceLit]
| destinations = table[$destinationLit]
| nodes = list(dict.fromkeys(pd.concat([sources, destinations]).tolist()))
| G = nx.Graph()
| for node in nodes:
| G.add_node(node)
| for _, row in table.iterrows():
| G.add_edges_from([(row[$sourceLit], row[$destinationLit])])
| pos = nx.spring_layout(G, k=0.5, iterations=50, seed=0)
| for n, p in pos.items():
| G.nodes[n]["pos"] = p
|
| edge_trace = go.Scatter(
| x=[],
| y=[],
| name="Edges",
| line=dict(width=0.5, color="#888"),
| hoverinfo="none",
| mode="lines",
| visible=True
| )
|
| for edge in G.edges():
| x0, y0 = G.nodes[edge[0]]["pos"]
| x1, y1 = G.nodes[edge[1]]["pos"]
| edge_trace["x"] += tuple([x0, x1, None])
| edge_trace["y"] += tuple([y0, y1, None])
|
| node_trace = go.Scatter(
| x=[],
| y=[],
| name="Nodes",
| text=[],
| mode="markers",
| hoverinfo="text",
| visible=True,
| marker=dict(
| showscale=True,
| colorscale="plasma",
| reversescale=True,
| color=[],
| size=15,
| colorbar=dict(
| thickness=10,
| title="Node Connections",
| xanchor="left",
| titleside="right"
| ),
| line=dict(width=0)
| )
| )
|
| for node in G.nodes():
| x, y = G.nodes[node]["pos"]
| node_trace["x"] += tuple([x])
| node_trace["y"] += tuple([y])
|
| for _, adjacencies in enumerate(G.adjacency()):
| node_trace["marker"]["color"] += tuple([len(adjacencies[1])])
| node_info = str(adjacencies[0]) + ": " + str(len(adjacencies[1])) + " connections."
| node_trace["text"] += tuple([node_info])
|
| fig = go.Figure(
| data=[edge_trace, node_trace],
| layout=go.Layout(
| title=$titleLit,
| hovermode="closest",
| showlegend=False,
| margin=dict(b=20, l=5, r=5, t=40),
| annotations=[
| dict(
| text="",
| showarrow=False,
| xref="paper",
| yref="paper"
| )
| ],
| xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
| yaxis=dict(showgrid=False, zeroline=False, showticklabels=False)
| )
| )
| fig.update_layout(
| margin=dict(l=0, r=0, t=0, b=0),
| legend=dict(
| itemclick=False,
| itemdoubleclick=False
| )
| )
| fig.write_json("output.json")
| fig.write_html("output.html")
| print("Network graph saved to output.html")""".stripMargin
}

}
Loading
Loading