feat: add historical, upscaler, and boxplot chart components
This commit is contained in:
@@ -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} />
|
||||||
|
}
|
||||||
@@ -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} />
|
||||||
|
}
|
||||||
@@ -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} />
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user