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 @@ -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}

Expand All @@ -48,7 +51,7 @@ import scala.jdk.CollectionConverters._
}
}
""")
class BulletChartOpDesc extends PythonOperatorDescriptor {
class BulletChartOpDesc extends PythonOperatorDescriptor with StandaloneCodeGenerator {

@JsonProperty(value = "value", required = true)
@JsonSchemaTitle("Value")
Expand Down Expand Up @@ -92,6 +95,11 @@ class BulletChartOpDesc extends PythonOperatorDescriptor {
OperatorGroupConstants.VISUALIZATION_FINANCIAL_GROUP
)

private def assertRequiredFields(): Unit = {
assert(value.nonEmpty)
assert(deltaReference.nonEmpty)
}

override def generatePythonCode(): String = {
// The reference keeps the 0 the generated code used to fall back to; an unset
// threshold stays absent as None.
Expand Down Expand Up @@ -237,4 +245,120 @@ class BulletChartOpDesc extends PythonOperatorDescriptor {
|"""
finalCode.encode
}

override def producesDataFrame(): Boolean = false

override def generateStandaloneCode(): String = {
assertRequiredFields()

// The column name is typed-in text, so it is emitted as a properly escaped
// Python literal. The numeric settings carry no text and are emitted as numbers,
// the same as the runtime path, so the two scripts agree without either of them
// parsing anything.
val valueLit = pyStringLiteral(value)
val deltaReferenceExpr = deltaReference.getOrElse(0.0).toString
val thresholdExpr = thresholdValue.map(_.toString).getOrElse("None")
val stepsExpr =
Option(steps)
.map(_.asScala.toSeq)
.getOrElse(Seq.empty)
.flatMap(step => step.start.zip(step.end))
.map { case (start, end) => s"""{"start": $start, "end": $end}""" }
.mkString("[", ", ", "]")

// render_error's continuation line keeps the runtime path's indentation — the
// HTML is triple-quoted, so those spaces reach the browser.
s"""def render_error(error_msg):
| return '''<h1>Bullet chart is not available.</h1>
| <p>Reason: {} </p>'''.format(error_msg)
|
|def generate_gray_gradient(step_count):
| colors = []
| for i in range(step_count):
| lightness = 90 - (i * (60 / max(1, step_count - 1)))
| colors.append(f"hsl(0, 0%, {lightness}%)")
| return colors
|
|def generate_valid_steps(steps_data):
| valid_steps = []
| step_errors = []
| for index, step in enumerate(steps_data):
| s_val = step["start"]
| e_val = step["end"]
| if s_val < e_val:
| valid_steps.append({"start": s_val, "end": e_val})
| else:
| step_errors.append(f"Step {index + 1}: start >= end ({s_val} >= {e_val})")
| return valid_steps, step_errors
|
|if in1df.empty:
| with open("output.html", "w", encoding="utf-8") as output:
| output.write(render_error("Input table is empty."))
|else:
| try:
| value_col = $valueLit
| delta_ref = $deltaReferenceExpr
| if value_col not in in1df.columns:
| with open("output.html", "w", encoding="utf-8") as output:
| output.write(render_error(f"Column '{value_col}' not found in input table."))
| else:
| table = in1df.dropna(subset=[value_col])
| if table.empty:
| with open("output.html", "w", encoding="utf-8") as output:
| output.write(render_error("No valid data rows found after dropping nulls."))
| else:
| threshold_val = $thresholdExpr
| valid_steps, step_errors = generate_valid_steps($stepsExpr)
| step_colors = generate_gray_gradient(len(valid_steps))
| steps_list = []
| for index, step_data in enumerate(valid_steps):
| steps_list.append({
| "range": [step_data["start"], step_data["end"]],
| "color": step_colors[index]
| })
| html_chunks = []
| first_fig = None
| for _, row in table.head(10).iterrows():
| try:
| actual = float(row[value_col])
| gauge_config = {'shape': 'bullet'}
| if steps_list:
| gauge_config['steps'] = steps_list
| max_range_values = [actual, delta_ref]
| if threshold_val is not None:
| max_range_values.append(threshold_val)
| for r in steps_list:
| max_range_values.append(r["range"][1])
| gauge_config['axis'] = {"range": [0, max(max_range_values) * 1.2]}
| if threshold_val is not None:
| gauge_config["threshold"] = {
| "value": threshold_val,
| "line": {"color": "red", "width": 2},
| "thickness": 1
| }
| fig = go.Figure(go.Indicator(
| mode="number+gauge+delta",
| value=actual,
| delta={"reference": delta_ref},
| gauge=gauge_config,
| domain={"x": [0.1, 1], "y": [0.1, 0.9]},
| title={"text": value_col}
| ))
| fig.update_layout(margin=dict(l=80, r=20, b=40, t=40), height=150)
| if first_fig is None:
| first_fig = fig
| html_chunk = plotly.io.to_html(fig, include_plotlyjs='cdn', auto_play=False)
| if step_errors:
| html_chunk += "<br><b>Step Errors:</b><ul>" + "".join([f"<li>{msg}</li>" for msg in step_errors]) + "</ul>"
| html_chunks.append(html_chunk)
| except Exception as e:
| html_chunks.append(render_error(f"Error generating bullet chart: {str(e)}"))
| if first_fig is not None:
| first_fig.write_json("output.json")
| with open("output.html", "w", encoding="utf-8") as output:
| output.write("<div>" + "".join(html_chunks) + "</div>")
| except Exception as e:
| with open("output.html", "w", encoding="utf-8") as output:
| output.write(render_error(f"General error: {str(e)}"))""".stripMargin
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ package org.apache.texera.amber.operator.visualization.carpetPlot
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 javax.validation.constraints.NotNull
Expand All @@ -41,7 +44,7 @@ import javax.validation.constraints.NotNull
}
}
""")
class CarpetPlotOpDesc extends PythonOperatorDescriptor {
class CarpetPlotOpDesc extends PythonOperatorDescriptor with StandaloneCodeGenerator {

@JsonProperty(value = "a", required = true)
@NotNull(message = "A-axis Attribute cannot be empty")
Expand Down Expand Up @@ -132,4 +135,27 @@ class CarpetPlotOpDesc extends PythonOperatorDescriptor {
finalCode.encode
}

override def producesDataFrame(): Boolean = false

override def generateStandaloneCode(): String = {
val aLit = pyStringLiteral(a)
val bLit = pyStringLiteral(b)
val yLit = pyStringLiteral(y)
s"""table = in1df.dropna(subset=[$aLit, $bLit, $yLit]).copy()
|if table.empty:
| print("Carpet plot error: No valid rows after removing nulls")
|else:
| table[$aLit] = table[$aLit].astype(float)
| table[$bLit] = table[$bLit].astype(float)
| table[$yLit] = table[$yLit].astype(float)
| fig = go.Figure(go.Carpet(
| a=table[$aLit],
| b=table[$bLit],
| y=table[$yLit]
| ))
| fig.write_json("output.json")
| fig.write_html("output.html")
| print("Carpet plot saved to output.json and output.html")""".stripMargin
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ package org.apache.texera.amber.operator.visualization.choroplethMap
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.metadata.annotations.AutofillAttributeName
import org.apache.texera.amber.operator.{PythonOperatorDescriptor, StandaloneCodeGenerator}
import org.apache.texera.amber.operator.metadata.annotations.{AutofillAttributeName, SampleColumn}
import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo}
import org.apache.texera.amber.pybuilder.PythonTemplateBuilder

Expand All @@ -44,14 +47,15 @@ import javax.validation.constraints.NotNull
}
}
""")
class ChoroplethMapOpDesc extends PythonOperatorDescriptor {
class ChoroplethMapOpDesc extends PythonOperatorDescriptor with StandaloneCodeGenerator {

@JsonProperty(value = "locations", required = true)
@JsonSchemaTitle("Locations Column")
@JsonPropertyDescription(
"Column used to describe location. Currently only supports countries and needs to be three-letter ISO country code"
)
@AutofillAttributeName
@SampleColumn("iso_country")
@NotNull(message = "Locations Column cannot be empty")
var locations: EncodableString = ""

Expand Down Expand Up @@ -128,4 +132,38 @@ class ChoroplethMapOpDesc extends PythonOperatorDescriptor {
|"""
finalCode.encode
}

override def producesDataFrame(): Boolean = false

override def generateStandaloneCode(): String = {
val locationsLit = pyStringLiteral(locations)
val colorLit = pyStringLiteral(color)
// 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>Choropleth map 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"Choropleth map error: {error_msg}")
|
|if in1df.empty:
| fail("Input table is empty.")
|else:
| in1df = in1df.dropna(subset=[$locationsLit, $colorLit])
| if in1df.empty:
| fail("No valid rows left (every row has at least 1 missing value).")
| else:
| fig = px.choropleth(in1df, locations=$locationsLit, color=$colorLit, color_continuous_scale=px.colors.sequential.Plasma)
| fig.update_layout(margin={"r":0,"t":0,"l":0,"b":0})
| fig.write_json("output.json")
| fig.write_html("output.html")
| print("Choropleth map saved to output.json and output.html")""".stripMargin
}
}
Loading
Loading