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="95%" stopColor="#10b981" stopOpacity={0} />
|
||||
</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>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
|
||||
<XAxis
|
||||
@@ -255,14 +259,16 @@ export default function GrowthAnalyticsChart({
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
<Line
|
||||
<Area
|
||||
yAxisId="left"
|
||||
type="monotone"
|
||||
dataKey="orders"
|
||||
stroke="#3b82f6"
|
||||
strokeWidth={2}
|
||||
dot={{ fill: "#3b82f6", r: 3 }}
|
||||
dot={false}
|
||||
activeDot={{ r: 4, strokeWidth: 0 }}
|
||||
name="Orders"
|
||||
fill="url(#colorOrdersGrowth)"
|
||||
/>
|
||||
<Area
|
||||
yAxisId="right"
|
||||
@@ -270,7 +276,8 @@ export default function GrowthAnalyticsChart({
|
||||
dataKey="revenue"
|
||||
stroke="#10b981"
|
||||
strokeWidth={3}
|
||||
dot={{ fill: "#10b981", r: 4 }}
|
||||
dot={false}
|
||||
activeDot={{ r: 5, strokeWidth: 0 }}
|
||||
name="Revenue"
|
||||
fill="url(#colorRevenueGrowth)"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -208,6 +208,7 @@ export default function RevenueChart({ timeRange, hideNumbers = false }: Revenue
|
||||
fillOpacity={1}
|
||||
fill="url(#colorRevenue)"
|
||||
strokeWidth={2}
|
||||
activeDot={{ r: 4, strokeWidth: 0 }}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
Reference in New Issue
Block a user