diff --git a/src/ui/src/components/visualizer/common/consts.ts b/src/ui/src/components/visualizer/common/consts.ts index 4dd10284..512a756e 100644 --- a/src/ui/src/components/visualizer/common/consts.ts +++ b/src/ui/src/components/visualizer/common/consts.ts @@ -41,6 +41,18 @@ export const NODE_ATTRS_TABLE_VALUE_MAX_WIDTH = 200; /** The height of attrs table row. */ export const NODE_ATTRS_TABLE_ROW_HEIGHT = 12; +/** The height of the summary row in node data provider. */ +export const EXPANDED_NODE_DATA_PROVIDER_SUMMARY_ROW_HEIGHT = 14; + +/** The top padding of the summary row in node data provider. */ +export const EXPANDED_NODE_DATA_PROVIDER_SUMMARY_TOP_PADDING = 6; + +/** The bottom padding of the summary row in node data provider. */ +export const EXPANDED_NODE_DATA_PROVIDER_SUMMARY_BOTTOM_PADDING = 6; + +/** The font size of the summary row in node data provider. */ +export const EXPANDED_NODE_DATA_PROVIDER_SYUMMARY_FONT_SIZE = 9; + /** The maximum number of children nodes under a group node. */ export const DEFAULT_GROUP_NODE_CHILDREN_COUNT_THRESHOLD = IS_EXTERNAL ? 1000 diff --git a/src/ui/src/components/visualizer/common/types.ts b/src/ui/src/components/visualizer/common/types.ts index b346d184..982db3a6 100644 --- a/src/ui/src/components/visualizer/common/types.ts +++ b/src/ui/src/components/visualizer/common/types.ts @@ -320,6 +320,27 @@ export declare interface NodeDataProviderGraphData { * The value for the hidden stat will be displayed as '-'. */ hideAggregatedStats?: AggregatedStat[]; + + /** + * Controls whether to display a detailed value distribution summary on the + * group node. + * + * By default, a color bar representing the value distribution of + * all descendant nodes is shown at the bottom of the group node. If this + * field is set to true, we will show a more detailed summary, with each + * value's label, percentage, and count shown on a separate line. + * + * For now this only works with non-numerical (e.g. string) node data values. + */ + showExpandedSummaryOnGroupNode?: boolean; + + /** + * Whether to display the label count columns in the children stats table in + * the side panel. + * + * For now this only works with non-numerical (e.g. string) node data values. + */ + showLabelCountColumnsInChildrenStatsTable?: boolean; } /** The top level node data provider data, indexed by graph id. */ @@ -348,6 +369,13 @@ export declare interface NodeDataProviderRunData { error?: string; } +/** Info for a value in a node data provider run. */ +export declare interface NodeDataProviderValueInfo { + label: string; + bgColor: string; + count: number; +} + /** The result data for a node in a node data provider run. */ export declare interface NodeDataProviderResultData { /** The original value of the result. */ diff --git a/src/ui/src/components/visualizer/common/utils.ts b/src/ui/src/components/visualizer/common/utils.ts index e29ce449..37b2794e 100644 --- a/src/ui/src/components/visualizer/common/utils.ts +++ b/src/ui/src/components/visualizer/common/utils.ts @@ -39,7 +39,9 @@ import { FieldLabel, KeyValueList, KeyValuePairs, + NodeDataProviderResultProcessedData, NodeDataProviderRunData, + NodeDataProviderValueInfo, NodeQuery, NodeQueryType, NodeStyleId, @@ -546,7 +548,7 @@ export function getOpNodeDataProviderKeyValuePairsForAttrsTable( runNames.includes(getRunName(run, {id: modelGraphId})), ); for (const run of runs) { - const result = (run.results || {})?.[modelGraphId]?.[node.id]; + const result = ((run.results || {})?.[modelGraphId] || {})[node.id]; if (config?.hideEmptyNodeDataEntries && !result) { continue; } @@ -1079,3 +1081,32 @@ export function getRunName( run.nodeDataProviderData?.[modelGraphIdLike?.id || '']?.name ?? run.runName ); } + +/** Generates the sorted value infos for the given group node. */ +export function genSortedValueInfos( + groupNode: GroupNode | undefined, + modelGraph: ModelGraph, + results: Record, +): NodeDataProviderValueInfo[] { + const bgColorToValueInfo: Record = {}; + const descendantsOpNodeIds = + groupNode?.descendantsOpNodeIds || modelGraph.nodes.map((node) => node.id); + for (const nodeId of descendantsOpNodeIds) { + const node = modelGraph.nodesById[nodeId]; + const bgColor = results[node.id]?.bgColor || ''; + if (bgColor) { + if (!bgColorToValueInfo[bgColor]) { + bgColorToValueInfo[bgColor] = { + label: `${results[nodeId]?.value || ''}`, + bgColor, + count: 1, + }; + } else { + bgColorToValueInfo[bgColor].count++; + } + } + } + return Object.values(bgColorToValueInfo).sort((a, b) => + a.bgColor.localeCompare(b.bgColor), + ); +} diff --git a/src/ui/src/components/visualizer/common/worker_events.ts b/src/ui/src/components/visualizer/common/worker_events.ts index fea5340a..a45d9e71 100644 --- a/src/ui/src/components/visualizer/common/worker_events.ts +++ b/src/ui/src/components/visualizer/common/worker_events.ts @@ -79,6 +79,7 @@ export declare interface ExpandOrCollapseGroupNodeRequest expand: boolean; showOnNodeItemTypes: Record; nodeDataProviderRuns: Record; + selectedNodeDataProviderRunId?: string; rendererId: string; paneId: string; // Expand or collapse all groups under the selected group. @@ -111,6 +112,7 @@ export declare interface RelayoutGraphRequest extends WorkerEventBase { modelGraphId: string; showOnNodeItemTypes: Record; nodeDataProviderRuns: Record; + selectedNodeDataProviderRunId?: string; targetDeepestGroupNodeIdsToExpand?: string[]; selectedNodeId: string; rendererId: string; @@ -142,6 +144,7 @@ export declare interface LocateNodeRequest extends WorkerEventBase { modelGraphId: string; showOnNodeItemTypes: Record; nodeDataProviderRuns: Record; + selectedNodeDataProviderRunId?: string; nodeId: string; rendererId: string; noNodeShake?: boolean; diff --git a/src/ui/src/components/visualizer/node_data_provider_summary_panel.ng.html b/src/ui/src/components/visualizer/node_data_provider_summary_panel.ng.html index d4e60cbc..9152e3ec 100644 --- a/src/ui/src/components/visualizer/node_data_provider_summary_panel.ng.html +++ b/src/ui/src/components/visualizer/node_data_provider_summary_panel.ng.html @@ -19,7 +19,8 @@
+ [class.selected]="isRunItemSelected(runItem)" + (click)="handleClickToggleVisibility(runItem, $event)">
{{i + 1}}
@@ -137,7 +138,7 @@ (click)="handleClickChildrenStatsHeader(col.colIndex)">
{{col.runIndex + 1}}
-
{{col.label}}
+
{{col.label}}
{{curChildrenStatSortingDirection === 'asc' ? 'arrow_upward' : 'arrow_downward'}} diff --git a/src/ui/src/components/visualizer/node_data_provider_summary_panel.scss b/src/ui/src/components/visualizer/node_data_provider_summary_panel.scss index 297bebe0..8ff6f982 100644 --- a/src/ui/src/components/visualizer/node_data_provider_summary_panel.scss +++ b/src/ui/src/components/visualizer/node_data_provider_summary_panel.scss @@ -129,6 +129,7 @@ align-items: center; overflow: hidden; padding: 2px 8px; + cursor: pointer; &.selected { background-color: #fff2d5; @@ -197,7 +198,7 @@ display: flex; flex-direction: column; transition: max-height 150ms ease-out; - overflow: hidden; + overflow-y: clip; &.collapsed { /* stylelint-disable-next-line declaration-no-important -- override element style */ @@ -289,6 +290,10 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + + &.multi-line { + white-space: pre; + } } } diff --git a/src/ui/src/components/visualizer/node_data_provider_summary_panel.ts b/src/ui/src/components/visualizer/node_data_provider_summary_panel.ts index 3ffc1f9c..b8c5eecf 100644 --- a/src/ui/src/components/visualizer/node_data_provider_summary_panel.ts +++ b/src/ui/src/components/visualizer/node_data_provider_summary_panel.ts @@ -37,8 +37,17 @@ import {debounceTime} from 'rxjs/operators'; import {AppService} from './app_service'; import {NODE_DATA_PROVIDER_SHOW_ON_NODE_TYPE_PREFIX} from './common/consts'; import {GroupNode, ModelGraph, OpNode} from './common/model_graph'; -import {AggregatedStat, NodeDataProviderRunData} from './common/types'; -import {getRunName, isGroupNode, isOpNode} from './common/utils'; +import { + AggregatedStat, + NodeDataProviderRunData, + NodeDataProviderValueInfo, +} from './common/types'; +import { + genSortedValueInfos, + getRunName, + isGroupNode, + isOpNode, +} from './common/utils'; import {InfoPanelService, SortingDirection} from './info_panel_service'; import {NodeDataProviderExtensionService} from './node_data_provider_extension_service'; import {Paginator} from './paginator'; @@ -98,6 +107,7 @@ interface ChildrenStatsCol { runIndex: number; label: string; hideInChildrenStatsTable?: boolean; + multiLineHeader?: boolean; } const CHILDREN_STATS = ['Sum %']; @@ -716,15 +726,45 @@ export class NodeDataProviderSummaryPanel implements OnChanges { // Generate children stats columns. this.childrenStatsCols = []; let childrenStatColIndex = 0; + const groupNode = this.curModelGraph.nodesById[ + this.rootGroupNodeId ?? '' + ] as GroupNode; + const runIdToValueInfos: Record = {}; for (let i = 0; i < runs.length; i++) { - for (const childrenStat of CHILDREN_STATS) { + const run = runs[i]; + let childrenStats = CHILDREN_STATS; + let valueInfos: NodeDataProviderValueInfo[] = []; + let multiLineHeader = false; + if ( + (run.nodeDataProviderData ?? {})[this.curModelGraph.id] + ?.showLabelCountColumnsInChildrenStatsTable + ) { + valueInfos = genSortedValueInfos( + groupNode, + this.curModelGraph, + (run.results ?? {})[this.curModelGraph.id], + ).sort((a, b) => a.label.localeCompare(b.label)); + runIdToValueInfos[run.runId] = valueInfos; + childrenStats = valueInfos.map((valueInfo) => `#${valueInfo.label}`); + multiLineHeader = true; + } + for (const childrenStat of childrenStats) { + let label = childrenStat; + if (runs.length > 1) { + if (multiLineHeader) { + label = `${this.getRunName(runs[i])}\n${childrenStat}`; + } else { + label = `${this.getRunName(runs[i])} • ${childrenStat}`; + } + } this.childrenStatsCols.push({ colIndex: childrenStatColIndex, runIndex: i, - label: `${this.getRunName(runs[i])} • ${childrenStat}`, + label, hideInChildrenStatsTable: runs[i].nodeDataProviderData?.[this.curModelGraph.id] ?.hideInChildrenStatsTable, + multiLineHeader, }); childrenStatColIndex++; } @@ -746,36 +786,72 @@ export class NodeDataProviderSummaryPanel implements OnChanges { const run = runs[runIndex]; const curResults = run.results || {}; // Sum pct. - let sumPct = 0; - let hasValue = false; - if (isOpNode(node)) { - const nodeResult = (curResults[this.curModelGraph.id] || {})[nodeId]; - const value = nodeResult?.value; - if (value != null && typeof value === 'number') { - sumPct = (value / stats[runIndex].sum) * 100; - hasValue = true; - } - } else if (isGroupNode(node)) { - let layerSum = 0; - const childrenIds = node.descendantsOpNodeIds || []; - for (const childNodeId of childrenIds) { + if (!runIdToValueInfos[run.runId]) { + let sumPct = 0; + let hasValue = false; + if (isOpNode(node)) { const nodeResult = (curResults[this.curModelGraph.id] || {})[ - childNodeId + nodeId ]; const value = nodeResult?.value; if (value != null && typeof value === 'number') { - layerSum += value; + sumPct = (value / stats[runIndex].sum) * 100; hasValue = true; } + } else if (isGroupNode(node)) { + let layerSum = 0; + const childrenIds = node.descendantsOpNodeIds || []; + for (const childNodeId of childrenIds) { + const nodeResult = (curResults[this.curModelGraph.id] || {})[ + childNodeId + ]; + const value = nodeResult?.value; + if (value != null && typeof value === 'number') { + layerSum += value; + hasValue = true; + } + } + sumPct = (layerSum / stats[runIndex].sum) * 100; + } + colValues.push(sumPct); + colStrs.push(hasValue ? sumPct.toFixed(1) : '-'); + colHidden.push( + run.nodeDataProviderData?.[this.curModelGraph.id] + ?.hideInChildrenStatsTable === true, + ); + } + // Label counts. + else { + const valueInfos = runIdToValueInfos[run.runId]; + const curResults = run.results || {}; + const nodeResult = (curResults[this.curModelGraph.id] || {})[nodeId]; + const value = nodeResult?.value || ''; + for (const valueInfo of valueInfos) { + let count = 0; + if (isOpNode(node)) { + if (valueInfo.label === value) { + count = 1; + } + } else if (isGroupNode(node)) { + const childrenIds = node.descendantsOpNodeIds || []; + for (const childNodeId of childrenIds) { + const nodeResult = (curResults[this.curModelGraph.id] || {})[ + childNodeId + ]; + const childValue = nodeResult?.value || ''; + if (childValue === valueInfo.label) { + count++; + } + } + } + colValues.push(count); + colStrs.push(`${count}`); + colHidden.push( + run.nodeDataProviderData?.[this.curModelGraph.id] + ?.hideInChildrenStatsTable === true, + ); } - sumPct = (layerSum / stats[runIndex].sum) * 100; } - colValues.push(sumPct); - colStrs.push(hasValue ? sumPct.toFixed(1) : '-'); - colHidden.push( - run.nodeDataProviderData?.[this.curModelGraph.id] - ?.hideInChildrenStatsTable === true, - ); } this.curChildrenStatRows.push({ id: nodeId, diff --git a/src/ui/src/components/visualizer/webgl_renderer.ts b/src/ui/src/components/visualizer/webgl_renderer.ts index d8ab57c9..ec1b4f5f 100644 --- a/src/ui/src/components/visualizer/webgl_renderer.ts +++ b/src/ui/src/components/visualizer/webgl_renderer.ts @@ -345,6 +345,8 @@ export class WebglRenderer implements OnInit, OnDestroy { private prevNodeDataProviderData: | Record | undefined = undefined; + private prevNodeDataProviderRun: NodeDataProviderRunData | undefined = + undefined; private readonly nodeBodies = new WebglRoundedRectangles(6); private readonly groupNodeIcons = new WebglTexts(this.threejsService); private readonly groupNodeIconBgs = new WebglRoundedRectangles(99); @@ -536,9 +538,14 @@ export class WebglRenderer implements OnInit, OnDestroy { effect(() => { const results = this.webglRendererNdpService.curNodeDataProviderResults(); + const run = this.webglRendererNdpService.curNodeDataProviderRun(); if (results !== this.prevNodeDataProviderData) { - this.handleCurNodeDataProviderResultsChanged(); + this.handleCurNodeDataProviderResultsChanged( + this.prevNodeDataProviderRun, + run, + ); this.prevNodeDataProviderData = results; + this.prevNodeDataProviderRun = run; } }); @@ -1389,6 +1396,11 @@ export class WebglRenderer implements OnInit, OnDestroy { modelGraphId: this.curModelGraph.id, showOnNodeItemTypes: this.curShowOnNodeItemTypes, nodeDataProviderRuns: this.curNodeDataProviderRuns, + selectedNodeDataProviderRunId: + this.nodeDataProviderExtensionService.getSelectedRunForModelGraph( + this.paneId, + this.curModelGraph, + )?.runId, nodeId, rendererId, noNodeShake, @@ -1416,6 +1428,11 @@ export class WebglRenderer implements OnInit, OnDestroy { modelGraphId: this.curModelGraph.id, showOnNodeItemTypes: showOnNodeItemTypes || this.curShowOnNodeItemTypes, nodeDataProviderRuns: this.curNodeDataProviderRuns, + selectedNodeDataProviderRunId: + this.nodeDataProviderExtensionService.getSelectedRunForModelGraph( + this.paneId, + this.curModelGraph, + )?.runId, selectedNodeId: nodeId, targetDeepestGroupNodeIdsToExpand, rendererId: this.rendererId, @@ -1808,10 +1825,31 @@ export class WebglRenderer implements OnInit, OnDestroy { } } - private handleCurNodeDataProviderResultsChanged() { - this.renderGraph(); - this.updateNodesStyles(); - this.webglRendererThreejsService.render(); + private handleCurNodeDataProviderResultsChanged( + prevRun: NodeDataProviderRunData | undefined, + curRun: NodeDataProviderRunData | undefined, + ) { + const prevShowExpandedSummaryOnGroupNode = + prevRun?.nodeDataProviderData?.[this.curModelGraph.id] + ?.showExpandedSummaryOnGroupNode; + const curShowExpandedSummaryOnGroupNode = + curRun?.nodeDataProviderData?.[this.curModelGraph.id] + ?.showExpandedSummaryOnGroupNode; + + // Relayout the graph if `showExpandedSummaryOnGroupNode` is changed + // between previous run and current run. + if ( + prevShowExpandedSummaryOnGroupNode !== curShowExpandedSummaryOnGroupNode + ) { + this.sendRelayoutGraphRequest(this.selectedNodeId); + } + // Re-render the graph without re-laying out if + // `showExpandedSummaryOnGroupNode` is not changed.do { + else { + this.renderGraph(); + this.updateNodesStyles(); + this.webglRendererThreejsService.render(); + } } private handleLocateNodeDone( @@ -1884,6 +1922,11 @@ export class WebglRenderer implements OnInit, OnDestroy { expand: expandOverride == null ? !node?.expanded : expandOverride, showOnNodeItemTypes: this.curShowOnNodeItemTypes, nodeDataProviderRuns: this.curNodeDataProviderRuns, + selectedNodeDataProviderRunId: + this.nodeDataProviderExtensionService.getSelectedRunForModelGraph( + this.paneId, + this.curModelGraph, + )?.runId, rendererId: this.rendererId, paneId: this.paneId, all, diff --git a/src/ui/src/components/visualizer/webgl_renderer_ndp_service.ts b/src/ui/src/components/visualizer/webgl_renderer_ndp_service.ts index 7dc33ad4..53e753ea 100644 --- a/src/ui/src/components/visualizer/webgl_renderer_ndp_service.ts +++ b/src/ui/src/components/visualizer/webgl_renderer_ndp_service.ts @@ -16,22 +16,29 @@ * ============================================================================== */ -import {computed, Injectable} from '@angular/core'; +import {computed, inject, Injectable} from '@angular/core'; import * as three from 'three'; import { + EXPANDED_NODE_DATA_PROVIDER_SUMMARY_BOTTOM_PADDING, + EXPANDED_NODE_DATA_PROVIDER_SUMMARY_ROW_HEIGHT, + EXPANDED_NODE_DATA_PROVIDER_SUMMARY_TOP_PADDING, + EXPANDED_NODE_DATA_PROVIDER_SYUMMARY_FONT_SIZE, NODE_DATA_PROVIDER_BG_COLOR_BAR_HEIGHT, WEBGL_ELEMENT_Y_FACTOR, } from './common/consts'; import {GroupNode} from './common/model_graph'; -import {isGroupNode} from './common/utils'; +import {FontWeight, NodeDataProviderValueInfo} from './common/types'; +import {genSortedValueInfos, isGroupNode} from './common/utils'; import {NodeDataProviderExtensionService} from './node_data_provider_extension_service'; +import {ThreejsService} from './threejs_service'; import {WebglRenderer} from './webgl_renderer'; import {WebglRendererThreejsService} from './webgl_renderer_threejs_service'; import { RoundedRectangleData, WebglRoundedRectangles, } from './webgl_rounded_rectangles'; +import {LabelData, WebglTexts} from './webgl_texts'; const NODE_DATA_PROVIDER_DISTRIBUTION_BAR_Y_OFFSET = WEBGL_ELEMENT_Y_FACTOR * 0.5; @@ -43,7 +50,7 @@ const THREE = three; */ @Injectable() export class WebglRendererNdpService { - readonly curNodeDataProviderResults = computed(() => { + readonly curNodeDataProviderRun = computed(() => { if (!this.webglRenderer) { return undefined; } @@ -53,13 +60,22 @@ export class WebglRendererNdpService { this.webglRenderer.paneId, this.webglRenderer.curModelGraph, ); + return selectedRun; + }); + + readonly curNodeDataProviderResults = computed(() => { + const selectedRun = this.curNodeDataProviderRun(); return (selectedRun?.results || {})[this.webglRenderer.curModelGraph.id]; }); private webglRenderer!: WebglRenderer; private webglRendererThreejsService!: WebglRendererThreejsService; + private readonly threejsService: ThreejsService = inject(ThreejsService); private readonly nodeDataProviderDistributionBars = new WebglRoundedRectangles(0); + private readonly nodeDataProviderSummaryTexts = new WebglTexts( + this.threejsService, + ); constructor( private readonly nodeDataProviderExtensionService: NodeDataProviderExtensionService, @@ -72,51 +88,69 @@ export class WebglRendererNdpService { } renderNodeDataProviderDistributionBars() { - const results = this.curNodeDataProviderResults() || {}; + const results = this.curNodeDataProviderRun() || {}; if (Object.keys(results).length === 0) { return; } - const {groupIdToDescendantsBgColorCounts, sortedBgColors} = - this.genGroupIdToDescendantsBgColorCounts(); + const curRun = this.curNodeDataProviderRun(); + if (!curRun) { + return; + } + + const groupIdToSortedValueInfos = this.genGroupIdToSortedValueInfos(); + const showExpandedSummaryOnGroupNode = (curRun.nodeDataProviderData ?? {})[ + this.webglRenderer.curModelGraph.id + ]?.showExpandedSummaryOnGroupNode; const rectangles: RoundedRectangleData[] = []; + const texts: LabelData[] = []; for (const {node, index} of this.webglRenderer.nodesToRender) { - if (!groupIdToDescendantsBgColorCounts[node.id]) { + if (!groupIdToSortedValueInfos[node.id]) { continue; } const groupNode = node as GroupNode; const groupNodeWidth = groupNode.width || 0; - const curBgColors = groupIdToDescendantsBgColorCounts[node.id]; - let countSum = 0; - for (const count of Object.values(curBgColors)) { - countSum += count; - } + const curSortedValueInfos = groupIdToSortedValueInfos[node.id]; + const countSum = curSortedValueInfos.reduce( + (sum, cur) => sum + cur.count, + 0, + ); let widthSum = 0; let colorIndex = 0; - for (const bgColor of sortedBgColors) { - if (curBgColors[bgColor] == null) { - continue; - } + let expandedSummaryHeight = 0; + if (showExpandedSummaryOnGroupNode && !groupNode.expanded) { + expandedSummaryHeight = + EXPANDED_NODE_DATA_PROVIDER_SUMMARY_ROW_HEIGHT * + curSortedValueInfos.length + + EXPANDED_NODE_DATA_PROVIDER_SUMMARY_TOP_PADDING + + EXPANDED_NODE_DATA_PROVIDER_SUMMARY_BOTTOM_PADDING; + } + + for (let i = 0; i < curSortedValueInfos.length; i++) { + const valueInfo = curSortedValueInfos[i]; + const bgColor = valueInfo.bgColor; if (bgColor === 'transparent') { continue; } - const count = curBgColors[bgColor]; + const count = valueInfo.count; const width = (count / countSum) * groupNodeWidth; const height = NODE_DATA_PROVIDER_BG_COLOR_BAR_HEIGHT; const x = widthSum; + const barY = + this.webglRenderer.getNodeY(groupNode) + + this.webglRenderer.getNodeHeight(groupNode) - + expandedSummaryHeight - + NODE_DATA_PROVIDER_BG_COLOR_BAR_HEIGHT + + height / 2; rectangles.push({ id: `${node.id}_${colorIndex}`, index: rectangles.length, bound: { x: this.webglRenderer.getNodeX(groupNode) + x + width / 2, - y: - this.webglRenderer.getNodeY(groupNode) + - this.webglRenderer.getNodeHeight(groupNode) - - NODE_DATA_PROVIDER_BG_COLOR_BAR_HEIGHT + - height / 2, + y: barY, width, height, }, @@ -130,6 +164,65 @@ export class WebglRendererNdpService { opacity: 1, }); + if (showExpandedSummaryOnGroupNode && !groupNode.expanded) { + // The "index" color block in the summary section. + const indexColorBlockY = + barY + + EXPANDED_NODE_DATA_PROVIDER_SUMMARY_TOP_PADDING + + height / 2 + + i * EXPANDED_NODE_DATA_PROVIDER_SUMMARY_ROW_HEIGHT + + EXPANDED_NODE_DATA_PROVIDER_SUMMARY_ROW_HEIGHT / 2; + rectangles.push({ + id: `${node.id}_${colorIndex}_summary`, + index: rectangles.length, + bound: { + x: this.webglRenderer.getNodeX(groupNode) + 8, + y: indexColorBlockY, + width: 3, + height: EXPANDED_NODE_DATA_PROVIDER_SUMMARY_ROW_HEIGHT - 2, + }, + yOffset: + WEBGL_ELEMENT_Y_FACTOR * index + + NODE_DATA_PROVIDER_DISTRIBUTION_BAR_Y_OFFSET, + isRounded: false, + borderColor: {r: 1, g: 1, b: 1}, + bgColor: new THREE.Color(bgColor), + borderWidth: 0, + opacity: 1, + }); + + // Color label. + texts.push({ + id: `${node.id}_${colorIndex}_summary`, + label: valueInfo.label, + height: EXPANDED_NODE_DATA_PROVIDER_SYUMMARY_FONT_SIZE, + hAlign: 'left', + vAlign: 'center', + weight: FontWeight.MEDIUM, + color: {r: 0, g: 0, b: 0}, + x: this.webglRenderer.getNodeX(groupNode) + 12, + y: 96, + z: indexColorBlockY, + }); + + // Pct + count. + texts.push({ + id: `${node.id}_${colorIndex}_summary_pct_count`, + label: `${Math.floor((count / countSum) * 100)}% (${count})`, + height: EXPANDED_NODE_DATA_PROVIDER_SYUMMARY_FONT_SIZE, + hAlign: 'right', + vAlign: 'center', + weight: FontWeight.MEDIUM, + color: {r: 0, g: 0, b: 0}, + x: + this.webglRenderer.getNodeX(groupNode) + + this.webglRenderer.getNodeWidth(groupNode) - + 6, + y: 96, + z: indexColorBlockY, + }); + } + widthSum += width; colorIndex++; } @@ -139,42 +232,38 @@ export class WebglRendererNdpService { this.webglRendererThreejsService.addToScene( this.nodeDataProviderDistributionBars.mesh, ); + this.nodeDataProviderSummaryTexts.generateMesh(texts, false, true, true); + this.webglRendererThreejsService.addToScene( + this.nodeDataProviderSummaryTexts.mesh, + ); } updateAnimationProgress(t: number) { this.nodeDataProviderDistributionBars.updateAnimationProgress(t); + this.nodeDataProviderSummaryTexts.updateAnimationProgress(t); } - private genGroupIdToDescendantsBgColorCounts(): { - groupIdToDescendantsBgColorCounts: Record>; - sortedBgColors: string[]; - } { + private genGroupIdToSortedValueInfos(): Record< + string, + NodeDataProviderValueInfo[] + > { const results = this.curNodeDataProviderResults() || {}; const groupIdToDescendantsBgColorCounts: Record< string, - Record + NodeDataProviderValueInfo[] > = {}; - const allBgColors = new Set(); for (const {node: groupNode} of this.webglRenderer.nodesToRender) { if (isGroupNode(groupNode) && !groupNode.expanded) { - const bgColorCounts: Record = {}; - for (const nodeId of groupNode.descendantsOpNodeIds || []) { - const node = this.webglRenderer.curModelGraph.nodesById[nodeId]; - const bgColor = results[node.id]?.bgColor || ''; - if (bgColor) { - if (bgColorCounts[bgColor] == null) { - bgColorCounts[bgColor] = 0; - } - bgColorCounts[bgColor]++; - allBgColors.add(bgColor); - } + const sortedValueInfos = genSortedValueInfos( + groupNode, + this.webglRenderer.curModelGraph, + results, + ); + if (sortedValueInfos.length > 0) { + groupIdToDescendantsBgColorCounts[groupNode.id] = sortedValueInfos; } - groupIdToDescendantsBgColorCounts[groupNode.id] = bgColorCounts; } } - return { - groupIdToDescendantsBgColorCounts, - sortedBgColors: [...allBgColors].sort((a, b) => a.localeCompare(b)), - }; + return groupIdToDescendantsBgColorCounts; } } diff --git a/src/ui/src/components/visualizer/worker/graph_expander.ts b/src/ui/src/components/visualizer/worker/graph_expander.ts index db658ca0..43ad1bd2 100644 --- a/src/ui/src/components/visualizer/worker/graph_expander.ts +++ b/src/ui/src/components/visualizer/worker/graph_expander.ts @@ -55,6 +55,7 @@ export class GraphExpander { string, NodeDataProviderRunData >, + private readonly selectedNodeDataProviderRunId: string | undefined, private readonly testMode = false, private readonly config?: VisualizerConfig, ) {} @@ -87,6 +88,7 @@ export class GraphExpander { this.dagre, this.showOnNodeItemTypes, this.nodeDataProviderRuns, + this.selectedNodeDataProviderRunId, this.testMode, this.config, ); @@ -111,6 +113,7 @@ export class GraphExpander { this.dagre, this.showOnNodeItemTypes, this.nodeDataProviderRuns, + this.selectedNodeDataProviderRunId, this.testMode, this.config, ); @@ -167,6 +170,7 @@ export class GraphExpander { this.dagre, this.showOnNodeItemTypes, this.nodeDataProviderRuns, + this.selectedNodeDataProviderRunId, this.testMode, this.config, ); @@ -188,6 +192,7 @@ export class GraphExpander { this.dagre, this.showOnNodeItemTypes, this.nodeDataProviderRuns, + this.selectedNodeDataProviderRunId, this.testMode, this.config, ); @@ -248,12 +253,14 @@ export class GraphExpander { this.modelGraph, this.showOnNodeItemTypes, this.nodeDataProviderRuns, + this.selectedNodeDataProviderRunId, ); groupNode.height = getNodeHeight( groupNode, this.modelGraph, this.showOnNodeItemTypes, this.nodeDataProviderRuns, + this.selectedNodeDataProviderRunId, this.testMode, true, this.config, @@ -276,6 +283,7 @@ export class GraphExpander { this.dagre, this.showOnNodeItemTypes, this.nodeDataProviderRuns, + this.selectedNodeDataProviderRunId, this.testMode, this.config, ); @@ -300,6 +308,7 @@ export class GraphExpander { this.dagre, this.showOnNodeItemTypes, this.nodeDataProviderRuns, + this.selectedNodeDataProviderRunId, this.testMode, this.config, ); @@ -361,6 +370,7 @@ export class GraphExpander { this.dagre, this.showOnNodeItemTypes, this.nodeDataProviderRuns, + this.selectedNodeDataProviderRunId, this.testMode, this.config, ); @@ -401,6 +411,7 @@ export class GraphExpander { this.dagre, this.showOnNodeItemTypes, this.nodeDataProviderRuns, + this.selectedNodeDataProviderRunId, this.testMode, this.config, ); diff --git a/src/ui/src/components/visualizer/worker/graph_layout.ts b/src/ui/src/components/visualizer/worker/graph_layout.ts index cbcdd9f6..9784c96a 100644 --- a/src/ui/src/components/visualizer/worker/graph_layout.ts +++ b/src/ui/src/components/visualizer/worker/graph_layout.ts @@ -17,6 +17,10 @@ */ import { + EXPANDED_NODE_DATA_PROVIDER_SUMMARY_BOTTOM_PADDING, + EXPANDED_NODE_DATA_PROVIDER_SUMMARY_ROW_HEIGHT, + EXPANDED_NODE_DATA_PROVIDER_SUMMARY_TOP_PADDING, + EXPANDED_NODE_DATA_PROVIDER_SYUMMARY_FONT_SIZE, LAYOUT_MARGIN_X, MAX_IO_ROWS_IN_ATTRS_TABLE, NODE_ATTRS_TABLE_FONT_SIZE, @@ -44,6 +48,7 @@ import { ShowOnNodeItemType, } from '../common/types'; import { + genSortedValueInfos, generateCurvePoints, getGroupNodeAttrsKeyValuePairsForAttrsTable, getGroupNodeFieldLabelsFromShowOnNodeItemTypes, @@ -114,6 +119,7 @@ export class GraphLayout { string, NodeDataProviderRunData >, + private readonly selectedNodeDataProviderRunId: string | undefined, private readonly testMode = false, private readonly config?: VisualizerConfig, ) { @@ -144,6 +150,7 @@ export class GraphLayout { this.modelGraph, this.showOnNodeItemTypes, this.nodeDataProviderRuns, + this.selectedNodeDataProviderRunId, this.testMode, false, this.config, @@ -264,6 +271,7 @@ export class GraphLayout { this.modelGraph, this.showOnNodeItemTypes, this.nodeDataProviderRuns, + this.selectedNodeDataProviderRunId, this.testMode, this.config, ); @@ -274,6 +282,7 @@ export class GraphLayout { this.modelGraph, this.showOnNodeItemTypes, this.nodeDataProviderRuns, + this.selectedNodeDataProviderRunId, this.testMode, this.config, ) + @@ -349,6 +358,7 @@ export function getNodeWidth( modelGraph: ModelGraph, showOnNodeItemTypes: Record, nodeDataProviderRuns: Record, + selectedNodeDataProviderRunId: string | undefined, testMode = false, config?: VisualizerConfig, ) { @@ -377,6 +387,7 @@ export function getNodeWidth( // Figure out the max width of all the labels and values respectively. let maxAttrLabelWidth = 0; let maxAttrValueWidth = 0; + let maxExpandedNodeDataProviderLabelWidth = 0; if (isOpNode(node)) { // Basic info. // @@ -485,6 +496,38 @@ export function getNodeWidth( maxAttrLabelWidth = Math.max(maxAttrLabelWidth, widths.maxAttrLabelWidth); maxAttrValueWidth = Math.max(maxAttrValueWidth, widths.maxAttrValueWidth); } + + // Expanded node data provider summary. + if ( + isGroupNode(node) && + !node.expanded && + selectedNodeDataProviderRunId && + nodeDataProviderRuns[selectedNodeDataProviderRunId] + ) { + const run = nodeDataProviderRuns[selectedNodeDataProviderRunId]; + const showExpandedSummaryOnGroupNode = + (run.nodeDataProviderData ?? {})[modelGraph.id] + ?.showExpandedSummaryOnGroupNode ?? false; + if (showExpandedSummaryOnGroupNode) { + const valueInfos = genSortedValueInfos( + node, + modelGraph, + (run.results ?? {})[modelGraph.id], + ); + for (const valueInfo of valueInfos) { + const labelWidth = + getLabelWidth( + `${valueInfo.label} 100% (${valueInfo.count})`, + EXPANDED_NODE_DATA_PROVIDER_SYUMMARY_FONT_SIZE, + false, + ) + 30; + maxExpandedNodeDataProviderLabelWidth = Math.max( + maxExpandedNodeDataProviderLabelWidth, + labelWidth, + ); + } + } + } } maxAttrValueWidth = Math.min( maxAttrValueWidth, @@ -498,7 +541,10 @@ export function getNodeWidth( if (attrsTableWidth !== NODE_ATTRS_TABLE_LABEL_VALUE_PADDING) { attrsTableWidth += ATTRS_TABLE_MARGIN_X * 2; } - return Math.max(MIN_NODE_WIDTH, Math.max(labelWidth, attrsTableWidth)); + return Math.max( + Math.max(MIN_NODE_WIDTH, Math.max(labelWidth, attrsTableWidth)), + maxExpandedNodeDataProviderLabelWidth, + ); } /** An utility function to get the node height. */ @@ -507,6 +553,7 @@ export function getNodeHeight( modelGraph: ModelGraph, showOnNodeItemTypes: Record, nodeDataProviderRuns: Record, + selectedNodeDataProviderRunId: string | undefined, testMode = false, forceRecalculate = false, config?: VisualizerConfig, @@ -540,11 +587,39 @@ export function getNodeHeight( ); } + // Count rows in the expanded node data provider data. + let expandedNodeDataProviderRowCount = 0; + if ( + isGroupNode(node) && + !node.expanded && + selectedNodeDataProviderRunId && + nodeDataProviderRuns[selectedNodeDataProviderRunId] + ) { + const run = nodeDataProviderRuns[selectedNodeDataProviderRunId]; + const showExpandedSummaryOnGroupNode = + (run.nodeDataProviderData ?? {})[modelGraph.id] + ?.showExpandedSummaryOnGroupNode ?? false; + if (showExpandedSummaryOnGroupNode) { + const valueInfos = genSortedValueInfos( + node, + modelGraph, + (run.results ?? {})[modelGraph.id], + ); + expandedNodeDataProviderRowCount = valueInfos.length; + } + } + return ( DEFAULT_NODE_HEIGHT + extraMultiLineLabelHeight + attrsTableRowCount * NODE_ATTRS_TABLE_ROW_HEIGHT + - (attrsTableRowCount > 0 ? NODE_ATTRS_TABLE_MARGIN_TOP - 4 : 0) + (attrsTableRowCount > 0 ? NODE_ATTRS_TABLE_MARGIN_TOP - 4 : 0) + + expandedNodeDataProviderRowCount * + EXPANDED_NODE_DATA_PROVIDER_SUMMARY_ROW_HEIGHT + + (expandedNodeDataProviderRowCount > 0 + ? EXPANDED_NODE_DATA_PROVIDER_SUMMARY_TOP_PADDING + + EXPANDED_NODE_DATA_PROVIDER_SUMMARY_BOTTOM_PADDING + : 0) ); } @@ -555,6 +630,7 @@ export function getLayoutGraph( modelGraph: ModelGraph, showOnNodeItemTypes: Record, nodeDataProviderRuns: Record, + selectedNodeDataProviderRunId: string | undefined, testMode = false, useFakeNodeSize = false, config?: VisualizerConfig, @@ -581,6 +657,7 @@ export function getLayoutGraph( modelGraph, showOnNodeItemTypes, nodeDataProviderRuns, + selectedNodeDataProviderRunId, testMode, config, )), @@ -591,6 +668,7 @@ export function getLayoutGraph( modelGraph, showOnNodeItemTypes, nodeDataProviderRuns, + selectedNodeDataProviderRunId, testMode, false, config, @@ -666,7 +744,7 @@ function getOpNodeAttrsTableRowCount( NODE_DATA_PROVIDER_SHOW_ON_NODE_TYPE_PREFIX, ) && Object.values(nodeDataProviderRuns).some((run) => { - const result = (run.results || {})?.[modelGraph.id]?.[node.id]; + const result = ((run.results || {})?.[modelGraph.id] || {})[node.id]; if (config?.hideEmptyNodeDataEntries && !result) { return false; } diff --git a/src/ui/src/components/visualizer/worker/graph_processor.ts b/src/ui/src/components/visualizer/worker/graph_processor.ts index c418e7d7..c755dd89 100644 --- a/src/ui/src/components/visualizer/worker/graph_processor.ts +++ b/src/ui/src/components/visualizer/worker/graph_processor.ts @@ -481,6 +481,7 @@ export class GraphProcessor { modelGraph, this.showOnNodeItemTypes, this.nodeDataProviderRuns, + undefined, this.testMode, // Use fake node size. true, diff --git a/src/ui/src/components/visualizer/worker/worker.ts b/src/ui/src/components/visualizer/worker/worker.ts index dbf5dd58..e2628fb6 100644 --- a/src/ui/src/components/visualizer/worker/worker.ts +++ b/src/ui/src/components/visualizer/worker/worker.ts @@ -112,6 +112,7 @@ self.addEventListener('message', (event: Event) => { workerEvent.groupNodeId, workerEvent.showOnNodeItemTypes, workerEvent.nodeDataProviderRuns, + workerEvent.selectedNodeDataProviderRunId, workerEvent.all === true, workerEvent.config, ); @@ -121,6 +122,7 @@ self.addEventListener('message', (event: Event) => { workerEvent.groupNodeId, workerEvent.showOnNodeItemTypes, workerEvent.nodeDataProviderRuns, + workerEvent.selectedNodeDataProviderRunId, workerEvent.all === true, workerEvent.config, ); @@ -146,6 +148,7 @@ self.addEventListener('message', (event: Event) => { modelGraph, workerEvent.showOnNodeItemTypes, workerEvent.nodeDataProviderRuns, + workerEvent.selectedNodeDataProviderRunId, workerEvent.targetDeepestGroupNodeIdsToExpand, workerEvent.clearAllExpandStates, workerEvent.config, @@ -176,6 +179,7 @@ self.addEventListener('message', (event: Event) => { modelGraph, workerEvent.showOnNodeItemTypes, workerEvent.nodeDataProviderRuns, + workerEvent.selectedNodeDataProviderRunId, workerEvent.nodeId, workerEvent.config, ); @@ -242,6 +246,7 @@ function handleProcessGraph( dagre, showItemOnNodeTypes, nodeDataProviderRuns, + undefined, ); try { layout.layout(); @@ -267,6 +272,7 @@ function handleExpandGroupNode( groupNodeId: string | undefined, showOnNodeItemTypes: Record, nodeDataProviderRuns: Record, + selectedNodeDataProviderRunId: string | undefined, all: boolean, config?: VisualizerConfig, ): string[] { @@ -275,6 +281,7 @@ function handleExpandGroupNode( dagre, showOnNodeItemTypes, nodeDataProviderRuns, + selectedNodeDataProviderRunId, false, config, ); @@ -343,6 +350,7 @@ function handleCollapseGroupNode( groupNodeId: string | undefined, showOnNodeItemTypes: Record, nodeDataProviderRuns: Record, + selectedNodeDataProviderRunId: string | undefined, all: boolean, config?: VisualizerConfig, ): string[] { @@ -351,6 +359,7 @@ function handleCollapseGroupNode( dagre, showOnNodeItemTypes, nodeDataProviderRuns, + selectedNodeDataProviderRunId, false, config, ); @@ -378,6 +387,7 @@ function handleReLayoutGraph( modelGraph: ModelGraph, showOnNodeItemTypes: Record, nodeDataProviderRuns: Record, + selectedNodeDataProviderRunId: string | undefined, targetDeepestGroupNodeIdsToExpand?: string[], clearAllExpandStates?: boolean, config?: VisualizerConfig, @@ -387,6 +397,7 @@ function handleReLayoutGraph( dagre, showOnNodeItemTypes, nodeDataProviderRuns, + selectedNodeDataProviderRunId, false, config, ); @@ -400,6 +411,7 @@ function handleLocateNode( modelGraph: ModelGraph, showOnNodeItemTypes: Record, nodeDataProviderRuns: Record, + selectedNodeDataProviderRunId: string | undefined, nodeId: string, config?: VisualizerConfig, ): string[] { @@ -408,6 +420,7 @@ function handleLocateNode( dagre, showOnNodeItemTypes, nodeDataProviderRuns, + selectedNodeDataProviderRunId, false, config, );