feat: add historical, upscaler, and boxplot chart components

This commit is contained in:
2026-04-26 15:45:26 +08:00
parent b4674be213
commit 20755fd0fe
3 changed files with 243 additions and 0 deletions
+77
View File
@@ -0,0 +1,77 @@
"use client"
import { useMemo } from "react"
import { EChartWrapper, CHART_THEME, getDeviceColor } from "./EChartWrapper"
import type { EChartsOption } from "echarts"
interface BoxplotEntry {
hardwareSlug: string
hardwareName: string
min: number
q1: number
median: number
q3: number
max: number
}
export function FpsBoxplot({
data,
className,
}: {
data: BoxplotEntry[]
className?: string
}) {
const option = useMemo<EChartsOption>(() => {
const categories = data.map((d) => d.hardwareName)
const boxData = data.map((d) => [d.min, d.q1, d.median, d.q3, d.max])
return {
tooltip: {
trigger: "item",
backgroundColor: "#1a1025",
borderColor: CHART_THEME.border,
textStyle: { color: CHART_THEME.text, fontSize: 12 },
formatter: (params) => {
const d = (params as { data: number[] }).data
if (!Array.isArray(d)) return ""
return `Min: ${d[0]}<br/>Q1: ${d[1]}<br/>Median: ${d[2]}<br/>Q3: ${d[3]}<br/>Max: ${d[4]}`
},
},
grid: { top: 16, right: 16, bottom: 24, left: 40 },
xAxis: {
type: "category",
data: categories,
axisLabel: { color: CHART_THEME.textMuted, fontSize: 10 },
axisLine: { lineStyle: { color: CHART_THEME.border } },
},
yAxis: {
type: "value",
name: "FPS",
nameTextStyle: { color: CHART_THEME.textMuted, fontSize: 10 },
axisLabel: { color: CHART_THEME.textMuted, fontSize: 10 },
splitLine: { lineStyle: { color: CHART_THEME.border, opacity: 0.3 } },
},
series: [
{
type: "boxplot",
data: boxData,
itemStyle: {
color: CHART_THEME.primary + "20",
borderColor: CHART_THEME.primary,
borderWidth: 2,
},
emphasis: {
itemStyle: {
borderColor: CHART_THEME.accent,
borderWidth: 3,
},
},
},
],
}
}, [data])
if (data.length === 0) return null
return <EChartWrapper option={option} height={220} className={className} />
}
+78
View File
@@ -0,0 +1,78 @@
"use client"
import { useMemo } from "react"
import { EChartWrapper, CHART_THEME, getDeviceColor } from "./EChartWrapper"
import type { EChartsOption } from "echarts"
interface HistoricalEntry {
period: string
entries: Array<{
hardwareSlug: string
avgFps: number
count: number
}>
}
export function HistoricalAreaChart({
data,
className,
}: {
data: HistoricalEntry[]
className?: string
}) {
const option = useMemo<EChartsOption>(() => {
const periods = data.map((d) => d.period)
const deviceSlugs = [
...new Set(data.flatMap((d) => d.entries.map((e) => e.hardwareSlug))),
]
const series = deviceSlugs.map((slug, idx) => ({
name: slug.replace(/-/g, " "),
type: "line" as const,
stack: "total",
areaStyle: { opacity: 0.3 },
emphasis: { focus: "series" as const },
smooth: true,
data: periods.map((period) => {
const entry = data
.find((d) => d.period === period)
?.entries.find((e) => e.hardwareSlug === slug)
return entry?.avgFps ?? null
}),
itemStyle: { color: getDeviceColor(idx) },
lineStyle: { color: getDeviceColor(idx) },
}))
return {
tooltip: {
trigger: "axis",
backgroundColor: "#1a1025",
borderColor: CHART_THEME.border,
textStyle: { color: CHART_THEME.text, fontSize: 12 },
},
legend: {
top: 0,
textStyle: { color: CHART_THEME.textMuted, fontSize: 11 },
},
grid: { top: 30, right: 16, bottom: 24, left: 40 },
xAxis: {
type: "category",
data: periods,
axisLabel: { color: CHART_THEME.textMuted, fontSize: 10 },
axisLine: { lineStyle: { color: CHART_THEME.border } },
},
yAxis: {
type: "value",
name: "AVG FPS",
nameTextStyle: { color: CHART_THEME.textMuted, fontSize: 10 },
axisLabel: { color: CHART_THEME.textMuted, fontSize: 10 },
splitLine: { lineStyle: { color: CHART_THEME.border, opacity: 0.3 } },
},
series,
}
}, [data])
if (data.length === 0) return null
return <EChartWrapper option={option} height={280} className={className} />
}
+88
View File
@@ -0,0 +1,88 @@
"use client"
import { useMemo } from "react"
import { EChartWrapper, CHART_THEME, getDeviceColor } from "./EChartWrapper"
import type { EChartsOption } from "echarts"
interface UpscalerStat {
fsrVersion: string
frameGenMethod: string
hardwareSlug: string
avgFps: number
count: number
}
function formatCombo(fsr: string, fg: string): string {
const parts: string[] = []
if (fsr !== "none") parts.push(fsr.toUpperCase())
if (fg !== "none") parts.push(fg === "fsr_fg" ? "FSR FG" : "DLSS FG")
return parts.length > 0 ? parts.join(" + ") : "Native"
}
export function UpscalerBarChart({
data,
className,
}: {
data: UpscalerStat[]
className?: string
}) {
const option = useMemo<EChartsOption>(() => {
const combos = [
...new Set(
data.map((d) => formatCombo(d.fsrVersion, d.frameGenMethod)),
),
]
const deviceSlugs = [...new Set(data.map((d) => d.hardwareSlug))]
const series = deviceSlugs.map((slug, idx) => ({
name: slug.replace(/-/g, " "),
type: "bar" as const,
data: combos.map((combo) => {
const match = data.find(
(d) =>
formatCombo(d.fsrVersion, d.frameGenMethod) === combo &&
d.hardwareSlug === slug,
)
return match?.avgFps ?? 0
}),
itemStyle: { color: getDeviceColor(idx), borderRadius: [4, 4, 0, 0] },
barGap: "10%",
}))
return {
tooltip: {
trigger: "axis",
backgroundColor: "#1a1025",
borderColor: CHART_THEME.border,
textStyle: { color: CHART_THEME.text, fontSize: 12 },
},
legend: {
top: 0,
textStyle: { color: CHART_THEME.textMuted, fontSize: 11 },
},
grid: { top: 30, right: 16, bottom: 50, left: 40 },
xAxis: {
type: "category",
data: combos,
axisLabel: {
color: CHART_THEME.textMuted,
fontSize: 10,
rotate: 30,
},
axisLine: { lineStyle: { color: CHART_THEME.border } },
},
yAxis: {
type: "value",
name: "AVG FPS",
nameTextStyle: { color: CHART_THEME.textMuted, fontSize: 10 },
axisLabel: { color: CHART_THEME.textMuted, fontSize: 10 },
splitLine: { lineStyle: { color: CHART_THEME.border, opacity: 0.3 } },
},
series,
}
}, [data])
if (data.length === 0) return null
return <EChartWrapper option={option} height={280} className={className} />
}