/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import type { DashboardFlowGraph, DashboardFlowLink, DashboardFlowNode, FlowBuildOptions, FlowFilterOptions, FlowLinkSelection, FlowMetric, FlowNodeFilter, FlowNodeKind, FlowOverflowMode, FlowQuotaDataItem, FlowRole, FlowSummary, ProcessedFlowData, } from '@/features/dashboard/types' import { getDashboardChartColors } from './charts' // eslint-disable-next-line @typescript-eslint/no-explicit-any type VChartSpec = Record type FlowMetrics = { quota: number tokens: number requests: number } type FlowSankeyLabels = { quota: string tokens: string requests: string share: string } type FlowPathNode = { id: string label: string kind: FlowNodeKind } type PreparedFlowPath = { path: FlowPathNode[] metrics: FlowMetrics } type FlowNodeRank = { node: FlowPathNode value: number } type FlowPathContext = { deletedTokenLabel?: (tokenId: number) => string } type FlowGraphOptions = { topNodeLimit?: number overflowMode?: FlowOverflowMode otherNodeLabel?: (kind: FlowNodeKind) => string activeNode?: FlowNodeFilter activeLink?: FlowLinkSelection maskSensitive?: boolean } type FlowHighlightSets = { nodes: Set links: Set } const EMPTY_FLOW_PATH_CONTEXT: FlowPathContext = {} const DEFAULT_FLOW_ROLE: FlowRole = 'user' const DEFAULT_FLOW_OVERFLOW_MODE: FlowOverflowMode = 'aggregate' const DEFAULT_FLOW_SANKEY_LABELS: FlowSankeyLabels = { quota: 'Quota', tokens: 'Tokens', requests: 'Requests', share: 'Share', } const DEFAULT_FLOW_CHART_COLOR = '#1664FF' const FLOW_NODE_KINDS: readonly FlowNodeKind[] = [ 'user', 'node', 'token', 'group', 'model', 'channel', ] const FLOW_NODE_KIND_SET = new Set(FLOW_NODE_KINDS) const OTHER_FLOW_NODE_IDS: Record = { user: 'user:__other__', node: 'node:__other__', token: 'token:__other__', group: 'group:__other__', model: 'model:__other__', channel: 'channel:__other__', } const DEFAULT_OTHER_FLOW_NODE_LABELS: Record = { user: 'Other users', node: 'Other nodes', token: 'Other tokens', group: 'Other groups', model: 'Other models', channel: 'Other channels', } // Kinds whose labels can leak identity (people, keys, infra, business setup). // Model names are public, so they stay visible even when masking is on. const SENSITIVE_FLOW_KINDS = new Set([ 'user', 'node', 'token', 'group', 'channel', ]) const OTHER_FLOW_NODE_ID_SET = new Set( Object.values(OTHER_FLOW_NODE_IDS) ) function numberValue(value: unknown): number { const n = Number(value) return Number.isFinite(n) ? n : 0 } function isFlowNodeKind(value: unknown): value is FlowNodeKind { return ( typeof value === 'string' && FLOW_NODE_KIND_SET.has(value as FlowNodeKind) ) } function rowMetrics(row: FlowQuotaDataItem): FlowMetrics { return { quota: numberValue(row.quota), tokens: numberValue(row.token_used), requests: numberValue(row.count), } } function metricValue(metrics: FlowMetrics, metric: FlowMetric): number { if (metric === 'requests') return metrics.requests if (metric === 'tokens') return metrics.tokens return metrics.quota } function userNode(row: FlowQuotaDataItem): FlowPathNode { const userID = numberValue(row.user_id) return { id: userID > 0 ? `user:${userID}` : `user:${row.username || 'unknown'}`, label: row.username || (userID > 0 ? `user-${userID}` : 'Unknown User'), kind: 'user', } } function nodeNameNode(row: FlowQuotaDataItem): FlowPathNode { const nodeName = row.node_name || 'default-node' return { id: `node:${nodeName}`, label: nodeName, kind: 'node', } } function tokenNode(row: FlowQuotaDataItem, ctx: FlowPathContext): FlowPathNode { const tokenID = numberValue(row.token_id) return { id: tokenID > 0 ? `token:${tokenID}` : `token:${row.token_name || 'unknown'}`, label: row.token_name || deletedTokenLabel(tokenID, ctx), kind: 'token', } } function deletedTokenLabel(tokenID: number, ctx: FlowPathContext): string { if (tokenID <= 0) return 'Unknown Token' return ctx.deletedTokenLabel?.(tokenID) ?? `token-${tokenID}` } function groupNode(row: FlowQuotaDataItem): FlowPathNode { const useGroup = row.use_group || 'unknown' return { id: `group:${useGroup}`, label: useGroup, kind: 'group', } } function modelNode(row: FlowQuotaDataItem): FlowPathNode { const model = row.model_name || 'unknown' return { id: `model:${model}`, label: row.model_name || 'Unknown Model', kind: 'model', } } function channelNode(row: FlowQuotaDataItem): FlowPathNode { const channelID = numberValue(row.channel_id) return { id: channelID > 0 ? `channel:${channelID}` : `channel:${row.channel_name || 'unknown'}`, label: row.channel_name || (channelID > 0 ? `channel-${channelID}` : 'Unknown'), kind: 'channel', } } const NODE_BUILDERS: Record< FlowNodeKind, (row: FlowQuotaDataItem, ctx: FlowPathContext) => FlowPathNode > = { user: userNode, node: nodeNameNode, token: tokenNode, group: groupNode, model: modelNode, channel: channelNode, } const ROLE_FLOW_STAGES: Record = { root: ['user', 'node', 'token', 'group', 'model', 'channel'], admin: ['user', 'group', 'model', 'channel'], user: ['token', 'group', 'model'], } // A Sankey needs at least two columns to draw any link, so hiding stages can // never collapse the path below this many columns. const MIN_FLOW_STAGES = 2 export function getFlowStages(role: FlowRole): FlowNodeKind[] { return ROLE_FLOW_STAGES[role] ?? ROLE_FLOW_STAGES.user } function resolveVisibleStages( role: FlowRole, visibleStages?: FlowNodeKind[] ): FlowNodeKind[] { const stages = getFlowStages(role) if (!visibleStages) return stages const visible = new Set(visibleStages) const filtered = stages.filter((stage) => visible.has(stage)) return filtered.length >= MIN_FLOW_STAGES ? filtered : stages } function flowPathForStages( row: FlowQuotaDataItem, stages: FlowNodeKind[], ctx: FlowPathContext = EMPTY_FLOW_PATH_CONTEXT ): FlowPathNode[] { return stages.map((stage) => NODE_BUILDERS[stage](row, ctx)) } function colorAt(index: number, palette?: readonly string[]): string { const colors = palette && palette.length > 0 ? palette : getDashboardChartColors(index + 1) if (colors.length === 0) return DEFAULT_FLOW_CHART_COLOR return colors[index % colors.length] ?? DEFAULT_FLOW_CHART_COLOR } function colorPalette( colorCount: number, palette?: readonly string[] ): readonly string[] { if (palette && palette.length > 0) return palette const colors = getDashboardChartColors(colorCount) return colors.length > 0 ? colors : [DEFAULT_FLOW_CHART_COLOR] } function alphaColor( color: string, alpha: number ): { color: string; alpha: number } { const normalized = color.trim() const hex = normalized.startsWith('#') ? normalized.slice(1) : normalized if (!/^[0-9a-f]{6}$/i.test(hex)) { return { color: normalized, alpha } } const value = Number.parseInt(hex, 16) const red = (value >> 16) & 255 const green = (value >> 8) & 255 const blue = value & 255 return { color: `rgba(${red}, ${green}, ${blue}, ${alpha.toFixed(2)})`, alpha: 1, } } function stableColorMap( keys: string[], palette?: readonly string[] ): Map { const map = new Map() const uniqueKeys = [...new Set(keys)] const colors = colorPalette(uniqueKeys.length, palette) uniqueKeys.forEach((key, index) => { map.set(key, colorAt(index, colors)) }) return map } function filterRows( rows: FlowQuotaDataItem[], options: FlowBuildOptions = {} ): FlowQuotaDataItem[] { const selectedUsers = new Set(options.selectedUsers ?? []) if (selectedUsers.size === 0) return rows return rows.filter((row) => selectedUsers.has(userNode(row).id)) } function nodeFilterKey(filter: FlowNodeFilter): string { return `${filter.kind}\u0000${filter.id}` } function normalizeSelectedNodeFilters( selectedNodes: readonly FlowNodeFilter[] | undefined, stages: readonly FlowNodeKind[] ): Map> { const visibleKinds = new Set(stages) const filters = new Map>() for (const filter of selectedNodes ?? []) { if (!visibleKinds.has(filter.kind)) continue const selected = filters.get(filter.kind) ?? new Set() selected.add(filter.id) filters.set(filter.kind, selected) } return filters } function pathMatchesNodeFilters( path: FlowPathNode[], filters: Map> ): boolean { if (filters.size === 0) return true const pathNodesByKind = new Map>() for (const node of path) { const ids = pathNodesByKind.get(node.kind) ?? new Set() ids.add(node.id) pathNodesByKind.set(node.kind, ids) } for (const [kind, selectedIds] of filters) { const pathIds = pathNodesByKind.get(kind) if (!pathIds) return false let hasSelectedNode = false for (const id of selectedIds) { if (pathIds.has(id)) { hasSelectedNode = true break } } if (!hasSelectedNode) return false } return true } function filterRowsByNodes( rows: FlowQuotaDataItem[], selectedNodes: readonly FlowNodeFilter[] | undefined, stages: FlowNodeKind[], ctx: FlowPathContext ): FlowQuotaDataItem[] { const filters = normalizeSelectedNodeFilters(selectedNodes, stages) if (filters.size === 0) return rows return rows.filter((row) => pathMatchesNodeFilters(flowPathForStages(row, stages, ctx), filters) ) } function selectedNodeFiltersExceptKind( selectedNodes: readonly FlowNodeFilter[] | undefined, kind: FlowNodeKind ): FlowNodeFilter[] | undefined { const filtered = (selectedNodes ?? []).filter( (filter) => filter.kind !== kind ) return filtered.length > 0 ? filtered : undefined } function addNode( map: Map, pathNode: FlowPathNode, metrics: FlowMetrics, metric: FlowMetric, color: string, colorKey: string ): void { const previous = map.get(pathNode.id) ?? { id: pathNode.id, label: pathNode.label, kind: pathNode.kind, value: 0, requests: 0, quota: 0, tokens: 0, color, colorKey, } previous.value += metricValue(metrics, metric) previous.requests += metrics.requests previous.quota += metrics.quota previous.tokens += metrics.tokens map.set(pathNode.id, previous) } function addLink( map: Map, source: FlowPathNode, target: FlowPathNode, metrics: FlowMetrics, metric: FlowMetric, color: string, colorKey: string ): void { const key = `${source.id}\u0000${target.id}` const previous = map.get(key) ?? { source: source.id, target: target.id, value: 0, requests: 0, quota: 0, tokens: 0, sourceLabel: source.label, targetLabel: target.label, color, linkColor: color, linkAlpha: 1, hoverColor: color, colorKey, share: 0, } previous.value += metricValue(metrics, metric) previous.requests += metrics.requests previous.quota += metrics.quota previous.tokens += metrics.tokens map.set(key, previous) } function assignLinkDisplayColors(links: DashboardFlowLink[]): void { const linksBySource = new Map() for (const link of links) { const sourceLinks = linksBySource.get(link.source) ?? [] sourceLinks.push(link) linksBySource.set(link.source, sourceLinks) } for (const sourceLinks of linksBySource.values()) { const sortedLinks = [...sourceLinks].sort( (a, b) => b.value - a.value || linkStableKey(a).localeCompare(linkStableKey(b)) ) const denominator = Math.max(sortedLinks.length - 1, 1) sortedLinks.forEach((link, index) => { const alpha = sortedLinks.length === 1 ? 0.34 : 0.24 + (index / denominator) * 0.2 const displayColor = alphaColor(link.color, alpha) link.linkColor = displayColor.color link.linkAlpha = displayColor.alpha link.hoverColor = link.color }) } } function byValueThenLabel( a: T, b: T ): number { return b.value - a.value || a.label.localeCompare(b.label) } function linkStableKey(link: Pick) { return `${link.source}\u0000${link.target}` } function pathLinkKey(source: FlowPathNode, target: FlowPathNode): string { return `${source.id}\u0000${target.id}` } function byLinkDrawPriority( a: DashboardFlowLink, b: DashboardFlowLink ): number { return ( Number(a.dimmed) - Number(b.dimmed) || Number(b.highlighted) - Number(a.highlighted) || b.value - a.value || linkStableKey(a).localeCompare(linkStableKey(b)) ) } function buildSummary(rows: FlowQuotaDataItem[]): FlowSummary { return rows.reduce( (summary, row) => { const metrics = rowMetrics(row) summary.quota += metrics.quota summary.tokens += metrics.tokens summary.requests += metrics.requests return summary }, { quota: 0, tokens: 0, requests: 0, } ) } function normalizeTopNodeLimit(limit?: number): number | undefined { if (limit === undefined) return undefined const parsed = Math.floor(limit) return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined } function otherFlowNode( kind: FlowNodeKind, labeler?: (kind: FlowNodeKind) => string ): FlowPathNode { return { id: OTHER_FLOW_NODE_IDS[kind], label: labeler?.(kind) ?? DEFAULT_OTHER_FLOW_NODE_LABELS[kind], kind, } } function buildTopNodeSets( rows: FlowQuotaDataItem[], metric: FlowMetric, stages: FlowNodeKind[], limit: number | undefined, ctx: FlowPathContext ): Map> | undefined { if (!limit) return undefined const totals = new Map>() for (const stage of stages) { totals.set(stage, new Map()) } for (const row of rows) { const metrics = rowMetrics(row) const value = metricValue(metrics, metric) const path = flowPathForStages(row, stages, ctx) for (const node of path) { const stageTotals = totals.get(node.kind) if (!stageTotals) continue const current = stageTotals.get(node.id) ?? { node, value: 0 } current.value += value stageTotals.set(node.id, current) } } const topSets = new Map>() for (const [kind, stageTotals] of totals) { const topIds = [...stageTotals.values()] .sort( (a, b) => b.value - a.value || a.node.label.localeCompare(b.node.label) || a.node.id.localeCompare(b.node.id) ) .slice(0, limit) .map((rank) => rank.node.id) topSets.set(kind, new Set(topIds)) } return topSets } function isTopFlowNode( node: FlowPathNode, topNodeSets?: Map> ): boolean { const topNodes = topNodeSets?.get(node.kind) return !topNodes || topNodes.has(node.id) } function applyTopNodeLimit( path: FlowPathNode[], topNodeSets: Map> | undefined, mode: FlowOverflowMode, labeler?: (kind: FlowNodeKind) => string ): FlowPathNode[] | undefined { if (!topNodeSets) return path const containsOverflowNode = path.some( (node) => !isTopFlowNode(node, topNodeSets) ) if (!containsOverflowNode) return path if (mode === 'hide') return undefined return path.map((node) => isTopFlowNode(node, topNodeSets) ? node : otherFlowNode(node.kind, labeler) ) } function pathContainsFlowNode( path: FlowPathNode[], filter: FlowNodeFilter ): boolean { return path.some((node) => node.kind === filter.kind && node.id === filter.id) } function pathContainsFlowLink( path: FlowPathNode[], link: FlowLinkSelection ): boolean { for (let i = 0; i < path.length - 1; i++) { if (path[i]?.id === link.source && path[i + 1]?.id === link.target) { return true } } return false } function buildFlowHighlightSets( preparedPaths: PreparedFlowPath[], activeNode: FlowNodeFilter | undefined, activeLink: FlowLinkSelection | undefined, stages: FlowNodeKind[] ): FlowHighlightSets | undefined { const nodeActive = Boolean(activeNode && stages.includes(activeNode.kind)) if (!nodeActive && !activeLink) return undefined // A link selection highlights paths that traverse that exact edge; otherwise // fall back to highlighting paths that pass through the active node. const matchesPath = (path: FlowPathNode[]): boolean => { if (activeLink) return pathContainsFlowLink(path, activeLink) return activeNode ? pathContainsFlowNode(path, activeNode) : false } const highlightedNodes = new Set() const highlightedLinks = new Set() for (const prepared of preparedPaths) { const { path } = prepared if (!matchesPath(path)) continue for (const node of path) { highlightedNodes.add(node.id) } for (let i = 0; i < path.length - 1; i++) { const source = path[i] const target = path[i + 1] if (!source || !target) continue highlightedLinks.add(pathLinkKey(source, target)) } } if (highlightedNodes.size === 0) return undefined return { nodes: highlightedNodes, links: highlightedLinks, } } // Fully masks a label. Nodes stay distinct because the Sankey identifies them // by `key` (the node id), not by this display text, so identical masked labels // never merge. const FLOW_MASK_TEXT = '\u2022\u2022\u2022\u2022' function maskFlowLabel(label: string): string { if (label.length === 0) return label return FLOW_MASK_TEXT } // Masks sensitive node/link labels in place. Node identity (`id`) is untouched, // so links, highlighting, and layout stay exactly the same; only the rendered // text changes. function maskFlowGraphLabels( nodes: Map, links: Map ): void { const maskedById = new Map() for (const node of nodes.values()) { if (!SENSITIVE_FLOW_KINDS.has(node.kind)) continue if (OTHER_FLOW_NODE_ID_SET.has(node.id)) continue const masked = maskFlowLabel(node.label) node.label = masked maskedById.set(node.id, masked) } if (maskedById.size === 0) return for (const link of links.values()) { const sourceMasked = maskedById.get(link.source) if (sourceMasked !== undefined) link.sourceLabel = sourceMasked const targetMasked = maskedById.get(link.target) if (targetMasked !== undefined) link.targetLabel = targetMasked } } function applyFlowHighlights( nodes: Iterable, links: Iterable, highlightSets: FlowHighlightSets | undefined ): void { if (!highlightSets) return for (const node of nodes) { node.highlighted = highlightSets.nodes.has(node.id) node.dimmed = !node.highlighted } for (const link of links) { const highlighted = highlightSets.links.has(linkStableKey(link)) link.highlighted = highlighted link.dimmed = !highlighted } } function buildFlowGraph( rows: FlowQuotaDataItem[], metric: FlowMetric, role: FlowRole, palette?: readonly string[], visibleStages?: FlowNodeKind[], ctx: FlowPathContext = EMPTY_FLOW_PATH_CONTEXT, options: FlowGraphOptions = {} ): DashboardFlowGraph { const stages = resolveVisibleStages(role, visibleStages) const topNodeSets = buildTopNodeSets( rows, metric, stages, normalizeTopNodeLimit(options.topNodeLimit), ctx ) const overflowMode = options.overflowMode ?? DEFAULT_FLOW_OVERFLOW_MODE const preparedPaths: PreparedFlowPath[] = [] for (const row of rows) { const path = applyTopNodeLimit( flowPathForStages(row, stages, ctx), topNodeSets, overflowMode, options.otherNodeLabel ) if (!path) continue preparedPaths.push({ path, metrics: rowMetrics(row) }) } const nodes = new Map() const links = new Map() const colors = stableColorMap( preparedPaths .map((prepared) => prepared.path[0]?.id) .filter((id): id is string => Boolean(id)) .sort((a, b) => a.localeCompare(b)), palette ) for (const prepared of preparedPaths) { const { path, metrics } = prepared const root = path[0] if (!root) continue const color = colors.get(root.id) ?? colorAt(0, palette) for (const node of path) { addNode(nodes, node, metrics, metric, color, root.id) } for (let i = 0; i < path.length - 1; i++) { const source = path[i] const target = path[i + 1] if (!source || !target) continue addLink(links, source, target, metrics, metric, color, root.id) } } if (options.maskSensitive) { maskFlowGraphLabels(nodes, links) } applyFlowHighlights( nodes.values(), links.values(), buildFlowHighlightSets( preparedPaths, options.activeNode, options.activeLink, stages ) ) const flowLinks = [...links.values()].sort( (a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target) ) const firstStepSources = new Set( preparedPaths .map((prepared) => prepared.path[0]?.id) .filter((id): id is string => Boolean(id)) ) const total = flowLinks .filter((link) => firstStepSources.has(link.source)) .reduce((sum, link) => sum + link.value, 0) for (const link of flowLinks) { link.share = total > 0 ? link.value / total : 0 } assignLinkDisplayColors(flowLinks) return { nodes: [...nodes.values()].sort(byValueThenLabel), links: flowLinks, } } function formatNumber(value: number): string { return Intl.NumberFormat(undefined, { maximumFractionDigits: 0 }).format( value ) } function buildUserFilterOptions( rows: FlowQuotaDataItem[], metric: FlowMetric = 'quota', palette?: readonly string[] ): FlowFilterOptions['users'] { const users = new Map< string, { label: string value: number color: string } >() const colors = stableColorMap( rows.map((row) => userNode(row).id).sort((a, b) => a.localeCompare(b)), palette ) for (const row of rows) { const user = userNode(row) if (!row.user_id && !row.username) continue const metrics = rowMetrics(row) const value = metricValue(metrics, metric) const current = users.get(user.id) ?? { label: user.label, value: 0, color: colors.get(user.id) ?? colorAt(0, palette), } current.value += value users.set(user.id, current) } return [...users.entries()] .map(([value, user]) => ({ value, label: user.label, valueLabel: formatNumber(user.value), valueRaw: user.value, color: user.color, })) .sort((a, b) => b.valueRaw - a.valueRaw || a.label.localeCompare(b.label)) } function buildNodeFilterOptions( rows: FlowQuotaDataItem[], metric: FlowMetric, role: FlowRole, visibleStages: FlowNodeKind[] | undefined, palette: readonly string[] | undefined, ctx: FlowPathContext, selectedNodes?: readonly FlowNodeFilter[] ): FlowFilterOptions['nodes'] { const stages = resolveVisibleStages(role, visibleStages) const stageOrder = new Map(stages.map((stage, index) => [stage, index])) const colorIds = new Set() for (const row of rows) { for (const node of flowPathForStages(row, stages, ctx)) { colorIds.add(node.id) } } const colors = stableColorMap( [...colorIds].sort((a, b) => a.localeCompare(b)), palette ) const options: FlowFilterOptions['nodes'] = [] for (const stage of stages) { const totals = new Map< string, { node: FlowPathNode value: number } >() const candidateRows = filterRowsByNodes( rows, selectedNodeFiltersExceptKind(selectedNodes, stage), stages, ctx ) for (const row of candidateRows) { const metrics = rowMetrics(row) const value = metricValue(metrics, metric) const node = NODE_BUILDERS[stage](row, ctx) const key = nodeFilterKey({ kind: node.kind, id: node.id }) const current = totals.get(key) ?? { node, value: 0 } current.value += value totals.set(key, current) } for (const rank of totals.values()) { options.push({ kind: rank.node.kind, value: rank.node.id, label: rank.node.label, valueLabel: formatNumber(rank.value), valueRaw: rank.value, color: colors.get(rank.node.id) ?? colorAt(0, palette), }) } } return options.sort( (a, b) => (stageOrder.get(a.kind) ?? 0) - (stageOrder.get(b.kind) ?? 0) || b.valueRaw - a.valueRaw || a.label.localeCompare(b.label) || a.value.localeCompare(b.value) ) } export function buildFlowFilterOptions( rows: FlowQuotaDataItem[], metric: FlowMetric = 'quota', palette?: readonly string[], role: FlowRole = DEFAULT_FLOW_ROLE, visibleStages?: FlowNodeKind[], selectedNodes?: readonly FlowNodeFilter[] ): FlowFilterOptions { return { users: buildUserFilterOptions(rows, metric, palette), nodes: buildNodeFilterOptions( rows, metric, role, visibleStages, palette, EMPTY_FLOW_PATH_CONTEXT, selectedNodes ), } } export function buildDashboardFlowData( rows: FlowQuotaDataItem[], metric: FlowMetric = 'quota', options: FlowBuildOptions = {} ): ProcessedFlowData { const role = options.role ?? DEFAULT_FLOW_ROLE const palette = options.colorPalette const ctx = { deletedTokenLabel: options.deletedTokenLabel, } const stages = resolveVisibleStages(role, options.visibleStages) const userFilteredRows = filterRows(rows, options) const filteredRows = filterRowsByNodes( userFilteredRows, options.selectedNodes, stages, ctx ) return { summary: buildSummary(filteredRows), flow: buildFlowGraph( filteredRows, metric, role, palette, options.visibleStages, ctx, { topNodeLimit: options.topNodeLimit, overflowMode: options.overflowMode, otherNodeLabel: options.otherNodeLabel, activeNode: options.activeNode, activeLink: options.activeLink, maskSensitive: options.maskSensitive, } ), filterOptions: { users: buildUserFilterOptions(rows, metric, palette), nodes: buildNodeFilterOptions( userFilteredRows, metric, role, options.visibleStages, palette, ctx, options.selectedNodes ), }, } } function recordValue(value: unknown): Record | undefined { return value && typeof value === 'object' ? (value as Record) : undefined } function sankeyDatumSource( datum: Record ): Record { const nested = datum.datum if (Array.isArray(nested)) { const depth = numberValue(datum.depth) return recordValue(nested[depth]) ?? recordValue(nested[0]) ?? datum } return recordValue(nested) ?? datum } function sankeyDatumValue( datum: Record, key: string ): unknown { if (datum[key] !== undefined) return datum[key] return sankeyDatumSource(datum)[key] } function sankeyDatumFlag(datum: Record, key: string): boolean { return sankeyDatumValue(datum, key) === true } export function flowSankeyDatumValue(datum: unknown, key: string): unknown { const record = recordValue(datum) return record ? sankeyDatumValue(record, key) : undefined } function isSankeyLinkDatum(datum: Record): boolean { return ( sankeyDatumValue(datum, 'source') !== undefined && sankeyDatumValue(datum, 'target') !== undefined ) } export function flowNodeFilterFromSankeyDatum( datum: unknown ): FlowNodeFilter | undefined { const record = recordValue(datum) if (!record || isSankeyLinkDatum(record)) return undefined const id = flowSankeyDatumValue(record, 'key') const kind = flowSankeyDatumValue(record, 'kind') if ( (typeof id === 'string' || typeof id === 'number') && isFlowNodeKind(kind) ) { return { kind, id: String(id) } } return undefined } function tooltipMetricLines( valueFormatter: (value: number) => string, labels: FlowSankeyLabels ) { const metricValue = (datum: Record, key: string) => numberValue(sankeyDatumValue(datum, key)) const formattedNumber = (datum: Record, key: string) => formatNumber(metricValue(datum, key)) const hasMetric = (datum: Record, key: string) => metricValue(datum, key) > 0 return [ { key: labels.quota, value: (datum: Record) => valueFormatter(metricValue(datum, 'quota')), }, { key: labels.tokens, value: (datum: Record) => formattedNumber(datum, 'tokens'), }, { key: labels.requests, value: (datum: Record) => formattedNumber(datum, 'requests'), }, { key: labels.share, value: (datum: Record) => `${(metricValue(datum, 'share') * 100).toFixed(1)}%`, visible: (datum: Record) => hasMetric(datum, 'share'), }, ] } export function buildFlowSankeySpec( flow: DashboardFlowGraph, title: string, valueFormatter: (value: number) => string = formatNumber, labels: FlowSankeyLabels = DEFAULT_FLOW_SANKEY_LABELS ): VChartSpec { return { type: 'sankey', data: [ { id: 'flow', values: [ { nodes: flow.nodes.map((node) => ({ key: node.id, name: node.label, rawLabel: node.label, kind: node.kind, value: node.value, requests: node.requests, quota: node.quota, tokens: node.tokens, color: node.color, colorKey: node.colorKey, highlighted: node.highlighted, dimmed: node.dimmed, })), links: flow.links .filter((link) => link.value > 0) .sort(byLinkDrawPriority) .map((link, index) => { let zIndex = 100_000 + index if (link.highlighted) { zIndex = 1_000_000 + index } else if (link.dimmed) { zIndex = index } return { source: link.source, target: link.target, linkKey: linkStableKey(link), sourceLabel: link.sourceLabel, targetLabel: link.targetLabel, value: link.value, requests: link.requests, quota: link.quota, tokens: link.tokens, color: link.color, linkColor: link.linkColor, linkAlpha: link.linkAlpha, hoverColor: link.hoverColor, colorKey: link.colorKey, share: link.share, highlighted: link.highlighted, dimmed: link.dimmed, zIndex, } }), }, ], }, ], categoryField: 'name', sourceField: 'source', targetField: 'target', valueField: 'value', nodeKey: 'key', direction: 'horizontal', nodeAlign: 'justify', crossNodeAlign: 'middle', linkSortBy: ( a: { value?: number; source?: string; target?: string; index?: number }, b: { value?: number; source?: string; target?: string; index?: number } ) => numberValue(b.value) - numberValue(a.value) || `${a.source ?? ''}\u0000${a.target ?? ''}`.localeCompare( `${b.source ?? ''}\u0000${b.target ?? ''}` ) || numberValue(a.index) - numberValue(b.index), nodeGap: 14, nodeWidth: 16, minLinkHeight: 2, minNodeHeight: 8, title: { visible: false, text: title, }, legends: { visible: false }, label: { visible: true, position: 'outside', limit: 220, interactive: false, style: { fill: '#475569', fontSize: 11, fontWeight: 600, }, }, node: { interactive: true, style: { fill: (datum: Record) => String(sankeyDatumValue(datum, 'color') ?? colorAt(0)), fillOpacity: (datum: Record) => { if (sankeyDatumFlag(datum, 'dimmed')) return 0.18 if (sankeyDatumFlag(datum, 'highlighted')) return 1 return 0.92 }, stroke: (datum: Record) => sankeyDatumFlag(datum, 'highlighted') ? 'rgba(15, 23, 42, 0.74)' : 'rgba(148, 163, 184, 0.45)', lineWidth: (datum: Record) => sankeyDatumFlag(datum, 'highlighted') ? 1.5 : 1, cursor: 'pointer', pickMode: 'accurate', }, state: { hover: { fillOpacity: 1, stroke: 'rgba(15, 23, 42, 0.68)', lineWidth: 1.5, }, selected: { fillOpacity: 1, stroke: 'rgba(15, 23, 42, 0.68)', lineWidth: 1.5, }, blur: { fillOpacity: 0.22, }, }, }, link: { interactive: true, style: { fill: (datum: Record) => String( sankeyDatumValue(datum, 'linkColor') ?? sankeyDatumValue(datum, 'color') ?? colorAt(0) ), fillOpacity: (datum: Record) => { if (sankeyDatumFlag(datum, 'dimmed')) return 0.08 if (sankeyDatumFlag(datum, 'highlighted')) return 0.86 return numberValue(sankeyDatumValue(datum, 'linkAlpha')) || 1 }, cursor: 'pointer', pickMode: 'accurate', boundsMode: 'accurate', zIndex: (datum: Record) => { const zIndex = sankeyDatumValue(datum, 'zIndex') if (zIndex !== undefined) return numberValue(zIndex) return 1_000_000_000 - numberValue(sankeyDatumValue(datum, 'value')) }, }, state: { hover: { fill: (datum: Record) => String( sankeyDatumValue(datum, 'hoverColor') ?? sankeyDatumValue(datum, 'color') ?? colorAt(0) ), fillOpacity: 0.9, }, selected: { fill: (datum: Record) => String( sankeyDatumValue(datum, 'hoverColor') ?? sankeyDatumValue(datum, 'color') ?? colorAt(0) ), fillOpacity: 0.9, }, blur: { fillOpacity: 0.22, }, }, }, // Highlighting is driven entirely by our own `highlighted`/`dimmed` data // flags (see fillOpacity above). VChart's built-in click emphasis is // disabled because its Sankey "related" handler crashes on click // (_handleLinkRelatedClick) and would otherwise fight our full-path // highlight. emphasis: { enable: false }, tooltip: { trigger: 'hover', activeType: 'mark', dimension: { visible: false }, group: { visible: false }, mark: { checkOverlap: true, positionMode: 'pointer', visible: (datum: Record) => isSankeyLinkDatum(datum) || sankeyDatumValue(datum, 'key') !== undefined, title: { value: (datum: Record) => { const source = sankeyDatumValue(datum, 'source') const target = sankeyDatumValue(datum, 'target') if (source && target) { const sourceLabel = sankeyDatumValue(datum, 'sourceLabel') const targetLabel = sankeyDatumValue(datum, 'targetLabel') return `${sourceLabel ?? source} -> ${targetLabel ?? target}` } return `${sankeyDatumValue(datum, 'name') ?? sankeyDatumValue(datum, 'rawLabel') ?? ''}` }, }, content: tooltipMetricLines(valueFormatter, labels), }, }, background: { fill: 'transparent' }, animation: false, } }