From 5404fdef2bdba2826d9d5e15df9511bcf6707dd1 Mon Sep 17 00:00:00 2001
From: "2569718930@qq.com" <2569718930@qq.com>
Date: Mon, 29 Jun 2026 21:48:47 +0800
Subject: [PATCH] Render probability distribution per city row
---
.../scan-terminal/ModelSummaryDashboard.tsx | 71 ++++++++-----------
.../__tests__/modelSummaryDashboard.test.ts | 12 ++--
frontend/lib/model-summary.ts | 32 ---------
3 files changed, 35 insertions(+), 80 deletions(-)
diff --git a/frontend/components/dashboard/scan-terminal/ModelSummaryDashboard.tsx b/frontend/components/dashboard/scan-terminal/ModelSummaryDashboard.tsx
index d7563e20..80360ffa 100644
--- a/frontend/components/dashboard/scan-terminal/ModelSummaryDashboard.tsx
+++ b/frontend/components/dashboard/scan-terminal/ModelSummaryDashboard.tsx
@@ -6,14 +6,12 @@ import { useEffect, useMemo, useRef, useState } from "react";
import type { ScanOpportunityRow } from "@/lib/dashboard-types";
import {
MODEL_SUMMARY_MODEL_COLUMNS,
- buildModelSummaryProbabilityColumns,
buildModelSummaryRows,
filterModelSummaryRows,
formatModelSummaryProbability,
formatModelSummaryLocalTime,
formatModelSummaryTemp,
hasModelSummaryForecastData,
- type ModelSummaryProbabilityColumn,
type ModelSummaryRow,
} from "@/lib/model-summary";
@@ -34,6 +32,7 @@ const SUMMARY_TEXT = {
region: { en: "Region", zh: "区域" },
localTime: { en: "Local Time", zh: "当地时间" },
gaussianMu: { en: "Gaussian μ", zh: "高斯 μ" },
+ probabilityDistribution: { en: "Probability Distribution", zh: "概率分布" },
median: { en: "Median", zh: "模型中位数" },
spread: { en: "Spread", zh: "分歧范围" },
empty: { en: "No model summary rows match the current filters.", zh: "当前筛选下没有模型汇总数据。" },
@@ -101,11 +100,9 @@ function FilterToggle({
function ModelSummaryRowView({
row,
nowMs,
- probabilityColumns,
}: {
row: ModelSummaryRow;
nowMs: number | null;
- probabilityColumns: ModelSummaryProbabilityColumn[];
}) {
return (
@@ -138,26 +135,32 @@ function ModelSummaryRowView({
|
|
- {probabilityColumns.map((column) => {
- const bucket = row.probabilityBucketMap[column.key] || null;
- const isTopBucket = bucket?.key === row.topProbabilityBucketKey;
- return (
-
-
- {formatModelSummaryProbability(bucket?.probability)}
-
- |
- );
- })}
+
+ {row.probabilityBuckets.length ? (
+
+ {row.probabilityBuckets.map((bucket) => {
+ const isTopBucket = bucket.key === row.topProbabilityBucketKey;
+ return (
+
+ {bucket.label}
+ {formatModelSummaryProbability(bucket.probability)}
+
+ );
+ })}
+
+ ) : (
+ —
+ )}
+ |
);
}
@@ -233,11 +236,6 @@ export function ModelSummaryDashboard({
}
}, [incomingSummaryRows, incomingHasForecastData]);
- const probabilityColumns = useMemo(
- () => buildModelSummaryProbabilityColumns(summaryRows),
- [summaryRows],
- );
-
const visibleRows = useMemo(
() =>
filterModelSummaryRows(summaryRows, {
@@ -295,7 +293,7 @@ export function ModelSummaryDashboard({
@@ -317,15 +315,9 @@ export function ModelSummaryDashboard({
|
{copy("gaussianMu", isEn)}
|
- {probabilityColumns.map((column) => (
-
- {column.label}
- |
- ))}
+
+ {copy("probabilityDistribution", isEn)}
+ |
@@ -334,7 +326,6 @@ export function ModelSummaryDashboard({
key={row.cityKey}
row={row}
nowMs={nowMs}
- probabilityColumns={probabilityColumns}
/>
))}
diff --git a/frontend/components/dashboard/scan-terminal/__tests__/modelSummaryDashboard.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/modelSummaryDashboard.test.ts
index 2e0aca9e..7a3a8670 100644
--- a/frontend/components/dashboard/scan-terminal/__tests__/modelSummaryDashboard.test.ts
+++ b/frontend/components/dashboard/scan-terminal/__tests__/modelSummaryDashboard.test.ts
@@ -2,7 +2,6 @@ import fs from "node:fs";
import path from "node:path";
import {
MODEL_SUMMARY_MODEL_COLUMNS,
- buildModelSummaryProbabilityColumns,
buildModelSummaryRows,
filterModelSummaryRows,
formatModelSummaryProbability,
@@ -132,12 +131,6 @@ export function runTests() {
parisRow.topProbabilityBucketKey === "31.5-32.5°C",
"model summary should map probability buckets and identify the top bucket",
);
- const probabilityColumns = buildModelSummaryProbabilityColumns(summaryRows);
- assert(
- probabilityColumns.map((column) => column.key).join("|") ===
- "30.5-31.5°C|31.5-32.5°C|32.5-33.5°C|34.5-35.5°C|35.5-36.5°C",
- "model summary should expose dynamic probability bucket columns sorted by temperature",
- );
assert(formatModelSummaryProbability(null) === "—", "missing probability should render as an em dash");
assert(formatModelSummaryProbability(0.424) === "42%", "probability buckets should render as rounded percentages");
assert(formatModelSummaryTemp(null, "°C") === "—", "missing model temperatures should render as an em dash");
@@ -204,10 +197,13 @@ export function runTests() {
modelSummarySource.includes("MODEL_SUMMARY_MODEL_COLUMNS") &&
modelSummarySource.includes("lastGoodSummaryRowsRef") &&
modelSummarySource.includes("hasModelSummaryForecastData") &&
- modelSummarySource.includes("buildModelSummaryProbabilityColumns") &&
modelSummarySource.includes("Gaussian μ") &&
modelSummarySource.includes("高斯 μ") &&
+ modelSummarySource.includes("Probability Distribution") &&
+ modelSummarySource.includes("概率分布") &&
modelSummarySource.includes("topProbabilityBucketKey") &&
+ modelSummarySource.includes("probabilityBuckets.map") &&
+ !modelSummarySource.includes("probabilityColumns") &&
modelSummarySource.includes("min-w-[96px]") &&
modelSummarySource.includes("Local Time") &&
modelSummarySource.includes("当地时间") &&
diff --git a/frontend/lib/model-summary.ts b/frontend/lib/model-summary.ts
index 0b2216aa..a958437d 100644
--- a/frontend/lib/model-summary.ts
+++ b/frontend/lib/model-summary.ts
@@ -29,14 +29,6 @@ export type ModelSummaryProbabilityBucket = {
probability: number;
};
-export type ModelSummaryProbabilityColumn = {
- key: string;
- label: string;
- lower: number;
- upper: number;
- unit: string;
-};
-
export type ModelSummaryRow = {
cityKey: string;
cityName: string;
@@ -324,27 +316,3 @@ export function hasModelSummaryForecastData(rows: ModelSummaryRow[]) {
return MODEL_SUMMARY_MODEL_COLUMNS.some((column) => row.models[column.key] != null);
});
}
-
-export function buildModelSummaryProbabilityColumns(
- rows: ModelSummaryRow[],
-): ModelSummaryProbabilityColumn[] {
- const byKey = new Map();
- rows.forEach((row) => {
- row.probabilityBuckets.forEach((bucket) => {
- if (byKey.has(bucket.key)) return;
- const unitMatch = bucket.key.match(/[^\d.\-\s]+$/);
- byKey.set(bucket.key, {
- key: bucket.key,
- label: bucket.label,
- lower: bucket.lower,
- upper: bucket.upper,
- unit: unitMatch?.[0] || row.tempSymbol || "°C",
- });
- });
- });
-
- return [...byKey.values()].sort((a, b) => {
- if (a.unit !== b.unit) return a.unit.localeCompare(b.unit);
- return a.lower - b.lower || a.upper - b.upper;
- });
-}