fix: hide probability bands on temperature chart
This commit is contained in:
@@ -41,7 +41,7 @@ function hasDrawableTemperatureChartContent({
|
|||||||
probabilityOverlay: ProbabilityOverlay | null;
|
probabilityOverlay: ProbabilityOverlay | null;
|
||||||
zoomedData: Array<Record<string, any>>;
|
zoomedData: Array<Record<string, any>>;
|
||||||
}) {
|
}) {
|
||||||
if (probabilityOverlay?.muLine || probabilityOverlay?.bands.length) return true;
|
void probabilityOverlay;
|
||||||
return activeSeries.some((series) =>
|
return activeSeries.some((series) =>
|
||||||
zoomedData.some((point, index) => {
|
zoomedData.some((point, index) => {
|
||||||
const value = point?.[series.key] ?? series.values[index];
|
const value = point?.[series.key] ?? series.values[index];
|
||||||
@@ -261,29 +261,6 @@ function TemperatureChartCanvasComponent({
|
|||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{probabilityOverlay && (
|
|
||||||
<span
|
|
||||||
className={clsx(
|
|
||||||
"inline-flex items-center gap-1.5 rounded border border-violet-200 bg-violet-50 px-1.5 py-0.5 text-[10px] font-bold text-violet-700",
|
|
||||||
canToggleRunwayDetails ? "" : "ml-auto",
|
|
||||||
)}
|
|
||||||
title={
|
|
||||||
probabilityOverlay.muLine
|
|
||||||
? probabilityOverlay.muLine.label
|
|
||||||
: isEn
|
|
||||||
? "Legacy Gaussian probability bands"
|
|
||||||
: "Legacy 高斯概率温度带"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className="h-2 w-2 rounded-full bg-violet-500/70" />
|
|
||||||
<span>{isEn ? "Gaussian" : "高斯概率"}</span>
|
|
||||||
{probabilityOverlay.muLine && (
|
|
||||||
<span className="font-mono text-violet-600">
|
|
||||||
μ {probabilityOverlay.muLine.value.toFixed(1)}{tempSymbol}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div ref={chartHostRef} className={clsx("relative flex-1", compact ? "min-h-[120px]" : "min-h-[220px]")}>
|
<div ref={chartHostRef} className={clsx("relative flex-1", compact ? "min-h-[120px]" : "min-h-[220px]")}>
|
||||||
{!shouldRenderChart && <TemperatureChartSkeleton compact={compact} />}
|
{!shouldRenderChart && <TemperatureChartSkeleton compact={compact} />}
|
||||||
@@ -316,16 +293,6 @@ function TemperatureChartCanvasComponent({
|
|||||||
domain={chartDomain}
|
domain={chartDomain}
|
||||||
ticks={intDegreeTicks ?? undefined}
|
ticks={intDegreeTicks ?? undefined}
|
||||||
/>
|
/>
|
||||||
{timeframe === "1D" && probabilityOverlay?.bands.map((band) => (
|
|
||||||
<ReferenceArea
|
|
||||||
key={band.key}
|
|
||||||
y1={band.lower}
|
|
||||||
y2={band.upper}
|
|
||||||
strokeOpacity={0}
|
|
||||||
fill="#8b5cf6"
|
|
||||||
fillOpacity={band.opacity}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{timeframe === "1D" && cityThresholds.map((t, idx) => {
|
{timeframe === "1D" && cityThresholds.map((t, idx) => {
|
||||||
const isSelected = row && (Number(row.target_threshold ?? row.target_value) === t.threshold);
|
const isSelected = row && (Number(row.target_threshold ?? row.target_value) === t.threshold);
|
||||||
const labelText = isEn
|
const labelText = isEn
|
||||||
@@ -348,20 +315,6 @@ function TemperatureChartCanvasComponent({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{timeframe === "1D" && probabilityOverlay?.muLine && (
|
|
||||||
<ReferenceLine
|
|
||||||
y={probabilityOverlay.muLine.value}
|
|
||||||
stroke="#7c3aed"
|
|
||||||
strokeDasharray="2 3"
|
|
||||||
strokeWidth={1.4}
|
|
||||||
label={{
|
|
||||||
value: compact ? undefined : probabilityOverlay.muLine.label,
|
|
||||||
fill: "#7c3aed",
|
|
||||||
fontSize: 9,
|
|
||||||
position: "insideTopLeft",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
filterNull={false}
|
filterNull={false}
|
||||||
cursor={{ stroke: "#94a3b8", strokeWidth: 1 }}
|
cursor={{ stroke: "#94a3b8", strokeWidth: 1 }}
|
||||||
@@ -378,7 +331,7 @@ function TemperatureChartCanvasComponent({
|
|||||||
payload={props.payload as ReadonlyArray<{ payload?: Record<string, any> }> | undefined}
|
payload={props.payload as ReadonlyArray<{ payload?: Record<string, any> }> | undefined}
|
||||||
data={zoomedData}
|
data={zoomedData}
|
||||||
series={activeSeries}
|
series={activeSeries}
|
||||||
probabilityOverlay={probabilityOverlay}
|
probabilityOverlay={null}
|
||||||
tempSymbol={tempSymbol}
|
tempSymbol={tempSymbol}
|
||||||
isEn={isEn}
|
isEn={isEn}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -118,42 +118,15 @@ function buildTooltipRows(
|
|||||||
.filter((item): item is TooltipRow => item !== null);
|
.filter((item): item is TooltipRow => item !== null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatProbabilityTemp(value: number, tempSymbol: string) {
|
|
||||||
return `${value.toFixed(1)}${tempSymbol}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildTooltipProbabilityRows(
|
function buildTooltipProbabilityRows(
|
||||||
probabilityOverlay: ProbabilityOverlay | null | undefined,
|
probabilityOverlay: ProbabilityOverlay | null | undefined,
|
||||||
tempSymbol: string,
|
tempSymbol: string,
|
||||||
isEn: boolean,
|
isEn: boolean,
|
||||||
): TooltipProbabilityRow[] {
|
): TooltipProbabilityRow[] {
|
||||||
if (!probabilityOverlay) return [];
|
void probabilityOverlay;
|
||||||
|
void tempSymbol;
|
||||||
const rows: TooltipProbabilityRow[] = [];
|
void isEn;
|
||||||
if (probabilityOverlay.muLine) {
|
return [];
|
||||||
rows.push({
|
|
||||||
key: "gaussian_mu",
|
|
||||||
label: isEn ? "Gaussian μ" : "高斯 μ",
|
|
||||||
value: formatProbabilityTemp(probabilityOverlay.muLine.value, tempSymbol),
|
|
||||||
color: "#7c3aed",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
probabilityOverlay.bands
|
|
||||||
.slice()
|
|
||||||
.sort((left, right) => right.probability - left.probability)
|
|
||||||
.forEach((band) => {
|
|
||||||
const range = `${band.lower.toFixed(1)}-${band.upper.toFixed(1)}${tempSymbol}`;
|
|
||||||
const probability = `${Math.round(band.probability * 100)}%`;
|
|
||||||
rows.push({
|
|
||||||
key: band.key,
|
|
||||||
label: isEn ? "Probability band" : "概率温度带",
|
|
||||||
value: `${range} ${probability}`,
|
|
||||||
color: "#8b5cf6",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return rows;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const __buildTemperatureTooltipRowsForTest = buildTooltipRows;
|
export const __buildTemperatureTooltipRowsForTest = buildTooltipRows;
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ const quickStart: Record<"zh" | "en", GuideCopy[]> = {
|
|||||||
body: "橙色 DEB Forecast 是融合模型和日内修正后的路径,用来判断后续升温或降温空间。",
|
body: "橙色 DEB Forecast 是融合模型和日内修正后的路径,用来判断后续升温或降温空间。",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "最后看概率",
|
title: "最后看图层",
|
||||||
body: "紫色区域和虚线表示高概率温度带,适合判断当前实测是否偏离主预期。",
|
body: "按需打开模型线或跑道明细,用来确认当前实测是否偏离 DEB 主路径。",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
en: [
|
en: [
|
||||||
@@ -56,8 +56,8 @@ const quickStart: Record<"zh" | "en", GuideCopy[]> = {
|
|||||||
body: "The orange DEB Forecast blends model context with intraday correction to frame the remaining move.",
|
body: "The orange DEB Forecast blends model context with intraday correction to frame the remaining move.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Check probability",
|
title: "Check layers",
|
||||||
body: "The purple band and dotted line show the high-probability temperature zone for fast deviation checks.",
|
body: "Use model lines or runway details only when needed to confirm whether live observations are drifting away from the DEB path.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -66,7 +66,7 @@ const legendItems: Record<"zh" | "en", GuideCopy[]> = {
|
|||||||
zh: [
|
zh: [
|
||||||
{ title: "实测 / 结算线", body: "优先展示结算跑道、官方站或城市核心实况,用于判断已兑现温度。" },
|
{ title: "实测 / 结算线", body: "优先展示结算跑道、官方站或城市核心实况,用于判断已兑现温度。" },
|
||||||
{ title: "DEB Forecast", body: "橙色预测路径,重点看它和实测线在峰值窗口前后的分歧。" },
|
{ title: "DEB Forecast", body: "橙色预测路径,重点看它和实测线在峰值窗口前后的分歧。" },
|
||||||
{ title: "高概率带", body: "紫色带表示当前概率分布的主要落点,虚线是概率均值附近。" },
|
{ title: "市场概率", body: "概率仍用于市场判断和后台分析,但默认不再占用温度主图空间。" },
|
||||||
{ title: "机场报文", body: "METAR / MGM 作为机场站参考,默认只在适合的城市自动显示。" },
|
{ title: "机场报文", body: "METAR / MGM 作为机场站参考,默认只在适合的城市自动显示。" },
|
||||||
{ title: "模型线", body: "ECMWF、GFS、ICON、GEM 等提供背景,默认弱化为辅助判断。" },
|
{ title: "模型线", body: "ECMWF、GFS、ICON、GEM 等提供背景,默认弱化为辅助判断。" },
|
||||||
{ title: "跑道明细", body: "打开后可查看各跑道传感器,关闭后仍保留结算跑道温度。" },
|
{ title: "跑道明细", body: "打开后可查看各跑道传感器,关闭后仍保留结算跑道温度。" },
|
||||||
@@ -74,7 +74,7 @@ const legendItems: Record<"zh" | "en", GuideCopy[]> = {
|
|||||||
en: [
|
en: [
|
||||||
{ title: "Live / settlement", body: "Settlement runway, official station, or core live observation used as the realized anchor." },
|
{ title: "Live / settlement", body: "Settlement runway, official station, or core live observation used as the realized anchor." },
|
||||||
{ title: "DEB Forecast", body: "Orange forecast path; focus on its gap versus live observations near the peak window." },
|
{ title: "DEB Forecast", body: "Orange forecast path; focus on its gap versus live observations near the peak window." },
|
||||||
{ title: "Probability band", body: "Purple band marks the main probability zone, with the dotted line near the probability mean." },
|
{ title: "Market probability", body: "Probability remains available for market analysis, but no longer occupies the main temperature chart by default." },
|
||||||
{ title: "Airport reports", body: "METAR / MGM are airport references and are auto-shown only where they are useful by default." },
|
{ title: "Airport reports", body: "METAR / MGM are airport references and are auto-shown only where they are useful by default." },
|
||||||
{ title: "Model lines", body: "ECMWF, GFS, ICON, GEM, and related model layers provide background context." },
|
{ title: "Model lines", body: "ECMWF, GFS, ICON, GEM, and related model layers provide background context." },
|
||||||
{ title: "Runway details", body: "When disabled, the chart still keeps the settlement runway temperature visible." },
|
{ title: "Runway details", body: "When disabled, the chart still keeps the settlement runway temperature visible." },
|
||||||
@@ -160,8 +160,8 @@ export function UsageGuideDashboard({ isEn }: { isEn: boolean }) {
|
|||||||
</h1>
|
</h1>
|
||||||
<p className="mt-2 max-w-3xl text-sm leading-6 text-slate-500">
|
<p className="mt-2 max-w-3xl text-sm leading-6 text-slate-500">
|
||||||
{isEn
|
{isEn
|
||||||
? "Start from live evidence, compare DEB, then use probability and layer toggles to confirm whether the city is moving away from the main path."
|
? "Start from live evidence, compare DEB, then use layer toggles to confirm whether the city is moving away from the main path."
|
||||||
: "先看实况锚点,再对照 DEB 路径,最后用概率带和图层显隐确认城市是否偏离主预期。"}
|
: "先看实况锚点,再对照 DEB 路径,最后用图层显隐确认城市是否偏离主预期。"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-2 text-center text-[11px] font-bold text-slate-600">
|
<div className="grid grid-cols-3 gap-2 text-center text-[11px] font-bold text-slate-600">
|
||||||
|
|||||||
+2
-9
@@ -1637,7 +1637,7 @@ export function runTests() {
|
|||||||
const firstBand = bandPoints[0].runway_band;
|
const firstBand = bandPoints[0].runway_band;
|
||||||
assert(Array.isArray(firstBand) && firstBand[0] === 24.0 && firstBand[1] === 26.0, "runway_band tuple values should match input limits");
|
assert(Array.isArray(firstBand) && firstBand[0] === 24.0 && firstBand[1] === 26.0, "runway_band tuple values should match input limits");
|
||||||
|
|
||||||
// ── Legacy Gaussian probability overlay test ──
|
// ── Legacy Gaussian probability data should not render as a visible chart overlay ──
|
||||||
const gaussianOverlayChart = __buildTemperatureChartDataForTest(
|
const gaussianOverlayChart = __buildTemperatureChartDataForTest(
|
||||||
{
|
{
|
||||||
city: "toronto",
|
city: "toronto",
|
||||||
@@ -1665,14 +1665,7 @@ export function runTests() {
|
|||||||
) as any;
|
) as any;
|
||||||
|
|
||||||
const gaussianOverlay = gaussianOverlayChart.probabilityOverlay;
|
const gaussianOverlay = gaussianOverlayChart.probabilityOverlay;
|
||||||
assert(gaussianOverlay, "legacy Gaussian probabilities should be exposed as a chart overlay");
|
assert(gaussianOverlay === null, "legacy Gaussian probabilities should not create visible chart overlays");
|
||||||
assert(gaussianOverlay.muLine?.value === 27.4, "legacy Gaussian μ should become a reference line");
|
|
||||||
assert(
|
|
||||||
gaussianOverlay.bands.some(
|
|
||||||
(band: any) => band.value === 27 && band.lower === 26.5 && band.upper === 27.5 && band.probability === 0.42,
|
|
||||||
),
|
|
||||||
"legacy Gaussian buckets should become horizontal probability temperature bands",
|
|
||||||
);
|
|
||||||
assert(
|
assert(
|
||||||
!gaussianOverlayChart.series.some((series: any) => String(series.key || "").includes("probability")),
|
!gaussianOverlayChart.series.some((series: any) => String(series.key || "").includes("probability")),
|
||||||
"legacy Gaussian probability distribution should not be rendered as a time-series line",
|
"legacy Gaussian probability distribution should not be rendered as a time-series line",
|
||||||
|
|||||||
+2
-6
@@ -73,11 +73,7 @@ export function runTests() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
probabilityRows.some((row) => row.key === "gaussian_mu" && row.value === "27.4°C"),
|
probabilityRows.length === 0,
|
||||||
"tooltip should expose the Gaussian μ line when the purple probability overlay is present",
|
"temperature tooltip should not show Gaussian μ or probability-band rows",
|
||||||
);
|
|
||||||
assert(
|
|
||||||
probabilityRows.some((row) => row.value.includes("26.5-27.5°C") && row.value.includes("42%")),
|
|
||||||
"tooltip should expose purple probability-band range and probability values",
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2168,7 +2168,9 @@ function buildFullDayChartData(
|
|||||||
return point;
|
return point;
|
||||||
});
|
});
|
||||||
|
|
||||||
const probabilityOverlay = buildLegacyGaussianProbabilityOverlay(row, hourly);
|
// Keep probability data in the payload for market views, but do not draw
|
||||||
|
// legacy Gaussian bands on the live temperature chart.
|
||||||
|
const probabilityOverlay = null;
|
||||||
|
|
||||||
return { data, series, probabilityOverlay };
|
return { data, series, probabilityOverlay };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user