Yicong-Huang commented on code in PR #6213:
URL: https://github.com/apache/texera/pull/6213#discussion_r3741174400


##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -391,6 +416,112 @@ export class WorkflowEditorComponent implements OnInit, 
AfterViewInit, OnDestroy
       });
   }
 
+  /**
+   * Drives the performance heat-map overlay (Layers > Performance). The 
overlay colors only the
+   * operator body fill, so it coexists with the execution-status border. 
Colors are derived from
+   * the WorkflowStatusService performance metrics via the pure 
heatmap-scoring helpers, so both the
+   * main canvas and the mini-map (shared model) update.
+   */
+  private handleHeatmapOverlay(): void {
+    // Repaint whenever the active view or the metrics change (only while a 
view is active).
+    combineLatest([this.wrapper.getHeatmapViewStream(), 
this.workflowStatusService.getPerformanceMetricsStream()])
+      .pipe(untilDestroyed(this))
+      .subscribe(([view]) => {
+        if (view !== null) {
+          this.repaintHeatmapColors(view);
+        }
+      });
+
+    // Restore default fills when the overlay is turned off.
+    this.wrapper
+      .getHeatmapViewStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(view => {
+        if (view === null) {
+          this.restoreAllOperatorFills();
+          this.heatmapTooltip = null;
+        }
+      });
+
+    // Paint newly (re)added operators when the overlay is active (e.g. after 
reload).

Review Comment:
   `changeOperatorDisableStatus` (`joint-ui.service.ts:502`, driven from 
`:713`) writes the same `rect.body/fill` this overlay owns, and nothing here 
repaints afterwards. With the overlay on, disabling an operator paints it 
`#E0E0E0` and re-enabling `#FFFFFF` — neither is on the ramp. It recovers only 
on the next metrics tick, view switch, or operator add, so an idle workflow 
stays mis-colored.
   
   The Regions layer closes its equivalent path at `:594`. Fix: subscribe 
`getDisabledOperatorsChangedStream()` beside the operator-add subscription 
below.



##########
frontend/src/app/workspace/component/heatmap-legend/heatmap-legend.component.scss:
##########
@@ -0,0 +1,54 @@
+/**
+ * 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.
+ */
+
+.heatmap-legend {
+  position: absolute;
+  // Bottom-left, lifted above the mini-map preview toggle in the corner. The 
mini-map
+  // itself occupies the bottom-right of the canvas.
+  bottom: 72px;
+  left: 0px;
+  z-index: 10;
+  padding: 8px 10px;
+  font-size: 12px;
+  background: rgba(255, 255, 255, 0.95);
+  border: 1px solid #d9d9d9;
+  border-radius: 4px;
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
+  pointer-events: none;
+
+  &__title {
+    font-weight: 600;
+    margin-bottom: 4px;
+  }
+
+  &__bar {
+    width: 140px;
+    height: 10px;
+    border-radius: 2px;
+    // Mirrors the cold -> hot stops in heatmap-color.ts (scoreToColor).
+    background: linear-gradient(to right, #5b9bd5, #ffffbf, #e05a52);

Review Comment:
   These are `COLD`/`MID`/`HOT` from `heatmap-color.ts:27-29`, re-declared. All 
copies agree today, so nothing is broken now. But a future ramp edit recolors 
the canvas and leaves this bar on the old gradient. `heatmap-color.spec.ts` 
won't catch it — it asserts the TS constants, not the CSS.
   
   The legend is a component, so it can bind the gradient from 
`scoreToColor(0)`, `scoreToColor(0.5)`, `scoreToColor(1)` and drop the literals.



##########
frontend/src/app/workspace/service/heatmap/heatmap-scoring.ts:
##########
@@ -0,0 +1,157 @@
+/**
+ * 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.
+ */
+
+import { OperatorPerformanceMetrics } from 
"../workflow-status/performance-metrics";
+
+/**
+ * The three heat-map views. Each answers a different "where should I look?"
+ * question; see {@link rawMetricForView} for the per-operator cost each uses.
+ * String-valued so the selection serializes readably (e.g. to localStorage).
+ */
+export enum HeatmapView {
+  Runtime = "runtime",
+  Throughput = "throughput",

Review Comment:
   This view's metric is seconds per row (`:50-51`) — the reciprocal of 
throughput, growing as throughput falls. Yet the legend and tooltip print 
"Throughput" above a hot end that means *low* throughput. The naming 
conventions reserve that word for the rate itself, so something directional 
like `TimePerRow` would read the way the ramp behaves.



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.scss:
##########
@@ -19,12 +19,41 @@
 
 #workflow-editor-wrapper {
   height: 100%;
+  // Anchors absolutely-positioned canvas overlays (e.g. the heat-map legend).
+  position: relative;
 }
 
 #workflow-editor {
   height: 100%;
 }
 
+// Smoothly animate operator body fill changes (e.g. heat-map recolor on view 
switch / toggle).
+::ng-deep #workflow-editor .body {
+  transition: fill 0.25s ease;

Review Comment:
   `::ng-deep` plus a global id selector makes this canvas-wide: every operator 
fill change now animates, including the disable/enable and validation paths, 
for users who never enable the overlay.
   
   If the animation is meant for the heat-map, scope it to a class the overlay 
toggles. If it is meant for everything, it deserves a line in the description, 
since it changes canvas behavior beyond this feature.



##########
frontend/src/app/workspace/component/workflow-editor/workflow-editor.component.ts:
##########
@@ -391,6 +416,112 @@ export class WorkflowEditorComponent implements OnInit, 
AfterViewInit, OnDestroy
       });
   }
 
+  /**
+   * Drives the performance heat-map overlay (Layers > Performance). The 
overlay colors only the
+   * operator body fill, so it coexists with the execution-status border. 
Colors are derived from
+   * the WorkflowStatusService performance metrics via the pure 
heatmap-scoring helpers, so both the
+   * main canvas and the mini-map (shared model) update.
+   */
+  private handleHeatmapOverlay(): void {
+    // Repaint whenever the active view or the metrics change (only while a 
view is active).
+    combineLatest([this.wrapper.getHeatmapViewStream(), 
this.workflowStatusService.getPerformanceMetricsStream()])
+      .pipe(untilDestroyed(this))
+      .subscribe(([view]) => {
+        if (view !== null) {
+          this.repaintHeatmapColors(view);
+        }
+      });
+
+    // Restore default fills when the overlay is turned off.
+    this.wrapper
+      .getHeatmapViewStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(view => {
+        if (view === null) {
+          this.restoreAllOperatorFills();
+          this.heatmapTooltip = null;
+        }
+      });
+
+    // Paint newly (re)added operators when the overlay is active (e.g. after 
reload).
+    this.workflowActionService
+      .getTexeraGraph()
+      .getOperatorAddStream()
+      .pipe(untilDestroyed(this))
+      .subscribe(() => {
+        const view = this.wrapper.getHeatmapView();
+        if (view !== null) {
+          this.repaintHeatmapColors(view);
+        }
+      });
+  }
+
+  private heatmapScores(view: HeatmapView): Record<string, number> {
+    const metrics = this.workflowStatusService.getCurrentPerformanceMetrics();
+    const rawById: Record<string, number> = {};
+    for (const operatorId of Object.keys(metrics)) {
+      rawById[operatorId] = rawMetricForView(metrics[operatorId], view);
+    }
+    return normalizeScores(rawById);
+  }
+
+  private repaintHeatmapColors(view: HeatmapView): void {
+    const scores = this.heatmapScores(view);
+    this.workflowActionService
+      .getTexeraGraph()
+      .getAllOperators()
+      .forEach(op => this.jointUIService.applyHeatmapColor(this.paper, 
op.operatorID, scores[op.operatorID]));
+  }
+
+  private restoreAllOperatorFills(): void {
+    this.workflowActionService
+      .getTexeraGraph()
+      .getAllOperators()
+      .forEach(op => this.jointUIService.restoreOperatorFill(this.paper, op));
+  }
+
+  /**
+   * Shows a small tooltip with the hovered operator's metric value and heat 
score for the active
+   * view. Only active while the heat-map overlay is on.
+   */
+  private handleHeatmapHover(): void {
+    fromJointPaperEvent(this.paper, "element:mouseenter")
+      .pipe(untilDestroyed(this))
+      .subscribe(([elementView, evt]) => {
+        const view = this.wrapper.getHeatmapView();
+        if (view === null) {
+          return;
+        }
+        const operatorId = elementView.model.id.toString();
+        if 
(!this.workflowActionService.getTexeraGraph().hasOperator(operatorId)) {
+          return;
+        }
+        const metrics = 
this.workflowStatusService.getCurrentPerformanceMetrics()[operatorId];
+        const score = this.heatmapScores(view)[operatorId];
+        const rect = this.editor.getBoundingClientRect();
+        const mouseEvent = evt as unknown as MouseEvent;
+        // Missing metrics render as "—" for both fields, so "no data yet" is 
not
+        // confused with a genuine zero value.
+        this.heatmapTooltip = {
+          x: mouseEvent.clientX - rect.left + 12,
+          y: mouseEvent.clientY - rect.top + 12,
+          title: heatmapViewTitle(view),
+          metricLabel: metrics ? formatMetricForView(rawMetricForView(metrics, 
view), view) : "—",
+          heatLabel: score === undefined ? "—" : `${Math.round(score * 100)}%`,
+        };
+        // JointJS paper events fire outside Angular's zone, so trigger change 
detection
+        // for the tooltip to render (mirrors the chat-popover handling).
+        this.changeDetectorRef.detectChanges();
+      });
+
+    fromJointPaperEvent(this.paper, "element:mouseleave")

Review Comment:
   The `mouseenter` branch returns early when no view is active; this one 
doesn't. Every canvas mouse-out therefore clears the tooltip and forces a 
synchronous change-detection pass, including for users who never open the 
overlay. Guarding on `this.wrapper.getHeatmapView() === null` before touching 
`heatmapTooltip` keeps the feature inert when it is off.



##########
frontend/src/app/workspace/service/heatmap/heatmap-color.spec.ts:
##########
@@ -0,0 +1,64 @@
+/**
+ * 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.
+ */
+
+import { HEATMAP_NO_DATA_COLOR, scoreToColor } from "./heatmap-color";
+
+describe("scoreToColor", () => {
+  it("maps 0 to the cold stop (blue)", () => {
+    expect(scoreToColor(0)).toBe("#5b9bd5");
+  });
+
+  it("maps 0.5 to the mid stop (pale yellow)", () => {
+    expect(scoreToColor(0.5)).toBe("#ffffbf");
+  });
+
+  it("maps 1 to the hot stop (red)", () => {
+    expect(scoreToColor(1)).toBe("#e05a52");
+  });
+
+  it("clamps values below 0 to the cold stop", () => {
+    expect(scoreToColor(-0.5)).toBe(scoreToColor(0));
+  });
+
+  it("clamps values above 1 to the hot stop", () => {
+    expect(scoreToColor(2)).toBe(scoreToColor(1));
+  });
+
+  it("treats a non-finite score as cold rather than emitting an invalid 
color", () => {

Review Comment:
   The second assertion pins `Infinity` to the hot stop, so the name describes 
only the NaN half.
   
   ```suggestion
     it("maps non-finite scores to the ramp endpoints (NaN -> cold, Infinity -> 
hot)", () => {
   ```



##########
frontend/src/app/workspace/component/heatmap-legend/heatmap-legend.component.scss:
##########
@@ -0,0 +1,54 @@
+/**
+ * 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.
+ */
+
+.heatmap-legend {
+  position: absolute;
+  // Bottom-left, lifted above the mini-map preview toggle in the corner. The 
mini-map
+  // itself occupies the bottom-right of the canvas.

Review Comment:
   The mini-map toggle is not in this corner — all of its buttons are `bottom: 
0; right: 0..90px` (`mini-map.component.scss:20-45`), and the next sentence 
says so. Worth naming whatever the 72px offset actually clears.
   
   ```suggestion
     // Bottom-left of the canvas; the mini-map and its toggle buttons sit in 
the
     // bottom-right corner (see mini-map.component.scss).
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to