🎨 fix(charts): improve dark mode chart readability

Ensure VChart labels and grid lines use theme-aware colors, and remove oversized rounded corners from ranking bar charts.
This commit is contained in:
CaIon
2026-05-26 12:30:13 +08:00
parent 9e283ab10b
commit f223db9330
3 changed files with 68 additions and 41 deletions
@@ -47,6 +47,19 @@ function formatDayLabel(date: string): string {
}) })
} }
function getChartThemeTokens(resolvedTheme: string) {
return {
textColor:
resolvedTheme === 'dark'
? 'rgba(255, 255, 255, 0.68)'
: 'rgba(15, 23, 42, 0.58)',
gridColor:
resolvedTheme === 'dark'
? 'rgba(255, 255, 255, 0.12)'
: 'rgba(15, 23, 42, 0.12)',
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Latency trend chart (24h, multi-group point-line chart) // Latency trend chart (24h, multi-group point-line chart)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -57,6 +70,7 @@ export function LatencyTrendChart(props: {
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { resolvedTheme, themeReady } = useChartTheme() const { resolvedTheme, themeReady } = useChartTheme()
const { textColor, gridColor } = getChartThemeTokens(resolvedTheme)
const spec = useMemo(() => { const spec = useMemo(() => {
if (props.series.length === 0) return null if (props.series.length === 0) return null
@@ -95,7 +109,7 @@ export function LatencyTrendChart(props: {
{ {
orient: 'bottom', orient: 'bottom',
label: { label: {
style: { fill: 'currentColor', fontSize: 10 }, style: { fill: textColor, fontSize: 10 },
}, },
tick: { visible: false }, tick: { visible: false },
}, },
@@ -103,13 +117,16 @@ export function LatencyTrendChart(props: {
orient: 'left', orient: 'left',
label: { label: {
formatMethod: (val: number | string) => `${val} ms`, formatMethod: (val: number | string) => `${val} ms`,
style: { fill: 'currentColor', fontSize: 10 }, style: { fill: textColor, fontSize: 10 },
},
grid: {
visible: true,
style: { lineDash: [3, 3], stroke: gridColor },
}, },
grid: { visible: true, style: { lineDash: [3, 3] } },
}, },
], ],
} }
}, [props.series, t]) }, [gridColor, props.series, t, textColor])
if (props.series.length === 0) { if (props.series.length === 0) {
return ( return (
@@ -151,6 +168,7 @@ export function UptimeTrendChart(props: {
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { resolvedTheme, themeReady } = useChartTheme() const { resolvedTheme, themeReady } = useChartTheme()
const { textColor, gridColor } = getChartThemeTokens(resolvedTheme)
const spec = useMemo(() => { const spec = useMemo(() => {
if (props.series.length === 0) return null if (props.series.length === 0) return null
@@ -207,7 +225,7 @@ export function UptimeTrendChart(props: {
{ {
orient: 'bottom', orient: 'bottom',
label: { label: {
style: { fill: 'currentColor', fontSize: 10 }, style: { fill: textColor, fontSize: 10 },
autoLimit: true, autoLimit: true,
}, },
tick: { visible: false }, tick: { visible: false },
@@ -218,13 +236,16 @@ export function UptimeTrendChart(props: {
max: 100, max: 100,
label: { label: {
formatMethod: (val: number | string) => `${val}%`, formatMethod: (val: number | string) => `${val}%`,
style: { fill: 'currentColor', fontSize: 10 }, style: { fill: textColor, fontSize: 10 },
},
grid: {
visible: true,
style: { lineDash: [3, 3], stroke: gridColor },
}, },
grid: { visible: true, style: { lineDash: [3, 3] } },
}, },
], ],
} }
}, [props.series, t]) }, [gridColor, props.series, t, textColor])
if (props.series.length === 0) { if (props.series.length === 0) {
return ( return (
@@ -266,6 +287,7 @@ export function ThroughputBarChart(props: {
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { resolvedTheme, themeReady } = useChartTheme() const { resolvedTheme, themeReady } = useChartTheme()
const { textColor, gridColor } = getChartThemeTokens(resolvedTheme)
const { customization } = useThemeCustomization() const { customization } = useThemeCustomization()
const barRadius = useThemeRadiusPx( const barRadius = useThemeRadiusPx(
'--radius-sm', '--radius-sm',
@@ -294,19 +316,22 @@ export function ThroughputBarChart(props: {
label: { label: {
visible: true, visible: true,
position: 'right', position: 'right',
style: { fontSize: 11, fill: 'currentColor' }, style: { fontSize: 11, fill: textColor },
formatMethod: (text: string) => `${text} t/s`, formatMethod: (text: string) => `${text} t/s`,
}, },
axes: [ axes: [
{ {
orient: 'left', orient: 'left',
label: { style: { fill: 'currentColor', fontSize: 10 } }, label: { style: { fill: textColor, fontSize: 10 } },
tick: { visible: false }, tick: { visible: false },
}, },
{ {
orient: 'bottom', orient: 'bottom',
label: { style: { fill: 'currentColor', fontSize: 10 } }, label: { style: { fill: textColor, fontSize: 10 } },
grid: { visible: true, style: { lineDash: [3, 3] } }, grid: {
visible: true,
style: { lineDash: [3, 3], stroke: gridColor },
},
}, },
], ],
tooltip: { tooltip: {
@@ -322,7 +347,7 @@ export function ThroughputBarChart(props: {
}, },
}, },
} }
}, [barRadius, filtered, t]) }, [barRadius, filtered, gridColor, t, textColor])
if (filtered.length === 0) { if (filtered.length === 0) {
return null return null
@@ -20,10 +20,8 @@ import { useMemo } from 'react'
import { VChart } from '@visactor/react-vchart' import { VChart } from '@visactor/react-vchart'
import { PieChart } from 'lucide-react' import { PieChart } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useThemeRadiusPx } from '@/lib/theme-radius'
import { useChartTheme } from '@/lib/use-chart-theme' import { useChartTheme } from '@/lib/use-chart-theme'
import { VCHART_OPTION } from '@/lib/vchart' import { VCHART_OPTION } from '@/lib/vchart'
import { useThemeCustomization } from '@/context/theme-customization-provider'
import { formatShare, formatTokens } from '../lib/format' import { formatShare, formatTokens } from '../lib/format'
import type { RankingPeriod, VendorRanking, VendorShareSeries } from '../types' import type { RankingPeriod, VendorRanking, VendorShareSeries } from '../types'
import { VendorLink } from './entity-links' import { VendorLink } from './entity-links'
@@ -104,11 +102,14 @@ type MarketShareSectionProps = {
export function MarketShareSection(props: MarketShareSectionProps) { export function MarketShareSection(props: MarketShareSectionProps) {
const { t } = useTranslation() const { t } = useTranslation()
const { resolvedTheme, themeReady } = useChartTheme() const { resolvedTheme, themeReady } = useChartTheme()
const { customization } = useThemeCustomization() const chartTextColor =
const barRadius = useThemeRadiusPx( resolvedTheme === 'dark'
'--radius-sm', ? 'rgba(255, 255, 255, 0.68)'
`${customization.preset}:${customization.radius}` : 'rgba(15, 23, 42, 0.58)'
) const chartGridColor =
resolvedTheme === 'dark'
? 'rgba(255, 255, 255, 0.12)'
: 'rgba(15, 23, 42, 0.12)'
const colourMap = useMemo( const colourMap = useMemo(
() => buildVendorColourMap(props.history.vendors.map((v) => v.name)), () => buildVendorColourMap(props.history.vendors.map((v) => v.name)),
@@ -137,15 +138,12 @@ export function MarketShareSection(props: MarketShareSectionProps) {
stack: true, stack: true,
paddingInner: 0.12, paddingInner: 0.12,
legends: { visible: false }, legends: { visible: false },
bar: {
style: barRadius == null ? {} : { cornerRadius: barRadius },
},
color: { specified: colourMap }, color: { specified: colourMap },
axes: [ axes: [
{ {
orient: 'bottom', orient: 'bottom',
label: { label: {
style: { fill: 'currentColor', fontSize: 10 }, style: { fill: chartTextColor, fontSize: 10 },
autoHide: true, autoHide: true,
autoLimit: true, autoLimit: true,
}, },
@@ -158,9 +156,12 @@ export function MarketShareSection(props: MarketShareSectionProps) {
label: { label: {
formatMethod: (val: number | string) => formatMethod: (val: number | string) =>
`${Math.round(Number(val) * 100)}%`, `${Math.round(Number(val) * 100)}%`,
style: { fill: 'currentColor', fontSize: 10 }, style: { fill: chartTextColor, fontSize: 10 },
},
grid: {
visible: true,
style: { lineDash: [3, 3], stroke: chartGridColor },
}, },
grid: { visible: true, style: { lineDash: [3, 3] } },
}, },
], ],
tooltip: { tooltip: {
@@ -202,7 +203,7 @@ export function MarketShareSection(props: MarketShareSectionProps) {
}, },
animationAppear: { duration: 500 }, animationAppear: { duration: 500 },
} }
}, [barRadius, colourMap, orderedPoints]) }, [chartGridColor, chartTextColor, colourMap, orderedPoints])
const visible = props.rows.slice(0, MAX_VENDORS_IN_LIST) const visible = props.rows.slice(0, MAX_VENDORS_IN_LIST)
const half = Math.ceil(visible.length / 2) const half = Math.ceil(visible.length / 2)
@@ -20,10 +20,8 @@ import { useMemo } from 'react'
import { VChart } from '@visactor/react-vchart' import { VChart } from '@visactor/react-vchart'
import { BarChart3, Trophy } from 'lucide-react' import { BarChart3, Trophy } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useThemeRadiusPx } from '@/lib/theme-radius'
import { useChartTheme } from '@/lib/use-chart-theme' import { useChartTheme } from '@/lib/use-chart-theme'
import { VCHART_OPTION } from '@/lib/vchart' import { VCHART_OPTION } from '@/lib/vchart'
import { useThemeCustomization } from '@/context/theme-customization-provider'
import { formatTokens } from '../lib/format' import { formatTokens } from '../lib/format'
import type { ModelHistorySeries, ModelRanking, RankingPeriod } from '../types' import type { ModelHistorySeries, ModelRanking, RankingPeriod } from '../types'
import { ModelLeaderboard } from './model-leaderboard' import { ModelLeaderboard } from './model-leaderboard'
@@ -52,11 +50,14 @@ type ModelsSectionProps = {
export function ModelsSection(props: ModelsSectionProps) { export function ModelsSection(props: ModelsSectionProps) {
const { t } = useTranslation() const { t } = useTranslation()
const { resolvedTheme, themeReady } = useChartTheme() const { resolvedTheme, themeReady } = useChartTheme()
const { customization } = useThemeCustomization() const chartTextColor =
const barRadius = useThemeRadiusPx( resolvedTheme === 'dark'
'--radius-sm', ? 'rgba(255, 255, 255, 0.68)'
`${customization.preset}:${customization.radius}` : 'rgba(15, 23, 42, 0.58)'
) const chartGridColor =
resolvedTheme === 'dark'
? 'rgba(255, 255, 255, 0.12)'
: 'rgba(15, 23, 42, 0.12)'
// Order points so the largest model appears at the bottom of every stack. // Order points so the largest model appears at the bottom of every stack.
const orderedPoints = useMemo(() => { const orderedPoints = useMemo(() => {
@@ -84,15 +85,12 @@ export function ModelsSection(props: ModelsSectionProps) {
yField: 'tokens', yField: 'tokens',
seriesField: 'model', seriesField: 'model',
stack: true, stack: true,
bar: {
style: barRadius == null ? {} : { cornerRadius: barRadius },
},
legends: { visible: false }, legends: { visible: false },
axes: [ axes: [
{ {
orient: 'bottom', orient: 'bottom',
label: { label: {
style: { fill: 'currentColor', fontSize: 10 }, style: { fill: chartTextColor, fontSize: 10 },
autoHide: true, autoHide: true,
autoLimit: true, autoLimit: true,
}, },
@@ -102,9 +100,12 @@ export function ModelsSection(props: ModelsSectionProps) {
orient: 'left', orient: 'left',
label: { label: {
formatMethod: (val: number | string) => formatTokens(Number(val)), formatMethod: (val: number | string) => formatTokens(Number(val)),
style: { fill: 'currentColor', fontSize: 10 }, style: { fill: chartTextColor, fontSize: 10 },
},
grid: {
visible: true,
style: { lineDash: [3, 3], stroke: chartGridColor },
}, },
grid: { visible: true, style: { lineDash: [3, 3] } },
}, },
], ],
tooltip: { tooltip: {
@@ -159,7 +160,7 @@ export function ModelsSection(props: ModelsSectionProps) {
}, },
animationAppear: { duration: 500 }, animationAppear: { duration: 500 },
} }
}, [barRadius, orderedPoints, t]) }, [chartGridColor, chartTextColor, orderedPoints, t])
return ( return (
<section className='bg-card overflow-hidden rounded-lg border'> <section className='bg-card overflow-hidden rounded-lg border'>