feat(stats): add unique user charts

This commit is contained in:
Adam 2026-06-20 14:54:15 -05:00
commit 24c70ec974
No known key found for this signature in database
GPG key ID: 9CB48779AF150E75
13 changed files with 327 additions and 127 deletions

View file

@ -45,6 +45,7 @@ const statsUnfurlUrl = new URL(statsUnfurlPath, statsCanonicalBaseUrl).toString(
const modelHeaderLinks: readonly HeaderLink[] = [
{ href: "#overview", label: "Overview" },
{ href: "#usage", label: "Usage" },
{ href: "#users", label: "Users" },
{ href: "#efficiency", label: "Efficiency" },
{ href: "#geo-breakdown", label: "Geo Breakdown" },
{ href: "#peers", label: "Peers" },
@ -175,6 +176,7 @@ export default function StatsModel() {
<ModelHero data={stats() ?? null} catalog={catalogEntry() ?? null} labName={labName()} />
<ModelOverview data={stats() ?? null} />
<ModelUsageSection data={stats()?.usage ?? []} />
<ModelUsersSection data={stats()?.usage ?? []} />
<ModelEfficiencySection data={stats() ?? null} catalog={catalogEntry() ?? null} />
<ModelGeoBreakdownSection data={stats()?.country ?? emptyCountryRecord()} />
<ModelPeersSection data={stats() ?? null} />
@ -358,14 +360,6 @@ function ModelOverview(props: { data: StatsModelData | null }) {
}
function ModelUsageSection(props: { data: ModelUsagePoint[] }) {
const [activeIndex, setActiveIndex] = createSignal<number>()
const max = createMemo(() => Math.max(0, ...props.data.map((item) => item.tokens)) || 1)
const activePoint = createMemo(() => {
const index = activeIndex()
if (index === undefined) return undefined
return props.data[index]
})
return (
<section id="usage" data-section="model-panel">
<SectionTitle title="Usage" description="Daily OpenCode Go token volume over the recent two-month window." />
@ -373,92 +367,144 @@ function ModelUsageSection(props: { data: ModelUsagePoint[] }) {
when={props.data.some((item) => item.tokens > 0)}
fallback={<ModelEmptyState title="No usage" description="No usage landed in the current window." />}
>
<div
data-component="model-usage-chart"
data-dense-labels={isModelUsageDense(props.data.length) ? "true" : undefined}
role="img"
aria-label="Daily token usage chart"
style={{ "--model-usage-count": props.data.length } as JSX.CSSProperties}
onPointerLeave={(event) => {
if (event.pointerType === "touch") return
setActiveIndex(undefined)
}}
>
<div data-slot="model-usage-axis" aria-hidden="true">
<For each={props.data}>
{(point, index) => (
<div
data-active={activeIndex() === index() ? "true" : undefined}
data-label-hidden={isModelUsageLabelHidden(index(), props.data.length) ? "true" : undefined}
>
<span data-slot="model-usage-label">
<span data-slot="model-usage-total">{formatTokens(point.tokens)}</span>
<span data-slot="model-usage-date">{point.date}</span>
</span>
</div>
)}
</For>
</div>
<div data-slot="model-usage-bars">
<For each={props.data}>
{(point, index) => (
<div
data-slot="model-usage-column"
role="button"
tabIndex={0}
aria-label={`${point.date} ${formatTokens(point.tokens)} tokens`}
data-active={activeIndex() === index() ? "true" : undefined}
data-muted={activeIndex() !== undefined && activeIndex() !== index() ? "true" : undefined}
onPointerDown={(event) => {
if (event.pointerType !== "touch") return
setActiveIndex(index())
}}
onPointerEnter={() => setActiveIndex(index())}
onPointerMove={(event) => {
if (event.pointerType === "touch") return
setActiveIndex(index())
}}
onClick={() => setActiveIndex(index())}
onFocus={() => setActiveIndex(index())}
onBlur={() => setActiveIndex(undefined)}
onKeyDown={(event) => {
if (event.key !== "Enter" && event.key !== " ") return
event.preventDefault()
setActiveIndex(index())
}}
>
<div
data-slot="model-usage-bar"
style={{ "--model-usage-fill": `${modelUsageHeight(point.tokens, max())}%` } as JSX.CSSProperties}
/>
<Show when={activeIndex() === index() && activePoint()}>
{(active) => (
<div
data-component="chart-tooltip"
data-placement={index() > props.data.length * 0.62 ? "left" : "right"}
>
<strong>{active().date}</strong>
<span>{formatTokens(active().tokens)} tokens</span>
<div data-slot="tooltip-divider" />
<p>
<span data-slot="tooltip-label">
<i /> Daily tokens
</span>
<b>{formatTokens(active().tokens)}</b>
</p>
</div>
)}
</Show>
</div>
)}
</For>
</div>
</div>
<ModelColumnChart data={props.data} metric="tokens" ariaLabel="Daily token usage chart" />
</Show>
</section>
)
}
function ModelUsersSection(props: { data: ModelUsagePoint[] }) {
return (
<section id="users" data-section="model-panel">
<SectionTitle title="Unique Users" description="Daily unique OpenCode Go users over the recent two-month window." />
<Show
when={props.data.some((item) => item.users > 0)}
fallback={<ModelEmptyState title="No user data" description="No user-bearing rows landed in the current window." />}
>
<ModelColumnChart data={props.data} metric="users" ariaLabel="Daily unique user chart" />
</Show>
</section>
)
}
function ModelColumnChart(props: {
data: ModelUsagePoint[]
metric: "tokens" | "users"
ariaLabel: string
}) {
const [activeIndex, setActiveIndex] = createSignal<number>()
const max = createMemo(() => Math.max(0, ...props.data.map((item) => modelUsageMetricValue(item, props.metric))) || 1)
const activePoint = createMemo(() => {
const index = activeIndex()
if (index === undefined) return undefined
return props.data[index]
})
return (
<div
data-component="model-usage-chart"
data-metric={props.metric}
data-dense-labels={isModelUsageDense(props.data.length) ? "true" : undefined}
role="img"
aria-label={props.ariaLabel}
style={{ "--model-usage-count": props.data.length } as JSX.CSSProperties}
onPointerLeave={(event) => {
if (event.pointerType === "touch") return
setActiveIndex(undefined)
}}
>
<div data-slot="model-usage-axis" aria-hidden="true">
<For each={props.data}>
{(point, index) => (
<div
data-active={activeIndex() === index() ? "true" : undefined}
data-label-hidden={isModelUsageLabelHidden(index(), props.data.length) ? "true" : undefined}
>
<span data-slot="model-usage-label">
<span data-slot="model-usage-total">{formatModelUsageValue(point, props.metric)}</span>
<span data-slot="model-usage-date">{point.date}</span>
</span>
</div>
)}
</For>
</div>
<div data-slot="model-usage-bars">
<For each={props.data}>
{(point, index) => (
<div
data-slot="model-usage-column"
role="button"
tabIndex={0}
aria-label={`${point.date} ${formatModelUsageValue(point, props.metric)} ${modelUsageLabel(props.metric)}`}
data-active={activeIndex() === index() ? "true" : undefined}
data-muted={activeIndex() !== undefined && activeIndex() !== index() ? "true" : undefined}
onPointerDown={(event) => {
if (event.pointerType !== "touch") return
setActiveIndex(index())
}}
onPointerEnter={() => setActiveIndex(index())}
onPointerMove={(event) => {
if (event.pointerType === "touch") return
setActiveIndex(index())
}}
onClick={() => setActiveIndex(index())}
onFocus={() => setActiveIndex(index())}
onBlur={() => setActiveIndex(undefined)}
onKeyDown={(event) => {
if (event.key !== "Enter" && event.key !== " ") return
event.preventDefault()
setActiveIndex(index())
}}
>
<div
data-slot="model-usage-bar"
style={{
"--model-usage-fill": `${modelUsageHeight(modelUsageMetricValue(point, props.metric), max())}%`,
} as JSX.CSSProperties}
/>
<Show when={activeIndex() === index() && activePoint()}>
{(active) => (
<div
data-component="chart-tooltip"
data-placement={index() > props.data.length * 0.62 ? "left" : "right"}
>
<strong>{active().date}</strong>
<span>
{formatModelUsageValue(active(), props.metric)} {modelUsageLabel(props.metric)}
</span>
<div data-slot="tooltip-divider" />
<p>
<span data-slot="tooltip-label">
<i /> Daily {modelUsageLabel(props.metric)}
</span>
<b>{formatModelUsageValue(active(), props.metric)}</b>
</p>
</div>
)}
</Show>
</div>
)}
</For>
</div>
</div>
)
}
function modelUsageMetricValue(point: ModelUsagePoint, metric: "tokens" | "users") {
if (metric === "users") return point.users
return point.tokens
}
function formatModelUsageValue(point: ModelUsagePoint, metric: "tokens" | "users") {
if (metric === "users") return formatUsers(point.users)
return formatTokens(point.tokens)
}
function modelUsageLabel(metric: "tokens" | "users") {
if (metric === "users") return "users"
return "tokens"
}
function ModelEfficiencySection(props: { data: StatsModelData | null; catalog: ModelCatalogEntry | null }) {
return (
<section id="efficiency" data-section="model-panel">
@ -834,6 +880,12 @@ function formatInteger(value: number) {
return new Intl.NumberFormat("en").format(value)
}
function formatUsers(value: number) {
if (value >= 1_000_000) return `${trimNumber(value / 1_000_000, value >= 10_000_000 ? 0 : 1)}M`
if (value >= 1_000) return `${trimNumber(value / 1_000, value >= 10_000 ? 0 : 1)}K`
return formatInteger(Math.round(value))
}
function formatPercent(value: number) {
return `${value.toFixed(value > 0 && value < 10 ? 1 : 0)}%`
}

View file

@ -1546,7 +1546,7 @@
color: var(--stats-text);
}
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"] {
[data-page="stats"] :is([data-section="top-models"], [data-section="unique-users"]) [data-component="chart-tooltip"] {
top: 110px;
box-sizing: border-box;
display: flex;
@ -1564,40 +1564,53 @@
color: var(--stats-text);
}
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"][data-placement="right"] {
[data-page="stats"]
:is([data-section="top-models"], [data-section="unique-users"])
[data-component="chart-tooltip"][data-placement="right"] {
right: auto;
left: calc(100% + 8px);
}
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"][data-placement="left"] {
[data-page="stats"]
:is([data-section="top-models"], [data-section="unique-users"])
[data-component="chart-tooltip"][data-placement="left"] {
right: calc(100% + 8px);
left: auto;
}
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"] strong,
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"] > span {
[data-page="stats"] :is([data-section="top-models"], [data-section="unique-users"]) [data-component="chart-tooltip"] strong,
[data-page="stats"]
:is([data-section="top-models"], [data-section="unique-users"])
[data-component="chart-tooltip"]
> span {
display: block;
font-size: 11px;
line-height: 12px;
white-space: nowrap;
}
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"] strong {
[data-page="stats"] :is([data-section="top-models"], [data-section="unique-users"]) [data-component="chart-tooltip"] strong {
padding: 8px 8px 0;
font-weight: 500;
}
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"] > span {
[data-page="stats"]
:is([data-section="top-models"], [data-section="unique-users"])
[data-component="chart-tooltip"]
> span {
padding: 4px 8px 8px;
color: var(--stats-muted);
}
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"] [data-slot="tooltip-divider"] {
[data-page="stats"]
:is([data-section="top-models"], [data-section="unique-users"])
[data-component="chart-tooltip"]
[data-slot="tooltip-divider"] {
height: 0.5px;
margin: 0;
}
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"] p {
[data-page="stats"] :is([data-section="top-models"], [data-section="unique-users"]) [data-component="chart-tooltip"] p {
grid-template-columns: minmax(0, 1fr) auto;
gap: 4px;
height: 16px;
@ -1608,36 +1621,50 @@
line-height: 12px;
}
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"] p[data-muted="true"] {
[data-page="stats"]
:is([data-section="top-models"], [data-section="unique-users"])
[data-component="chart-tooltip"]
p[data-muted="true"] {
opacity: 0.46;
}
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"] [data-slot="tooltip-divider"] + p {
[data-page="stats"]
:is([data-section="top-models"], [data-section="unique-users"])
[data-component="chart-tooltip"]
[data-slot="tooltip-divider"]
+ p {
margin-top: 8px;
}
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"] p:last-child {
[data-page="stats"]
:is([data-section="top-models"], [data-section="unique-users"])
[data-component="chart-tooltip"]
p:last-child {
margin-bottom: 8px;
}
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"] [data-slot="tooltip-label"] {
[data-page="stats"]
:is([data-section="top-models"], [data-section="unique-users"])
[data-component="chart-tooltip"]
[data-slot="tooltip-label"] {
grid-template-columns: 16px minmax(0, 1fr);
gap: 4px;
}
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"] i {
[data-page="stats"] :is([data-section="top-models"], [data-section="unique-users"]) [data-component="chart-tooltip"] i {
width: 6px;
height: 6px;
justify-self: center;
}
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"] b {
[data-page="stats"] :is([data-section="top-models"], [data-section="unique-users"]) [data-component="chart-tooltip"] b {
font-weight: 500;
}
[data-page="stats"]
:is(
[data-section="leaderboard"],
[data-section="unique-users"],
[data-section="market-share"],
[data-section="geo-breakdown"],
[data-section="token-cost"],
@ -3264,10 +3291,12 @@
background: #242424f2;
}
[data-page="stats"][data-theme="dark"] [data-section="top-models"] [data-component="chart-tooltip"],
[data-page="stats"][data-theme="dark"]
:is([data-section="top-models"], [data-section="unique-users"])
[data-component="chart-tooltip"],
:root[data-stats-theme="dark"]
[data-page="stats"]:not([data-theme="light"])
[data-section="top-models"]
:is([data-section="top-models"], [data-section="unique-users"])
[data-component="chart-tooltip"] {
background: #242424f2;
box-shadow:
@ -3276,10 +3305,13 @@
0 4px 8px #00000052;
}
[data-page="stats"][data-theme="dark"] [data-section="top-models"] [data-component="chart-tooltip"] > span,
[data-page="stats"][data-theme="dark"]
:is([data-section="top-models"], [data-section="unique-users"])
[data-component="chart-tooltip"]
> span,
:root[data-stats-theme="dark"]
[data-page="stats"]:not([data-theme="light"])
[data-section="top-models"]
:is([data-section="top-models"], [data-section="unique-users"])
[data-component="chart-tooltip"]
> span {
color: var(--stats-faint);
@ -3544,6 +3576,7 @@
@media (max-width: 74rem) {
[data-page="stats"] [data-section="top-models"],
[data-page="stats"] [data-section="leaderboard"],
[data-page="stats"] [data-section="unique-users"],
[data-page="stats"] [data-section="market-share"],
[data-page="stats"] [data-section="geo-breakdown"],
[data-page="stats"] [data-section="token-cost"],
@ -3703,6 +3736,7 @@
@media (max-width: 47.999rem) {
[data-page="stats"] [data-section="top-models"],
[data-page="stats"] [data-section="leaderboard"],
[data-page="stats"] [data-section="unique-users"],
[data-page="stats"] [data-section="market-share"],
[data-page="stats"] [data-section="geo-breakdown"],
[data-page="stats"] [data-section="token-cost"],
@ -4011,7 +4045,7 @@
display: block;
}
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"] {
[data-page="stats"] :is([data-section="top-models"], [data-section="unique-users"]) [data-component="chart-tooltip"] {
position: fixed;
top: auto;
right: 12px;
@ -4025,7 +4059,9 @@
transform: none;
}
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"][data-placement] {
[data-page="stats"]
:is([data-section="top-models"], [data-section="unique-users"])
[data-component="chart-tooltip"][data-placement] {
right: 12px;
left: 12px;
}

View file

@ -171,6 +171,7 @@ export default function StatsHome() {
<>
<Hero updatedAt={stats().updatedAt} />
<TopModelsSection data={stats().usage} leaderboard={stats().leaderboard} />
<UniqueUsersSection data={stats().users} />
<SessionCostSection data={stats().sessionCost} />
<TokenCostSection data={stats().tokenCost} catalog={catalog() ?? null} />
<CacheRatioSection data={stats().cacheRatio} />
@ -598,6 +599,8 @@ function FilterPills<T extends string>(props: {
function TopModelsChart(props: {
data: UsagePoint[]
range: UsageRange
metric?: "tokens" | "users"
ariaLabel?: string
activeModel: string | undefined
onActiveModelChange: (model: string | undefined) => void
}) {
@ -606,6 +609,7 @@ function TopModelsChart(props: {
const maxTotal = createMemo(() => getTopModelsMaxTotal(props.data))
const segmentOrder = createMemo(() => getTopModelsSegmentOrder(props.data))
const activePoint = createMemo(() => props.data[activeIndex() ?? -1])
const metric = createMemo(() => props.metric ?? "tokens")
createEffect(() => scrollDenseChartToEnd(chartRef, props.range, props.data.length))
@ -614,9 +618,10 @@ function TopModelsChart(props: {
ref={chartRef}
data-component="top-models-chart"
data-range={props.range}
data-metric={metric()}
data-dense-labels={isDenseColumnRange(props.range) ? "true" : undefined}
role="img"
aria-label="Stacked top model usage chart"
aria-label={props.ariaLabel ?? "Stacked top model usage chart"}
style={{ "--top-models-count": props.data.length } as JSX.CSSProperties}
onPointerLeave={(event) => {
if (event.pointerType === "touch") return
@ -633,7 +638,7 @@ function TopModelsChart(props: {
data-mobile-hidden={isTopModelsMobileAxisHidden(index(), props.data.length) ? "true" : undefined}
>
<span data-slot="axis-label">
<span data-slot="axis-total">{formatTokens(usageTotal(day))}</span>
<span data-slot="axis-total">{formatUsageChartValue(usageTotal(day), metric())}</span>
<span data-slot="axis-date">
<span data-slot="axis-date-full">{day.date}</span>
<span data-slot="axis-date-mobile">{formatTopModelsMobileDate(day.date, props.range)}</span>
@ -657,7 +662,7 @@ function TopModelsChart(props: {
data-slot="top-models-bar"
role="button"
tabIndex={0}
aria-label={`${day.date} ${formatTokens(usageTotal(day))}`}
aria-label={`${day.date} ${formatUsageChartValue(usageTotal(day), metric())} ${usageChartTotalLabel(metric())}`}
data-active={activeIndex() === dayIndex() ? "true" : undefined}
data-muted={activeIndex() !== undefined && activeIndex() !== dayIndex() ? "true" : undefined}
style={{ "--top-models-bar-height": `${getTopModelsBarHeight(usageTotal(day), maxTotal())}%` }}
@ -739,7 +744,9 @@ function TopModelsChart(props: {
data-placement={dayIndex() > props.data.length * 0.62 ? "left" : "right"}
>
<strong>{point().date}</strong>
<span>{formatTokens(usageTotal(point()))} total</span>
<span>
{formatUsageChartValue(usageTotal(point()), metric())} {usageChartTotalLabel(metric())}
</span>
<div data-slot="tooltip-divider" />
<For each={visibleTopModelsSegments(point())}>
{(item) => (
@ -759,7 +766,7 @@ function TopModelsChart(props: {
/>{" "}
{item.segment.model}
</span>
<b>{formatTokens(item.segment.value)}</b>
<b>{formatUsageChartValue(item.segment.value, metric())}</b>
</p>
)}
</For>
@ -774,6 +781,31 @@ function TopModelsChart(props: {
)
}
function UniqueUsersSection(props: { data: StatsHomeData["users"] }) {
const [activeModel, setActiveModel] = createSignal<string>()
const data = createMemo(() => props.data.Go["2M"])
return (
<section id="unique-users" data-section="unique-users">
<SectionBridge label="TOP MODELS" href="#top-models" />
<SectionTitle title="Unique Users" description="Daily unique OpenCode Go users by model." />
<Show
when={data().some((item) => usageTotal(item) > 0)}
fallback={<EmptyState title="No user data" description="No user-bearing model_stat rows matched this window." />}
>
<TopModelsChart
data={data()}
range="2M"
metric="users"
ariaLabel="Stacked unique user chart by model"
activeModel={activeModel()}
onActiveModelChange={setActiveModel}
/>
</Show>
</section>
)
}
function isTopModelsBlankHover(bar: HTMLElement, clientY: number) {
const stack = bar.querySelector<HTMLElement>('[data-slot="top-models-stack"]')
if (!stack) return true
@ -864,6 +896,22 @@ function formatTokens(value: number) {
return `${Math.round(value * 1000)}B`
}
function formatUsageChartValue(value: number, metric: "tokens" | "users") {
if (metric === "users") return formatUsers(value)
return formatTokens(value)
}
function usageChartTotalLabel(metric: "tokens" | "users") {
if (metric === "users") return "model users"
return "total"
}
function formatUsers(value: number) {
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(value >= 10_000_000 ? 0 : 1)}M`
if (value >= 1_000) return `${(value / 1_000).toFixed(value >= 10_000 ? 0 : 1)}K`
return new Intl.NumberFormat("en").format(Math.round(value))
}
function Leaderboard(props: {
data: LeaderboardEntry[]
activeModel: string | undefined