Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5b8dd57
feat(workflow-compiling-service): export a workflow as a standalone P…
kz930 Sep 1, 2026
a7558eb
test(workflow-compiling-service): run an operator both ways and compa…
kz930 Sep 1, 2026
cba725f
test(workflow-compiling-service): verify a generated script against t…
kz930 Sep 1, 2026
6e54a1b
Merge remote-tracking branch 'upstream/main' into feat/standalone-viz…
kz930 Sep 2, 2026
bc8fc17
ci: give the verify spec a job with an interpreter, and keep it out o…
kz930 Sep 2, 2026
fb7d72b
Merge remote-tracking branch 'myfork/feat/standalone-verify-harness' …
kz930 Sep 2, 2026
eca7c52
feat(visualization): export the table and relation charts as Python
kz930 Sep 2, 2026
5b4c475
feat(visualization): export the word cloud, and verify it alongside t…
kz930 Sep 2, 2026
294b29e
feat(visualization): export the continuous error bands with the line …
kz930 Sep 2, 2026
bde4cd6
test(verify): assert the tiers this batch actually changes
kz930 Sep 2, 2026
4bf1dcb
chore: leave the harness to the change that introduces it
kz930 Sep 2, 2026
e057dd9
Merge upstream/main
kz930 Sep 2, 2026
6d6067e
chore: leave the verification rows to the harness change
kz930 Sep 2, 2026
1760f5b
chore: move the column charts and the tables into their own changes
kz930 Sep 2, 2026
c129550
docs: say the thing once
kz930 Sep 2, 2026
06bc667
feat(visualization): declare the plotly the charts draw with
kz930 Sep 4, 2026
04edbc8
feat(visualization): leave the plotly mixin to the charts that use it
kz930 Sep 4, 2026
42b6610
fix(visualization): draw from a copy rather than from the frame hande…
kz930 Sep 4, 2026
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,14 @@ 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.visualization.PlotlyStandaloneCode
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 +47,7 @@ import javax.validation.constraints.{NotEmpty, NotNull}
}
}
""")
class IcicleChartOpDesc extends PythonOperatorDescriptor {
class IcicleChartOpDesc extends PythonOperatorDescriptor with PlotlyStandaloneCode {
@JsonProperty(required = true)
@JsonSchemaTitle("Hierarchy Path")
@JsonPropertyDescription(
Expand Down Expand Up @@ -132,4 +136,46 @@ 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:
| # On a copy: the same frame can feed another branch of the plan, and
| # both the assignment and the drop below would otherwise reach it.
| chart_df = in1df.copy()
| chart_df[$valueLit] = chart_df[chart_df[$valueLit] > 0][$valueLit]
| chart_df = chart_df.dropna(subset=[$attributes])
| if chart_df.empty:
| fail("value column contains only non-positive numbers or nulls.")
| else:
| fig = px.icicle(chart_df, 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
@@ -0,0 +1,42 @@
/*
* 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

import org.apache.texera.amber.operator.StandaloneCodeGenerator

/**
* A generator whose emitted code draws with plotly.
*
* Mixed in rather than stated per operator because the three modules are one
* dependency: a chart reaching for `px` today and `go` tomorrow would otherwise
* have to remember to edit a list that nothing checks. What the mixin does say,
* and the reason it is not simply always emitted, is that an operator NOT
* mixing it in draws nothing, so a script built only from those runs wherever
* pandas is installed.
*/
trait PlotlyStandaloneCode extends StandaloneCodeGenerator {

override def standaloneImports(): Seq[String] =
Seq(
"import plotly.express as px",
"import plotly.graph_objects as go",
"import plotly.io"
)
}
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,49 @@ 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. Bound to a name
| # of its own: the same frame can feed another branch of the plan, which
| # must still see every row.
| chart_df = in1df.dropna(subset=[${pyStringLiteral(xVal)}, ${pyStringLiteral(yVal)}])
| if chart_df.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(chart_df) < 2:
| _write_error("input table has fewer than two rows to cluster.")
| else:
| x = np.array(chart_df[${pyStringLiteral(xVal)}])
| y = np.array(chart_df[${pyStringLiteral(yVal)}])
| data = np.column_stack((x, y))
| labels = chart_df[${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,14 @@ 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.visualization.PlotlyStandaloneCode
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 +46,7 @@ import javax.validation.constraints.{NotEmpty, NotNull}
}
}
""")
class HierarchyChartOpDesc extends PythonOperatorDescriptor {
class HierarchyChartOpDesc extends PythonOperatorDescriptor with PlotlyStandaloneCode {
@JsonProperty(required = true)
@JsonSchemaTitle("Chart Type")
@JsonPropertyDescription("Treemap or Sunburst")
Expand Down Expand Up @@ -137,4 +141,46 @@ 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:
| # On a copy: the same frame can feed another branch of the plan, and
| # both the assignment and the drop below would otherwise reach it.
| chart_df = in1df.copy()
| chart_df[$valueLit] = chart_df[chart_df[$valueLit] > 0][$valueLit]
| chart_df = chart_df.dropna(subset=[$attributes])
| if chart_df.empty:
| fail("value column contains only non-positive numbers or nulls.")
| else:
| fig = px.${hierarchyChartType.getPlotlyExpressApiName}(chart_df, 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
}

}
Loading
Loading