From 78c8785e31d39efa1597b42653f23a51a4c17887 Mon Sep 17 00:00:00 2001 From: codejagaban Date: Sun, 27 Sep 2026 12:46:07 +0100 Subject: [PATCH 1/2] feat: Add analytics reporting windows --- .../admin/Dashboard/analyticsRange.ts | 147 ++++++++++++++++++ tests/unit/analytics-range.unit.spec.ts | 65 ++++++++ 2 files changed, 212 insertions(+) create mode 100644 src/components/admin/Dashboard/analyticsRange.ts create mode 100644 tests/unit/analytics-range.unit.spec.ts diff --git a/src/components/admin/Dashboard/analyticsRange.ts b/src/components/admin/Dashboard/analyticsRange.ts new file mode 100644 index 0000000..7c3c395 --- /dev/null +++ b/src/components/admin/Dashboard/analyticsRange.ts @@ -0,0 +1,147 @@ +import { + addCalendarDays, + dateTimeInZone, + type CalendarDate, + zonedMidnight, +} from '@/utilities/storeTime' + +export const ANALYTICS_RANGES = ['today', '7d', '30d', '90d', 'ytd', 'custom'] as const +export type AnalyticsRange = (typeof ANALYTICS_RANGES)[number] + +export const ANALYTICS_COMPARISONS = ['previous', 'year'] as const +export type AnalyticsComparison = (typeof ANALYTICS_COMPARISONS)[number] + +const MAX_CUSTOM_DAYS = 366 + +const dateKey = (date: CalendarDate) => + `${date.year}-${String(date.month).padStart(2, '0')}-${String(date.day).padStart(2, '0')}` + +const parseDateKey = (value: string | undefined): CalendarDate | undefined => { + const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value ?? '') + if (!match) return undefined + + const date = { year: Number(match[1]), month: Number(match[2]), day: Number(match[3]) } + const rendered = new Date(Date.UTC(date.year, date.month - 1, date.day, 12)) + if ( + rendered.getUTCFullYear() !== date.year || + rendered.getUTCMonth() + 1 !== date.month || + rendered.getUTCDate() !== date.day + ) { + return undefined + } + return date +} + +const dayNumber = (date: CalendarDate) => Date.UTC(date.year, date.month - 1, date.day) / 86_400_000 +const daysBetween = (start: CalendarDate, end: CalendarDate) => dayNumber(end) - dayNumber(start) + +const daysInMonth = (year: number, month: number) => new Date(Date.UTC(year, month, 0)).getUTCDate() + +const previousYear = (date: CalendarDate): CalendarDate => ({ + year: date.year - 1, + month: date.month, + day: Math.min(date.day, daysInMonth(date.year - 1, date.month)), +}) + +const isRange = (value: string | undefined): value is AnalyticsRange => + ANALYTICS_RANGES.includes(value as AnalyticsRange) + +const isComparison = (value: string | undefined): value is AnalyticsComparison => + ANALYTICS_COMPARISONS.includes(value as AnalyticsComparison) + +export type AnalyticsWindow = { + comparison: AnalyticsComparison + comparisonLabel: string + days: number + end: Date + endDate: string + priorEnd: Date + priorStart: Date + range: AnalyticsRange + rangeLabel: string + start: Date + startDate: string + today: Date + todayDate: string +} + +export const analyticsWindow = ({ + comparison: comparisonInput, + from, + now, + range: rangeInput, + timeZone, + to, +}: { + comparison?: string + from?: string + now: Date + range?: string + timeZone: string + to?: string +}): AnalyticsWindow => { + const localNow = dateTimeInZone(now, timeZone) + const today = { year: localNow.year, month: localNow.month, day: localNow.day } + const comparison = isComparison(comparisonInput) ? comparisonInput : 'previous' + let range: AnalyticsRange = isRange(rangeInput) ? rangeInput : '30d' + let startDate: CalendarDate + let endDate = today + + if (range === 'custom') { + const requestedStart = parseDateKey(from) + const requestedEnd = parseDateKey(to) + const requestedDays = + requestedStart && requestedEnd ? daysBetween(requestedStart, requestedEnd) + 1 : 0 + + if ( + !requestedStart || + !requestedEnd || + requestedDays < 1 || + requestedDays > MAX_CUSTOM_DAYS || + daysBetween(today, requestedEnd) > 0 + ) { + range = '30d' + startDate = addCalendarDays(today, -29) + } else { + startDate = requestedStart + endDate = requestedEnd + } + } else if (range === 'ytd') { + startDate = { year: today.year, month: 1, day: 1 } + } else { + const days = range === 'today' ? 1 : Number.parseInt(range, 10) + startDate = addCalendarDays(today, -(days - 1)) + } + + const days = daysBetween(startDate, endDate) + 1 + const start = zonedMidnight(startDate, timeZone) + const end = zonedMidnight(addCalendarDays(endDate, 1), timeZone) + + const priorStartDate = + comparison === 'year' ? previousYear(startDate) : addCalendarDays(startDate, -days) + const priorEndDate = comparison === 'year' ? addCalendarDays(previousYear(endDate), 1) : startDate + + const labels: Record, string> = { + today: 'Today', + '7d': 'Last 7 days', + '30d': 'Last 30 days', + '90d': 'Last 90 days', + ytd: 'Year to date', + } + + return { + comparison, + comparisonLabel: comparison === 'year' ? 'vs previous year' : 'vs previous period', + days, + end, + endDate: dateKey(endDate), + priorEnd: zonedMidnight(priorEndDate, timeZone), + priorStart: zonedMidnight(priorStartDate, timeZone), + range, + rangeLabel: range === 'custom' ? `${dateKey(startDate)} to ${dateKey(endDate)}` : labels[range], + start, + startDate: dateKey(startDate), + today: zonedMidnight(today, timeZone), + todayDate: dateKey(today), + } +} diff --git a/tests/unit/analytics-range.unit.spec.ts b/tests/unit/analytics-range.unit.spec.ts new file mode 100644 index 0000000..e639a7a --- /dev/null +++ b/tests/unit/analytics-range.unit.spec.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from 'vitest' + +import { analyticsWindow } from '@/components/admin/Dashboard/analyticsRange' + +describe('dashboard analytics ranges', () => { + const now = new Date('2026-09-27T12:00:00.000Z') + + it('builds a store-local rolling range and previous-period comparison', () => { + const window = analyticsWindow({ now, range: '7d', timeZone: 'Europe/London' }) + + expect(window).toMatchObject({ + comparison: 'previous', + days: 7, + endDate: '2026-09-27', + range: '7d', + startDate: '2026-09-21', + }) + expect(window.start.toISOString()).toBe('2026-09-20T23:00:00.000Z') + expect(window.priorStart.toISOString()).toBe('2026-09-13T23:00:00.000Z') + expect(window.priorEnd.toISOString()).toBe(window.start.toISOString()) + }) + + it('compares a custom range with the same dates in the previous year', () => { + const window = analyticsWindow({ + comparison: 'year', + from: '2026-03-01', + now, + range: 'custom', + timeZone: 'Europe/London', + to: '2026-03-31', + }) + + expect(window.days).toBe(31) + expect(window.priorStart.toISOString()).toBe('2025-03-01T00:00:00.000Z') + expect(window.priorEnd.toISOString()).toBe('2025-03-31T23:00:00.000Z') + }) + + it('uses January 1 for year-to-date in the store timezone', () => { + const window = analyticsWindow({ now, range: 'ytd', timeZone: 'Europe/London' }) + + expect(window.startDate).toBe('2026-01-01') + expect(window.endDate).toBe('2026-09-27') + expect(window.days).toBe(270) + }) + + it('falls back safely when a custom range is invalid or too large', () => { + const reversed = analyticsWindow({ + from: '2026-09-27', + now, + range: 'custom', + timeZone: 'Europe/London', + to: '2026-09-01', + }) + const oversized = analyticsWindow({ + from: '2025-01-01', + now, + range: 'custom', + timeZone: 'Europe/London', + to: '2026-09-01', + }) + + expect(reversed).toMatchObject({ days: 30, range: '30d', startDate: '2026-08-29' }) + expect(oversized).toMatchObject({ days: 30, range: '30d', startDate: '2026-08-29' }) + }) +}) From cc38c36d4e49cc50416a39798f578c4590d49861 Mon Sep 17 00:00:00 2001 From: codejagaban Date: Sun, 27 Sep 2026 12:46:08 +0100 Subject: [PATCH 2/2] feat: Add dashboard date controls --- .../admin/AnalyticsControls/index.scss | 98 ++++++++++++ .../admin/AnalyticsControls/index.tsx | 139 ++++++++++++++++++ src/components/admin/Dashboard/index.tsx | 88 ++++++++--- src/components/admin/Dashboard/queries.ts | 17 ++- src/components/admin/DashboardView/index.tsx | 15 +- src/components/admin/RevenueTrend/index.tsx | 16 +- 6 files changed, 339 insertions(+), 34 deletions(-) create mode 100644 src/components/admin/AnalyticsControls/index.scss create mode 100644 src/components/admin/AnalyticsControls/index.tsx diff --git a/src/components/admin/AnalyticsControls/index.scss b/src/components/admin/AnalyticsControls/index.scss new file mode 100644 index 0000000..f1af536 --- /dev/null +++ b/src/components/admin/AnalyticsControls/index.scss @@ -0,0 +1,98 @@ +.analytics-controls { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.85rem 1.25rem; + + &__ranges, + &__comparison { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.2rem; + } + + &__range, + &__comparison a { + padding: 0.48rem 0.68rem; + border-radius: var(--style-radius-s); + color: var(--theme-elevation-600); + text-decoration: none; + transition: + background-color 120ms ease, + color 120ms ease; + + &:hover { + color: var(--theme-elevation-900); + background: var(--theme-elevation-100); + } + + &[aria-current] { + color: var(--theme-elevation-950); + background: var(--theme-elevation-150); + font-weight: 600; + } + } + + &__comparison { + margin-left: auto; + + > span { + margin-right: 0.3rem; + color: var(--theme-elevation-450); + font-size: 0.82rem; + } + } + + &__custom { + flex: 1 0 100%; + display: flex; + flex-wrap: wrap; + align-items: end; + gap: 0.75rem; + padding-top: 0.75rem; + + label { + display: grid; + gap: 0.35rem; + color: var(--theme-elevation-550); + font-size: 0.78rem; + } + + input { + min-height: 2.4rem; + padding: 0.45rem 0.65rem; + border: 1px solid var(--theme-elevation-200); + border-radius: var(--style-radius-s); + color: var(--theme-elevation-900); + background: var(--theme-input-bg); + font: inherit; + } + + button { + min-height: 2.4rem; + padding: 0.45rem 0.9rem; + border: 0; + border-radius: var(--style-radius-s); + color: var(--theme-elevation-0); + background: var(--theme-elevation-900); + font: inherit; + font-weight: 600; + cursor: pointer; + } + + p { + flex: 1 0 100%; + margin: 0; + color: var(--theme-error-500); + font-size: 0.8rem; + } + } + + @media (max-width: 58rem) { + &__comparison { + flex-basis: 100%; + margin-left: 0; + } + } +} diff --git a/src/components/admin/AnalyticsControls/index.tsx b/src/components/admin/AnalyticsControls/index.tsx new file mode 100644 index 0000000..cb9b26f --- /dev/null +++ b/src/components/admin/AnalyticsControls/index.tsx @@ -0,0 +1,139 @@ +'use client' + +import Link from 'next/link' +import { useRouter } from 'next/navigation' +import React from 'react' +import { useForm } from 'react-hook-form' + +import type { AnalyticsComparison, AnalyticsRange } from '../Dashboard/analyticsRange' + +import './index.scss' + +type Props = { + adminPath: string + comparison: AnalyticsComparison + endDate: string + maxDate: string + range: AnalyticsRange + startDate: string +} + +type CustomRangeFields = { from: string; to: string } + +const calendarDays = (from: string, to: string) => + Math.floor((Date.parse(`${to}T12:00:00Z`) - Date.parse(`${from}T12:00:00Z`)) / 86_400_000) + 1 + +const options: Array<{ label: string; value: AnalyticsRange }> = [ + { label: 'Today', value: 'today' }, + { label: '7 days', value: '7d' }, + { label: '30 days', value: '30d' }, + { label: '90 days', value: '90d' }, + { label: 'Year to date', value: 'ytd' }, + { label: 'Custom', value: 'custom' }, +] + +const hrefFor = ( + adminPath: string, + range: AnalyticsRange, + comparison: AnalyticsComparison, + from?: string, + to?: string, +) => { + const query = new URLSearchParams({ range, compare: comparison }) + if (range === 'custom' && from && to) { + query.set('from', from) + query.set('to', to) + } + return `${adminPath}?${query.toString()}` +} + +export const AnalyticsControls: React.FC = ({ + adminPath, + comparison, + endDate, + maxDate, + range, + startDate, +}) => { + const router = useRouter() + const { + formState: { errors }, + handleSubmit, + register, + } = useForm({ defaultValues: { from: startDate, to: endDate } }) + + const applyCustom = handleSubmit(({ from, to }) => { + router.push(hrefFor(adminPath, 'custom', comparison, from, to)) + }) + + return ( +
+ + +
+ Compare + + Previous period + + + Previous year + +
+ + {range === 'custom' && ( +
+ + + + {(errors.from?.message || errors.to?.message) && ( +

{errors.from?.message || errors.to?.message}

+ )} +
+ )} +
+ ) +} diff --git a/src/components/admin/Dashboard/index.tsx b/src/components/admin/Dashboard/index.tsx index 71d5502..3987b43 100644 --- a/src/components/admin/Dashboard/index.tsx +++ b/src/components/admin/Dashboard/index.tsx @@ -2,6 +2,7 @@ import config from '@payload-config' import { getPayload } from 'payload' import React from 'react' +import { AnalyticsControls } from '../AnalyticsControls' import { IconAverage, IconOrders, IconProducts, IconRevenue, IconStock } from '../icons' import { KpiStrip, type Kpi } from '../KpiStrip' import { OrderHealth } from '../OrderHealth' @@ -12,15 +13,14 @@ import { addCalendarDays, dateTimeInZone, DEFAULT_STORE_TIME_ZONE, - reportingWindow, zonedMidnight, } from '@/utilities/storeTime' +import { analyticsWindow } from './analyticsRange' import { getDashboardOrderData } from './queries' import './index.scss' const baseClass = 'store-overview' -const WINDOW = 30 const LOW_STOCK_AT = 25 const money = (minor: number, currency: string) => @@ -32,7 +32,14 @@ const change = (now: number, before: number) => type StockRow = { title: string; qty: number } -async function getOverview() { +type DashboardFilters = { + comparison?: string + from?: string + range?: string + to?: string +} + +async function getOverview(filters: DashboardFilters) { const payload = await getPayload({ config }) const [users, productCount, settings] = await Promise.all([ @@ -44,18 +51,22 @@ async function getOverview() { const currency = settings.currency || DEFAULT_CURRENCY_CODE const timeZone = settings.timeZone || DEFAULT_STORE_TIME_ZONE - const { end, priorStart, start: windowStart, today: startOfToday } = reportingWindow({ - days: WINDOW, + const window = analyticsWindow({ + comparison: filters.comparison, + from: filters.from, now: new Date(), + range: filters.range, timeZone, + to: filters.to, }) const orderData = await getDashboardOrderData({ currency, - days: WINDOW, - end, + days: window.days, + end: window.end, payload, - priorStart, - start: windowStart, + priorEnd: window.priorEnd, + priorStart: window.priorStart, + start: window.start, timeZone, }) const { current, prior } = orderData @@ -101,7 +112,10 @@ async function getOverview() { }), ]) const lowStock: StockRow[] = [ - ...simple.docs.map((p) => ({ title: String(p.title ?? 'Untitled'), qty: Number(p.inventory ?? 0) })), + ...simple.docs.map((p) => ({ + title: String(p.title ?? 'Untitled'), + qty: Number(p.inventory ?? 0), + })), ...variants.docs.map((v) => ({ title: String((typeof v.product === 'object' && v.product?.title) || v.title || 'Variant'), qty: Number(v.inventory ?? 0), @@ -133,8 +147,7 @@ async function getOverview() { popular, lowStock, unfinishedPayments: orderData.unfinishedPayments, - windowStart, - endLabel: startOfToday, + window, } } @@ -155,8 +168,12 @@ const dayLabelsFrom = (start: Date, count: number, timeZone: string) => { ) } -export const Dashboard: React.FC<{ name?: string }> = async ({ name }) => { - const s = await getOverview() +export const Dashboard: React.FC<{ + adminPath: string + filters?: DashboardFilters + name?: string +}> = async ({ adminPath, filters = {}, name }) => { + const s = await getOverview(filters) const kpis: Kpi[] = [ { @@ -164,11 +181,28 @@ export const Dashboard: React.FC<{ name?: string }> = async ({ name }) => { label: 'Net sales', value: money(s.currentRevenue, s.currency), delta: s.revenueChange, - compare: `${money(s.grossSales, s.currency)} gross ยท ${money(s.refunds, s.currency)} refunded`, + compare: s.window.comparisonLabel, + }, + { + Icon: IconOrders, + label: 'Orders', + value: s.currentOrders.toLocaleString('en-US'), + delta: s.ordersChange, + compare: s.window.comparisonLabel, + }, + { + Icon: IconAverage, + label: 'Average order', + value: money(s.currentAov, s.currency), + delta: s.aovChange, + compare: s.window.comparisonLabel, + }, + { + Icon: IconProducts, + label: 'Products live', + value: String(s.products), + compare: `${s.customers} customers`, }, - { Icon: IconOrders, label: 'Orders', value: s.currentOrders.toLocaleString('en-US'), delta: s.ordersChange, compare: 'vs previous 30 days' }, - { Icon: IconAverage, label: 'Average order', value: money(s.currentAov, s.currency), delta: s.aovChange, compare: 'vs previous 30 days' }, - { Icon: IconProducts, label: 'Products live', value: String(s.products), compare: `${s.customers} customers` }, ] return ( @@ -186,6 +220,15 @@ export const Dashboard: React.FC<{ name?: string }> = async ({ name }) => {

+ +
@@ -201,12 +244,13 @@ export const Dashboard: React.FC<{ name?: string }> = async ({ name }) => { { label: 'Net tax', value: money(s.taxes - s.taxRefunds, s.currency) }, { label: 'Total sales', value: money(s.totalSales, s.currency) }, ]} - current={{ label: 'Last 30 days', points: s.currentDaily }} + comparisonLabel={s.window.comparisonLabel} + current={{ label: s.window.rangeLabel, points: s.currentDaily }} currency={s.currency} - dayLabels={dayLabelsFrom(s.windowStart, s.currentDaily.length, s.timeZone)} + dayLabels={dayLabelsFrom(s.window.start, s.currentDaily.length, s.timeZone)} delta={s.revenueChange} - endLabel={dayLabel(s.endLabel, s.timeZone)} - startLabel={dayLabel(s.windowStart, s.timeZone)} + endLabel={dayLabel(new Date(s.window.end.getTime() - 1), s.timeZone)} + startLabel={dayLabel(s.window.start, s.timeZone)} title="Net sales" total={money(s.currentRevenue, s.currency)} /> diff --git a/src/components/admin/Dashboard/queries.ts b/src/components/admin/Dashboard/queries.ts index 51b87f8..e2eee2d 100644 --- a/src/components/admin/Dashboard/queries.ts +++ b/src/components/admin/Dashboard/queries.ts @@ -39,7 +39,7 @@ type StatusRow = { count: string | number; status: keyof StatusCount } type ProductRow = { product_id: number; units: string | number } type TransactionRow = { count: string | number; status: string } -const rowsFrom = (result: unknown): T[] => { +const rowsFrom = (result: unknown): T[] => { if (typeof result !== 'object' || result === null || !('rows' in result)) return [] return Array.isArray(result.rows) ? (result.rows as T[]) : [] } @@ -74,6 +74,7 @@ export async function getDashboardOrderData({ days, end, payload, + priorEnd, priorStart, start, timeZone, @@ -82,6 +83,7 @@ export async function getDashboardOrderData({ days: number end: Date payload: Payload + priorEnd: Date priorStart: Date start: Date timeZone: string @@ -99,13 +101,18 @@ export async function getDashboardOrderData({ COALESCE("shipping_refund_total", 0) AS "shipping_refund_total", COALESCE("tax_refund_total", 0) AS "tax_refund_total" FROM "orders" - WHERE "created_at" >= ${priorStart} - AND "created_at" < ${end} + WHERE ( + ("created_at" >= ${start} AND "created_at" < ${end}) + OR ("created_at" >= ${priorStart} AND "created_at" < ${priorEnd}) + ) AND ("currency" = ${currency} OR "currency" IS NULL) AND "status" IS DISTINCT FROM 'cancelled' ) SELECT - CASE WHEN "created_at" >= ${start} THEN 'current' ELSE 'prior' END AS "period", + CASE + WHEN "created_at" >= ${start} AND "created_at" < ${end} THEN 'current' + ELSE 'prior' + END AS "period", TO_CHAR("created_at" AT TIME ZONE ${timeZone}, 'YYYY-MM-DD') AS "day", SUM("subtotal") AS "gross_sales", SUM("discount_total") AS "discounts", @@ -135,6 +142,8 @@ export async function getDashboardOrderData({ payload.db.drizzle.execute(sql` SELECT COALESCE("status"::text, 'processing') AS "status", COUNT(*) AS "count" FROM "orders" + WHERE "created_at" >= ${start} AND "created_at" < ${end} + AND ("currency" = ${currency} OR "currency" IS NULL) GROUP BY "status" `), payload.db.drizzle.execute(sql` diff --git a/src/components/admin/DashboardView/index.tsx b/src/components/admin/DashboardView/index.tsx index f9ac4ab..56354f5 100644 --- a/src/components/admin/DashboardView/index.tsx +++ b/src/components/admin/DashboardView/index.tsx @@ -14,7 +14,9 @@ import { Dashboard } from '../Dashboard' * on its own. The surrounding chrome comes from the admin layout, so the nav and * header are untouched. */ -export async function DashboardView({ initPageResult }: AdminViewServerProps) { +const queryString = (value: unknown) => (typeof value === 'string' ? value : undefined) + +export async function DashboardView({ clientConfig, initPageResult }: AdminViewServerProps) { const user = initPageResult.req.user const fullName = typeof user?.name === 'string' ? user.name : undefined const firstName = fullName ? fullName.split(' ')[0] : undefined @@ -24,7 +26,16 @@ export async function DashboardView({ initPageResult }: AdminViewServerProps) { - + ) diff --git a/src/components/admin/RevenueTrend/index.tsx b/src/components/admin/RevenueTrend/index.tsx index 2915f86..fee1dc7 100644 --- a/src/components/admin/RevenueTrend/index.tsx +++ b/src/components/admin/RevenueTrend/index.tsx @@ -18,6 +18,7 @@ type Props = { /** One label per point, so a hovered day can name itself. */ dayLabels?: string[] currency?: string + comparisonLabel?: string title?: string } @@ -100,6 +101,7 @@ export const RevenueTrend: React.FC = ({ endLabel, dayLabels, currency = 'USD', + comparisonLabel = 'vs previous period', title = 'Total revenue', }) => { const [mode, setMode] = React.useState('bars') @@ -133,14 +135,16 @@ export const RevenueTrend: React.FC = ({ const plotW = W - PAD.left - PAD.right const plotH = H - PAD.top - PAD.bottom - const n = Math.max(current.points.length, 2) + const pointCount = current.points.length + const n = Math.max(pointCount, 2) const peak = Math.max(1, ...current.points) const magnitude = Math.pow(10, Math.floor(Math.log10(peak))) const top = Math.ceil(peak / (magnitude / 2)) * (magnitude / 2) const ticks = [top, top / 2, 0] - const x = (i: number) => PAD.left + (i / (n - 1)) * plotW + const x = (i: number) => + pointCount === 1 ? PAD.left + plotW / 2 : PAD.left + (i / (n - 1)) * plotW const y = (v: number) => PAD.top + plotH - (v / top) * plotH const path = (pts: number[]) => smoothPath(pts.map((v, i) => ({ x: x(i), y: y(v) }))) @@ -200,7 +204,7 @@ export const RevenueTrend: React.FC = ({ {Math.abs(delta as number).toFixed(0)}% )} - vs previous 30 days + {comparisonLabel}
@@ -251,7 +255,7 @@ export const RevenueTrend: React.FC = ({ tabIndex={0} viewBox={`0 0 ${W} ${H}`} > - {`Revenue for each of the last ${n} days`} + {`${current.label} revenue by day`} {ticks.map((t) => ( @@ -272,13 +276,13 @@ export const RevenueTrend: React.FC = ({ ) : ( current.points.map((v, i) => { // Keep each day visually distinct instead of forming a dense block. - const w = Math.max(4, (plotW / n) * 0.66) + const w = Math.min(48, Math.max(0.8, (plotW / n) * 0.66)) return ( 0 ? 1.5 : 0, PAD.top + plotH - y(v))} key={i} - rx={2} + rx={Math.min(2, w / 2)} width={w} x={x(i) - w / 2} y={y(v)}