فهرست منبع

feat(data): improved model usage section

Adam 2 ماه پیش
والد
کامیت
c3b9c9d888
3فایلهای تغییر یافته به همراه554 افزوده شده و 3 حذف شده
  1. 1 1
      packages/stats/app/src/i18n.ts
  2. 295 1
      packages/stats/app/src/routes/[lab]/[model].tsx
  3. 258 1
      packages/stats/app/src/routes/index.css

+ 1 - 1
packages/stats/app/src/i18n.ts

@@ -210,7 +210,7 @@ const en = {
   "model.totalModels": "{{count}} models",
   "model.momentum": "Momentum",
   "model.vsPreviousWindow": "vs previous window",
-  "model.usageDescription": "Daily OpenCode Go token volume over the recent two-month window.",
+  "model.usageDescription": "Daily token volume.",
   "model.noUsageTitle": "No usage",
   "model.noUsageDescription": "No usage landed in the current window.",
   "model.dailyTokenChart": "Daily token usage chart",

+ 295 - 1
packages/stats/app/src/routes/[lab]/[model].tsx

@@ -15,7 +15,7 @@ import {
   type UsageRange,
 } from "@opencode-ai/stats-core/domain/home"
 import { createAsync, query, useParams } from "@solidjs/router"
-import { createMemo, createSignal, For, onMount, Show, type JSX } from "solid-js"
+import { createMemo, createSignal, createUniqueId, For, onMount, Show, type JSX } from "solid-js"
 import { getRequestEvent } from "solid-js/web"
 import type { FeatureCollection, GeometryObject, GeoJsonProperties } from "geojson"
 import type { GeometryCollection, Topology } from "topojson-specification"
@@ -121,6 +121,7 @@ export default function StatsModel() {
   const modelHeaderLinks = createMemo<readonly HeaderLink[]>(() => [
     { href: "#overview", label: i18n.t("nav.overview") },
     { href: "#momentum", label: i18n.t("model.momentum") },
+    { href: "#usage", label: i18n.t("nav.usage") },
     { href: "#efficiency", label: i18n.t("nav.efficiency") },
     { href: "#geo-breakdown", label: i18n.t("nav.geoBreakdown") },
     { href: "#peers", label: i18n.t("nav.peers") },
@@ -183,6 +184,7 @@ export default function StatsModel() {
                 />
                 <ModelOverview catalog={catalogEntry() ?? null} />
                 <ModelMomentumSection data={stats() ?? null} />
+                <ModelUsageSection data={stats() ?? null} />
                 <ModelEfficiencySection data={stats() ?? null} catalog={catalogEntry() ?? null} />
                 <ModelGeoBreakdownSection data={stats()?.country ?? emptyCountryRecord()} />
                 <ModelPeersSection data={stats() ?? null} />
@@ -561,6 +563,211 @@ function MomentumMetric(props: { label: string; value: string; watermark?: strin
   )
 }
 
+function ModelUsageSection(props: { data: StatsModelData | null }) {
+  const i18n = useI18n()
+  const activeLineClipId = createUniqueId()
+  const activeLineMaskId = createUniqueId()
+  const [activeIndex, setActiveIndex] = createSignal<number>()
+  const usage = createMemo(() => props.data?.usage ?? [])
+  const tokenMax = createMemo(() => Math.max(0, ...usage().map((item) => item.tokens)) || 1)
+  const linePoints = createMemo(() =>
+    usage().map((point, index) => ({
+      point,
+      x: modelUsagePointX(index, usage().length),
+      y: modelUsageLineY(point.tokens, tokenMax()),
+    })),
+  )
+  const tokenLinePath = createMemo(() => modelUsageLinePath(linePoints()))
+  const activeLineBreak = createMemo(() => {
+    const index = activeIndex()
+    if (index === undefined || linePoints().length < 2) return undefined
+    return modelUsageColumnBounds(index, linePoints().length)
+  })
+  const activeLineClip = createMemo(() => {
+    const index = activeIndex()
+    if (index === undefined || linePoints().length < 2) return undefined
+    return modelUsageColumnInnerBounds(index, linePoints().length)
+  })
+  const activeTooltip = createMemo(() => {
+    const index = activeIndex()
+    const point = index === undefined ? undefined : usage()[index]
+    if (index === undefined || !point) return undefined
+    const bounds = modelUsageColumnBounds(index, usage().length)
+    return {
+      bounds,
+      index,
+      point,
+      tokenY: linePoints()[index]?.y ?? 100,
+      tooltipY: ((linePoints()[index]?.y ?? 100) * 370) / 450,
+    }
+  })
+  const monthTicks = createMemo(() => modelUsageMonthTicks(usage(), props.data?.updatedAt ?? null))
+
+  return (
+    <section id="usage" data-section="model-panel">
+      <SectionTitle href="#usage" title={i18n.t("nav.usage")} description={i18n.t("model.usageDescription")} />
+      <Show
+        when={usage().some((item) => item.tokens > 0)}
+        fallback={
+          <ModelEmptyState title={i18n.t("model.noUsageTitle")} description={i18n.t("model.noUsageDescription")} />
+        }
+      >
+        <div
+          data-component="model-usage-chart"
+          data-variant="model-trend"
+          role="img"
+          aria-label={i18n.t("model.dailyTokenChart")}
+          style={{ "--model-usage-count": usage().length } as JSX.CSSProperties}
+          onPointerLeave={(event) => {
+            if (event.pointerType === "touch") return
+            setActiveIndex(undefined)
+          }}
+        >
+          <div data-slot="model-trend-plot">
+            <Show when={tokenLinePath()}>
+              {(path) => (
+                <>
+                  <svg
+                    data-slot="model-trend-line"
+                    data-layer="base"
+                    viewBox="0 0 100 100"
+                    preserveAspectRatio="none"
+                    aria-hidden="true"
+                  >
+                    <Show when={activeLineBreak()} fallback={<path data-slot="model-trend-line-base" d={path()} />}>
+                      {(lineBreak) => (
+                        <>
+                          <defs>
+                            <mask id={activeLineMaskId} maskUnits="userSpaceOnUse">
+                              <rect x="0" y="-2" width="100" height="104" fill="white" />
+                              <rect x={lineBreak().x} y="-2" width={lineBreak().width} height="104" fill="black" />
+                            </mask>
+                          </defs>
+                          <path data-slot="model-trend-line-base" d={path()} mask={`url(#${activeLineMaskId})`} />
+                        </>
+                      )}
+                    </Show>
+                  </svg>
+                  <Show when={activeLineClip()}>
+                    {(clip) => (
+                      <svg
+                        data-slot="model-trend-line"
+                        data-layer="active"
+                        viewBox="0 0 100 100"
+                        preserveAspectRatio="none"
+                        aria-hidden="true"
+                      >
+                        <defs>
+                          <clipPath id={activeLineClipId} clipPathUnits="userSpaceOnUse">
+                            <rect x={clip().x} y="-2" width={clip().width} height="104" />
+                          </clipPath>
+                        </defs>
+                        <path data-slot="model-trend-line-active" d={path()} clip-path={`url(#${activeLineClipId})`} />
+                      </svg>
+                    )}
+                  </Show>
+                </>
+              )}
+            </Show>
+            <Show when={linePoints().at(-1)}>
+              {(point) => (
+                <span
+                  data-slot="model-trend-end-marker"
+                  aria-hidden="true"
+                  style={
+                    {
+                      "--model-trend-end-x": `${point().x}%`,
+                      "--model-trend-end-top": `${(point().y * 370) / 450}%`,
+                    } as JSX.CSSProperties
+                  }
+                />
+              )}
+            </Show>
+            <div data-slot="model-trend-bars">
+              <For each={usage()}>
+                {(point, index) => (
+                  <div
+                    data-slot="model-trend-column"
+                    role="button"
+                    tabIndex={0}
+                    aria-label={`${point.date} ${formatTokens(point.tokens)} ${i18n.t("lab.tokens")}`}
+                    data-active={activeIndex() === index() ? "true" : undefined}
+                    data-muted={activeIndex() !== undefined && activeIndex() !== index() ? "true" : undefined}
+                    style={
+                      {
+                        "--model-trend-token-height": `${modelUsageStripHeight(point.tokens, tokenMax())}px`,
+                      } as JSX.CSSProperties
+                    }
+                    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-trend-token-bar" />
+                  </div>
+                )}
+              </For>
+            </div>
+            <Show when={activeTooltip()} keyed>
+              {(active) => (
+                <div
+                  data-component="chart-tooltip"
+                  data-placement={active.index > usage().length * 0.62 ? "left" : "right"}
+                  style={
+                    {
+                      "--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
+                  }
+                >
+                  <strong>{formatModelUsageTooltipDate(active.point.date, props.data?.updatedAt ?? null)}</strong>
+                  <span>
+                    {formatTokens(active.point.tokens)} {i18n.t("lab.tokens")}
+                  </span>
+                  <div data-slot="tooltip-divider" />
+                  <p>
+                    <span data-slot="tooltip-label">
+                      <i data-kind="tokens" /> {i18n.t("lab.dailyTokens")}
+                    </span>
+                    <b>{formatTokens(active.point.tokens)}</b>
+                  </p>
+                </div>
+              )}
+            </Show>
+          </div>
+          <div data-slot="model-trend-months" aria-hidden="true">
+            <For each={monthTicks()}>
+              {(tick) => (
+                <span
+                  data-align={tick.align}
+                  style={{ "--model-trend-month-left": `${tick.left}%` } as JSX.CSSProperties}
+                >
+                  {tick.label}
+                </span>
+              )}
+            </For>
+          </div>
+        </div>
+      </Show>
+    </section>
+  )
+}
+
 function ModelEfficiencySection(props: { data: StatsModelData | null; catalog: ModelCatalogEntry | null }) {
   const i18n = useI18n()
   return (
@@ -1014,6 +1221,93 @@ function formatMomentumDateLabel(date: string) {
   return `${shortMonths[parsed.getUTCMonth()]} ${parsed.getUTCDate()}`
 }
 
+function modelUsageStripHeight(value: number, max: number) {
+  if (value <= 0 || max <= 0) return 0
+  return Math.max(2, (value / max) * 40)
+}
+
+function modelUsageLineY(value: number, max: number) {
+  if (value <= 0 || max <= 0) return 100
+  return Math.max(0, 100 - (value / max) * 100)
+}
+
+function modelUsagePointX(index: number, count: number) {
+  if (count <= 1) return 50
+  return ((index + 0.5) / count) * 100
+}
+
+function modelUsageColumnBounds(index: number, count: number) {
+  if (count <= 0) return { x: 0, width: 100 }
+  return { x: (index / count) * 100, width: 100 / count }
+}
+
+function modelUsageColumnInnerBounds(index: number, count: number) {
+  const bounds = modelUsageColumnBounds(index, count)
+  const inset = Math.min(bounds.width * 0.1, 0.24)
+  return { x: bounds.x + inset, width: Math.max(0.001, bounds.width - inset * 2) }
+}
+
+type ModelUsageLinePoint = { x: number; y: number }
+
+function modelUsageLinePath(points: ModelUsageLinePoint[]) {
+  if (points.length === 0) return ""
+  if (points.length === 1) return `M ${formatUsagePathNumber(points[0].x)} ${formatUsagePathNumber(points[0].y)}`
+
+  return points.slice(0, -1).reduce(
+    (path, point, index) => {
+      const next = points[index + 1]
+      const previous = points[index - 1] ?? point
+      const afterNext = points[index + 2] ?? next
+      const controlA = {
+        x: point.x + (next.x - previous.x) / 6,
+        y: clampUsagePercent(point.y + (next.y - previous.y) / 6),
+      }
+      const controlB = {
+        x: next.x - (afterNext.x - point.x) / 6,
+        y: clampUsagePercent(next.y - (afterNext.y - point.y) / 6),
+      }
+      return `${path} C ${formatUsagePathNumber(controlA.x)} ${formatUsagePathNumber(controlA.y)}, ${formatUsagePathNumber(controlB.x)} ${formatUsagePathNumber(controlB.y)}, ${formatUsagePathNumber(next.x)} ${formatUsagePathNumber(next.y)}`
+    },
+    `M ${formatUsagePathNumber(points[0].x)} ${formatUsagePathNumber(points[0].y)}`,
+  )
+}
+
+function formatUsagePathNumber(value: number) {
+  return Number(value.toFixed(3))
+}
+
+function clampUsagePercent(value: number) {
+  return Math.max(0, Math.min(100, value))
+}
+
+function modelUsageMonthTicks(points: ModelUsagePoint[], updatedAt: string | null) {
+  const seen = new Set<string>()
+  return points.flatMap((point, index) => {
+    const parsed = parseMomentumDate(point.date, updatedAt)
+    const label = shortMonths[parsed.getUTCMonth()]
+    if (!label || seen.has(label)) return []
+    seen.add(label)
+    return [
+      {
+        label,
+        left: modelUsagePointX(index, points.length),
+        align: index === 0 ? "start" : index >= points.length - 2 ? "end" : "center",
+      },
+    ]
+  })
+}
+
+function formatModelUsageTooltipDate(value: string, updatedAt: string | null) {
+  const date = parseMomentumDate(value, updatedAt)
+  if (date.getUTCFullYear() === 1970) return value
+  return new Intl.DateTimeFormat("en", {
+    month: "short",
+    day: "numeric",
+    year: "numeric",
+    timeZone: "UTC",
+  }).format(date)
+}
+
 function formatRankLabel(rank: number | null) {
   if (rank === null) return "--"
   return `#${String(rank).padStart(2, "0")}`

+ 258 - 1
packages/stats/app/src/routes/index.css

@@ -4871,6 +4871,200 @@
   background: var(--lab-usage-line);
 }
 
+[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: #dbdbdb;
+  --model-trend-line: #808080;
+  --model-trend-active: #3b5cf6;
+  --model-trend-band: #f2f2f2;
+  grid-template-rows: minmax(0, 450px) 14px;
+  gap: 40px;
+  height: 504px;
+}
+
+[data-page="stats"] [data-slot="model-trend-plot"] {
+  position: relative;
+  min-width: 0;
+  min-height: 0;
+  overflow: visible;
+}
+
+[data-page="stats"] [data-slot="model-trend-plot"]::after {
+  position: absolute;
+  inset: 0;
+  z-index: 0;
+  background: linear-gradient(to bottom, transparent 42%, color-mix(in srgb, var(--stats-text) 3%, transparent));
+  content: "";
+  pointer-events: none;
+}
+
+[data-page="stats"] [data-slot="model-trend-line"] {
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: auto;
+  left: 0;
+  display: block;
+  width: 100%;
+  height: calc(100% - var(--model-trend-line-bottom));
+  overflow: visible;
+  pointer-events: none;
+}
+
+[data-page="stats"] [data-slot="model-trend-line"][data-layer="base"] {
+  z-index: 2;
+}
+
+[data-page="stats"] [data-slot="model-trend-line"][data-layer="active"] {
+  z-index: 4;
+}
+
+[data-page="stats"] [data-slot="model-trend-line"] path {
+  fill: none;
+  stroke-linecap: round;
+  stroke-linejoin: round;
+  stroke-width: 1.5;
+  vector-effect: non-scaling-stroke;
+}
+
+[data-page="stats"] [data-slot="model-trend-line-base"] {
+  stroke: var(--model-trend-line);
+}
+
+[data-page="stats"] [data-slot="model-trend-line-active"] {
+  stroke: var(--model-trend-active);
+}
+
+[data-page="stats"] [data-slot="model-trend-end-marker"] {
+  position: absolute;
+  top: var(--model-trend-end-top);
+  left: var(--model-trend-end-x);
+  z-index: 3;
+  width: 6px;
+  height: 6px;
+  background: var(--model-trend-bar);
+  transform: translate(-50%, -50%);
+  pointer-events: none;
+}
+
+[data-page="stats"] [data-slot="model-trend-bars"] {
+  position: absolute;
+  inset: 0;
+  z-index: 3;
+  display: flex;
+  gap: 0;
+  min-width: 0;
+}
+
+[data-page="stats"] [data-slot="model-trend-column"] {
+  position: relative;
+  box-sizing: border-box;
+  flex: 1 1 0;
+  min-width: 0;
+  height: 100%;
+  outline: none;
+  cursor: pointer;
+}
+
+[data-page="stats"] [data-slot="model-trend-column"]::before {
+  position: absolute;
+  inset: 0;
+  z-index: 1;
+  background: transparent;
+  content: "";
+  pointer-events: none;
+  transition: background 120ms ease;
+}
+
+[data-page="stats"] [data-slot="model-trend-column"][data-active="true"]::before,
+[data-page="stats"] [data-slot="model-trend-column"]:focus-visible::before {
+  background: var(--model-trend-band);
+  box-shadow:
+    inset var(--model-trend-column-gutter) 0 0 var(--stats-bg),
+    inset calc(var(--model-trend-column-gutter) * -1) 0 0 var(--stats-bg);
+}
+
+[data-page="stats"] [data-slot="model-trend-token-bar"] {
+  position: absolute;
+  right: var(--model-trend-column-gutter);
+  bottom: 0;
+  left: var(--model-trend-column-gutter);
+  z-index: 2;
+  height: var(--model-trend-token-height);
+  background: var(--model-trend-bar);
+  transition:
+    background 120ms ease,
+    opacity 120ms ease;
+}
+
+[data-page="stats"] [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"] {
+  opacity: 0.64;
+}
+
+[data-page="stats"] [data-slot="model-trend-months"] {
+  position: relative;
+  height: 14px;
+  color: var(--stats-muted);
+  font-size: 11px;
+  font-weight: 500;
+  line-height: 14px;
+}
+
+[data-page="stats"] [data-slot="model-trend-months"] span {
+  position: absolute;
+  top: 0;
+  left: var(--model-trend-month-left);
+  transform: translateX(-50%);
+}
+
+[data-page="stats"] [data-slot="model-trend-months"] span[data-align="start"] {
+  transform: none;
+}
+
+[data-page="stats"] [data-slot="model-trend-months"] span[data-align="end"] {
+  transform: translateX(-100%);
+}
+
+[data-page="stats"] [data-component="model-usage-chart"][data-variant="model-trend"] [data-component="chart-tooltip"] {
+  z-index: 6;
+  top: clamp(12px, calc(var(--model-trend-tooltip-y) * 1% - 46px), calc(100% - 108px));
+  width: 192px;
+  min-width: 192px;
+  background: var(--stats-bg);
+}
+
+[data-page="stats"]
+  [data-component="model-usage-chart"][data-variant="model-trend"]
+  [data-component="chart-tooltip"][data-placement="right"] {
+  right: auto;
+  left: calc(var(--model-trend-tooltip-right) + 8px);
+}
+
+[data-page="stats"]
+  [data-component="model-usage-chart"][data-variant="model-trend"]
+  [data-component="chart-tooltip"][data-placement="left"] {
+  right: calc(100% - var(--model-trend-tooltip-left) + 8px);
+  left: auto;
+}
+
+[data-page="stats"] [data-component="model-usage-chart"][data-variant="model-trend"] [data-component="chart-tooltip"] p {
+  height: 16px;
+  margin: 8px 0;
+}
+
+[data-page="stats"]
+  [data-component="model-usage-chart"][data-variant="model-trend"]
+  [data-component="chart-tooltip"]
+  i[data-kind="tokens"] {
+  background: var(--model-trend-active);
+}
+
 [data-page="stats"] [data-component="model-peer-list"] {
   display: grid;
   gap: 8px;
@@ -4966,6 +5160,15 @@
   --lab-usage-band: #242424;
 }
 
+[data-page="stats"][data-theme="dark"] [data-component="model-usage-chart"][data-variant="model-trend"],
+:root[data-stats-theme="dark"]
+  [data-page="stats"]:not([data-theme="light"])
+  [data-component="model-usage-chart"][data-variant="model-trend"] {
+  --model-trend-bar: #3a3a3a;
+  --model-trend-line: #a3a3a3;
+  --model-trend-band: #242424;
+}
+
 [data-page="stats"][data-theme="dark"] [data-component="chart-tooltip"],
 :root[data-stats-theme="dark"] [data-page="stats"]:not([data-theme="light"]) [data-component="chart-tooltip"] {
   background: #242424f2;
@@ -5131,6 +5334,14 @@
     --lab-usage-band: #242424;
   }
 
+  :root:not([data-stats-theme="light"])
+    [data-page="stats"]:not([data-theme="light"])
+    [data-component="model-usage-chart"][data-variant="model-trend"] {
+    --model-trend-bar: #3a3a3a;
+    --model-trend-line: #a3a3a3;
+    --model-trend-band: #242424;
+  }
+
   :root:not([data-stats-theme="light"]) [data-page="stats"]:not([data-theme="light"]) [data-component="chart-tooltip"] {
     background: #242424f2;
   }
@@ -5229,15 +5440,27 @@
 [data-page="stats"][data-theme="dark"]
   [data-component="model-usage-chart"][data-variant="lab-usage"]
   [data-component="chart-tooltip"],
+[data-page="stats"][data-theme="dark"]
+  [data-component="model-usage-chart"][data-variant="model-trend"]
+  [data-component="chart-tooltip"],
 :root[data-stats-theme="dark"]
   [data-page="stats"]:not([data-theme="light"])
   [data-component="model-usage-chart"][data-variant="lab-usage"]
   [data-component="chart-tooltip"],
+:root[data-stats-theme="dark"]
+  [data-page="stats"]:not([data-theme="light"])
+  [data-component="model-usage-chart"][data-variant="model-trend"]
+  [data-component="chart-tooltip"],
 :root:not([data-stats-theme="light"])
   [data-page="stats"]:not([data-theme="light"])
   [data-component="model-usage-chart"][data-variant="lab-usage"]
   [data-component="chart-tooltip"],
-[data-page="stats"] [data-component="model-usage-chart"][data-variant="lab-usage"] [data-component="chart-tooltip"] {
+:root:not([data-stats-theme="light"])
+  [data-page="stats"]:not([data-theme="light"])
+  [data-component="model-usage-chart"][data-variant="model-trend"]
+  [data-component="chart-tooltip"],
+[data-page="stats"] [data-component="model-usage-chart"][data-variant="lab-usage"] [data-component="chart-tooltip"],
+[data-page="stats"] [data-component="model-usage-chart"][data-variant="model-trend"] [data-component="chart-tooltip"] {
   background: var(--stats-bg);
 }
 
@@ -5514,6 +5737,11 @@
     margin-top: 16px;
   }
 
+  [data-page="stats"] [data-component="model-usage-chart"][data-variant="model-trend"] [data-component="chart-tooltip"] {
+    position: absolute;
+    margin-top: 0;
+  }
+
   [data-page="stats"] [data-component="footer"] {
     padding: 88px 24px 24px;
   }
@@ -5935,6 +6163,35 @@
     min-width: 0;
   }
 
+  [data-page="stats"] [data-component="model-usage-chart"][data-variant="model-trend"] {
+    --model-usage-mobile-bar-width: 20px;
+    --model-usage-mobile-edge-space: 48px;
+    --model-usage-mobile-track-width: calc(
+      var(--model-usage-count) * var(--model-usage-mobile-bar-width) + var(--model-usage-mobile-edge-space)
+    );
+    --model-trend-line-bottom: 104px;
+    grid-template-rows: minmax(0, 360px) 14px;
+    gap: 24px;
+    height: 398px;
+  }
+
+  [data-page="stats"] [data-component="model-usage-chart"][data-variant="model-trend"] [data-slot="model-trend-plot"],
+  [data-page="stats"] [data-component="model-usage-chart"][data-variant="model-trend"] [data-slot="model-trend-months"] {
+    direction: ltr;
+    width: var(--model-usage-mobile-track-width);
+    min-width: var(--model-usage-mobile-track-width);
+  }
+
+  [data-page="stats"] [data-component="model-usage-chart"][data-variant="model-trend"] [data-slot="model-trend-column"] {
+    flex: 0 0 var(--model-usage-mobile-bar-width);
+  }
+
+  [data-page="stats"] [data-component="model-usage-chart"][data-variant="model-trend"] [data-component="chart-tooltip"] {
+    top: auto;
+    width: auto;
+    min-width: 0;
+  }
+
   [data-page="stats"] [data-component="model-peer-list"] a {
     grid-template-columns: 30px minmax(0, 1fr) minmax(64px, auto);
   }