SI formatting of large visitor numbers.

This commit is contained in:
2026-08-21 21:36:57 +00:00
parent b7d5b23ae6
commit 3be2d08ac9
4 changed files with 22 additions and 8 deletions
+5 -4
View File
@@ -10,6 +10,7 @@ import {
calcTotalViews,
copyIp,
copyList,
formatCount,
formatAbuseRows,
formatCrawlerRows,
formatVisitRows,
@@ -123,8 +124,8 @@ function countryName(code) {
<p v-else-if="!data" class="loading">loading</p>
<template v-else>
<section class="totals">
<div><strong>{{ visits.length }}</strong> visits</div>
<div><strong>{{ totalViews }}</strong> page views</div>
<div><strong :title="String(visits.length)">{{ formatCount(visits.length) }}</strong> visits</div>
<div><strong :title="String(totalViews)">{{ formatCount(totalViews) }}</strong> page views</div>
</section>
<VisitorCharts :data="data" :range="range" />
@@ -207,7 +208,7 @@ function countryName(code) {
<a v-for="(s, si) in c.pages" :key="si"
:href="s.path" :title="`${s.title}${s.count > 1 ? ` (${s.count} hits)` : ''}`"
@click="$emit('close')">
<small v-if="s.count > 1" class="muted">{{ s.count }}×</small>{{ s.slug }}
<small v-if="s.count > 1" class="muted">{{ formatCount(s.count) }}×</small>{{ s.slug }}
</a>
</td>
<td class="ip-ua-cell">
@@ -257,7 +258,7 @@ function countryName(code) {
@click="copyList(a.allUas, $event)">
<div v-for="(u, ui) in a.uas.slice(0, ABUSE_MAX_LINES)" :key="ui"
class="list-line">
<small v-if="u.count > 1" class="muted">{{ u.count }}×</small>{{ u.ua }}
<small v-if="u.count > 1" class="muted">{{ formatCount(u.count) }}×</small>{{ u.ua }}
</div>
<div v-if="a.uas.length > ABUSE_MAX_LINES" class="list-line">
<small class="muted">+{{ a.uas.length - ABUSE_MAX_LINES }} more</small>
+3 -2
View File
@@ -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))
<text :transform="`translate(${x.x}, ${x.y}) scale(${x.r - 4})`" class="tnodeslug" :style="{ '--node-r': x.r - 4 }">
<textPath href="#tnode-label-arc" startOffset="50%" text-anchor="middle" side="right">{{ x.label }}</textPath>
</text>
<text :x="x.x" :y="x.y + 4" class="tnodecount">{{ x.count }}</text>
<text :x="x.x" :y="x.y + 4" class="tnodecount">{{ formatCount(x.count) }}</text>
</a>
</g>
<g v-for="n in graph.nodes" :key="n.path">
@@ -150,7 +151,7 @@ onBeforeUnmount(() => cancelAnimationFrame(rafId))
<textPath href="#tnode-label-arc" startOffset="50%" text-anchor="middle" side="right">{{ n.label }}</textPath>
</text>
<text :x="n.x" :y="n.y + 4" class="tnodecount">
{{ n.readMin ? `${n.views}×${n.readMin}m` : n.views }}
{{ n.readMin ? `${formatCount(n.views)}×${n.readMin}m` : formatCount(n.views) }}
</text>
</a>
<template v-else>
+3 -2
View File
@@ -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)
}
+11
View File
@@ -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