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

@@ -201,6 +201,10 @@ export default function GrowthAnalyticsChart({
<stop offset="5%" stopColor="#10b981" stopOpacity={0.8} /> <stop offset="5%" stopColor="#10b981" stopOpacity={0.8} />
<stop offset="95%" stopColor="#10b981" stopOpacity={0} /> <stop offset="95%" stopColor="#10b981" stopOpacity={0} />
</linearGradient> </linearGradient>
<linearGradient id="colorOrdersGrowth" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.6} />
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
</linearGradient>
</defs> </defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" /> <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
<XAxis <XAxis
@@ -255,14 +259,16 @@ export default function GrowthAnalyticsChart({
return null; return null;
}} }}
/> />
<Line <Area
yAxisId="left" yAxisId="left"
type="monotone" type="monotone"
dataKey="orders" dataKey="orders"
stroke="#3b82f6" stroke="#3b82f6"
strokeWidth={2} strokeWidth={2}
dot={{ fill: "#3b82f6", r: 3 }} dot={false}
activeDot={{ r: 4, strokeWidth: 0 }}
name="Orders" name="Orders"
fill="url(#colorOrdersGrowth)"
/> />
<Area <Area
yAxisId="right" yAxisId="right"
@@ -270,7 +276,8 @@ export default function GrowthAnalyticsChart({
dataKey="revenue" dataKey="revenue"
stroke="#10b981" stroke="#10b981"
strokeWidth={3} strokeWidth={3}
dot={{ fill: "#10b981", r: 4 }} dot={false}
activeDot={{ r: 5, strokeWidth: 0 }}
name="Revenue" name="Revenue"
fill="url(#colorRevenueGrowth)" fill="url(#colorRevenueGrowth)"
/> />

View File

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

View File

@@ -208,6 +208,7 @@ export default function RevenueChart({ timeRange, hideNumbers = false }: Revenue
fillOpacity={1} fillOpacity={1}
fill="url(#colorRevenue)" fill="url(#colorRevenue)"
strokeWidth={2} strokeWidth={2}
activeDot={{ r: 4, strokeWidth: 0 }}
/> />
</AreaChart> </AreaChart>
</ResponsiveContainer> </ResponsiveContainer>