update miletruth page
This commit is contained in:
152
src/components/optimization/MetricsPanel.tsx
Normal file
152
src/components/optimization/MetricsPanel.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { motion, useTransform, type MotionValue } from "framer-motion";
|
||||
import { COLORS, KPIS, Kpi, rgba } from "./constants";
|
||||
|
||||
type Props = {
|
||||
/** shared scroll progress (0 → 1) */
|
||||
scroll: MotionValue<number>;
|
||||
};
|
||||
|
||||
const COUNT_START = 0.7;
|
||||
const COUNT_END = 0.97;
|
||||
|
||||
function Metric({ kpi, scroll, index }: { kpi: Kpi; scroll: MotionValue<number>; index: number }) {
|
||||
const [jitter, setJitter] = React.useState(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
// Tiny micro-fluctuation between -0.4% and +0.4%
|
||||
const val = (Math.random() - 0.5) * 0.008;
|
||||
setJitter(val);
|
||||
}, 800 + Math.random() * 600); // slightly staggered updates
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Single reactive string — updates without re-rendering React.
|
||||
const value = useTransform(scroll, (v) => {
|
||||
const t = Math.min(1, Math.max(0, (v - COUNT_START) / (COUNT_END - COUNT_START)));
|
||||
let n = kpi.before + (kpi.after - kpi.before) * t;
|
||||
|
||||
// Apply high-frequency live operational fluctuations on settled state
|
||||
if (t > 0.95) {
|
||||
n = n * (1 + jitter);
|
||||
}
|
||||
|
||||
const afterSide = t > 0.5;
|
||||
const prefix = afterSide ? kpi.prefixAfter ?? "" : kpi.prefixBefore ?? "";
|
||||
|
||||
if (kpi.key === "delayed" && kpi.after === 0 && t > 0.95) {
|
||||
return `0`;
|
||||
}
|
||||
|
||||
return `${prefix}${Math.round(n)}${kpi.suffix ?? ""}`;
|
||||
});
|
||||
|
||||
const label = useTransform(scroll, (v) =>
|
||||
v > (COUNT_START + COUNT_END) / 2 ? kpi.labelAfter : kpi.labelBefore,
|
||||
);
|
||||
|
||||
const fromColor = kpi.key === "orders" ? COLORS.cyan : COLORS.red;
|
||||
const toColor = COLORS.green;
|
||||
const accent = useTransform(
|
||||
scroll,
|
||||
[COUNT_START, COUNT_END],
|
||||
[rgba(fromColor, 1), rgba(toColor, 1)],
|
||||
);
|
||||
const glow = useTransform(
|
||||
scroll,
|
||||
[COUNT_START, COUNT_END],
|
||||
[rgba(fromColor, 0.28), rgba(toColor, 0.32)],
|
||||
);
|
||||
const boxShadow = useTransform(glow, (g) => `0 10px 40px -12px ${g}, inset 0 1px 0 rgba(255,255,255,0.06)`);
|
||||
const borderColor = useTransform(
|
||||
scroll,
|
||||
[COUNT_START, COUNT_END],
|
||||
[rgba(fromColor, 0.4), rgba(toColor, 0.45)],
|
||||
);
|
||||
|
||||
// Improvement arrow appears once optimized.
|
||||
const arrowOpacity = useTransform(scroll, [COUNT_END - 0.12, COUNT_END], [0, 1]);
|
||||
// Bar fills as the counter morphs from before → after.
|
||||
const fillScale = useTransform(scroll, [COUNT_START, COUNT_END], [0.12, 1]);
|
||||
|
||||
const trendColor = kpi.goodWhenLower ? COLORS.green : (kpi.key === "orders" ? COLORS.cyan : COLORS.green);
|
||||
|
||||
const sparklinePath = React.useMemo(() => {
|
||||
switch (kpi.key) {
|
||||
case "distance":
|
||||
case "vehicles":
|
||||
return "M 0,3 C 16,3 32,17 64,17"; // Downward trend
|
||||
case "orders":
|
||||
return "M 0,17 C 16,6 32,19 64,8"; // Stable high wavy trend
|
||||
case "delayed":
|
||||
case "cost":
|
||||
return "M 0,1 C 16,1 28,19 64,19"; // Sharp drop
|
||||
default:
|
||||
return "M 0,10 L 64,10";
|
||||
}
|
||||
}, [kpi.key]);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="dm-opt-metric"
|
||||
style={{ boxShadow, borderColor, animationDelay: `${0.06 * index}s` }}
|
||||
>
|
||||
<div className="dm-opt-metric__top">
|
||||
<motion.span className="dm-opt-metric__label">{label}</motion.span>
|
||||
<motion.span className="dm-opt-metric__arrow" style={{ opacity: arrowOpacity, color: COLORS.green }}>
|
||||
{kpi.goodWhenLower ? "▼" : "▲"}
|
||||
</motion.span>
|
||||
</div>
|
||||
<motion.div className="dm-opt-metric__value" style={{ color: accent }}>
|
||||
{value}
|
||||
</motion.div>
|
||||
|
||||
{/* Sparkline trend graphic */}
|
||||
<div className="dm-opt-metric__sparkline" style={{ position: "absolute", bottom: 8, right: 12, opacity: 0.32, width: 64, height: 20, pointerEvents: "none" }}>
|
||||
<svg width="100%" height="100%" viewBox="0 0 64 20" fill="none">
|
||||
<defs>
|
||||
<linearGradient id={`grad-${kpi.key}`} x1="0" y1="0" x2="0" y2="20" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stopColor={trendColor} stopOpacity="0.22" />
|
||||
<stop offset="100%" stopColor={trendColor} stopOpacity="0.0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
d={sparklinePath}
|
||||
stroke={trendColor}
|
||||
strokeWidth="1.2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d={`${sparklinePath} L 64,20 L 0,20 Z`}
|
||||
fill={`url(#grad-${kpi.key})`}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="dm-opt-metric__bar">
|
||||
<motion.div
|
||||
className="dm-opt-metric__fill"
|
||||
style={{ background: accent, scaleX: fillScale }}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
const MetricCard = React.memo(Metric);
|
||||
|
||||
function MetricsPanel({ scroll }: Props) {
|
||||
return (
|
||||
<div className="dm-opt-metrics" role="group" aria-label="Optimization results">
|
||||
{KPIS.map((kpi, i) => (
|
||||
<MetricCard key={kpi.key} kpi={kpi} scroll={scroll} index={i} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(MetricsPanel);
|
||||
Reference in New Issue
Block a user