diff --git a/.changelog/unreleased/9997-referral-chart-hourly-labels.md b/.changelog/unreleased/9997-referral-chart-hourly-labels.md
new file mode 100644
index 00000000..e6dd9bb1
--- /dev/null
+++ b/.changelog/unreleased/9997-referral-chart-hourly-labels.md
@@ -0,0 +1,8 @@
+---
+type: fixed
+area: referrals
+pr: 9997
+breaking: false
+---
+
+Referral charts now display distinct hourly labels for the 24-hour view without duplicate chart keys or repeated dates.
diff --git a/apps/web/src/features/referrals/components/shared/stat-chart-card.test.tsx b/apps/web/src/features/referrals/components/shared/stat-chart-card.test.tsx
index 4fb7d007..339d5432 100644
--- a/apps/web/src/features/referrals/components/shared/stat-chart-card.test.tsx
+++ b/apps/web/src/features/referrals/components/shared/stat-chart-card.test.tsx
@@ -1,9 +1,39 @@
-import { afterEach, describe, expect, it } from "vitest"
+import { afterEach, describe, expect, it, vi } from "vitest"
import { cleanup, render, screen } from "@testing-library/react"
import { StatChartCard } from "./stat-chart-card"
describe("StatChartCard", () => {
- afterEach(cleanup)
+ afterEach(() => {
+ cleanup()
+ vi.useRealTimers()
+ })
+
+ it.each(["24h", "7d", "30d", "90d", "total"] as const)(
+ "renders five distinct labels for the %s period",
+ (period) => {
+ vi.useFakeTimers()
+ vi.setSystemTime(new Date("2026-10-15T12:30:00"))
+
+ const { container } = render(
+ ,
+ )
+ const labels = Array.from(container.querySelectorAll('text[y="100"]')).map(
+ (element) => element.textContent,
+ )
+
+ expect(labels).toHaveLength(5)
+ expect(new Set(labels).size).toBe(labels.length)
+ if (period === "24h") {
+ expect(labels).toEqual(
+ expect.arrayContaining([expect.stringMatching(/^\d{2}:\d{2}$/)]),
+ )
+ expect(labels.every((label) => /^\d{2}:\d{2}$/.test(label))).toBe(true)
+ } else {
+ expect(labels.every((label) => /^\d{2}\/\d{2}$/.test(label))).toBe(true)
+ }
+ },
+ )
+
it("renders the title", () => {
render(
,
diff --git a/apps/web/src/features/referrals/components/shared/stat-chart-card.tsx b/apps/web/src/features/referrals/components/shared/stat-chart-card.tsx
index 490f42b1..bfbb49e6 100644
--- a/apps/web/src/features/referrals/components/shared/stat-chart-card.tsx
+++ b/apps/web/src/features/referrals/components/shared/stat-chart-card.tsx
@@ -5,8 +5,11 @@ import { formatUsd } from "@/shared/lib/format"
function xAxisLabels(period: TimePeriod): Array {
const now = new Date()
+ const pad = (value: number) => String(value).padStart(2, "0")
const fmt = (d: Date) =>
- `${String(d.getMonth() + 1).padStart(2, "0")}/${String(d.getDate()).padStart(2, "0")}`
+ period === "24h"
+ ? `${pad(d.getHours())}:${pad(d.getMinutes())}`
+ : `${pad(d.getMonth() + 1)}/${pad(d.getDate())}`
const msAgo = (ms: number) => new Date(now.getTime() - ms)
const day = 86_400_000
@@ -18,7 +21,9 @@ function xAxisLabels(period: TimePeriod): Array {
total: 365 * day,
}
- const total = ranges[period]
+ // Exact 24-hour endpoints share a clock time. Exclude the first minute
+ // so five HH:mm labels are meaningful and distinct without adding a date.
+ const total = period === "24h" ? ranges[period] - 60_000 : ranges[period]
return Array.from({ length: 5 }, (_, i) => fmt(msAgo(total - (i / 4) * total)))
}
@@ -118,7 +123,7 @@ export function StatChartCard({ title, tooltip, value, period, accent = "blue" }
const x = 28 + (i / (labels.length - 1)) * 368
return (