Last active
May 5, 2025 22:39
-
-
Save bigmabigma/c787ab7200162312963ba2fca76dfe88 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { PieChart, Pie, Cell, BarChart, Bar, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer, LineChart, Line, AreaChart, Area } from "recharts"; | |
import { MetricsRow } from "./MetricsRow"; | |
// Expenses data | |
const expensesData = [ | |
{ name: "Rental Cost", value: 24374, color: "#9b87f5" }, | |
{ name: "Wages", value: 19500, color: "#F97316" }, | |
{ name: "Medical Equipment", value: 12847, color: "#0EA5E9" }, | |
{ name: "Supplies", value: 16538, color: "#10b981" }, | |
{ name: "Other", value: 12589, color: "#D946EF" }, | |
]; | |
// Custom Multi-Section Gauge Chart Component | |
const GaugeChart = ({ value, total, data }) => { | |
// Calculate total percentage (0-100) | |
const percentage = Math.min(100, Math.max(0, (value / total) * 100)); | |
// For the gauge arc - we use a semi-circle (180 degrees) | |
const startAngle = 180; | |
const endAngle = 0; | |
// Calculate total sum for the data | |
const sum = data.reduce((acc, item) => acc + item.value, 0); | |
// Create gauge sections based on the data proportions | |
const sections = []; | |
let currentAngle = startAngle; | |
data.forEach(item => { | |
const sectionPercentage = (item.value / sum) * 100; | |
const sectionAngle = (sectionPercentage / 100) * (startAngle - endAngle); | |
const nextAngle = currentAngle - sectionAngle; | |
sections.push({ | |
startAngle: currentAngle, | |
endAngle: nextAngle, | |
value: sectionPercentage, | |
color: item.color, | |
name: item.name | |
}); | |
currentAngle = nextAngle; | |
}); | |
return ( | |
<div className="w-full h-full flex flex-col items-center justify-center"> | |
{/* Legend at the top */} | |
<div className="mb-2 text-center"> | |
<span className="text-green-500 text-sm font-medium flex items-center justify-center"> | |
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 10l7-7m0 0l7 7m-7-7v18" /> | |
</svg> | |
5 pts | |
</span> | |
<div className="text-5xl font-bold mt-1">{Math.floor(percentage)}</div> | |
<div className="text-lg font-semibold mt-1">Excellent</div> | |
<div className="text-xs text-gray-400 mt-1">PERFORMANCE</div> | |
</div> | |
<ResponsiveContainer width="100%" height="75%"> | |
<PieChart> | |
{/* Background arc - light gray */} | |
<Pie | |
data={[{ value: 100 }]} | |
cx="50%" | |
cy="60%" | |
startAngle={startAngle} | |
endAngle={endAngle} | |
innerRadius={80} | |
outerRadius={100} | |
fill="#e5e7eb" | |
stroke="none" | |
dataKey="value" | |
isAnimationActive={false} | |
/> | |
{/* Colored arc sections with gaps */} | |
{sections.map((section, index) => { | |
// Add gap by reducing the endAngle slightly | |
const gapSize = 3; // Angle in degrees for the gap | |
const adjustedEndAngle = section.endAngle + gapSize; | |
return ( | |
<Pie | |
key={`section-${index}`} | |
data={[{ value: section.value, name: section.name }]} | |
cx="50%" | |
cy="60%" | |
startAngle={section.startAngle} | |
endAngle={adjustedEndAngle} | |
innerRadius={80} | |
outerRadius={100} | |
cornerRadius={5} // Rounded edges | |
stroke="none" | |
fill={section.color} | |
dataKey="value" | |
/> | |
); | |
})} | |
{/* The dot at the end of the last section */} | |
<Pie | |
data={[{ value: 1 }]} | |
cx="50%" | |
cy="60%" | |
startAngle={endAngle + 3} // Small offset | |
endAngle={endAngle - 3} | |
innerRadius={95} | |
outerRadius={105} | |
fill="#fff" // White dot | |
stroke={sections[sections.length-1]?.color || "#10b981"} | |
strokeWidth={2} | |
dataKey="value" | |
/> | |
</PieChart> | |
</ResponsiveContainer> | |
</div> | |
); | |
}; | |
// Revenue data | |
const revenueData = [ | |
{ month: "Jan", value: 4000, color: "bg-purple-500" }, | |
{ month: "Feb", value: 3000, color: "bg-purple-500" }, | |
{ month: "Mar", value: 5000, color: "bg-purple-500" }, | |
{ month: "Apr", value: 7000, color: "bg-purple-500" }, | |
{ month: "May", value: 6000, color: "bg-purple-500" }, | |
{ month: "Jun", value: 8000, color: "bg-purple-500" }, | |
]; | |
// Patient data | |
const patientData = [ | |
{ month: "Jan", newPatients: 45, returningPatients: 120, color: "bg-blue-500" }, | |
{ month: "Feb", newPatients: 50, returningPatients: 130, color: "bg-blue-500" }, | |
{ month: "Mar", newPatients: 35, returningPatients: 125, color: "bg-blue-500" }, | |
{ month: "Apr", newPatients: 60, returningPatients: 140, color: "bg-blue-500" }, | |
{ month: "May", newPatients: 75, returningPatients: 150, color: "bg-blue-500" }, | |
{ month: "Jun", newPatients: 90, returningPatients: 160, color: "bg-blue-500" }, | |
]; | |
// Growth data | |
const growthData = [ | |
{ month: "Jan", value: 10, color: "bg-green-500" }, | |
{ month: "Feb", value: 15, color: "bg-green-500" }, | |
{ month: "Mar", value: 13, color: "bg-green-500" }, | |
{ month: "Apr", value: 20, color: "bg-green-500" }, | |
{ month: "May", value: 25, color: "bg-green-500" }, | |
{ month: "Jun", value: 30, color: "bg-green-500" }, | |
]; | |
export function Dashboard() { | |
// Calculate total expenses | |
const totalExpenses = expensesData.reduce((sum, item) => sum + item.value, 0); | |
// Calculate total revenue | |
const totalRevenue = revenueData.reduce((sum, item) => sum + item.value, 0); | |
// Calculate total patients | |
const totalNewPatients = patientData.reduce((sum, item) => sum + item.newPatients, 0); | |
const totalReturningPatients = patientData.reduce((sum, item) => sum + item.returningPatients, 0); | |
// Calculate average growth | |
const averageGrowth = growthData.reduce((sum, item) => sum + item.value, 0) / growthData.length; | |
return ( | |
<div className="container mx-auto py-8 px-4"> | |
<h1 className="text-3xl font-bold mb-6">Business Analytics Dashboard</h1> | |
{/* Expenses Section with Gauge Chart */} | |
<MetricsRow | |
title="Expense Breakdown" | |
description="This chart shows the distribution of expenses across different categories. Rental costs and wages make up the largest portions of our expenses." | |
totalValue={`$${totalExpenses.toLocaleString()}`} | |
totalLabel="Total Expenses" | |
chart={ | |
<GaugeChart | |
value={78} | |
total={100} | |
data={expensesData} | |
/> | |
} | |
metrics={expensesData.map(item => ({ | |
title: item.name, | |
value: `$${item.value.toLocaleString()}`, | |
color: item.color, | |
}))} | |
/> | |
{/* Revenue Section */} | |
<MetricsRow | |
title="Monthly Revenue" | |
description="Our revenue has shown a steady increase over the past six months, with a notable spike in April and June. This suggests our new services are gaining traction." | |
totalValue={`$${totalRevenue.toLocaleString()}`} | |
totalLabel="Total Revenue" | |
chart={ | |
<ResponsiveContainer width="100%" height="100%"> | |
<BarChart data={revenueData}> | |
<Legend layout="horizontal" verticalAlign="top" align="center" /> | |
<XAxis dataKey="month" /> | |
<YAxis /> | |
<Tooltip formatter={(value) => [`$${value.toLocaleString()}`, 'Revenue']} /> | |
<Bar dataKey="value" fill="#8b5cf6" /> | |
</BarChart> | |
</ResponsiveContainer> | |
} | |
metrics={[ | |
{ title: "Highest Month", value: `$${Math.max(...revenueData.map(d => d.value)).toLocaleString()}`, color: "bg-purple-500" }, | |
{ title: "Average Monthly", value: `$${(totalRevenue / revenueData.length).toLocaleString()}`, color: "bg-purple-400" }, | |
{ title: "Q2 Revenue", value: `$${revenueData.slice(3, 6).reduce((sum, item) => sum + item.value, 0).toLocaleString()}`, color: "bg-purple-600" }, | |
{ title: "Year Projection", value: `$${(totalRevenue * 2).toLocaleString()}`, color: "bg-purple-700" }, | |
]} | |
/> | |
{/* Patient Section */} | |
<MetricsRow | |
title="Patient Statistics" | |
description="Our patient base continues to grow with both new and returning patients increasing. The ratio of returning to new patients indicates strong patient loyalty and satisfaction." | |
totalValue={`${(totalNewPatients + totalReturningPatients).toLocaleString()}`} | |
totalLabel="Total Patients" | |
chart={ | |
<ResponsiveContainer width="100%" height="100%"> | |
<AreaChart data={patientData}> | |
<Legend layout="horizontal" verticalAlign="top" align="center" /> | |
<XAxis dataKey="month" /> | |
<YAxis /> | |
<Tooltip /> | |
<Area type="monotone" dataKey="newPatients" stackId="1" stroke="#3b82f6" fill="#93c5fd" /> | |
<Area type="monotone" dataKey="returningPatients" stackId="1" stroke="#1d4ed8" fill="#60a5fa" /> | |
</AreaChart> | |
</ResponsiveContainer> | |
} | |
metrics={[ | |
{ title: "New Patients", value: totalNewPatients.toString(), color: "bg-blue-400" }, | |
{ title: "Returning Patients", value: totalReturningPatients.toString(), color: "bg-blue-600" }, | |
{ title: "Patient Retention", value: "87%", color: "bg-blue-500" }, | |
{ title: "Avg. Visit Duration", value: "42 min", color: "bg-blue-300" }, | |
]} | |
/> | |
{/* Growth Section */} | |
<MetricsRow | |
title="Business Growth" | |
description="Our month-over-month growth continues to trend upward, showing the effectiveness of our expansion strategy and marketing efforts." | |
totalValue={`${averageGrowth.toFixed(1)}%`} | |
totalLabel="Average Growth" | |
chart={ | |
<ResponsiveContainer width="100%" height="100%"> | |
<LineChart data={growthData}> | |
<Legend layout="horizontal" verticalAlign="top" align="center" /> | |
<XAxis dataKey="month" /> | |
<YAxis /> | |
<Tooltip formatter={(value) => [`${value}%`, 'Growth']} /> | |
<Line type="monotone" dataKey="value" stroke="#22c55e" strokeWidth={2} /> | |
</LineChart> | |
</ResponsiveContainer> | |
} | |
metrics={[ | |
{ title: "MoM Growth", value: `${growthData[growthData.length - 1].value}%`, color: "bg-green-500" }, | |
{ title: "YoY Growth", value: "32%", color: "bg-green-600" }, | |
{ title: "New Service Growth", value: "45%", color: "bg-green-400" }, | |
{ title: "Market Share", value: "18%", color: "bg-green-700" }, | |
]} | |
/> | |
</div> | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { cn } from "@/lib/utils"; | |
interface MetricCardProps { | |
title: string; | |
value: string; | |
color: string; | |
} | |
export function MetricCard({ title, value, color }: MetricCardProps) { | |
// Determine if we're dealing with a Tailwind class or a hex color | |
const isTailwindClass = color.startsWith('bg-'); | |
return ( | |
<div className="bg-white p-4 rounded-lg shadow-sm hover:shadow-md transition-shadow"> | |
<div className="flex items-center gap-2 mb-1"> | |
{isTailwindClass ? ( | |
<div className={cn("w-3 h-3 rounded-full", color)} /> | |
) : ( | |
<div | |
className="w-3 h-3 rounded-full" | |
style={{ backgroundColor: color }} | |
/> | |
)} | |
<h3 className="text-gray-500 font-medium">{title}</h3> | |
</div> | |
<p className="text-2xl font-bold">{value}</p> | |
</div> | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from "react"; | |
import { Card } from "@/components/ui/card"; | |
import { MetricCard } from "./MetricCard"; | |
interface Metric { | |
title: string; | |
value: string; | |
color: string; | |
} | |
interface MetricsRowProps { | |
title: string; | |
description: string; | |
totalValue: string; | |
totalLabel: string; | |
chart: React.ReactNode; | |
metrics: Metric[]; | |
} | |
export function MetricsRow({ | |
title, | |
description, | |
totalValue, | |
totalLabel, | |
chart, | |
metrics, | |
}: MetricsRowProps) { | |
return ( | |
<Card className="p-6 mb-6"> | |
<h2 className="text-xl font-bold mb-4">{title}</h2> | |
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
{/* Left section - Chart */} | |
<div className="w-full h-64 md:h-80 relative flex flex-col"> | |
{chart} | |
</div> | |
{/* Right section - Content and Metrics */} | |
<div className="flex flex-col"> | |
{/* Description at the top */} | |
<div className="mb-4"> | |
<p className="text-gray-600">{description}</p> | |
</div> | |
{/* Metrics at the bottom */} | |
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mt-auto"> | |
{metrics.map((metric, index) => ( | |
<MetricCard | |
key={index} | |
title={metric.title} | |
value={metric.value} | |
color={metric.color} | |
/> | |
))} | |
</div> | |
</div> | |
</div> | |
</Card> | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{/* Expenses Section with Gauge Chart */} | |
<MetricsRow | |
title="Expense Breakdown" | |
description="This chart shows the distribution of expenses across different categories. Rental costs and wages make up the largest portions of our expenses." | |
totalValue={`$${totalExpenses.toLocaleString()}`} | |
totalLabel="Total Expenses" | |
chart={ | |
<GaugeChart | |
value={78} | |
total={100} | |
data={expensesData} | |
/> | |
} | |
metrics={expensesData.map(item => ({ | |
title: item.name, | |
value: `$${item.value.toLocaleString()}`, | |
color: item.color, | |
}))} | |
/> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment