Improve chart visuals and add null safety in analytics
All checks were successful
Build Frontend / build (push) Successful in 1m19s

Refactored GrowthAnalyticsChart to use Area for 'orders' with gradient fill and improved dot handling. Enhanced PredictionsChart with consistent null checks for predictions data, improved tooltip rendering, and adjusted chart margins and axis styles. Updated RevenueChart to add activeDot styling for better interactivity.
This commit is contained in:
g
2026-01-12 04:52:40 +00:00
parent 1933ef4007
commit a0605e47de
3 changed files with 64 additions and 50 deletions

View File

@@ -214,7 +214,7 @@ export default function PredictionsChart({
if (!predictions?.sales?.dailyPredictions) return [];
const baselineData = baselinePredictions?.sales?.dailyPredictions || [];
const simulatedDailyData = predictions.sales.dailyPredictions;
const simulatedDailyData = predictions?.sales?.dailyPredictions || [];
return simulatedDailyData.map((d: any, idx: number) => ({
...d,
@@ -307,7 +307,7 @@ export default function PredictionsChart({
Predictions & Forecasting
</CardTitle>
<CardDescription>
{predictions.sales.aiModel?.used
{predictions?.sales?.aiModel?.used
? "AI neural network + statistical models for sales, demand, and inventory"
: "AI-powered predictions for sales, demand, and inventory"}
</CardDescription>
@@ -372,13 +372,13 @@ export default function PredictionsChart({
</CardTitle>
</CardHeader>
<CardContent>
{predictions.sales.predicted !== null ? (
{predictions?.sales?.predicted !== null && predictions?.sales?.predicted !== undefined ? (
<div className="space-y-2">
<Tooltip>
<TooltipTrigger asChild>
<div className="text-2xl font-bold w-fit cursor-help">
<CountUp
end={predictions.sales.predicted}
end={predictions?.sales?.predicted || 0}
duration={1.5}
separator=","
decimals={2}
@@ -397,13 +397,13 @@ export default function PredictionsChart({
<span className="inline-flex cursor-help">
<Badge
className={getConfidenceColor(
predictions.sales.confidence,
predictions?.sales?.confidence || "low",
)}
>
{getConfidenceLabel(predictions.sales.confidence)} Confidence
{predictions.sales.confidenceScore !== undefined && (
{getConfidenceLabel(predictions?.sales?.confidence || "low")} Confidence
{predictions?.sales?.confidenceScore !== undefined && (
<span className="ml-1 opacity-75">
({Math.round(predictions.sales.confidenceScore * 100)}%)
({Math.round((predictions?.sales?.confidenceScore || 0) * 100)}%)
</span>
)}
</Badge>
@@ -414,15 +414,15 @@ export default function PredictionsChart({
</TooltipContent>
</Tooltip>
{predictions.sales.aiModel?.used && (
{predictions?.sales?.aiModel?.used && (
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex cursor-help">
<Badge variant="outline" className="bg-purple-500/10 text-purple-700 dark:text-purple-400 border-purple-500/30">
🤖 AI Powered
{predictions.sales.aiModel.modelAccuracy !== undefined && (
{predictions?.sales?.aiModel?.modelAccuracy !== undefined && (
<span className="ml-1 opacity-75">
({Math.round(predictions.sales.aiModel.modelAccuracy * 100)}%)
({Math.round((predictions?.sales?.aiModel?.modelAccuracy || 0) * 100)}%)
</span>
)}
</Badge>
@@ -433,29 +433,29 @@ export default function PredictionsChart({
</TooltipContent>
</Tooltip>
)}
{predictions.sales.trend && (
{predictions?.sales?.trend && (
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex cursor-help">
<Badge
variant="outline"
className={
predictions.sales.trend.direction === "up"
predictions?.sales?.trend?.direction === "up"
? "text-green-600 border-green-600"
: predictions.sales.trend.direction === "down"
: predictions?.sales?.trend?.direction === "down"
? "text-red-600 border-red-600"
: ""
}
>
{predictions.sales.trend.direction === "up" && (
{predictions?.sales?.trend?.direction === "up" && (
<TrendingUp className="h-3 w-3 mr-1" />
)}
{predictions.sales.trend.direction === "down" && (
{predictions?.sales?.trend?.direction === "down" && (
<TrendingDown className="h-3 w-3 mr-1" />
)}
{predictions.sales.trend.direction === "up"
{predictions?.sales?.trend?.direction === "up"
? "Trending Up"
: predictions.sales.trend.direction === "down"
: predictions?.sales?.trend?.direction === "down"
? "Trending Down"
: "Stable"}
</Badge>
@@ -470,24 +470,24 @@ export default function PredictionsChart({
Next {daysAhead} days
</span>
</div>
{predictions.sales.predictedOrders && (
{predictions?.sales?.predictedOrders && (
<div className="text-sm text-muted-foreground">
~{Math.round(predictions.sales.predictedOrders)}{" "}
~{Math.round(predictions?.sales?.predictedOrders || 0)}{" "}
orders
</div>
)}
{!predictions.sales.confidenceIntervals &&
predictions.sales.minPrediction &&
predictions.sales.maxPrediction && (
{!predictions?.sales?.confidenceIntervals &&
predictions?.sales?.minPrediction &&
predictions?.sales?.maxPrediction && (
<div className="text-xs text-muted-foreground">
Range: {formatGBP(predictions.sales.minPrediction)} -{" "}
{formatGBP(predictions.sales.maxPrediction)}
Range: {formatGBP(predictions?.sales?.minPrediction || 0)} -{" "}
{formatGBP(predictions?.sales?.maxPrediction || 0)}
</div>
)}
</div>
) : (
<div className="text-sm text-muted-foreground">
{predictions.sales.message ||
{predictions?.sales?.message ||
"Insufficient data for prediction"}
</div>
)}
@@ -559,8 +559,8 @@ export default function PredictionsChart({
</Alert>
{/* Daily Predictions Chart */}
{predictions.sales.dailyPredictions &&
predictions.sales.dailyPredictions.length > 0 && (
{predictions?.sales?.dailyPredictions &&
predictions?.sales?.dailyPredictions.length > 0 && (
<Card>
<CardHeader className="pb-3 flex flex-row items-center justify-between space-y-0">
<CardTitle className="text-sm font-medium">
@@ -606,7 +606,7 @@ export default function PredictionsChart({
</Button>
</CardHeader>
<CardContent>
<div className="h-[300px] w-full mt-4 relative">
<div className="h-80 w-full mt-4 relative">
{isSimulating && (
<div className="absolute inset-0 flex items-center justify-center bg-background/50 z-10">
<RefreshCw className="h-6 w-6 animate-spin text-muted-foreground" />
@@ -615,12 +615,7 @@ export default function PredictionsChart({
<ResponsiveContainer width="100%" height="100%">
<AreaChart
data={chartData}
margin={{
top: 5,
right: 10,
left: 0,
bottom: 0,
}}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<defs>
<linearGradient id="colorBaseline" x1="0" y1="0" x2="0" y2="1">
@@ -651,28 +646,37 @@ export default function PredictionsChart({
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
<XAxis
dataKey="formattedDate"
stroke="hsl(var(--muted-foreground))"
fontSize={12}
tick={{ fontSize: 12, fill: "hsl(var(--muted-foreground))" }}
tickLine={false}
axisLine={false}
/>
<YAxis
stroke="hsl(var(--muted-foreground))"
fontSize={12}
tick={{ fontSize: 12, fill: "hsl(var(--muted-foreground))" }}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `£${value}`}
/>
<RechartsTooltip
contentStyle={{
backgroundColor: "hsl(var(--background))",
borderColor: "hsl(var(--border))",
borderRadius: "var(--radius)",
cursor={{ fill: "transparent", stroke: "hsl(var(--muted-foreground))", strokeDasharray: "3 3" }}
content={({ active, payload }) => {
if (active && payload?.length) {
const data = payload[0].payload;
return (
<div className="bg-background border border-border p-3 rounded-lg shadow-lg">
<p className="font-medium mb-2">{data.formattedDate}</p>
<p className="text-sm text-purple-600">
Baseline: <span className="font-semibold">{formatGBP(data.baseline)}</span>
</p>
{committedSimulationFactor !== 0 && (
<p className="text-sm text-green-600">
Simulated: <span className="font-semibold">{formatGBP(data.simulated)}</span>
</p>
)}
</div>
);
}
return null;
}}
formatter={(value: number, name: string) => [
formatGBP(value),
name === "baseline" ? "Baseline" : "Simulated"
]}
/>
{/* Always show baseline as solid line */}
<Area
@@ -682,6 +686,7 @@ export default function PredictionsChart({
fillOpacity={committedSimulationFactor !== 0 ? 0.3 : 1}
fill="url(#colorBaseline)"
strokeWidth={committedSimulationFactor !== 0 ? 1 : 2}
activeDot={{ r: 4, strokeWidth: 0 }}
/>
{/* Show simulated line when simulation is active */}
{committedSimulationFactor !== 0 && (
@@ -693,6 +698,7 @@ export default function PredictionsChart({
fill="url(#colorSimulated)"
strokeDasharray="5 5"
strokeWidth={2}
activeDot={{ r: 5, strokeWidth: 0 }}
/>
)}
</AreaChart>