Improve chart visuals and add null safety in analytics
All checks were successful
Build Frontend / build (push) Successful in 1m19s
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:
@@ -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)"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user