🎨 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:
@@ -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)
|
||||||
|
|||||||
+15
-14
@@ -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'>
|
||||||
|
|||||||
Reference in New Issue
Block a user