Improve chart visualization and UI layout #1
135
src/App.jsx
135
src/App.jsx
@ -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
|
// Process price history based on time range
|
||||||
const processPriceHistory = (priceHistory, timeRange) => {
|
const processPriceHistory = (priceHistory, timeRange) => {
|
||||||
if (!priceHistory || priceHistory.length === 0) return [];
|
if (!priceHistory || priceHistory.length === 0) return [];
|
||||||
@ -426,22 +459,12 @@ const ApartmentDashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
{priceDrops.length > 0 && (
|
||||||
{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">
|
||||||
<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" />
|
||||||
<Bell className="w-4 h-4" />
|
<span>{priceDrops.length} price drops</span>
|
||||||
<span>{priceDrops.length} price drops</span>
|
</div>
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-4 mt-4">
|
<div className="flex items-center space-x-4 mt-4">
|
||||||
@ -503,6 +526,39 @@ const ApartmentDashboard = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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 */}
|
{/* Charts */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||||
{/* Price History Chart */}
|
{/* Price History Chart */}
|
||||||
@ -515,7 +571,7 @@ const ApartmentDashboard = () => {
|
|||||||
dataKey="date"
|
dataKey="date"
|
||||||
tickFormatter={(date) => formatDate(date)}
|
tickFormatter={(date) => formatDate(date)}
|
||||||
/>
|
/>
|
||||||
<YAxis tickFormatter={(value) => formatPrice(value)} />
|
<YAxis tickFormatter={(value) => formatPrice(value)} domain={getAxisDomain(priceHistory.filter(d => d.avgPrice !== null), 'avgPrice')} />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
formatter={(value, name) => [formatPrice(value), 'Avg Price']}
|
formatter={(value, name) => [formatPrice(value), 'Avg Price']}
|
||||||
labelFormatter={(date) => formatDate(date)}
|
labelFormatter={(date) => formatDate(date)}
|
||||||
@ -535,7 +591,7 @@ const ApartmentDashboard = () => {
|
|||||||
dataKey="date"
|
dataKey="date"
|
||||||
tickFormatter={(date) => formatDate(date)}
|
tickFormatter={(date) => formatDate(date)}
|
||||||
/>
|
/>
|
||||||
<YAxis />
|
<YAxis domain={getAxisDomain(priceHistory, 'unitCount')} />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
formatter={(value, name) => [value, 'Available Units']}
|
formatter={(value, name) => [value, 'Available Units']}
|
||||||
labelFormatter={(date) => formatDate(date)}
|
labelFormatter={(date) => formatDate(date)}
|
||||||
@ -578,7 +634,7 @@ const ApartmentDashboard = () => {
|
|||||||
<BarChart data={planStats}>
|
<BarChart data={planStats}>
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
<XAxis dataKey="name" angle={-45} textAnchor="end" height={80} />
|
<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']} />
|
<Tooltip formatter={(value, name) => [formatPrice(value), 'Avg Price']} />
|
||||||
<Bar dataKey="avgPrice" fill="#8B0000" />
|
<Bar dataKey="avgPrice" fill="#8B0000" />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
@ -586,39 +642,6 @@ const ApartmentDashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
</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}>
|
<ComposedChart data={analyticsData.monthlyTrends}>
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
<XAxis dataKey="month" tickFormatter={(m) => m ? m.substring(2) : ''} />
|
<XAxis dataKey="month" tickFormatter={(m) => m ? m.substring(2) : ''} />
|
||||||
<YAxis yAxisId="left" tickFormatter={(v) => `$${(v/1000).toFixed(1)}k`} />
|
<YAxis yAxisId="left" tickFormatter={(v) => `$${(v/1000).toFixed(1)}k`} domain={getAxisDomain(analyticsData.monthlyTrends, 'avgPrice')} />
|
||||||
<YAxis yAxisId="right" orientation="right" />
|
<YAxis yAxisId="right" orientation="right" domain={getAxisDomain(analyticsData.monthlyTrends, 'unitCount')} />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
formatter={(value, name) => {
|
formatter={(value, name) => {
|
||||||
if (name === 'avgPrice') return [formatPrice(value), 'Avg Price'];
|
if (name === 'avgPrice') return [formatPrice(value), 'Avg Price'];
|
||||||
@ -821,7 +844,7 @@ const ApartmentDashboard = () => {
|
|||||||
}))}>
|
}))}>
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
<XAxis dataKey="monthName" />
|
<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']} />
|
<Tooltip formatter={(value) => [formatPrice(value), 'Avg Price']} />
|
||||||
<Bar dataKey="avgPrice" fill="#8B0000" />
|
<Bar dataKey="avgPrice" fill="#8B0000" />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
@ -839,7 +862,7 @@ const ApartmentDashboard = () => {
|
|||||||
<BarChart data={analyticsData.pricePerSqftByBed}>
|
<BarChart data={analyticsData.pricePerSqftByBed}>
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
<XAxis dataKey="bedrooms" tickFormatter={(v) => `${v} BR`} />
|
<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
|
<Tooltip
|
||||||
formatter={(value, name) => {
|
formatter={(value, name) => {
|
||||||
if (name === 'avgPricePerSqft') return [`$${value.toFixed(2)}/sqft`, 'Price/Sqft'];
|
if (name === 'avgPricePerSqft') return [`$${value.toFixed(2)}/sqft`, 'Price/Sqft'];
|
||||||
|
|||||||
Reference in New Issue
Block a user