From 24969173bb37e8a9b67a7714938a293b2b623dd7 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Tue, 14 Jul 2026 14:08:17 +0330 Subject: [PATCH] feat: enhance Tauri charts with deduped gridline labels and add tests for bar and line charts --- .../components/dashboard/TauriCharts.test.tsx | 48 +++++++++++++++++++ .../components/dashboard/TauriCharts.tsx | 12 +++-- .../dashboard/TauriDashboardView.tsx | 11 +++-- .../components/dashboard/TauriStatCards.tsx | 2 +- 4 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 assets/admin/components/dashboard/TauriCharts.test.tsx diff --git a/assets/admin/components/dashboard/TauriCharts.test.tsx b/assets/admin/components/dashboard/TauriCharts.test.tsx new file mode 100644 index 00000000..18f5b4ba --- /dev/null +++ b/assets/admin/components/dashboard/TauriCharts.test.tsx @@ -0,0 +1,48 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { TauriBarChart, TauriLineChart, type ChartPoint } from './TauriCharts'; + +const zero: ChartPoint[] = Array.from({ length: 7 }, (_, i) => ({ label: `روز ${i}`, value: 0 })); +const real: ChartPoint[] = [ + { label: '۷ خرداد', value: 15 }, + { label: '۸ خرداد', value: 40 }, + { label: '۹ خرداد', value: 48 }, +]; + +const EMPTY = 'داده‌ای برای نمایش نیست'; + +describe('TauriBarChart', () => { + it('shows the empty state when every value is zero', () => { + render(); + expect(screen.getByText(EMPTY)).toBeInTheDocument(); + }); + + it('shows the empty state when there is no data', () => { + render(); + expect(screen.getByText(EMPTY)).toBeInTheDocument(); + }); + + it('renders the chart (x labels, no empty state) with real data', () => { + render(); + expect(screen.queryByText(EMPTY)).not.toBeInTheDocument(); + expect(screen.getByText('۸ خرداد')).toBeInTheDocument(); + }); +}); + +describe('TauriLineChart', () => { + it('shows the empty state when every value is zero', () => { + render(); + expect(screen.getByText(EMPTY)).toBeInTheDocument(); + }); + + it('shows the empty state with fewer than two points', () => { + render(); + expect(screen.getByText(EMPTY)).toBeInTheDocument(); + }); + + it('renders the chart with real data', () => { + const { container } = render(); + expect(screen.queryByText(EMPTY)).not.toBeInTheDocument(); + expect(container.querySelector('svg path')).toBeTruthy(); + }); +}); diff --git a/assets/admin/components/dashboard/TauriCharts.tsx b/assets/admin/components/dashboard/TauriCharts.tsx index 96ccfb06..9f36f252 100644 --- a/assets/admin/components/dashboard/TauriCharts.tsx +++ b/assets/admin/components/dashboard/TauriCharts.tsx @@ -13,7 +13,8 @@ export interface ChartPoint { value: number; } -/** ~5 rounded gridline ticks covering [0, max], top → bottom. */ +/** ~5 rounded gridline ticks covering [0, max], top → bottom. Deduped so tiny + * integer ranges (e.g. max=1) never render repeated labels. */ function niceTicks(max: number, count = 4): number[] { const safeMax = max > 0 ? max : 1; const rawStep = safeMax / count; @@ -21,10 +22,11 @@ function niceTicks(max: number, count = 4): number[] { const norm = rawStep / mag; const niceNorm = norm <= 1 ? 1 : norm <= 2 ? 2 : norm <= 5 ? 5 : 10; const step = niceNorm * mag; - const top = step * count; const ticks: number[] = []; for (let i = count; i >= 0; i--) ticks.push(Math.round(step * i)); - return ticks; // e.g. [400,300,200,100,0] + // collapse duplicate rounded labels (keeps gridlines proportional via ticks[0]) + const deduped = ticks.filter((t, i) => i === 0 || t !== ticks[i - 1]); + return deduped.length >= 2 ? deduped : [ticks[0], 0]; } const faNum = new Intl.NumberFormat('fa-IR'); @@ -85,7 +87,7 @@ function ChartFrame({ /** Bar chart — thin #5559CE columns (source: BarPlot, categoryGapRatio 0.7). */ export function TauriBarChart({ data }: { data: ChartPoint[] }) { - if (!data.length) { + if (!data.length || !data.some((d) => d.value > 0)) { return ; } const max = Math.max(...data.map((d) => d.value), 1); @@ -115,7 +117,7 @@ export function TauriBarChart({ data }: { data: ChartPoint[] }) { /** Line + area chart — #5559CE line over a #3A6FF8 gradient (source: LinePlot). */ export function TauriLineChart({ data }: { data: ChartPoint[] }) { - if (data.length < 2) { + if (data.length < 2 || !data.some((d) => d.value > 0)) { return ; } const values = data.map((d) => d.value); diff --git a/assets/admin/components/dashboard/TauriDashboardView.tsx b/assets/admin/components/dashboard/TauriDashboardView.tsx index 440d8a62..a360fada 100644 --- a/assets/admin/components/dashboard/TauriDashboardView.tsx +++ b/assets/admin/components/dashboard/TauriDashboardView.tsx @@ -40,7 +40,7 @@ function ChartTitle({ text }: { text: string }) { function ChartCard({ title, selectorOptions, children }: { title: string; selectorOptions: string[]; children: React.ReactNode }) { return ( -
+
@@ -75,12 +75,13 @@ export function TauriDashboardView({ formatRial, }: TauriDashboardViewProps) { return ( -
+
{/* Cards */} - {/* Charts */} -
+ {/* Charts — grid-2 (1fr 1fr, collapses to 1fr on narrow) for reliable + full-width equal columns, matching the rest of the admin dashboard */} +
@@ -90,7 +91,7 @@ export function TauriDashboardView({
{/* New appointments list */} -
+

لیست نوبت‌های جدید

+
    {cards.map((c) => ( ))}