mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
feat(stats): improve market share chart (#47115)
This commit is contained in:
@@ -2195,6 +2195,7 @@ body {
|
|||||||
|
|
||||||
[data-page="stats"] [data-component="market-share"] {
|
[data-page="stats"] [data-component="market-share"] {
|
||||||
--market-gap: 12px;
|
--market-gap: 12px;
|
||||||
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto minmax(0, 1fr);
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -2360,6 +2361,106 @@ body {
|
|||||||
font-weight: 500;
|
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"] {
|
[data-page="stats"] [data-slot="market-footer"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -7208,6 +7309,11 @@ body {
|
|||||||
[data-page="stats"][data-theme="dark"]
|
[data-page="stats"][data-theme="dark"]
|
||||||
:is([data-section="top-models"], [data-section="unique-users"])
|
:is([data-section="top-models"], [data-section="unique-users"])
|
||||||
[data-component="chart-tooltip"],
|
[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"]
|
:root[data-stats-theme="dark"]
|
||||||
[data-page="stats"]:not([data-theme="light"])
|
[data-page="stats"]:not([data-theme="light"])
|
||||||
:is([data-section="top-models"], [data-section="unique-users"])
|
:is([data-section="top-models"], [data-section="unique-users"])
|
||||||
@@ -8560,6 +8666,25 @@ body {
|
|||||||
transform: none;
|
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"]
|
[data-page="stats"]
|
||||||
:is([data-section="top-models"], [data-section="unique-users"])
|
:is([data-section="top-models"], [data-section="unique-users"])
|
||||||
[data-component="chart-tooltip"][data-placement] {
|
[data-component="chart-tooltip"][data-placement] {
|
||||||
|
|||||||
@@ -928,7 +928,7 @@ function MarketShareSection(props: { data: MarketDay[] }) {
|
|||||||
const [inspecting, setInspecting] = createSignal(false)
|
const [inspecting, setInspecting] = createSignal(false)
|
||||||
const authorOrder = createMemo(() => getMarketAuthorOrder(props.data))
|
const authorOrder = createMemo(() => getMarketAuthorOrder(props.data))
|
||||||
const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(props.data.length - 1, 0)))
|
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 (
|
return (
|
||||||
<section
|
<section
|
||||||
@@ -947,7 +947,7 @@ function MarketShareSection(props: { data: MarketDay[] }) {
|
|||||||
description={i18n.t("home.marketShareDescription")}
|
description={i18n.t("home.marketShareDescription")}
|
||||||
/>
|
/>
|
||||||
<Show
|
<Show
|
||||||
when={activeDay()}
|
when={today()}
|
||||||
fallback={<EmptyState title={i18n.t("home.noMarketTitle")} description={i18n.t("home.noMarketDescription")} />}
|
fallback={<EmptyState title={i18n.t("home.noMarketTitle")} description={i18n.t("home.noMarketDescription")} />}
|
||||||
>
|
>
|
||||||
{(day) => (
|
{(day) => (
|
||||||
@@ -963,19 +963,14 @@ function MarketShareSection(props: { data: MarketDay[] }) {
|
|||||||
setActiveIndex(index)
|
setActiveIndex(index)
|
||||||
setInspecting(true)
|
setInspecting(true)
|
||||||
}}
|
}}
|
||||||
onActiveAuthorChange={(author) => {
|
onActiveAuthorChange={setActiveAuthor}
|
||||||
setActiveAuthor(author)
|
onInspectingChange={setInspecting}
|
||||||
setInspecting(true)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<MarketShareList
|
<MarketShareList
|
||||||
data={day().authors}
|
data={rankedMarketAuthors(day())}
|
||||||
authorOrder={authorOrder()}
|
authorOrder={authorOrder()}
|
||||||
activeAuthor={activeAuthor()}
|
activeAuthor={activeAuthor()}
|
||||||
onActiveAuthorChange={(author) => {
|
onActiveAuthorChange={setActiveAuthor}
|
||||||
setActiveAuthor(author)
|
|
||||||
setInspecting(true)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -983,11 +978,7 @@ function MarketShareSection(props: { data: MarketDay[] }) {
|
|||||||
<div data-slot="market-footer">
|
<div data-slot="market-footer">
|
||||||
<p>
|
<p>
|
||||||
<span>[*]</span>
|
<span>[*]</span>
|
||||||
<strong>
|
<strong>{formatMarketDate(today(), i18n.t("home.noData"))}</strong>
|
||||||
{inspecting()
|
|
||||||
? formatMarketDate(activeDay(), i18n.t("home.noData"))
|
|
||||||
: formatMarketRange(props.data, i18n.t("home.noData"))}
|
|
||||||
</strong>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -1002,10 +993,15 @@ function MarketShare(props: {
|
|||||||
activeAuthor: string | undefined
|
activeAuthor: string | undefined
|
||||||
inspecting: boolean
|
inspecting: boolean
|
||||||
onActiveIndexChange: (index: number) => void
|
onActiveIndexChange: (index: number) => void
|
||||||
onActiveAuthorChange: (author: string) => void
|
onActiveAuthorChange: (author: string | undefined) => void
|
||||||
|
onInspectingChange: (inspecting: boolean) => void
|
||||||
}) {
|
}) {
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
let chartRef: HTMLDivElement | undefined
|
let chartRef: HTMLDivElement | undefined
|
||||||
|
const inspectDay = (index: number) => {
|
||||||
|
props.onActiveIndexChange(index)
|
||||||
|
props.onActiveAuthorChange(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
createEffect(() => scrollDenseChartToEnd(chartRef, props.range, props.data.length))
|
createEffect(() => scrollDenseChartToEnd(chartRef, props.range, props.data.length))
|
||||||
|
|
||||||
@@ -1018,6 +1014,10 @@ function MarketShare(props: {
|
|||||||
role="img"
|
role="img"
|
||||||
aria-label={i18n.t("home.marketChart")}
|
aria-label={i18n.t("home.marketChart")}
|
||||||
style={{ "--market-count": props.data.length } as JSX.CSSProperties}
|
style={{ "--market-count": props.data.length } as JSX.CSSProperties}
|
||||||
|
onPointerLeave={(event) => {
|
||||||
|
if (event.pointerType === "touch") return
|
||||||
|
props.onInspectingChange(false)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div data-slot="market-labels">
|
<div data-slot="market-labels">
|
||||||
<For each={props.data}>
|
<For each={props.data}>
|
||||||
@@ -1028,8 +1028,11 @@ function MarketShare(props: {
|
|||||||
data-active={props.inspecting && props.activeIndex === index() ? "true" : undefined}
|
data-active={props.inspecting && props.activeIndex === index() ? "true" : undefined}
|
||||||
data-label-hidden={isColumnLabelHidden(index(), props.data.length) ? "true" : undefined}
|
data-label-hidden={isColumnLabelHidden(index(), props.data.length) ? "true" : undefined}
|
||||||
data-mobile-hidden={isMarketMobileLabelHidden(index(), props.data.length) ? "true" : undefined}
|
data-mobile-hidden={isMarketMobileLabelHidden(index(), props.data.length) ? "true" : undefined}
|
||||||
onClick={() => props.onActiveIndexChange(index())}
|
aria-describedby={props.inspecting && props.activeIndex === index() ? "market-share-tooltip" : undefined}
|
||||||
onPointerEnter={() => props.onActiveIndexChange(index())}
|
onBlur={() => props.onInspectingChange(false)}
|
||||||
|
onClick={() => inspectDay(index())}
|
||||||
|
onFocus={() => inspectDay(index())}
|
||||||
|
onPointerEnter={() => inspectDay(index())}
|
||||||
>
|
>
|
||||||
<span data-slot="market-axis-label">
|
<span data-slot="market-axis-label">
|
||||||
<span data-slot="market-total">{formatTrillions(day.total)}</span>
|
<span data-slot="market-total">{formatTrillions(day.total)}</span>
|
||||||
@@ -1049,8 +1052,11 @@ function MarketShare(props: {
|
|||||||
type="button"
|
type="button"
|
||||||
aria-label={`${day.date} ${formatTrillions(day.total)}`}
|
aria-label={`${day.date} ${formatTrillions(day.total)}`}
|
||||||
data-active={props.inspecting && props.activeIndex === index() ? "true" : undefined}
|
data-active={props.inspecting && props.activeIndex === index() ? "true" : undefined}
|
||||||
onClick={() => props.onActiveIndexChange(index())}
|
aria-describedby={props.inspecting && props.activeIndex === index() ? "market-share-tooltip" : undefined}
|
||||||
onPointerEnter={() => props.onActiveIndexChange(index())}
|
onBlur={() => props.onInspectingChange(false)}
|
||||||
|
onClick={() => inspectDay(index())}
|
||||||
|
onFocus={() => inspectDay(index())}
|
||||||
|
onPointerEnter={() => inspectDay(index())}
|
||||||
>
|
>
|
||||||
<For each={stackedMarketAuthors(day, props.authorOrder)}>
|
<For each={stackedMarketAuthors(day, props.authorOrder)}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
@@ -1090,6 +1096,45 @@ function MarketShare(props: {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1248,6 +1293,10 @@ function getMarketSegmentColor(author: string, color: string, activeAuthor: stri
|
|||||||
return "var(--stats-bar-idle)"
|
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>) {
|
function stackedMarketAuthors(day: MarketDay, order: Map<string, number>) {
|
||||||
return day.authors
|
return day.authors
|
||||||
.map((author, index) => ({ author, index }))
|
.map((author, index) => ({ author, index }))
|
||||||
@@ -1302,16 +1351,6 @@ function formatMarketDate(day: MarketDay | undefined, fallback: string) {
|
|||||||
return formatMarketDateLabel(day.date)
|
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) {
|
function formatMarketDateLabel(label: string) {
|
||||||
const parts = marketDateParts(label)
|
const parts = marketDateParts(label)
|
||||||
const year = new Date().getFullYear()
|
const year = new Date().getFullYear()
|
||||||
|
|||||||
Reference in New Issue
Block a user