diff --git a/src/app/(home)/_components/dashboard/country-distribution.tsx b/src/app/(home)/_components/dashboard/country-distribution.tsx
index b90ceda..81e2827 100644
--- a/src/app/(home)/_components/dashboard/country-distribution.tsx
+++ b/src/app/(home)/_components/dashboard/country-distribution.tsx
@@ -1,5 +1,6 @@
"use client";
+import { formatShortNumber } from "@/lib/format-number";
import { cn } from "@/lib/utils";
import gsap from "gsap";
import { useLayoutEffect, useMemo, useRef } from "react";
@@ -155,7 +156,7 @@ export function CountryDistribution({ countries, isLoading }: Props) {
Total
- {isLoading ? "—" : totalShown}
+ {isLoading ? "—" : formatShortNumber(totalShown, { plus: true })}
diff --git a/src/app/(home)/_components/dashboard/notice-types.tsx b/src/app/(home)/_components/dashboard/notice-types.tsx
index 3b7ff2c..bcfc66b 100644
--- a/src/app/(home)/_components/dashboard/notice-types.tsx
+++ b/src/app/(home)/_components/dashboard/notice-types.tsx
@@ -1,5 +1,6 @@
"use client";
+import { formatShortNumber } from "@/lib/format-number";
import gsap from "gsap";
import { useLayoutEffect, useMemo, useRef } from "react";
import { TenderIcon } from "./icons";
@@ -94,7 +95,9 @@ export function NoticeTypeBreakdown({ noticeTypes, isLoading }: Props) {
{isLoading ? "Loading…" : labelize(n.type)}
- {isLoading ? "—" : `${n.count} · ${pct}%`}
+ {isLoading
+ ? "—"
+ : `${formatShortNumber(n.count, { plus: true })} · ${pct}%`}
diff --git a/src/lib/format-number.ts b/src/lib/format-number.ts
index a4267e5..e6a6c8e 100644
--- a/src/lib/format-number.ts
+++ b/src/lib/format-number.ts
@@ -1,10 +1,56 @@
-export function compactFormat(value: number) {
- const formatter = new Intl.NumberFormat("en", {
- notation: "compact",
- compactDisplay: "short",
- });
+export type FormatShortNumberOptions = {
+ /** Prefix positive values with "+" (e.g. +37k) */
+ plus?: boolean;
+ /** Fraction digits for K / M / B suffixes (default 1) */
+ maximumFractionDigits?: number;
+};
- return formatter.format(value);
+function trimTrailingZeros(value: string) {
+ return value.replace(/\.0+$/, "").replace(/(\.[0-9]*?)0+$/, "$1");
+}
+
+function formatScaled(value: number, maximumFractionDigits: number) {
+ if (Number.isInteger(value)) return String(value);
+ return trimTrailingZeros(value.toFixed(maximumFractionDigits));
+}
+
+/**
+ * Shortens large numbers for display: 37030 → "37k", 2500000 → "2.5M".
+ * Use `plus: true` for "+37k" style labels (e.g. chart totals).
+ */
+export function formatShortNumber(
+ value: number,
+ options?: FormatShortNumberOptions,
+): string {
+ const { plus = false, maximumFractionDigits = 1 } = options ?? {};
+
+ if (!Number.isFinite(value)) return "—";
+
+ const sign =
+ value < 0 ? "-" : plus && value > 0 ? "+" : "";
+ const abs = Math.abs(value);
+
+ let body: string;
+
+ if (abs < 1000) {
+ body =
+ abs % 1 === 0
+ ? String(Math.round(abs))
+ : trimTrailingZeros(abs.toFixed(maximumFractionDigits));
+ } else if (abs < 1_000_000) {
+ body = `${formatScaled(abs / 1000, maximumFractionDigits)}k`;
+ } else if (abs < 1_000_000_000) {
+ body = `${formatScaled(abs / 1_000_000, maximumFractionDigits)}M`;
+ } else {
+ body = `${formatScaled(abs / 1_000_000_000, maximumFractionDigits)}B`;
+ }
+
+ return `${sign}${body}`;
+}
+
+/** @deprecated Prefer `formatShortNumber` — kept for existing imports */
+export function compactFormat(value: number) {
+ return formatShortNumber(value);
}
export function standardFormat(value: number) {
@@ -12,4 +58,4 @@ export function standardFormat(value: number) {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
-}
\ No newline at end of file
+}