feat(stats): refine top models chart scaling
This commit is contained in:
@@ -17,13 +17,19 @@ import {
|
||||
} from "@opencode-ai/stats-core/domain/home"
|
||||
import { runtime } from "@opencode-ai/stats-core/runtime"
|
||||
import { createAsync, query } from "@solidjs/router"
|
||||
import { scaleBand, scaleLinear } from "d3-scale"
|
||||
import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show, type JSX } from "solid-js"
|
||||
import { getRequestEvent } from "solid-js/web"
|
||||
|
||||
const products = ["All Users", "Zen", "Go", "Enterprise"] as const
|
||||
const tokenProducts = ["Zen", "Go", "Enterprise"] as const
|
||||
const ranges = ["1D", "1W", "1M", "3M", "YTD", "ALL"] as const
|
||||
const products = ["All Users", "Zen", "Go"] as const
|
||||
const tokenProducts = ["Zen", "Go"] as const
|
||||
const ranges = ["1D", "1W", "2W", "1M", "2M"] as const
|
||||
const rangeLabels: Record<UsageRange, string> = {
|
||||
"1D": "1 Day",
|
||||
"1W": "1 Week",
|
||||
"2W": "2 Weeks",
|
||||
"1M": "1 Month",
|
||||
"2M": "2 Months",
|
||||
}
|
||||
const headerLinks = [
|
||||
{ href: "#top-models", label: "Top Models" },
|
||||
{ href: "#leaderboard", label: "Leaderboard" },
|
||||
@@ -31,7 +37,19 @@ const headerLinks = [
|
||||
{ href: "#token-cost", label: "Token Cost" },
|
||||
{ href: "#session-cost", label: "Session Cost" },
|
||||
] as const
|
||||
const usageColors = ["#ff5d64", "#ff8a00", "#8bef00", "#12c8b3", "#18c7dc", "#6c7dff", "#9d73f7"]
|
||||
const usageColors = [
|
||||
"#ed6aff",
|
||||
"#a684ff",
|
||||
"#7c86ff",
|
||||
"#51a2ff",
|
||||
"#00d3f2",
|
||||
"#00d5be",
|
||||
"#00bc7d",
|
||||
"#9ae600",
|
||||
"#ffb900",
|
||||
"#ff8904",
|
||||
"#ff6467",
|
||||
]
|
||||
const marketColors = ["#ed6aff", "#a684ff", "#7c86ff", "#51a2ff", "#00d3f2", "#00d5be", "#00bc7d", "#9ae600", "#ffb900"]
|
||||
const countryPositions = [
|
||||
{ x: 112, y: 96 },
|
||||
@@ -79,7 +97,7 @@ export default function StatsHome() {
|
||||
{(stats) => (
|
||||
<>
|
||||
<Hero updatedAt={stats().updatedAt} />
|
||||
<UsageSection data={stats().usage} />
|
||||
<TopModelsSection data={stats().usage} />
|
||||
<LeaderboardSection data={stats().leaderboard} />
|
||||
<MarketShareSection data={stats().market} />
|
||||
<TokenCostSection data={stats().tokenCost} />
|
||||
@@ -180,23 +198,154 @@ function formatUpdatedAt(value: string, timeZone: string) {
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
function UsageSection(props: { data: StatsHomeData["usage"] }) {
|
||||
function TopModelsSection(props: { data: StatsHomeData["usage"] }) {
|
||||
const [product, setProduct] = createSignal<UsageProduct>("All Users")
|
||||
const [range, setRange] = createSignal<UsageRange>("1W")
|
||||
const [sheet, setSheet] = createSignal<"product" | "range">()
|
||||
const data = createMemo(() => props.data[product()][range()])
|
||||
|
||||
createEffect(() => {
|
||||
if (!sheet()) return
|
||||
if (typeof document === "undefined") return
|
||||
const htmlOverflow = document.documentElement.style.overflow
|
||||
const bodyOverflow = document.body.style.overflow
|
||||
document.documentElement.style.overflow = "hidden"
|
||||
document.body.style.overflow = "hidden"
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") setSheet(undefined)
|
||||
}
|
||||
document.addEventListener("keydown", onKeyDown)
|
||||
onCleanup(() => {
|
||||
document.documentElement.style.overflow = htmlOverflow
|
||||
document.body.style.overflow = bodyOverflow
|
||||
document.removeEventListener("keydown", onKeyDown)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<ChartSection id="top-models" title="Usage">
|
||||
<section id="top-models" data-section="top-models">
|
||||
<h2 data-slot="top-models-title">
|
||||
<strong>Top models.</strong> <span>Usage of models across OpenCode.</span>
|
||||
</h2>
|
||||
<div data-slot="top-models-mobile-controls">
|
||||
<MobileFilterButton
|
||||
label="Product filter"
|
||||
value={product()}
|
||||
expanded={sheet() === "product"}
|
||||
onClick={() => setSheet(sheet() === "product" ? undefined : "product")}
|
||||
/>
|
||||
<MobileFilterButton
|
||||
label="Date range"
|
||||
value={range()}
|
||||
expanded={sheet() === "range"}
|
||||
onClick={() => setSheet(sheet() === "range" ? undefined : "range")}
|
||||
/>
|
||||
</div>
|
||||
<Show
|
||||
when={data().some((item) => usageTotal(item) > 0)}
|
||||
fallback={<EmptyState title="No usage data" description="No model_stat rows matched this product and range." />}
|
||||
>
|
||||
<UsageChart data={data()} />
|
||||
<TopModelsChart data={data()} range={range()} />
|
||||
</Show>
|
||||
<div data-slot="chart-footer">
|
||||
<StatsFilters product={product()} range={range()} onProductSelect={setProduct} onRangeSelect={setRange} />
|
||||
</div>
|
||||
</ChartSection>
|
||||
<Show when={sheet()}>
|
||||
{(kind) => (
|
||||
<MobileFilterSheet
|
||||
kind={kind()}
|
||||
product={product()}
|
||||
range={range()}
|
||||
onProductSelect={(value) => {
|
||||
setProduct(value)
|
||||
setSheet(undefined)
|
||||
}}
|
||||
onRangeSelect={(value) => {
|
||||
setRange(value)
|
||||
setSheet(undefined)
|
||||
}}
|
||||
onClose={() => setSheet(undefined)}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function MobileFilterButton(props: { label: string; value: string; expanded: boolean; onClick: () => void }) {
|
||||
return (
|
||||
<button
|
||||
data-slot="mobile-filter-button"
|
||||
type="button"
|
||||
aria-label={props.label}
|
||||
aria-expanded={props.expanded ? "true" : "false"}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<span>{props.value}</span>
|
||||
<ChevronDown />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function MobileFilterSheet(props: {
|
||||
kind: "product" | "range"
|
||||
product: UsageProduct
|
||||
range: UsageRange
|
||||
onProductSelect: (product: UsageProduct) => void
|
||||
onRangeSelect: (range: UsageRange) => void
|
||||
onClose: () => void
|
||||
}) {
|
||||
return (
|
||||
<div data-component="mobile-filter-sheet" role="presentation" onClick={props.onClose}>
|
||||
<div data-slot="filter-sheet-panel" role="radiogroup" aria-label={props.kind === "product" ? "Product filter" : "Date range"}>
|
||||
<Show
|
||||
when={props.kind === "product"}
|
||||
fallback={
|
||||
<For each={ranges}>
|
||||
{(item) => (
|
||||
<button
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={props.range === item}
|
||||
data-active={props.range === item ? "true" : undefined}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
props.onRangeSelect(item)
|
||||
}}
|
||||
>
|
||||
{rangeLabels[item]}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
}
|
||||
>
|
||||
<For each={products}>
|
||||
{(item) => (
|
||||
<button
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={props.product === item}
|
||||
data-active={props.product === item ? "true" : undefined}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
props.onProductSelect(item)
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ChevronDown() {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true" fill="none">
|
||||
<path d="M5 7L8 10L11 7" stroke="currentColor" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -252,158 +401,118 @@ function FilterPills<T extends string>(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function UsageChart(props: { data: UsagePoint[] }) {
|
||||
function TopModelsChart(props: { data: UsagePoint[]; range: UsageRange }) {
|
||||
const [activeIndex, setActiveIndex] = createSignal<number>()
|
||||
const [activeSegment, setActiveSegment] = createSignal<number>()
|
||||
const height = 434
|
||||
const width = 920
|
||||
const headerOffset = 46
|
||||
const segmentGap = 2
|
||||
const maxTotal = createMemo(() => Math.max(1, Math.max(...props.data.map((item) => usageTotal(item))) * 1.02))
|
||||
const maxTotal = createMemo(() => getTopModelsMaxTotal(props.data))
|
||||
const activePoint = createMemo(() => props.data[activeIndex() ?? -1])
|
||||
const y = createMemo(() => scaleLinear([0, maxTotal()], [height, 0]))
|
||||
const x = createMemo(() =>
|
||||
scaleBand(
|
||||
props.data.map((_, index) => String(index)),
|
||||
[0, width],
|
||||
).paddingInner(0.08),
|
||||
)
|
||||
const activeBar = createMemo(() => {
|
||||
const index = activeIndex()
|
||||
const point = activePoint()
|
||||
if (index === undefined) return
|
||||
if (!point) return
|
||||
return {
|
||||
point,
|
||||
x: x()(String(index)) ?? 0,
|
||||
width: x().bandwidth(),
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div data-component="usage-chart">
|
||||
<svg viewBox={`0 0 ${width} ${height + headerOffset}`} role="img" aria-label="Stacked usage chart">
|
||||
<defs>
|
||||
<pattern id="stats-usage-dot-grid" width="6" height="6" patternUnits="userSpaceOnUse">
|
||||
<rect x="1" y="1" width="2" height="2" fill="var(--stats-dot)" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<div data-component="top-models-chart" data-range={props.range} role="img" aria-label="Stacked top model usage chart">
|
||||
<div data-slot="top-models-axis" aria-hidden="true">
|
||||
<For each={props.data}>
|
||||
{(day, dayIndex) => {
|
||||
const barX = x()(String(dayIndex())) ?? 0
|
||||
const barWidth = x().bandwidth()
|
||||
const stackTop = y()(usageTotal(day))
|
||||
return (
|
||||
<g
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`${day.date} ${formatTokens(usageTotal(day))}`}
|
||||
data-active={activeIndex() === dayIndex() ? "true" : undefined}
|
||||
onPointerEnter={() => {
|
||||
setActiveIndex(dayIndex())
|
||||
setActiveSegment(undefined)
|
||||
}}
|
||||
onPointerLeave={(event) => {
|
||||
if (event.pointerType === "touch") return
|
||||
setActiveIndex(undefined)
|
||||
setActiveSegment(undefined)
|
||||
}}
|
||||
onClick={() => setActiveIndex(dayIndex())}
|
||||
onFocus={() => {
|
||||
setActiveIndex(dayIndex())
|
||||
setActiveSegment(undefined)
|
||||
}}
|
||||
onBlur={() => {
|
||||
setActiveIndex(undefined)
|
||||
setActiveSegment(undefined)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return
|
||||
event.preventDefault()
|
||||
setActiveIndex(dayIndex())
|
||||
}}
|
||||
>
|
||||
<rect
|
||||
x={barX}
|
||||
y="0"
|
||||
width={barWidth}
|
||||
height={height + headerOffset}
|
||||
fill="transparent"
|
||||
pointer-events="all"
|
||||
/>
|
||||
<text x={barX} y="17" class="chart-total">
|
||||
{formatTokens(usageTotal(day))}
|
||||
</text>
|
||||
<text x={barX} y="34" class="chart-date">
|
||||
{day.date}
|
||||
</text>
|
||||
<rect x={barX} y={headerOffset} width={barWidth} height={stackTop} fill="url(#stats-usage-dot-grid)" />
|
||||
<For each={day.segments}>
|
||||
{(segment, index) => {
|
||||
const previous = day.segments.slice(0, index()).reduce((sum, item) => sum + item.value, 0)
|
||||
const segmentHeight = y()(previous) - y()(previous + segment.value)
|
||||
const segmentInset = index() === day.segments.length - 1 ? 0 : segmentGap
|
||||
return (
|
||||
<rect
|
||||
x={barX}
|
||||
y={headerOffset + y()(previous + segment.value) + segmentInset}
|
||||
width={barWidth}
|
||||
height={Math.max(segmentHeight - segmentInset, 0)}
|
||||
data-segment-active={
|
||||
activeIndex() === dayIndex() && activeSegment() === index() ? "true" : undefined
|
||||
}
|
||||
opacity={getUsageSegmentOpacity(activeIndex() === dayIndex(), activeSegment(), index())}
|
||||
fill={activeIndex() === dayIndex() ? usageColors[index()] : "var(--stats-bar-idle)"}
|
||||
onPointerEnter={(event) => {
|
||||
event.stopPropagation()
|
||||
setActiveIndex(dayIndex())
|
||||
setActiveSegment(index())
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</g>
|
||||
)
|
||||
}}
|
||||
{(day, index) => (
|
||||
<div data-active={activeIndex() === index() ? "true" : undefined}>
|
||||
<span data-slot="axis-label">
|
||||
<span>{formatTokens(usageTotal(day))}</span>
|
||||
<span>{day.date}</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</svg>
|
||||
<Show when={activeBar()}>
|
||||
{(bar) => (
|
||||
<div
|
||||
data-component="chart-tooltip"
|
||||
data-placement={bar().x > width * 0.62 ? "left" : "right"}
|
||||
style={getUsageTooltipStyle(bar().x, bar().width, width)}
|
||||
>
|
||||
<strong>{bar().point.date}</strong>
|
||||
<span>{formatTokens(usageTotal(bar().point))} total</span>
|
||||
<div data-slot="tooltip-divider" />
|
||||
<For each={bar().point.segments}>
|
||||
{(segment, index) => (
|
||||
<p data-active={activeSegment() === index() ? "true" : undefined}>
|
||||
<span data-slot="tooltip-label">
|
||||
<i style={{ background: usageColors[index()] }} /> {segment.model}
|
||||
</span>
|
||||
<b>{formatTokens(segment.value)}</b>
|
||||
</p>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
<div data-slot="top-models-bars">
|
||||
<For each={props.data}>
|
||||
{(day, dayIndex) => (
|
||||
<div
|
||||
data-slot="top-models-bar"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`${day.date} ${formatTokens(usageTotal(day))}`}
|
||||
data-active={activeIndex() === dayIndex() ? "true" : undefined}
|
||||
data-muted={activeIndex() !== undefined && activeIndex() !== dayIndex() ? "true" : undefined}
|
||||
style={{ "--top-models-bar-height": `${getTopModelsBarHeight(usageTotal(day), maxTotal())}%` }}
|
||||
onPointerEnter={() => setActiveIndex(dayIndex())}
|
||||
onPointerLeave={(event) => {
|
||||
if (event.pointerType === "touch") return
|
||||
setActiveIndex(undefined)
|
||||
}}
|
||||
onClick={() => setActiveIndex(dayIndex())}
|
||||
onFocus={() => setActiveIndex(dayIndex())}
|
||||
onBlur={() => setActiveIndex(undefined)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter" && event.key !== " ") return
|
||||
event.preventDefault()
|
||||
setActiveIndex(dayIndex())
|
||||
}}
|
||||
>
|
||||
<div data-slot="top-models-stack" style={{ "grid-template-rows": getTopModelsSegmentRows(day) }}>
|
||||
<For each={visibleTopModelsSegments(day)}>
|
||||
{(item) => (
|
||||
<i
|
||||
style={{
|
||||
background: getTopModelsSegmentColor(
|
||||
item.index,
|
||||
activeIndex() !== undefined && activeIndex() !== dayIndex(),
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<Show when={activeIndex() === dayIndex() && activePoint()}>
|
||||
{(point) => (
|
||||
<div data-component="chart-tooltip" data-placement={dayIndex() > props.data.length * 0.62 ? "left" : "right"}>
|
||||
<strong>{point().date}</strong>
|
||||
<span>{formatTokens(usageTotal(point()))} total</span>
|
||||
<div data-slot="tooltip-divider" />
|
||||
<For each={visibleTopModelsSegments(point())}>
|
||||
{(item) => (
|
||||
<p>
|
||||
<span data-slot="tooltip-label">
|
||||
<i style={{ background: usageColors[item.index] }} /> {item.segment.model}
|
||||
</span>
|
||||
<b>{formatTokens(item.segment.value)}</b>
|
||||
</p>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function getUsageTooltipStyle(barX: number, barWidth: number, width: number) {
|
||||
if (barX > width * 0.62) return { left: "auto", right: `${((width - barX + 12) / width) * 100}%` }
|
||||
return { left: `${((barX + barWidth + 12) / width) * 100}%`, right: "auto" }
|
||||
function getTopModelsBarHeight(total: number, max: number) {
|
||||
if (total <= 0) return 0
|
||||
return Math.max(2, Math.min(100, (total / max) * 100))
|
||||
}
|
||||
|
||||
function getUsageSegmentOpacity(isActiveBar: boolean, activeSegment: number | undefined, index: number) {
|
||||
if (!isActiveBar) return 1
|
||||
if (activeSegment === undefined) return 1
|
||||
return activeSegment === index ? 1 : 0.38
|
||||
function getTopModelsMaxTotal(data: UsagePoint[]) {
|
||||
const max = Math.max(0, ...data.map((item) => usageTotal(item)))
|
||||
if (max === 0) return 1
|
||||
if (data.length === 1) return max * 1.75
|
||||
return max
|
||||
}
|
||||
|
||||
function getTopModelsSegmentRows(point: UsagePoint) {
|
||||
const total = usageTotal(point)
|
||||
if (total <= 0) return ""
|
||||
return visibleTopModelsSegments(point)
|
||||
.map((item) => `${(item.segment.value / total) * 100}%`)
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
function visibleTopModelsSegments(point: UsagePoint) {
|
||||
return point.segments.map((segment, index) => ({ segment, index })).filter((item) => item.segment.value > 0)
|
||||
}
|
||||
|
||||
function getTopModelsSegmentColor(index: number, muted: boolean) {
|
||||
if (muted) return "var(--stats-layer-2)"
|
||||
return usageColors[index] ?? "var(--stats-text)"
|
||||
}
|
||||
|
||||
function usageTotal(point: UsagePoint) {
|
||||
|
||||
Reference in New Issue
Block a user