diff --git a/frontend/src/app/workspace/component/heatmap-legend/heatmap-legend.component.html b/frontend/src/app/workspace/component/heatmap-legend/heatmap-legend.component.html new file mode 100644 index 00000000000..b086fddf549 --- /dev/null +++ b/frontend/src/app/workspace/component/heatmap-legend/heatmap-legend.component.html @@ -0,0 +1,29 @@ + + +
+
{{ legend.title }}
+
+
+ {{ legend.minLabel }} + {{ legend.maxLabel }} +
+
diff --git a/frontend/src/app/workspace/component/heatmap-legend/heatmap-legend.component.scss b/frontend/src/app/workspace/component/heatmap-legend/heatmap-legend.component.scss new file mode 100644 index 00000000000..25f7c339c2a --- /dev/null +++ b/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); + } + + &__labels { + display: flex; + justify-content: space-between; + margin-top: 2px; + color: #666; + } +} diff --git a/frontend/src/app/workspace/component/heatmap-legend/heatmap-legend.component.ts b/frontend/src/app/workspace/component/heatmap-legend/heatmap-legend.component.ts new file mode 100644 index 00000000000..ef8448bc8f5 --- /dev/null +++ b/frontend/src/app/workspace/component/heatmap-legend/heatmap-legend.component.ts @@ -0,0 +1,79 @@ +/** + * 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 { Component } from "@angular/core"; +import { AsyncPipe, NgIf } from "@angular/common"; +import { combineLatest, Observable } from "rxjs"; +import { map } from "rxjs/operators"; +import { WorkflowActionService } from "../../service/workflow-graph/model/workflow-action.service"; +import { WorkflowStatusService } from "../../service/workflow-status/workflow-status.service"; +import { OperatorPerformanceMetrics } from "../../service/workflow-status/performance-metrics"; +import { + formatMetricForView, + HeatmapView, + heatmapViewTitle, + rawMetricForView, +} from "../../service/heatmap/heatmap-scoring"; + +interface HeatmapLegendState { + readonly view: HeatmapView; + readonly title: string; + readonly minLabel: string; + readonly maxLabel: string; +} + +/** + * Presentational legend for the performance heat-map overlay. Shows the active view's name, the + * cold -> hot color scale, and the actual min/max metric values behind that scale. Hidden when the + * overlay is off (view is null). + */ +@Component({ + selector: "texera-heatmap-legend", + templateUrl: "./heatmap-legend.component.html", + styleUrls: ["./heatmap-legend.component.scss"], + imports: [NgIf, AsyncPipe], +}) +export class HeatmapLegendComponent { + public readonly legend$: Observable; + + constructor( + private workflowActionService: WorkflowActionService, + private workflowStatusService: WorkflowStatusService + ) { + this.legend$ = combineLatest([ + this.workflowActionService.getJointGraphWrapper().getHeatmapViewStream(), + this.workflowStatusService.getPerformanceMetricsStream(), + ]).pipe(map(([view, metrics]) => (view === null ? null : this.buildState(view, metrics)))); + } + + private buildState(view: HeatmapView, metrics: Record): HeatmapLegendState { + const raws = Object.values(metrics) + .map(m => rawMetricForView(m, view)) + .filter(v => Number.isFinite(v)); + const hasData = raws.length > 0; + const min = hasData ? Math.min(...raws) : 0; + const max = hasData ? Math.max(...raws) : 0; + return { + view, + title: heatmapViewTitle(view), + minLabel: hasData ? formatMetricForView(min, view) : "—", + maxLabel: hasData ? formatMetricForView(max, view) : "—", + }; + } +} diff --git a/frontend/src/app/workspace/component/menu/menu.component.html b/frontend/src/app/workspace/component/menu/menu.component.html index 85a856eaba8..ec189470275 100644 --- a/frontend/src/app/workspace/component/menu/menu.component.html +++ b/frontend/src/app/workspace/component/menu/menu.component.html @@ -197,6 +197,39 @@ >Status +
  • + +
  • +
  • + + + + + +