Improve top models chart mobile axis

This commit is contained in:
Adam 2026-05-28 14:31:44 -05:00
parent 93ba2dd24a
commit b623d86f10
No known key found for this signature in database
GPG key ID: 9CB48779AF150E75
2 changed files with 84 additions and 13 deletions

View file

@ -630,6 +630,7 @@
}
[data-page="stats"] [data-component="top-models-chart"] {
--top-models-bar-gap: 12px;
position: relative;
display: grid;
grid-template-rows: 34px minmax(0, 1fr);
@ -641,10 +642,13 @@
[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"] {
gap: var(--top-models-bar-gap);
}
[data-page="stats"] [data-slot="top-models-axis"] > div {
flex: 1 1 0;
min-width: 0;
@ -664,43 +668,54 @@
flex-direction: column;
}
[data-page="stats"] [data-slot="axis-date-mobile"] {
display: none;
}
[data-page="stats"] [data-slot="top-models-bars"] {
width: calc(100% + var(--top-models-bar-gap));
margin-inline: calc(var(--top-models-bar-gap) / -2);
min-height: 0;
}
[data-page="stats"] [data-slot="top-models-bar"] {
position: relative;
box-sizing: border-box;
flex: 1 1 0;
min-width: 0;
height: 100%;
padding-inline: calc(var(--top-models-bar-gap) / 2);
outline: none;
cursor: pointer;
}
[data-page="stats"] [data-slot="top-models-bar"]::before {
position: absolute;
inset: 0;
inset: 0 calc(var(--top-models-bar-gap) / 2);
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-position: center top;
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-position: center top;
-webkit-mask-repeat: repeat;
-webkit-mask-size: 6px 6px;
}
[data-page="stats"] [data-slot="top-models-stack"] {
position: absolute;
right: 0;
right: calc(var(--top-models-bar-gap) / 2);
bottom: 0;
left: 0;
left: calc(var(--top-models-bar-gap) / 2);
z-index: 1;
display: grid;
height: var(--top-models-bar-height);
min-height: 0;
overflow: hidden;
background: var(--stats-bg);
box-shadow: 0 -4px 0 var(--stats-bg);
}
[data-page="stats"] [data-slot="top-models-stack"] i {
@ -1118,6 +1133,10 @@
line-height: 12px;
}
[data-page="stats"] [data-section="top-models"] [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 {
margin-top: 8px;
}
@ -2026,11 +2045,21 @@
position: absolute;
left: 50%;
width: max-content;
max-width: 96px;
max-width: 72px;
transform: rotate(-90deg) translateX(-50%);
transform-origin: left center;
}
[data-page="stats"] [data-slot="top-models-axis"] > div[data-mobile-hidden="true"] [data-slot="axis-label"],
[data-page="stats"] [data-slot="axis-total"],
[data-page="stats"] [data-slot="axis-date-full"] {
display: none;
}
[data-page="stats"] [data-slot="axis-date-mobile"] {
display: block;
}
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"] {
display: none;
}

View file

@ -403,6 +403,7 @@ function FilterPills<T extends string>(props: {
function TopModelsChart(props: { data: UsagePoint[]; range: UsageRange }) {
const [activeIndex, setActiveIndex] = createSignal<number>()
const [activeSegment, setActiveSegment] = createSignal<number>()
const maxTotal = createMemo(() => getTopModelsMaxTotal(props.data))
const activePoint = createMemo(() => props.data[activeIndex() ?? -1])
@ -411,10 +412,16 @@ function TopModelsChart(props: { data: UsagePoint[]; range: UsageRange }) {
<div data-slot="top-models-axis" aria-hidden="true">
<For each={props.data}>
{(day, index) => (
<div data-active={activeIndex() === index() ? "true" : undefined}>
<div
data-active={activeIndex() === index() ? "true" : undefined}
data-mobile-hidden={isTopModelsMobileAxisHidden(index(), props.data.length) ? "true" : undefined}
>
<span data-slot="axis-label">
<span>{formatTokens(usageTotal(day))}</span>
<span>{day.date}</span>
<span data-slot="axis-total">{formatTokens(usageTotal(day))}</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>
</span>
</span>
</div>
)}
@ -431,30 +438,49 @@ function TopModelsChart(props: { data: UsagePoint[]; range: UsageRange }) {
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())}
onPointerEnter={() => {
setActiveIndex(dayIndex())
setActiveSegment(undefined)
}}
onPointerLeave={(event) => {
if (event.pointerType === "touch") return
setActiveIndex(undefined)
setActiveSegment(undefined)
}}
onClick={() => setActiveIndex(dayIndex())}
onFocus={() => setActiveIndex(dayIndex())}
onBlur={() => setActiveIndex(undefined)}
onFocus={() => {
setActiveIndex(dayIndex())
setActiveSegment(undefined)
}}
onBlur={() => {
setActiveIndex(undefined)
setActiveSegment(undefined)
}}
onKeyDown={(event) => {
if (event.key !== "Enter" && event.key !== " ") return
event.preventDefault()
setActiveIndex(dayIndex())
setActiveSegment(undefined)
}}
>
<div data-slot="top-models-stack" style={{ "grid-template-rows": getTopModelsSegmentRows(day) }}>
<For each={visibleTopModelsSegments(day)}>
{(item) => (
<i
data-series={item.index}
data-active={activeSegment() === item.index ? "true" : undefined}
style={{
background: getTopModelsSegmentColor(
item.index,
activeIndex() !== undefined && activeIndex() !== dayIndex(),
activeSegment(),
),
}}
onPointerEnter={(event) => {
event.stopPropagation()
setActiveIndex(dayIndex())
setActiveSegment(item.index)
}}
/>
)}
</For>
@ -467,7 +493,12 @@ function TopModelsChart(props: { data: UsagePoint[]; range: UsageRange }) {
<div data-slot="tooltip-divider" />
<For each={visibleTopModelsSegments(point())}>
{(item) => (
<p>
<p
data-active={activeSegment() === item.index ? "true" : undefined}
data-muted={
activeSegment() !== undefined && activeSegment() !== item.index ? "true" : undefined
}
>
<span data-slot="tooltip-label">
<i style={{ background: usageColors[item.index] }} /> {item.segment.model}
</span>
@ -510,11 +541,22 @@ function visibleTopModelsSegments(point: UsagePoint) {
return point.segments.map((segment, index) => ({ segment, index })).filter((item) => item.segment.value > 0)
}
function getTopModelsSegmentColor(index: number, muted: boolean) {
function getTopModelsSegmentColor(index: number, muted: boolean, activeSegment: number | undefined) {
if (activeSegment !== undefined)
return activeSegment === index ? (usageColors[index] ?? "var(--stats-text)") : "var(--stats-layer-2)"
if (muted) return "var(--stats-layer-2)"
return usageColors[index] ?? "var(--stats-text)"
}
function isTopModelsMobileAxisHidden(index: number, count: number) {
return count > 7 && index % 2 === 1
}
function formatTopModelsMobileDate(label: string, range: UsageRange) {
if (range === "1M" || range === "2M") return label.split(" - ")[0] ?? label
return label
}
function usageTotal(point: UsagePoint) {
return point.segments.reduce((sum, item) => sum + item.value, 0)
}