From fc09646973b68d6646a3ca3b57a17faddfd50f34 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 6 Jul 2026 15:02:20 -0500 Subject: [PATCH] fix(data): refine model trend charts --- packages/stats/app/src/i18n.ts | 2 +- .../stats/app/src/routes/[lab]/[model].tsx | 154 +++++++++++++++--- packages/stats/app/src/routes/index.css | 54 +++++- 3 files changed, 179 insertions(+), 31 deletions(-) diff --git a/packages/stats/app/src/i18n.ts b/packages/stats/app/src/i18n.ts index 2edb164cb4..5f7597f122 100644 --- a/packages/stats/app/src/i18n.ts +++ b/packages/stats/app/src/i18n.ts @@ -214,7 +214,7 @@ const en = { "model.noUsageTitle": "No usage", "model.noUsageDescription": "No usage landed in the current window.", "model.dailyTokenChart": "Daily token usage chart", - "model.usersDescription": "Daily unique OpenCode Go users over the recent two-month window.", + "model.usersDescription": "Daily unique users.", "model.noUsersTitle": "No user data", "model.noUsersDescription": "No user-bearing rows landed in the current window.", "model.dailyUserChart": "Daily unique user chart", diff --git a/packages/stats/app/src/routes/[lab]/[model].tsx b/packages/stats/app/src/routes/[lab]/[model].tsx index f783a60cb7..e7f32681b7 100644 --- a/packages/stats/app/src/routes/[lab]/[model].tsx +++ b/packages/stats/app/src/routes/[lab]/[model].tsx @@ -122,6 +122,7 @@ export default function StatsModel() { { href: "#overview", label: i18n.t("nav.overview") }, { href: "#momentum", label: i18n.t("model.momentum") }, { href: "#usage", label: i18n.t("nav.usage") }, + { href: "#unique-users", label: i18n.t("model.uniqueUsers") }, { href: "#efficiency", label: i18n.t("nav.efficiency") }, { href: "#geo-breakdown", label: i18n.t("nav.geoBreakdown") }, { href: "#peers", label: i18n.t("nav.peers") }, @@ -185,6 +186,7 @@ export default function StatsModel() { + @@ -565,19 +567,82 @@ function MomentumMetric(props: { label: string; value: string; watermark?: strin function ModelUsageSection(props: { data: StatsModelData | null }) { const i18n = useI18n() + return ( + point.tokens} + formatValue={formatTokens} + valueUnit={i18n.t("lab.tokens")} + rowLabel={i18n.t("lab.dailyTokens")} + lineTone="muted" + /> + ) +} + +function ModelUniqueUsersSection(props: { data: StatsModelData | null }) { + const i18n = useI18n() + return ( + point.users} + formatValue={formatUsers} + valueUnit={i18n.t("format.users")} + rowLabel={i18n.t("model.uniqueUsers")} + lineTone="active" + activeLineBaseTone="muted" + highlightBars={false} + area + /> + ) +} + +function ModelTrendSection(props: { + data: StatsModelData | null + id: string + title: string + description: string + ariaLabel: string + emptyTitle: string + emptyDescription: string + value: (point: ModelUsagePoint) => number + formatValue: (value: number) => string + valueUnit: string + rowLabel: string + lineTone: "muted" | "active" + activeLineBaseTone?: "muted" | "active" + highlightBars?: boolean + area?: boolean +}) { const activeLineClipId = createUniqueId() const activeLineMaskId = createUniqueId() + const areaGradientId = createUniqueId() const [activeIndex, setActiveIndex] = createSignal() + const highlightBars = createMemo(() => props.highlightBars ?? true) + const lineScale = 326 / 450 + const areaBottom = 100 / lineScale const usage = createMemo(() => props.data?.usage ?? []) - const tokenMax = createMemo(() => Math.max(0, ...usage().map((item) => item.tokens)) || 1) + const valueMax = createMemo(() => Math.max(0, ...usage().map((item) => props.value(item))) || 1) const linePoints = createMemo(() => usage().map((point, index) => ({ point, x: modelUsagePointX(index, usage().length), - y: modelUsageLineY(point.tokens, tokenMax()), + y: modelUsageLineY(props.value(point), valueMax()), })), ) - const tokenLinePath = createMemo(() => modelUsageLinePath(linePoints())) + const linePath = createMemo(() => modelUsageLinePath(linePoints())) + const areaPath = createMemo(() => modelUsageAreaPath(linePoints(), areaBottom)) const activeLineBreak = createMemo(() => { const index = activeIndex() if (index === undefined || linePoints().length < 2) return undefined @@ -597,26 +662,24 @@ function ModelUsageSection(props: { data: StatsModelData | null }) { bounds, index, point, - tokenY: linePoints()[index]?.y ?? 100, - tooltipY: ((linePoints()[index]?.y ?? 100) * 370) / 450, + tooltipY: (linePoints()[index]?.y ?? 100) * lineScale, } }) const monthTicks = createMemo(() => modelUsageMonthTicks(usage(), props.data?.updatedAt ?? null)) return ( - - + + item.tokens > 0)} - fallback={ - - } + when={usage().some((item) => props.value(item) > 0)} + fallback={} > { if (event.pointerType === "touch") return @@ -624,9 +687,28 @@ function ModelUsageSection(props: { data: StatsModelData | null }) { }} > - + {(path) => ( <> + + {(area) => ( + + + + + + + + + + + )} + - }> + } + > {(lineBreak) => ( <> @@ -643,7 +728,12 @@ function ModelUsageSection(props: { data: StatsModelData | null }) { - + > )} @@ -673,11 +763,12 @@ function ModelUsageSection(props: { data: StatsModelData | null }) { {(point) => ( @@ -690,12 +781,14 @@ function ModelUsageSection(props: { data: StatsModelData | null }) { data-slot="model-trend-column" role="button" tabIndex={0} - aria-label={`${point.date} ${formatTokens(point.tokens)} ${i18n.t("lab.tokens")}`} + aria-label={`${point.date} ${props.formatValue(props.value(point))} ${props.valueUnit}`} data-active={activeIndex() === index() ? "true" : undefined} - data-muted={activeIndex() !== undefined && activeIndex() !== index() ? "true" : undefined} + data-muted={ + highlightBars() && activeIndex() !== undefined && activeIndex() !== index() ? "true" : undefined + } style={ { - "--model-trend-token-height": `${modelUsageStripHeight(point.tokens, tokenMax())}px`, + "--model-trend-token-height": modelUsageStripHeight(props.value(point), valueMax()), } as JSX.CSSProperties } onPointerDown={(event) => { @@ -716,7 +809,9 @@ function ModelUsageSection(props: { data: StatsModelData | null }) { setActiveIndex(index()) }} > - + + + )} @@ -730,21 +825,20 @@ function ModelUsageSection(props: { data: StatsModelData | null }) { { "--model-trend-tooltip-left": `${active.bounds.x}%`, "--model-trend-tooltip-right": `${active.bounds.x + active.bounds.width}%`, - "--model-trend-token-y": `${active.tokenY}`, "--model-trend-tooltip-y": `${active.tooltipY}`, } as JSX.CSSProperties } > {formatModelUsageTooltipDate(active.point.date, props.data?.updatedAt ?? null)} - {formatTokens(active.point.tokens)} {i18n.t("lab.tokens")} + {props.formatValue(props.value(active.point))} {props.valueUnit} - {i18n.t("lab.dailyTokens")} + {props.rowLabel} - {formatTokens(active.point.tokens)} + {props.formatValue(props.value(active.point))} )} @@ -1222,8 +1316,8 @@ function formatMomentumDateLabel(date: string) { } function modelUsageStripHeight(value: number, max: number) { - if (value <= 0 || max <= 0) return 0 - return Math.max(2, (value / max) * 40) + if (value <= 0 || max <= 0) return "0px" + return `max(2px, ${(value / max) * 100}%)` } function modelUsageLineY(value: number, max: number) { @@ -1272,6 +1366,14 @@ function modelUsageLinePath(points: ModelUsageLinePoint[]) { ) } +function modelUsageAreaPath(points: ModelUsageLinePoint[], bottom: number) { + const path = modelUsageLinePath(points) + if (!path || points.length === 0) return "" + const first = points[0] + const last = points[points.length - 1] + return `${path} L ${formatUsagePathNumber(last.x)} ${formatUsagePathNumber(bottom)} L ${formatUsagePathNumber(first.x)} ${formatUsagePathNumber(bottom)} Z` +} + function formatUsagePathNumber(value: number) { return Number(value.toFixed(3)) } diff --git a/packages/stats/app/src/routes/index.css b/packages/stats/app/src/routes/index.css index 9e139b31a3..8d9a50d7ae 100644 --- a/packages/stats/app/src/routes/index.css +++ b/packages/stats/app/src/routes/index.css @@ -4874,7 +4874,8 @@ [data-page="stats"] [data-component="model-usage-chart"][data-variant="model-trend"] { --model-trend-gap: 4px; --model-trend-column-gutter: calc(var(--model-trend-gap) / 2); - --model-trend-line-bottom: 80px; + --model-trend-bar-height: 76px; + --model-trend-line-bottom: 124px; --model-trend-bar: #dbdbdb; --model-trend-line: #808080; --model-trend-active: #3b5cf6; @@ -4921,6 +4922,21 @@ z-index: 4; } +[data-page="stats"] [data-slot="model-trend-area-layer"] { + position: absolute; + inset: 0; + z-index: 1; + display: block; + width: 100%; + height: 100%; + overflow: visible; + pointer-events: none; +} + +[data-page="stats"] [data-slot="model-trend-area-layer"] [data-slot="model-trend-area"] { + stroke: none; +} + [data-page="stats"] [data-slot="model-trend-line"] path { fill: none; stroke-linecap: round; @@ -4933,6 +4949,10 @@ stroke: var(--model-trend-line); } +[data-page="stats"] [data-slot="model-trend-line-base"][data-tone="active"] { + stroke: var(--model-trend-active); +} + [data-page="stats"] [data-slot="model-trend-line-active"] { stroke: var(--model-trend-active); } @@ -4949,6 +4969,10 @@ pointer-events: none; } +[data-page="stats"] [data-slot="model-trend-end-marker"][data-tone="active"] { + background: var(--model-trend-active); +} + [data-page="stats"] [data-slot="model-trend-bars"] { position: absolute; inset: 0; @@ -4986,12 +5010,20 @@ inset calc(var(--model-trend-column-gutter) * -1) 0 0 var(--stats-bg); } -[data-page="stats"] [data-slot="model-trend-token-bar"] { +[data-page="stats"] [data-slot="model-trend-token-band"] { position: absolute; right: var(--model-trend-column-gutter); bottom: 0; left: var(--model-trend-column-gutter); z-index: 2; + height: var(--model-trend-bar-height); +} + +[data-page="stats"] [data-slot="model-trend-token-bar"] { + position: absolute; + right: 0; + bottom: 0; + left: 0; height: var(--model-trend-token-height); background: var(--model-trend-bar); transition: @@ -4999,11 +5031,17 @@ opacity 120ms ease; } -[data-page="stats"] [data-slot="model-trend-column"][data-active="true"] [data-slot="model-trend-token-bar"] { +[data-page="stats"] + [data-component="model-usage-chart"][data-highlight-bars="true"] + [data-slot="model-trend-column"][data-active="true"] + [data-slot="model-trend-token-bar"] { background: var(--model-trend-active); } -[data-page="stats"] [data-slot="model-trend-column"][data-muted="true"] [data-slot="model-trend-token-bar"] { +[data-page="stats"] + [data-component="model-usage-chart"][data-highlight-bars="true"] + [data-slot="model-trend-column"][data-muted="true"] + [data-slot="model-trend-token-bar"] { opacity: 0.64; } @@ -5065,6 +5103,13 @@ background: var(--model-trend-active); } +[data-page="stats"] + [data-component="model-usage-chart"][data-variant="model-trend"] + [data-component="chart-tooltip"] + i[data-kind="users"] { + background: var(--model-trend-active); +} + [data-page="stats"] [data-component="model-peer-list"] { display: grid; gap: 8px; @@ -6169,6 +6214,7 @@ --model-usage-mobile-track-width: calc( var(--model-usage-count) * var(--model-usage-mobile-bar-width) + var(--model-usage-mobile-edge-space) ); + --model-trend-bar-height: 64px; --model-trend-line-bottom: 104px; grid-template-rows: minmax(0, 360px) 14px; gap: 24px;
- {i18n.t("lab.dailyTokens")} + {props.rowLabel} - {formatTokens(active.point.tokens)} + {props.formatValue(props.value(active.point))}