Improve chart visualization and UI layout #1

Merged
stephen merged 3 commits from chart-axis-improvements into main 2026-01-21 11:03:13 -07:00

View File

@ -157,6 +157,39 @@ const ApartmentDashboard = () => {
}
};
// Calculate nice axis bounds that round to reasonable values
const getAxisDomain = (data, dataKey, padding = 0.1) => {
if (!data || data.length === 0) return [0, 100];
const values = data.map(d => d[dataKey]).filter(v => v != null && !isNaN(v));
if (values.length === 0) return [0, 100];
const min = Math.min(...values);
const max = Math.max(...values);
const range = max - min;
// Add padding to the range
const paddedMin = min - range * padding;
const paddedMax = max + range * padding;
// Determine a nice interval based on the range
const getNiceInterval = (range) => {
if (range <= 10) return 1;
if (range <= 50) return 5;
if (range <= 100) return 10;
if (range <= 500) return 50;
if (range <= 1000) return 100;
if (range <= 5000) return 500;
return 1000;
};
const interval = getNiceInterval(range);
const niceMin = Math.floor(paddedMin / interval) * interval;
const niceMax = Math.ceil(paddedMax / interval) * interval;
return [Math.max(0, niceMin), niceMax];
};
// Process price history based on time range
const processPriceHistory = (priceHistory, timeRange) => {
if (!priceHistory || priceHistory.length === 0) return [];
@ -426,22 +459,12 @@ const ApartmentDashboard = () => {
</div>
)}
</div>
<div className="flex items-center space-x-2">
{priceDrops.length > 0 && (
<div className="flex items-center space-x-2 bg-yellow-100 text-yellow-800 px-3 py-1 rounded-full text-sm">
<Bell className="w-4 h-4" />
<span>{priceDrops.length} price drops</span>
</div>
)}
<button
onClick={fetchData}
disabled={loading}
className="flex items-center space-x-2 bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50"
>
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
<span>Refresh</span>
</button>
</div>
{priceDrops.length > 0 && (
<div className="flex items-center space-x-2 bg-yellow-100 text-yellow-800 px-3 py-1 rounded-full text-sm">
<Bell className="w-4 h-4" />
<span>{priceDrops.length} price drops</span>
</div>
)}
</div>
<div className="flex items-center space-x-4 mt-4">
@ -503,6 +526,39 @@ const ApartmentDashboard = () => {
/>
</div>
{/* Recent Activity */}
{recentActivity.length > 0 && (
<div className="bg-white rounded-lg shadow-md p-6 mb-8">
<h3 className="text-lg font-semibold mb-4">Recent Activity</h3>
<div className="space-y-4">
{recentActivity.map((activity, index) => (
<div key={index} className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
<div className="flex items-center space-x-3">
{activity.type === 'new' && <div className="w-3 h-3 bg-green-500 rounded-full"></div>}
{activity.type === 'rented' && <div className="w-3 h-3 bg-red-500 rounded-full"></div>}
{activity.type === 'price_drop' && <div className="w-3 h-3 bg-orange-500 rounded-full"></div>}
<div>
<p className="font-medium">
{activity.type === 'new' && `New unit available: ${activity.unitCode}`}
{activity.type === 'rented' && `Unit rented: ${activity.unitCode}`}
{activity.type === 'price_drop' && `Price drop: ${activity.unitCode}`}
</p>
<p className="text-sm text-gray-500">
{activity.planName} • {activity.bedCount}BR/{activity.bathCount}BA
{activity.type === 'price_drop'
? ` • ${formatPrice(activity.oldPrice)} → ${formatPrice(activity.newPrice)}`
: activity.price ? ` • ${formatPrice(activity.price)}` : ''
}
</p>
</div>
</div>
<span className="text-sm text-gray-500">{formatDate(activity.date)}</span>
</div>
))}
</div>
</div>
)}
{/* Charts */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
{/* Price History Chart */}
@ -515,8 +571,8 @@ const ApartmentDashboard = () => {
dataKey="date"
tickFormatter={(date) => formatDate(date)}
/>
<YAxis tickFormatter={(value) => formatPrice(value)} />
<Tooltip
<YAxis tickFormatter={(value) => formatPrice(value)} domain={getAxisDomain(priceHistory.filter(d => d.avgPrice !== null), 'avgPrice')} />
<Tooltip
formatter={(value, name) => [formatPrice(value), 'Avg Price']}
labelFormatter={(date) => formatDate(date)}
/>
@ -531,11 +587,11 @@ const ApartmentDashboard = () => {
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={priceHistory}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="date"
<XAxis
dataKey="date"
tickFormatter={(date) => formatDate(date)}
/>
<YAxis />
<YAxis domain={getAxisDomain(priceHistory, 'unitCount')} />
<Tooltip
formatter={(value, name) => [value, 'Available Units']}
labelFormatter={(date) => formatDate(date)}
@ -578,7 +634,7 @@ const ApartmentDashboard = () => {
<BarChart data={planStats}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" angle={-45} textAnchor="end" height={80} />
<YAxis tickFormatter={(value) => formatPrice(value)} />
<YAxis tickFormatter={(value) => formatPrice(value)} domain={getAxisDomain(planStats, 'avgPrice')} />
<Tooltip formatter={(value, name) => [formatPrice(value), 'Avg Price']} />
<Bar dataKey="avgPrice" fill="#8B0000" />
</BarChart>
@ -586,39 +642,6 @@ const ApartmentDashboard = () => {
</div>
</div>
)}
{/* Recent Activity */}
{recentActivity.length > 0 && (
<div className="bg-white rounded-lg shadow-md p-6">
<h3 className="text-lg font-semibold mb-4">Recent Activity</h3>
<div className="space-y-4">
{recentActivity.map((activity, index) => (
<div key={index} className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
<div className="flex items-center space-x-3">
{activity.type === 'new' && <div className="w-3 h-3 bg-green-500 rounded-full"></div>}
{activity.type === 'rented' && <div className="w-3 h-3 bg-red-500 rounded-full"></div>}
{activity.type === 'price_drop' && <div className="w-3 h-3 bg-orange-500 rounded-full"></div>}
<div>
<p className="font-medium">
{activity.type === 'new' && `New unit available: ${activity.unitCode}`}
{activity.type === 'rented' && `Unit rented: ${activity.unitCode}`}
{activity.type === 'price_drop' && `Price drop: ${activity.unitCode}`}
</p>
<p className="text-sm text-gray-500">
{activity.planName} • {activity.bedCount}BR/{activity.bathCount}BA
{activity.type === 'price_drop'
? ` • ${formatPrice(activity.oldPrice)} → ${formatPrice(activity.newPrice)}`
: activity.price ? ` • ${formatPrice(activity.price)}` : ''
}
</p>
</div>
</div>
<span className="text-sm text-gray-500">{formatDate(activity.date)}</span>
</div>
))}
</div>
</div>
)}
</>
} />
@ -792,8 +815,8 @@ const ApartmentDashboard = () => {
<ComposedChart data={analyticsData.monthlyTrends}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" tickFormatter={(m) => m ? m.substring(2) : ''} />
<YAxis yAxisId="left" tickFormatter={(v) => `$${(v/1000).toFixed(1)}k`} />
<YAxis yAxisId="right" orientation="right" />
<YAxis yAxisId="left" tickFormatter={(v) => `$${(v/1000).toFixed(1)}k`} domain={getAxisDomain(analyticsData.monthlyTrends, 'avgPrice')} />
<YAxis yAxisId="right" orientation="right" domain={getAxisDomain(analyticsData.monthlyTrends, 'unitCount')} />
<Tooltip
formatter={(value, name) => {
if (name === 'avgPrice') return [formatPrice(value), 'Avg Price'];
@ -821,7 +844,7 @@ const ApartmentDashboard = () => {
}))}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="monthName" />
<YAxis tickFormatter={(v) => `$${(v/1000).toFixed(1)}k`} domain={['dataMin - 100', 'dataMax + 100']} />
<YAxis tickFormatter={(v) => `$${(v/1000).toFixed(1)}k`} domain={getAxisDomain(analyticsData.seasonality, 'avgPrice')} />
<Tooltip formatter={(value) => [formatPrice(value), 'Avg Price']} />
<Bar dataKey="avgPrice" fill="#8B0000" />
</BarChart>
@ -839,7 +862,7 @@ const ApartmentDashboard = () => {
<BarChart data={analyticsData.pricePerSqftByBed}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="bedrooms" tickFormatter={(v) => `${v} BR`} />
<YAxis tickFormatter={(v) => `$${v.toFixed(2)}`} />
<YAxis tickFormatter={(v) => `$${v.toFixed(2)}`} domain={getAxisDomain(analyticsData.pricePerSqftByBed, 'avgPricePerSqft')} />
<Tooltip
formatter={(value, name) => {
if (name === 'avgPricePerSqft') return [`$${value.toFixed(2)}/sqft`, 'Price/Sqft'];