Improve terminal chart loading states
This commit is contained in:
@@ -64,10 +64,37 @@ function shouldKeepTemperatureChartLoading({
|
||||
zoomedData: Array<Record<string, any>>;
|
||||
}) {
|
||||
if (!row?.city) return false;
|
||||
if (isHourlyLoading) return true;
|
||||
if (!isHourlyLoading) return false;
|
||||
return !hasDrawableTemperatureChartContent({ activeSeries, probabilityOverlay, zoomedData });
|
||||
}
|
||||
|
||||
function TemperatureChartSkeleton({ compact }: { compact: boolean }) {
|
||||
const horizontalLines = compact ? 5 : 7;
|
||||
const verticalLines = compact ? 5 : 8;
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 overflow-hidden bg-white">
|
||||
<div className="absolute inset-x-3 bottom-7 top-4 rounded-sm border border-slate-100">
|
||||
{Array.from({ length: horizontalLines }).map((_, index) => (
|
||||
<span
|
||||
key={`h-${index}`}
|
||||
className="absolute left-0 right-0 border-t border-dashed border-sky-100"
|
||||
style={{ top: `${(index / Math.max(1, horizontalLines - 1)) * 100}%` }}
|
||||
/>
|
||||
))}
|
||||
{Array.from({ length: verticalLines }).map((_, index) => (
|
||||
<span
|
||||
key={`v-${index}`}
|
||||
className="absolute bottom-0 top-0 border-l border-dashed border-sky-100"
|
||||
style={{ left: `${(index / Math.max(1, verticalLines - 1)) * 100}%` }}
|
||||
/>
|
||||
))}
|
||||
<div className="absolute inset-x-10 top-1/3 h-10 rounded bg-slate-100/60" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TemperatureChartCanvasComponent({
|
||||
isEn,
|
||||
compact,
|
||||
@@ -173,6 +200,11 @@ function TemperatureChartCanvasComponent({
|
||||
hasRunwayData &&
|
||||
individualRunwaySeriesCount > 1 &&
|
||||
collapsedRunwaySeries.length < chartSeries.length;
|
||||
const hasDrawableChartContent = hasDrawableTemperatureChartContent({
|
||||
activeSeries,
|
||||
probabilityOverlay,
|
||||
zoomedData,
|
||||
});
|
||||
const shouldShowChartLoading = shouldKeepTemperatureChartLoading({
|
||||
row,
|
||||
isHourlyLoading,
|
||||
@@ -180,6 +212,9 @@ function TemperatureChartCanvasComponent({
|
||||
probabilityOverlay,
|
||||
zoomedData,
|
||||
});
|
||||
const shouldRenderChart = canRenderChart && hasDrawableChartContent;
|
||||
const shouldShowEmptyState = Boolean(row?.city) && !isHourlyLoading && !hasDrawableChartContent;
|
||||
const shouldShowBackgroundRefresh = isHourlyLoading && hasDrawableChartContent;
|
||||
|
||||
return (
|
||||
<div className={clsx("relative flex flex-1 flex-col p-2", compact ? "min-h-[120px]" : "min-h-[240px]")}>
|
||||
@@ -242,7 +277,8 @@ function TemperatureChartCanvasComponent({
|
||||
)}
|
||||
</div>
|
||||
<div ref={chartHostRef} className={clsx("relative flex-1", compact ? "min-h-[120px]" : "min-h-[220px]")}>
|
||||
{canRenderChart && !shouldShowChartLoading && (
|
||||
{!shouldRenderChart && <TemperatureChartSkeleton compact={compact} />}
|
||||
{shouldRenderChart && (
|
||||
<ReComposedChart
|
||||
width={chartWidth}
|
||||
height={chartHeight}
|
||||
@@ -384,7 +420,20 @@ function TemperatureChartCanvasComponent({
|
||||
))}
|
||||
</ReComposedChart>
|
||||
)}
|
||||
{shouldShowEmptyState && (
|
||||
<div className="pointer-events-none absolute inset-0 grid place-items-center px-4 text-center">
|
||||
<div className="rounded border border-slate-200 bg-white/90 px-3 py-2 text-[11px] font-semibold text-slate-500 shadow-sm">
|
||||
{isEn ? "No drawable chart data yet" : "暂无可绘制图表数据"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{shouldShowBackgroundRefresh && (
|
||||
<div className="pointer-events-none absolute right-3 top-12 z-10 inline-flex items-center gap-1.5 rounded border border-slate-200 bg-white/80 px-2 py-1 text-[10px] font-semibold text-slate-500 shadow-sm backdrop-blur-[1px]">
|
||||
<span className="h-2.5 w-2.5 animate-spin rounded-full border-2 border-slate-200 border-t-blue-500" />
|
||||
<span>{isEn ? "Updating" : "更新中"}</span>
|
||||
</div>
|
||||
)}
|
||||
{shouldShowChartLoading && (
|
||||
<div className="pointer-events-none absolute inset-2 z-10 grid place-items-center bg-white/65 backdrop-blur-[1px]">
|
||||
<div className="flex items-center gap-2 rounded border border-slate-200 bg-white px-3 py-2 text-[11px] font-semibold text-slate-600 shadow-sm">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { __buildTemperatureStatsLabelsForTest } from "@/components/dashboard/scan-terminal/TemperatureStatsBars";
|
||||
import { temp } from "@/components/dashboard/scan-terminal/utils";
|
||||
|
||||
function assert(condition: unknown, message: string) {
|
||||
if (!condition) throw new Error(message);
|
||||
@@ -46,4 +47,8 @@ export function runTests() {
|
||||
|
||||
assert(zh.primary === "天文台实测 (10分钟)", "Chinese primary label should remain unchanged");
|
||||
assert(zh.compactSecondary === "当日最高", "Chinese Shenzhen compact secondary label should remain 当日最高");
|
||||
|
||||
assert(temp(null, "°C") === "--", "empty temperature values should not render as 0.0°C while city detail is loading");
|
||||
assert(temp(undefined, "°C") === "--", "undefined temperature values should not render as 0.0°C while city detail is loading");
|
||||
assert(temp("", "°C") === "--", "blank temperature values should not render as 0.0°C while city detail is loading");
|
||||
}
|
||||
|
||||
@@ -121,6 +121,19 @@ export function runTests() {
|
||||
);
|
||||
assert(
|
||||
__shouldKeepTemperatureChartLoadingForTest({
|
||||
row: { city: "Moscow" } as any,
|
||||
isHourlyLoading: true,
|
||||
activeSeries: [],
|
||||
probabilityOverlay: null,
|
||||
zoomedData: [
|
||||
{ label: "00:00", ts: 1 },
|
||||
{ label: "05:00", ts: 2 },
|
||||
],
|
||||
}),
|
||||
"temperature chart should show the loading skeleton while the first detail fetch is in flight and no drawable data exists",
|
||||
);
|
||||
assert(
|
||||
!__shouldKeepTemperatureChartLoadingForTest({
|
||||
row: { city: "Moscow" } as any,
|
||||
isHourlyLoading: false,
|
||||
activeSeries: [],
|
||||
@@ -130,7 +143,7 @@ export function runTests() {
|
||||
{ label: "05:00", ts: 2 },
|
||||
],
|
||||
}),
|
||||
"temperature chart must keep loading instead of rendering an empty axis grid when no drawable series is available",
|
||||
"temperature chart must stop showing an indefinite loading overlay after the detail fetch finishes without drawable data",
|
||||
);
|
||||
assert(
|
||||
!__shouldKeepTemperatureChartLoadingForTest({
|
||||
@@ -153,4 +166,25 @@ export function runTests() {
|
||||
}),
|
||||
"temperature chart should render once a visible series has drawable values",
|
||||
);
|
||||
assert(
|
||||
!__shouldKeepTemperatureChartLoadingForTest({
|
||||
row: { city: "Moscow" } as any,
|
||||
isHourlyLoading: true,
|
||||
activeSeries: [
|
||||
{
|
||||
key: "current",
|
||||
label: "Current reference",
|
||||
source: "Live",
|
||||
color: "#009688",
|
||||
values: [13, 13],
|
||||
},
|
||||
] as any,
|
||||
probabilityOverlay: null,
|
||||
zoomedData: [
|
||||
{ label: "00:00", ts: 1, current: 13 },
|
||||
{ label: "05:00", ts: 2, current: 13 },
|
||||
],
|
||||
}),
|
||||
"temperature chart must render seeded or cached data immediately while full detail continues loading in the background",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ export function money(value: unknown) {
|
||||
}
|
||||
|
||||
export function temp(value: unknown, unit?: string | null) {
|
||||
if (value == null) return "--";
|
||||
if (typeof value === "string" && value.trim() === "") return "--";
|
||||
const n = Number(value);
|
||||
if (!Number.isFinite(n)) return "--";
|
||||
return `${n.toFixed(1)}${unit || "°"}`;
|
||||
|
||||
Reference in New Issue
Block a user