diff --git a/frontend/src/AnalyticsView.vue b/frontend/src/AnalyticsView.vue
index fa8465e..302ff39 100644
--- a/frontend/src/AnalyticsView.vue
+++ b/frontend/src/AnalyticsView.vue
@@ -10,6 +10,7 @@ import {
calcTotalViews,
copyIp,
copyList,
+ formatCount,
formatAbuseRows,
formatCrawlerRows,
formatVisitRows,
@@ -123,8 +124,8 @@ function countryName(code) {
- {{ visits.length }} visits
- {{ totalViews }} page views
+ {{ formatCount(visits.length) }} visits
+ {{ formatCount(totalViews) }} page views
@@ -207,7 +208,7 @@ function countryName(code) {
- {{ s.count }}×{{ s.slug }}
+ {{ formatCount(s.count) }}×{{ s.slug }}
@@ -257,7 +258,7 @@ function countryName(code) {
@click="copyList(a.allUas, $event)">
- {{ u.count }}×{{ u.ua }}
+ {{ formatCount(u.count) }}×{{ u.ua }}
+{{ a.uas.length - ABUSE_MAX_LINES }} more
diff --git a/frontend/src/TransitionGraph.vue b/frontend/src/TransitionGraph.vue
index 2164d5b..43b01ef 100644
--- a/frontend/src/TransitionGraph.vue
+++ b/frontend/src/TransitionGraph.vue
@@ -8,6 +8,7 @@
*/
import { computed, onBeforeUnmount, shallowRef, watch } from 'vue'
import { rangeWindow, WEEK } from './analytics/time.js'
+import { formatCount } from './analytics/format.js'
import {
TNODE_R,
BEAD_R,
@@ -140,7 +141,7 @@ onBeforeUnmount(() => cancelAnimationFrame(rafId))
{{ x.label }}
- {{ x.count }}
+ {{ formatCount(x.count) }}
@@ -150,7 +151,7 @@ onBeforeUnmount(() => cancelAnimationFrame(rafId))
{{ n.label }}
- {{ n.readMin ? `${n.views}×${n.readMin}m` : n.views }}
+ {{ n.readMin ? `${formatCount(n.views)}×${n.readMin}m` : formatCount(n.views) }}
diff --git a/frontend/src/analytics/chart.js b/frontend/src/analytics/chart.js
index 461906e..469ba20 100644
--- a/frontend/src/analytics/chart.js
+++ b/frontend/src/analytics/chart.js
@@ -6,6 +6,7 @@
*/
import { DAY, HOUR, MIN5, WEEK, mondayUTC } from './time.js'
+import { formatCount } from './format.js'
export const CHART_W = 720
export const CHART_H = 180
@@ -412,7 +413,7 @@ export function fmtTick(t, span) {
return d.toLocaleDateString(undefined, { year: 'numeric', timeZone: 'UTC' })
}
-/** Y labels: integers when the step allows, one decimal for fractional steps. */
+/** Y labels use the same compact formatter as text labels. */
export function fmtY(v) {
- return Number.isInteger(v) ? String(v) : v.toFixed(1)
+ return formatCount(v)
}
diff --git a/frontend/src/analytics/format.js b/frontend/src/analytics/format.js
index 0dc823b..3741c54 100644
--- a/frontend/src/analytics/format.js
+++ b/frontend/src/analytics/format.js
@@ -200,6 +200,17 @@ export function formatWhenIso(ts) {
return `${new Date(ts).toISOString().split('.')[0]}Z`
}
+/**
+ * Compact visitor counts: plain below 1k, then 1.2k / 10k / 1.2M.
+ * Truncated, not rounded.
+ */
+export function formatCount(n) {
+ if (n < 1000) return String(n)
+ if (n < 10000) return `${Math.trunc(n / 1000)}.${Math.trunc((n % 1000) / 100)}k`
+ if (n < 1_000_000) return `${Math.trunc(n / 1000)}k`
+ return `${Math.trunc(n / 1_000_000)}.${Math.trunc((n % 1_000_000) / 100_000)}M`
+}
+
/**
* Format recent visits for display, newest first. Each step is a linked slug
* pointing to its article; external referers/origins are shown as their
|