| |
| |
| |
| |
|
|
| import { CHART_CONFIG, THRESHOLDS } from "./constants"; |
| import type { GroupStats } from "@/types"; |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function groupRowBySuffix( |
| row: Record<string, number>, |
| ): Record<string, number | Record<string, number>> { |
| const result: Record<string, number | Record<string, number>> = {}; |
| const suffixGroups: Record<string, Record<string, number>> = {}; |
|
|
| for (const [key, value] of Object.entries(row)) { |
| if (key === "timestamp") { |
| result["timestamp"] = value; |
| continue; |
| } |
|
|
| const parts = key.split(CHART_CONFIG.SERIES_NAME_DELIMITER); |
| if (parts.length === 2) { |
| const [prefix, suffix] = parts; |
| if (!suffixGroups[suffix]) suffixGroups[suffix] = {}; |
| suffixGroups[suffix][prefix] = value; |
| } else { |
| result[key] = value; |
| } |
| } |
|
|
| for (const [suffix, group] of Object.entries(suffixGroups)) { |
| const keys = Object.keys(group); |
| if (keys.length === 1) { |
| |
| const fullName = `${keys[0]}${CHART_CONFIG.SERIES_NAME_DELIMITER}${suffix}`; |
| result[fullName] = group[keys[0]]; |
| } else { |
| result[suffix] = group; |
| } |
| } |
|
|
| return result; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function buildSuffixGroupsMap( |
| numericKeys: string[], |
| ): Record<string, string[]> { |
| const suffixGroupsMap: Record<string, string[]> = {}; |
|
|
| for (const key of numericKeys) { |
| const parts = key.split(CHART_CONFIG.SERIES_NAME_DELIMITER); |
| const suffix = parts[1] || parts[0]; |
| if (!suffixGroupsMap[suffix]) suffixGroupsMap[suffix] = []; |
| suffixGroupsMap[suffix].push(key); |
| } |
|
|
| return suffixGroupsMap; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function computeGroupStats( |
| chartData: Record<string, number>[], |
| suffixGroups: string[][], |
| ): Record<string, GroupStats> { |
| const groupStats: Record<string, GroupStats> = {}; |
|
|
| suffixGroups.forEach((group) => { |
| let min = Infinity; |
| let max = -Infinity; |
|
|
| for (const row of chartData) { |
| for (const key of group) { |
| const v = row[key]; |
| if (typeof v === "number" && !isNaN(v)) { |
| if (v < min) min = v; |
| if (v > max) max = v; |
| } |
| } |
| } |
|
|
| |
| groupStats[group[0]] = { min, max }; |
| }); |
|
|
| return groupStats; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function groupByScale( |
| suffixGroups: string[][], |
| groupStats: Record<string, GroupStats>, |
| ): Record<string, string[][]> { |
| const scaleGroups: Record<string, string[][]> = {}; |
| const used = new Set<string>(); |
|
|
| for (const group of suffixGroups) { |
| const groupId = group[0]; |
| if (used.has(groupId)) continue; |
|
|
| const { min, max } = groupStats[groupId]; |
| if (!isFinite(min) || !isFinite(max)) continue; |
|
|
| const logMin = Math.log10(Math.abs(min) + THRESHOLDS.EPSILON); |
| const logMax = Math.log10(Math.abs(max) + THRESHOLDS.EPSILON); |
| const unit: string[][] = [group]; |
| used.add(groupId); |
|
|
| for (const other of suffixGroups) { |
| const otherId = other[0]; |
| if (used.has(otherId) || otherId === groupId) continue; |
|
|
| const { min: omin, max: omax } = groupStats[otherId]; |
| if (!isFinite(omin) || !isFinite(omax) || omin === omax) continue; |
|
|
| const ologMin = Math.log10(Math.abs(omin) + THRESHOLDS.EPSILON); |
| const ologMax = Math.log10(Math.abs(omax) + THRESHOLDS.EPSILON); |
|
|
| if ( |
| Math.abs(logMin - ologMin) <= THRESHOLDS.SCALE_GROUPING && |
| Math.abs(logMax - ologMax) <= THRESHOLDS.SCALE_GROUPING |
| ) { |
| unit.push(other); |
| used.add(otherId); |
| } |
| } |
|
|
| scaleGroups[groupId] = unit; |
| } |
|
|
| return scaleGroups; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function flattenScaleGroups( |
| scaleGroups: Record<string, string[][]>, |
| ): string[][] { |
| return Object.values(scaleGroups) |
| .sort((a, b) => b.length - a.length) |
| .flatMap((suffixGroupArr) => { |
| const merged = suffixGroupArr.flat(); |
| if (merged.length > CHART_CONFIG.MAX_SERIES_PER_GROUP) { |
| const subgroups: string[][] = []; |
| for ( |
| let i = 0; |
| i < merged.length; |
| i += CHART_CONFIG.MAX_SERIES_PER_GROUP |
| ) { |
| subgroups.push( |
| merged.slice(i, i + CHART_CONFIG.MAX_SERIES_PER_GROUP), |
| ); |
| } |
| return subgroups; |
| } |
| return [merged]; |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function processChartDataGroups( |
| seriesNames: string[], |
| chartData: Record<string, number>[], |
| ): string[][] { |
| |
| const numericKeys = seriesNames.filter((k) => k !== "timestamp"); |
| const suffixGroupsMap = buildSuffixGroupsMap(numericKeys); |
| const suffixGroups = Object.values(suffixGroupsMap); |
|
|
| |
| const groupStats = computeGroupStats(chartData, suffixGroups); |
|
|
| |
| const scaleGroups = groupByScale(suffixGroups, groupStats); |
|
|
| |
| return flattenScaleGroups(scaleGroups); |
| } |
|
|