feat: enhance Tauri charts with smooth curves and forecasting

- Implemented a smooth curve rendering for the TauriLineChart using a cubic Bezier path.
- Added a forecasting feature to project trends based on recent data points.
- Updated TauriDashboardView to pass the current month for accurate forecasting.
- Refactored TauriLineChart to handle actual and forecasted data points distinctly.
- Introduced gradient strokes and glow effects to align with ApexCharts styling.
- Enhanced user interaction with hover markers and tooltips for forecasted data.
- Added a new test suite for ClinicDetailPage to ensure proper rendering and functionality.
This commit is contained in:
hamed
2026-07-19 11:25:45 +03:30
parent 15366788d5
commit e7de7aa88b
6 changed files with 492 additions and 234 deletions
@@ -88,4 +88,80 @@ describe('TauriLineChart', () => {
expect(screen.queryByText(EMPTY)).not.toBeInTheDocument();
expect(container.querySelector('svg path')).toBeTruthy();
});
/** سبک «gradient line»: خط از گرادیان افقی رنگ می‌گیرد، نه یک رنگ ثابت. */
it('paints the line with the horizontal gradient and a glow', () => {
const { container } = render(<TauriLineChart data={real} />);
const line = container.querySelectorAll('svg path')[1] as SVGPathElement;
expect(line.getAttribute('stroke')).toBe('url(#tdIncomeStroke)');
expect(line.getAttribute('filter')).toBe('url(#tdIncomeGlow)');
expect(container.querySelector('#tdIncomeStroke')).toBeTruthy();
expect(container.querySelector('#tdIncomeGlow feDropShadow')).toBeTruthy();
});
/** نقطه و تولتیپ برای هر داده ساخته می‌شود و با هاور نمایان می‌شود. */
it('renders a hover marker and value tooltip per point', () => {
const { container } = render(<TauriLineChart data={real} />);
expect(container.querySelectorAll('.td-hit')).toHaveLength(real.length);
expect(container.querySelectorAll('.td-dot')).toHaveLength(real.length);
// مقدارها با ارقام فارسی در تولتیپ
expect(screen.getByText('۴۸')).toBeInTheDocument();
});
});
/**
* ماه‌های نیامده صفر برمی‌گردند؛ رسم‌کردنشان یک خط صاف تا انتهای سال می‌ساخت.
* از آخرین دادهٔ واقعی به بعد باید روند نقطه‌چین ادامه پیدا کند.
*/
describe('TauriLineChart — ادامهٔ پیش‌بینی', () => {
/** ۴ ماه واقعیِ صعودی + ۸ ماه نیامده (صفر) */
const year: ChartPoint[] = Array.from({ length: 12 }, (_, i) => ({
label: `ماه ${i + 1}`,
value: i < 4 ? (i + 1) * 10 : 0,
}));
it('بدون actualCount خط پیش‌بینی ندارد (رفتار قبلی حفظ می‌شود)', () => {
const { container } = render(<TauriLineChart data={year} />);
expect(container.querySelector('.td-forecast')).toBeNull();
expect(screen.queryByText('نقطه‌چین: پیش‌بینی')).not.toBeInTheDocument();
});
it('با actualCount ماه‌های باقی‌مانده را نقطه‌چین ادامه می‌دهد', () => {
const { container } = render(<TauriLineChart data={year} actualCount={4} />);
const forecast = container.querySelector('.td-forecast');
expect(forecast).toBeTruthy();
expect(forecast!.getAttribute('stroke')).toBe('var(--accent)');
expect(screen.getByText('نقطه‌چین: پیش‌بینی')).toBeInTheDocument();
});
it('روند صعودی را ادامه می‌دهد و صفرها را رسم نمی‌کند', () => {
render(<TauriLineChart data={year} actualCount={4} />);
// شیب ۱۰ در ماه، آخرین واقعی ۴۰ ⇒ ماه پنجم ۵۰
expect(screen.getByText('پیش‌بینی: ۵۰')).toBeInTheDocument();
// آخرین ماه: ۴۰ + ۸×۱۰ = ۱۲۰
expect(screen.getByText('پیش‌بینی: ۱۲۰')).toBeInTheDocument();
});
it('پیش‌بینی هرگز منفی نمی‌شود', () => {
const falling: ChartPoint[] = Array.from({ length: 12 }, (_, i) => ({
label: `ماه ${i + 1}`,
value: i < 3 ? 100 - i * 45 : 0,
}));
const { container } = render(<TauriLineChart data={falling} actualCount={3} />);
const tips = Array.from(container.querySelectorAll('.td-tip'))
.map((el) => el.textContent ?? '')
.filter((t) => t.startsWith('پیش‌بینی'));
expect(tips.length).toBeGreaterThan(0);
expect(tips.some((t) => t.includes('-') || t.includes(''))).toBe(false);
});
it('سال کامل (actualCount برابر طول داده) پیش‌بینی ندارد', () => {
const full: ChartPoint[] = Array.from({ length: 12 }, (_, i) => ({ label: `ماه ${i + 1}`, value: 10 + i }));
const { container } = render(<TauriLineChart data={full} actualCount={12} />);
expect(container.querySelector('.td-forecast')).toBeNull();
});
});