Improve top models chart mobile axis
This commit is contained in:
@@ -630,6 +630,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"] [data-component="top-models-chart"] {
|
[data-page="stats"] [data-component="top-models-chart"] {
|
||||||
|
--top-models-bar-gap: 12px;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 34px minmax(0, 1fr);
|
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-axis"],
|
||||||
[data-page="stats"] [data-slot="top-models-bars"] {
|
[data-page="stats"] [data-slot="top-models-bars"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
|
||||||
min-width: 0;
|
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 {
|
[data-page="stats"] [data-slot="top-models-axis"] > div {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -664,43 +668,54 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="axis-date-mobile"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="top-models-bars"] {
|
[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;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="top-models-bar"] {
|
[data-page="stats"] [data-slot="top-models-bar"] {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
padding-inline: calc(var(--top-models-bar-gap) / 2);
|
||||||
outline: none;
|
outline: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="top-models-bar"]::before {
|
[data-page="stats"] [data-slot="top-models-bar"]::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0 calc(var(--top-models-bar-gap) / 2);
|
||||||
content: "";
|
content: "";
|
||||||
background: var(--stats-dot);
|
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-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-repeat: repeat;
|
||||||
mask-size: 6px 6px;
|
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-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-repeat: repeat;
|
||||||
-webkit-mask-size: 6px 6px;
|
-webkit-mask-size: 6px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="top-models-stack"] {
|
[data-page="stats"] [data-slot="top-models-stack"] {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: calc(var(--top-models-bar-gap) / 2);
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: calc(var(--top-models-bar-gap) / 2);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: grid;
|
display: grid;
|
||||||
height: var(--top-models-bar-height);
|
height: var(--top-models-bar-height);
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--stats-bg);
|
background: var(--stats-bg);
|
||||||
|
box-shadow: 0 -4px 0 var(--stats-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="top-models-stack"] i {
|
[data-page="stats"] [data-slot="top-models-stack"] i {
|
||||||
@@ -1118,6 +1133,10 @@
|
|||||||
line-height: 12px;
|
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 {
|
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"] [data-slot="tooltip-divider"] + p {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
@@ -2026,11 +2045,21 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
max-width: 96px;
|
max-width: 72px;
|
||||||
transform: rotate(-90deg) translateX(-50%);
|
transform: rotate(-90deg) translateX(-50%);
|
||||||
transform-origin: left center;
|
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"] {
|
[data-page="stats"] [data-section="top-models"] [data-component="chart-tooltip"] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -403,6 +403,7 @@ function FilterPills<T extends string>(props: {
|
|||||||
|
|
||||||
function TopModelsChart(props: { data: UsagePoint[]; range: UsageRange }) {
|
function TopModelsChart(props: { data: UsagePoint[]; range: UsageRange }) {
|
||||||
const [activeIndex, setActiveIndex] = createSignal<number>()
|
const [activeIndex, setActiveIndex] = createSignal<number>()
|
||||||
|
const [activeSegment, setActiveSegment] = createSignal<number>()
|
||||||
const maxTotal = createMemo(() => getTopModelsMaxTotal(props.data))
|
const maxTotal = createMemo(() => getTopModelsMaxTotal(props.data))
|
||||||
const activePoint = createMemo(() => props.data[activeIndex() ?? -1])
|
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">
|
<div data-slot="top-models-axis" aria-hidden="true">
|
||||||
<For each={props.data}>
|
<For each={props.data}>
|
||||||
{(day, index) => (
|
{(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 data-slot="axis-label">
|
||||||
<span>{formatTokens(usageTotal(day))}</span>
|
<span data-slot="axis-total">{formatTokens(usageTotal(day))}</span>
|
||||||
<span>{day.date}</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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -431,30 +438,49 @@ function TopModelsChart(props: { data: UsagePoint[]; range: UsageRange }) {
|
|||||||
data-active={activeIndex() === dayIndex() ? "true" : undefined}
|
data-active={activeIndex() === dayIndex() ? "true" : undefined}
|
||||||
data-muted={activeIndex() !== undefined && activeIndex() !== dayIndex() ? "true" : undefined}
|
data-muted={activeIndex() !== undefined && activeIndex() !== dayIndex() ? "true" : undefined}
|
||||||
style={{ "--top-models-bar-height": `${getTopModelsBarHeight(usageTotal(day), maxTotal())}%` }}
|
style={{ "--top-models-bar-height": `${getTopModelsBarHeight(usageTotal(day), maxTotal())}%` }}
|
||||||
onPointerEnter={() => setActiveIndex(dayIndex())}
|
onPointerEnter={() => {
|
||||||
|
setActiveIndex(dayIndex())
|
||||||
|
setActiveSegment(undefined)
|
||||||
|
}}
|
||||||
onPointerLeave={(event) => {
|
onPointerLeave={(event) => {
|
||||||
if (event.pointerType === "touch") return
|
if (event.pointerType === "touch") return
|
||||||
setActiveIndex(undefined)
|
setActiveIndex(undefined)
|
||||||
|
setActiveSegment(undefined)
|
||||||
}}
|
}}
|
||||||
onClick={() => setActiveIndex(dayIndex())}
|
onClick={() => setActiveIndex(dayIndex())}
|
||||||
onFocus={() => setActiveIndex(dayIndex())}
|
onFocus={() => {
|
||||||
onBlur={() => setActiveIndex(undefined)}
|
setActiveIndex(dayIndex())
|
||||||
|
setActiveSegment(undefined)
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
setActiveIndex(undefined)
|
||||||
|
setActiveSegment(undefined)
|
||||||
|
}}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key !== "Enter" && event.key !== " ") return
|
if (event.key !== "Enter" && event.key !== " ") return
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
setActiveIndex(dayIndex())
|
setActiveIndex(dayIndex())
|
||||||
|
setActiveSegment(undefined)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div data-slot="top-models-stack" style={{ "grid-template-rows": getTopModelsSegmentRows(day) }}>
|
<div data-slot="top-models-stack" style={{ "grid-template-rows": getTopModelsSegmentRows(day) }}>
|
||||||
<For each={visibleTopModelsSegments(day)}>
|
<For each={visibleTopModelsSegments(day)}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<i
|
<i
|
||||||
|
data-series={item.index}
|
||||||
|
data-active={activeSegment() === item.index ? "true" : undefined}
|
||||||
style={{
|
style={{
|
||||||
background: getTopModelsSegmentColor(
|
background: getTopModelsSegmentColor(
|
||||||
item.index,
|
item.index,
|
||||||
activeIndex() !== undefined && activeIndex() !== dayIndex(),
|
activeIndex() !== undefined && activeIndex() !== dayIndex(),
|
||||||
|
activeSegment(),
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
|
onPointerEnter={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
setActiveIndex(dayIndex())
|
||||||
|
setActiveSegment(item.index)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
@@ -467,7 +493,12 @@ function TopModelsChart(props: { data: UsagePoint[]; range: UsageRange }) {
|
|||||||
<div data-slot="tooltip-divider" />
|
<div data-slot="tooltip-divider" />
|
||||||
<For each={visibleTopModelsSegments(point())}>
|
<For each={visibleTopModelsSegments(point())}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<p>
|
<p
|
||||||
|
data-active={activeSegment() === item.index ? "true" : undefined}
|
||||||
|
data-muted={
|
||||||
|
activeSegment() !== undefined && activeSegment() !== item.index ? "true" : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
<span data-slot="tooltip-label">
|
<span data-slot="tooltip-label">
|
||||||
<i style={{ background: usageColors[item.index] }} /> {item.segment.model}
|
<i style={{ background: usageColors[item.index] }} /> {item.segment.model}
|
||||||
</span>
|
</span>
|
||||||
@@ -510,11 +541,22 @@ function visibleTopModelsSegments(point: UsagePoint) {
|
|||||||
return point.segments.map((segment, index) => ({ segment, index })).filter((item) => item.segment.value > 0)
|
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)"
|
if (muted) return "var(--stats-layer-2)"
|
||||||
return usageColors[index] ?? "var(--stats-text)"
|
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) {
|
function usageTotal(point: UsagePoint) {
|
||||||
return point.segments.reduce((sum, item) => sum + item.value, 0)
|
return point.segments.reduce((sum, item) => sum + item.value, 0)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user