mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-31 21:50:53 +00:00
feat(stats): refine top models chart scaling
This commit is contained in:
parent
982b7d39e7
commit
f55b70b4e3
3 changed files with 687 additions and 174 deletions
|
|
@ -407,6 +407,9 @@
|
|||
[data-page="stats"] [data-section="chart"],
|
||||
[data-page="stats"] [data-section="newsletter"] {
|
||||
border-bottom: 1px solid var(--stats-line);
|
||||
box-shadow:
|
||||
inset 1px 0 var(--stats-line),
|
||||
inset -1px 0 var(--stats-line);
|
||||
padding: var(--stats-section-padding) var(--stats-page-padding);
|
||||
}
|
||||
|
||||
|
|
@ -590,6 +593,208 @@
|
|||
min-height: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="top-models"] {
|
||||
box-sizing: border-box;
|
||||
margin-top: 1px;
|
||||
box-shadow:
|
||||
0 -1px var(--stats-line),
|
||||
inset 0 -1px var(--stats-line),
|
||||
inset 1px 0 var(--stats-line),
|
||||
inset -1px 0 var(--stats-line);
|
||||
padding: 80px 40px;
|
||||
color: var(--stats-text);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="top-models-title"] {
|
||||
max-width: 1200px;
|
||||
margin-bottom: 40px;
|
||||
color: var(--stats-muted);
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="top-models-title"] strong {
|
||||
color: var(--stats-text);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="top-models-title"] span {
|
||||
color: var(--stats-muted);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="top-models-mobile-controls"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="top-models-chart"] {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-rows: 34px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
height: 560px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="top-models-axis"],
|
||||
[data-page="stats"] [data-slot="top-models-bars"] {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="top-models-axis"] > div {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
color: var(--stats-faint);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="top-models-axis"] > div[data-active="true"] {
|
||||
color: var(--stats-text);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="axis-label"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="top-models-bars"] {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="top-models-bar"] {
|
||||
position: relative;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="top-models-bar"]::before {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
content: "";
|
||||
background: var(--stats-dot);
|
||||
mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E");
|
||||
mask-repeat: repeat;
|
||||
mask-size: 6px 6px;
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E");
|
||||
-webkit-mask-repeat: repeat;
|
||||
-webkit-mask-size: 6px 6px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="top-models-stack"] {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
height: var(--top-models-bar-height);
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--stats-bg);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="top-models-stack"] i {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
min-height: 0;
|
||||
transition: background 120ms ease;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="top-models-stack"] i + i {
|
||||
border-top: 2px solid var(--stats-bg);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="mobile-filter-button"] {
|
||||
display: inline-flex;
|
||||
flex: 1 1 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
height: 32px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
padding: 0 12px;
|
||||
color: var(--stats-text);
|
||||
background: var(--stats-bg);
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
box-shadow:
|
||||
0 0 0 0 #00000024,
|
||||
0 0 0 0.5px #00000024,
|
||||
0 1px 1.5px 0 #0000001a;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="mobile-filter-button"] svg {
|
||||
flex: 0 0 auto;
|
||||
color: var(--stats-muted);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="mobile-filter-sheet"] {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 30;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding: 0 8px 8px;
|
||||
background: #00000066;
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="filter-sheet-panel"] {
|
||||
width: 100%;
|
||||
background: var(--stats-bg);
|
||||
box-shadow: 0 8px 24px #00000026;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="filter-sheet-panel"] button {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
padding: 0 42px;
|
||||
color: var(--stats-faint);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
border-bottom: 1px solid var(--stats-line);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="filter-sheet-panel"] button:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="filter-sheet-panel"] button[data-active="true"] {
|
||||
color: var(--stats-text);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="filter-sheet-panel"] button[data-active="true"]::before {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
content: "";
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="section-header"] {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
@ -636,6 +841,25 @@
|
|||
background: var(--stats-text);
|
||||
}
|
||||
|
||||
[data-page="stats"] button[data-slot="mobile-filter-button"] {
|
||||
padding: 0 12px;
|
||||
color: var(--stats-text);
|
||||
background: var(--stats-bg);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="filter-sheet-panel"] button {
|
||||
padding: 0 42px;
|
||||
color: var(--stats-faint);
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="filter-sheet-panel"] button[data-active="true"] {
|
||||
color: var(--stats-text);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="usage-chart"],
|
||||
[data-page="stats"] [data-component="country-map"] {
|
||||
position: relative;
|
||||
|
|
@ -710,6 +934,10 @@
|
|||
margin-top: 32px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="top-models"] [data-slot="chart-footer"] {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="usage-filter"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -720,44 +948,51 @@
|
|||
}
|
||||
|
||||
[data-page="stats"] [data-component="usage-filter"][data-variant="range"] {
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="usage-filter"] button {
|
||||
color: var(--stats-faint);
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 17px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="usage-filter"] button:hover {
|
||||
color: var(--stats-text);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="usage-filter"] button[data-active="true"] {
|
||||
color: var(--stats-text);
|
||||
background: transparent;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="usage-filter"][data-variant="product"] button[data-active="true"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="usage-filter"][data-variant="product"] button[data-active="true"]::before {
|
||||
content: "";
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--stats-text);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: linear-gradient(var(--stats-text), var(--stats-text)) center / 6px 6px no-repeat;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="usage-filter"][data-variant="range"] button {
|
||||
min-width: 34px;
|
||||
padding: 3px 7px;
|
||||
height: 24px;
|
||||
width: 36px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="usage-filter"][data-variant="range"] button[data-active="true"] {
|
||||
border-color: var(--stats-line-strong);
|
||||
background: var(--stats-layer-2);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="chart-tooltip"],
|
||||
|
|
@ -821,6 +1056,91 @@
|
|||
color: var(--stats-text);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"] {
|
||||
top: 110px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
width: 192px;
|
||||
min-width: 192px;
|
||||
padding: 0;
|
||||
background: #fffffff2;
|
||||
border: 0;
|
||||
box-shadow:
|
||||
0 0 0 0.5px #00000024,
|
||||
0 8px 16px #0000000f,
|
||||
0 4px 8px #00000014;
|
||||
color: var(--stats-text);
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="top-models"] [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"] {
|
||||
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 {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
line-height: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"] strong {
|
||||
padding: 8px 8px 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="top-models"] [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"] {
|
||||
height: 0.5px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"] p {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 4px;
|
||||
height: 16px;
|
||||
margin: 4px 0 0;
|
||||
padding: 0 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="top-models"] [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 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="top-models"] [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 {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"] b {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="leaderboard"] {
|
||||
display: block;
|
||||
}
|
||||
|
|
@ -1547,6 +1867,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media (max-width: 74rem) {
|
||||
[data-page="stats"] [data-section="top-models"] {
|
||||
padding: 64px 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 58rem) {
|
||||
[data-page="stats"] {
|
||||
--stats-page-padding: 24px;
|
||||
|
|
@ -1584,6 +1910,15 @@
|
|||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="top-models"] [data-slot="chart-footer"] {
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="top-models"] [data-component="usage-filter"] {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="leaderboard-grid"],
|
||||
[data-page="stats"] [data-slot="leaderboard-compact"] {
|
||||
grid-template-columns: 1fr;
|
||||
|
|
@ -1653,6 +1988,54 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media (max-width: 47.999rem) {
|
||||
[data-page="stats"] [data-section="top-models"] {
|
||||
padding: 48px 24px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="top-models-title"] {
|
||||
margin-bottom: 32px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="top-models-mobile-controls"] {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="top-models"] [data-slot="chart-footer"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-component="top-models-chart"] {
|
||||
grid-template-rows: 40px minmax(0, 1fr);
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="top-models-axis"] > div {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 40px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-slot="axis-label"] {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
width: max-content;
|
||||
max-width: 96px;
|
||||
transform: rotate(-90deg) translateX(-50%);
|
||||
transform-origin: left center;
|
||||
}
|
||||
|
||||
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
[data-page="stats"] [data-component="footer"],
|
||||
[data-page="stats"] [data-component="legal"] {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { ProviderStatRepo, type ProviderStatMetric } from "./provider"
|
|||
|
||||
export type UsageProduct = "All Users" | "Zen" | "Go" | "Enterprise"
|
||||
export type TokenProduct = "Zen" | "Go" | "Enterprise"
|
||||
export type UsageRange = "1D" | "1W" | "1M" | "3M" | "YTD" | "ALL"
|
||||
export type UsageRange = "1D" | "1W" | "2W" | "1M" | "2M" | "3M" | "YTD" | "ALL"
|
||||
export type UsagePoint = { date: string; segments: { model: string; value: number }[] }
|
||||
export type MarketDay = { date: string; total: number; authors: { author: string; share: number; tokens: number }[] }
|
||||
export type LeaderboardEntry = { model: string; author: string; tokens: number; change: number; rank: number }
|
||||
|
|
@ -134,9 +134,9 @@ function buildUsagePoints(rows: StatMetricRow[], product: UsageProduct, range: U
|
|||
return {
|
||||
date: bucket.label,
|
||||
segments: [
|
||||
...segmentTokens.map((item) => ({ model: item.model, value: round(item.tokens / 1_000_000_000_000, 2) })),
|
||||
{ model: "Other", value: round(Math.max(totalTokens - knownTokens, 0) / 1_000_000_000_000, 2) },
|
||||
].filter((item) => item.value > 0),
|
||||
...segmentTokens.map((item) => ({ model: item.model, value: round(item.tokens / 1_000_000_000_000, 4) })),
|
||||
{ model: "Other", value: round(Math.max(totalTokens - knownTokens, 0) / 1_000_000_000_000, 4) },
|
||||
],
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -309,13 +309,17 @@ function getWindow(range: UsageRange, earliest: number, latest: number): DateWin
|
|||
? latest
|
||||
: range === "1W"
|
||||
? latest - 6 * DAY_MS
|
||||
: range === "1M"
|
||||
? latest - 29 * DAY_MS
|
||||
: range === "3M"
|
||||
? latest - 89 * DAY_MS
|
||||
: range === "YTD"
|
||||
? Date.UTC(new Date(latest).getUTCFullYear(), 0, 1)
|
||||
: earliest,
|
||||
: range === "2W"
|
||||
? latest - 13 * DAY_MS
|
||||
: range === "1M"
|
||||
? latest - 27 * DAY_MS
|
||||
: range === "2M"
|
||||
? latest - 55 * DAY_MS
|
||||
: range === "3M"
|
||||
? latest - 89 * DAY_MS
|
||||
: range === "YTD"
|
||||
? Date.UTC(new Date(latest).getUTCFullYear(), 0, 1)
|
||||
: earliest,
|
||||
)
|
||||
const duration = end - start
|
||||
return { start, end, previousStart: start - duration, previousEnd: start }
|
||||
|
|
@ -323,12 +327,21 @@ function getWindow(range: UsageRange, earliest: number, latest: number): DateWin
|
|||
|
||||
function createBuckets(window: DateWindow, range: UsageRange): Bucket[] {
|
||||
const span = Math.max(window.end - window.start, DAY_MS)
|
||||
const count = Math.max(1, Math.min(7, Math.ceil(span / DAY_MS)))
|
||||
const count =
|
||||
range === "1D"
|
||||
? 1
|
||||
: range === "2W"
|
||||
? 14
|
||||
: range === "1M"
|
||||
? 4
|
||||
: range === "2M"
|
||||
? 8
|
||||
: Math.max(1, Math.min(7, Math.ceil(span / DAY_MS)))
|
||||
const size = span / count
|
||||
return Array.from({ length: count }, (_, index) => {
|
||||
const start = window.start + index * size
|
||||
const end = index === count - 1 ? window.end : window.start + (index + 1) * size
|
||||
return { start, end, label: formatBucketLabel(start, range) }
|
||||
return { start, end, label: formatBucketLabel(start, end, range) }
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -353,7 +366,9 @@ function createRangeRecord<T>(value: (range: UsageRange) => T): Record<UsageRang
|
|||
return {
|
||||
"1D": value("1D"),
|
||||
"1W": value("1W"),
|
||||
"2W": value("2W"),
|
||||
"1M": value("1M"),
|
||||
"2M": value("2M"),
|
||||
"3M": value("3M"),
|
||||
YTD: value("YTD"),
|
||||
ALL: value("ALL"),
|
||||
|
|
@ -428,13 +443,19 @@ function periodKeyTime(value: string) {
|
|||
return Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]))
|
||||
}
|
||||
|
||||
function formatBucketLabel(value: number, range: UsageRange) {
|
||||
const date = new Date(value)
|
||||
function formatBucketLabel(start: number, end: number, range: UsageRange) {
|
||||
const date = new Date(start)
|
||||
if (range === "YTD") return months[date.getUTCMonth()]
|
||||
if (range === "ALL")
|
||||
return date.getUTCFullYear() === new Date().getUTCFullYear()
|
||||
? months[date.getUTCMonth()]
|
||||
: String(date.getUTCFullYear())
|
||||
if (range === "1M" || range === "2M") return `${formatDay(start)} - ${formatDay(end - DAY_MS)}`
|
||||
return formatDay(start)
|
||||
}
|
||||
|
||||
function formatDay(value: number) {
|
||||
const date = new Date(value)
|
||||
return `${months[date.getUTCMonth()]} ${date.getUTCDate()}`
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue