feat(stats): improve market share chart (#47115)

This commit is contained in:
Adam
2026-09-03 16:03:32 -05:00
committed by GitHub
parent a935432b5c
commit 8a6cf2c9aa
2 changed files with 195 additions and 31 deletions
+125
View File
@@ -2195,6 +2195,7 @@ body {
[data-page="stats"] [data-component="market-share"] {
--market-gap: 12px;
position: relative;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
gap: 12px;
@@ -2360,6 +2361,106 @@ body {
font-weight: 500;
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] {
top: 52px;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 0;
width: 228px;
min-width: 228px;
padding: 0;
border: 0;
background: #fffffff2;
box-shadow:
0 0 0 0.5px #00000024,
0 8px 16px #0000000f,
0 4px 8px #00000014;
color: var(--stats-text);
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"][data-placement="right"] {
right: auto;
left: calc(var(--market-tooltip-left) + 8px);
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"][data-placement="left"] {
right: calc(var(--market-tooltip-right) + 8px);
left: auto;
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] strong,
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] > span {
display: block;
font-size: 11px;
line-height: 12px;
white-space: nowrap;
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] strong {
padding: 8px 8px 0;
font-weight: 500;
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] > span {
padding: 4px 8px 8px;
color: var(--stats-muted);
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] [data-slot="tooltip-divider"] {
height: 0.5px;
margin: 0;
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] p {
grid-template-columns: minmax(0, 1fr) auto auto;
gap: 8px;
height: 16px;
margin: 4px 0 0;
padding: 0 8px;
font-size: 11px;
font-weight: 500;
line-height: 12px;
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] p[data-muted="true"] {
opacity: 0.46;
}
[data-page="stats"]
[data-component="market-share"]
> [data-component="chart-tooltip"]
[data-slot="tooltip-divider"]
+ p {
margin-top: 8px;
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] p:last-child {
margin-bottom: 8px;
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] [data-slot="tooltip-label"] {
grid-template-columns: 16px minmax(0, 1fr);
gap: 4px;
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] i {
width: 6px;
height: 6px;
justify-self: center;
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] em,
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] b {
font-style: normal;
font-weight: 500;
white-space: nowrap;
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] em {
color: var(--stats-muted);
}
[data-page="stats"] [data-slot="market-footer"] {
display: flex;
align-items: center;
@@ -7208,6 +7309,11 @@ body {
[data-page="stats"][data-theme="dark"]
:is([data-section="top-models"], [data-section="unique-users"])
[data-component="chart-tooltip"],
:root[data-stats-theme="dark"]
[data-page="stats"]:not([data-theme="light"])
[data-component="market-share"]
> [data-component="chart-tooltip"],
[data-page="stats"][data-theme="dark"] [data-component="market-share"] > [data-component="chart-tooltip"],
:root[data-stats-theme="dark"]
[data-page="stats"]:not([data-theme="light"])
:is([data-section="top-models"], [data-section="unique-users"])
@@ -8560,6 +8666,25 @@ body {
transform: none;
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"] {
position: fixed;
top: auto;
right: 12px;
bottom: 12px;
left: 12px;
z-index: 40;
width: auto;
min-width: 0;
max-height: min(320px, 48vh);
overflow: auto;
transform: none;
}
[data-page="stats"] [data-component="market-share"] > [data-component="chart-tooltip"][data-placement] {
right: 12px;
left: 12px;
}
[data-page="stats"]
:is([data-section="top-models"], [data-section="unique-users"])
[data-component="chart-tooltip"][data-placement] {
+70 -31
View File
@@ -928,7 +928,7 @@ function MarketShareSection(props: { data: MarketDay[] }) {
const [inspecting, setInspecting] = createSignal(false)
const authorOrder = createMemo(() => getMarketAuthorOrder(props.data))
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(props.data.length - 1, 0)))
const activeDay = createMemo(() => props.data[selectedIndex()])
const today = createMemo(() => props.data[props.data.length - 1])
return (
<section
@@ -947,7 +947,7 @@ function MarketShareSection(props: { data: MarketDay[] }) {
description={i18n.t("home.marketShareDescription")}
/>
<Show
when={activeDay()}
when={today()}
fallback={<EmptyState title={i18n.t("home.noMarketTitle")} description={i18n.t("home.noMarketDescription")} />}
>
{(day) => (
@@ -963,19 +963,14 @@ function MarketShareSection(props: { data: MarketDay[] }) {
setActiveIndex(index)
setInspecting(true)
}}
onActiveAuthorChange={(author) => {
setActiveAuthor(author)
setInspecting(true)
}}
onActiveAuthorChange={setActiveAuthor}
onInspectingChange={setInspecting}
/>
<MarketShareList
data={day().authors}
data={rankedMarketAuthors(day())}
authorOrder={authorOrder()}
activeAuthor={activeAuthor()}
onActiveAuthorChange={(author) => {
setActiveAuthor(author)
setInspecting(true)
}}
onActiveAuthorChange={setActiveAuthor}
/>
</>
)}
@@ -983,11 +978,7 @@ function MarketShareSection(props: { data: MarketDay[] }) {
<div data-slot="market-footer">
<p>
<span>[*]</span>
<strong>
{inspecting()
? formatMarketDate(activeDay(), i18n.t("home.noData"))
: formatMarketRange(props.data, i18n.t("home.noData"))}
</strong>
<strong>{formatMarketDate(today(), i18n.t("home.noData"))}</strong>
</p>
</div>
</section>
@@ -1002,10 +993,15 @@ function MarketShare(props: {
activeAuthor: string | undefined
inspecting: boolean
onActiveIndexChange: (index: number) => void
onActiveAuthorChange: (author: string) => void
onActiveAuthorChange: (author: string | undefined) => void
onInspectingChange: (inspecting: boolean) => void
}) {
const i18n = useI18n()
let chartRef: HTMLDivElement | undefined
const inspectDay = (index: number) => {
props.onActiveIndexChange(index)
props.onActiveAuthorChange(undefined)
}
createEffect(() => scrollDenseChartToEnd(chartRef, props.range, props.data.length))
@@ -1018,6 +1014,10 @@ function MarketShare(props: {
role="img"
aria-label={i18n.t("home.marketChart")}
style={{ "--market-count": props.data.length } as JSX.CSSProperties}
onPointerLeave={(event) => {
if (event.pointerType === "touch") return
props.onInspectingChange(false)
}}
>
<div data-slot="market-labels">
<For each={props.data}>
@@ -1028,8 +1028,11 @@ function MarketShare(props: {
data-active={props.inspecting && props.activeIndex === index() ? "true" : undefined}
data-label-hidden={isColumnLabelHidden(index(), props.data.length) ? "true" : undefined}
data-mobile-hidden={isMarketMobileLabelHidden(index(), props.data.length) ? "true" : undefined}
onClick={() => props.onActiveIndexChange(index())}
onPointerEnter={() => props.onActiveIndexChange(index())}
aria-describedby={props.inspecting && props.activeIndex === index() ? "market-share-tooltip" : undefined}
onBlur={() => props.onInspectingChange(false)}
onClick={() => inspectDay(index())}
onFocus={() => inspectDay(index())}
onPointerEnter={() => inspectDay(index())}
>
<span data-slot="market-axis-label">
<span data-slot="market-total">{formatTrillions(day.total)}</span>
@@ -1049,8 +1052,11 @@ function MarketShare(props: {
type="button"
aria-label={`${day.date} ${formatTrillions(day.total)}`}
data-active={props.inspecting && props.activeIndex === index() ? "true" : undefined}
onClick={() => props.onActiveIndexChange(index())}
onPointerEnter={() => props.onActiveIndexChange(index())}
aria-describedby={props.inspecting && props.activeIndex === index() ? "market-share-tooltip" : undefined}
onBlur={() => props.onInspectingChange(false)}
onClick={() => inspectDay(index())}
onFocus={() => inspectDay(index())}
onPointerEnter={() => inspectDay(index())}
>
<For each={stackedMarketAuthors(day, props.authorOrder)}>
{(item) => (
@@ -1090,6 +1096,45 @@ function MarketShare(props: {
)}
</For>
</div>
<Show when={props.inspecting ? props.data[props.activeIndex] : undefined} keyed>
{(day) => (
<div
id="market-share-tooltip"
data-component="chart-tooltip"
data-placement={props.activeIndex > props.data.length * 0.62 ? "left" : "right"}
role="tooltip"
style={
{
"--market-tooltip-left": `${((props.activeIndex + 0.5) / props.data.length) * 100}%`,
"--market-tooltip-right": `${100 - ((props.activeIndex + 0.5) / props.data.length) * 100}%`,
} as JSX.CSSProperties
}
>
<strong>{day.date}</strong>
<span>
{formatTrillions(day.total)} {i18n.t("home.total")}
</span>
<div data-slot="tooltip-divider" />
<For each={rankedMarketAuthors(day)}>
{(item, index) => (
<p
data-active={props.activeAuthor === item.author ? "true" : undefined}
data-muted={
props.activeAuthor !== undefined && props.activeAuthor !== item.author ? "true" : undefined
}
>
<span data-slot="tooltip-label">
<i style={{ background: getRankColor(item.author, index(), props.authorOrder, marketColors) }} />
<span data-slot="tooltip-name">{item.author}</span>
</span>
<em>{formatTrillions(item.tokens)}</em>
<b>{item.share.toFixed(1)}%</b>
</p>
)}
</For>
</div>
)}
</Show>
</div>
)
}
@@ -1248,6 +1293,10 @@ function getMarketSegmentColor(author: string, color: string, activeAuthor: stri
return "var(--stats-bar-idle)"
}
function rankedMarketAuthors(day: MarketDay) {
return day.authors.toSorted((a, b) => b.tokens - a.tokens || a.author.localeCompare(b.author))
}
function stackedMarketAuthors(day: MarketDay, order: Map<string, number>) {
return day.authors
.map((author, index) => ({ author, index }))
@@ -1302,16 +1351,6 @@ function formatMarketDate(day: MarketDay | undefined, fallback: string) {
return formatMarketDateLabel(day.date)
}
function formatMarketRange(data: MarketDay[], fallback: string) {
const first = data[0]?.date
const last = data[data.length - 1]?.date
if (!first || !last) return fallback
const start = marketDateParts(first).start
const end = marketDateParts(last).end
if (start === end) return formatMarketDateLabel(start)
return `${start} ${new Date().getFullYear()} → ${end} ${new Date().getFullYear()}`
}
function formatMarketDateLabel(label: string) {
const parts = marketDateParts(label)
const year = new Date().getFullYear()