|
|
@@ -16,12 +16,10 @@ import {
|
|
|
import Papa from 'papaparse';
|
|
|
import { API_BASE } from '../config';
|
|
|
|
|
|
-const PerformanceCard = ({ title, height = 300, children }) => (
|
|
|
- <div className="mb-8 last:mb-0">
|
|
|
- <div className="flex items-center justify-between mb-4">
|
|
|
- <h3 className="text-sm font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">{title}</h3>
|
|
|
- </div>
|
|
|
- <div className="theme-surface border theme-border rounded-xl p-4 shadow-sm h-[300px]">
|
|
|
+const PerformanceCard = ({ title, children }) => (
|
|
|
+ <div className="mb-6 last:mb-0">
|
|
|
+ <h3 className="text-[10px] md:text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2 px-1">{title}</h3>
|
|
|
+ <div className="theme-surface border theme-border rounded-xl p-2 md:p-5 shadow-sm h-[280px] md:h-[380px]">
|
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
|
{children}
|
|
|
</ResponsiveContainer>
|
|
|
@@ -32,14 +30,16 @@ const PerformanceCard = ({ title, height = 300, children }) => (
|
|
|
const CustomTooltip = ({ active, payload, label }) => {
|
|
|
if (active && payload && payload.length) {
|
|
|
return (
|
|
|
- <div className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-3 rounded-lg shadow-xl text-xs">
|
|
|
- <p className="font-bold mb-2 border-b border-gray-100 dark:border-gray-800 pb-1">Sample {label}</p>
|
|
|
- {payload.map((entry, index) => (
|
|
|
- <div key={index} className="flex items-center justify-between gap-4 my-1">
|
|
|
- <span style={{ color: entry.color }} className="font-medium">{entry.name}:</span>
|
|
|
- <span className="font-mono text-gray-600 dark:text-gray-300">{entry.value.toLocaleString()}</span>
|
|
|
- </div>
|
|
|
- ))}
|
|
|
+ <div className="bg-white/95 dark:bg-gray-900/95 backdrop-blur-sm border border-gray-200 dark:border-gray-700 p-2 md:p-3 rounded-lg shadow-xl text-[10px] md:text-[12px]">
|
|
|
+ <p className="font-bold mb-1 border-b border-gray-100 dark:border-gray-800 pb-1">Sample {label}</p>
|
|
|
+ <div className="max-h-64 md:max-h-[600px] overflow-y-auto pr-1">
|
|
|
+ {payload.map((entry, index) => (
|
|
|
+ <div key={index} className="flex items-center justify-between gap-4 my-0.5">
|
|
|
+ <span style={{ color: entry.color }} className="font-medium truncate max-w-[100px] md:max-w-none">{entry.name}:</span>
|
|
|
+ <span className="font-mono text-gray-600 dark:text-gray-300">{entry.value.toLocaleString()}</span>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
@@ -51,6 +51,14 @@ const CsvGraph = ({ rawData }) => {
|
|
|
const [columns, setColumns] = useState([]);
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
const [error, setError] = useState(null);
|
|
|
+ const [isMobile, setIsMobile] = useState(false);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ const checkMobile = () => setIsMobile(window.innerWidth < 768);
|
|
|
+ checkMobile();
|
|
|
+ window.addEventListener('resize', checkMobile);
|
|
|
+ return () => window.removeEventListener('resize', checkMobile);
|
|
|
+ }, []);
|
|
|
|
|
|
useEffect(() => {
|
|
|
const parseData = async () => {
|
|
|
@@ -165,7 +173,7 @@ const CsvGraph = ({ rawData }) => {
|
|
|
minFps,
|
|
|
fps5PercentLows,
|
|
|
above45Percent,
|
|
|
- avgPower: avg(groups.power.power),
|
|
|
+ avgPower: avg(groups.power.power) ? avg(groups.power.power) / 1000 : null,
|
|
|
maxTemp: max(groups.power.temp)
|
|
|
};
|
|
|
}, [data, groups]);
|
|
|
@@ -180,10 +188,10 @@ const CsvGraph = ({ rawData }) => {
|
|
|
const seconds = totalSeconds % 60;
|
|
|
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
|
|
}}
|
|
|
- tick={{ fontSize: 9 }}
|
|
|
+ tick={{ fontSize: isMobile ? 8 : 10 }}
|
|
|
stroke="#9ca3af"
|
|
|
interval="preserveStartEnd"
|
|
|
- minTickGap={40}
|
|
|
+ minTickGap={isMobile ? 30 : 60}
|
|
|
/>
|
|
|
);
|
|
|
|
|
|
@@ -206,33 +214,33 @@ const CsvGraph = ({ rawData }) => {
|
|
|
const isGeneric = !groups.fps.fps && !groups.cpu.total && !groups.power.power;
|
|
|
|
|
|
return (
|
|
|
- <div className="w-full space-y-6">
|
|
|
+ <div className="w-full space-y-4 md:space-y-6">
|
|
|
{/* Summary Stats */}
|
|
|
{stats && (
|
|
|
- <div className="grid grid-cols-3 md:grid-cols-6 bg-gray-100 dark:bg-gray-800/40 rounded-lg divide-x divide-y md:divide-y-0 divide-gray-200 dark:divide-gray-700 border theme-border overflow-hidden">
|
|
|
- <div className="text-center py-2 px-1">
|
|
|
- <p className="text-[9px] font-bold text-gray-500 uppercase tracking-tighter opacity-70">Avg FPS</p>
|
|
|
- <p className="text-lg font-black theme-text leading-none mt-1">{stats.avgFps !== null ? stats.avgFps.toFixed(1) : '-'}</p>
|
|
|
+ <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 bg-gray-100 dark:bg-gray-800/40 rounded-lg divide-x divide-y divide-gray-200 dark:divide-gray-700 border theme-border overflow-hidden">
|
|
|
+ <div className="text-center py-2 md:py-4 px-1 border-t-0 border-l-0">
|
|
|
+ <p className="text-[8px] md:text-[10px] font-bold text-gray-500 uppercase tracking-tighter opacity-70">Avg FPS</p>
|
|
|
+ <p className="text-base md:text-2xl font-black theme-text leading-none mt-1">{stats.avgFps !== null ? stats.avgFps.toFixed(1) : '-'}</p>
|
|
|
</div>
|
|
|
- <div className="text-center py-2 px-1">
|
|
|
- <p className="text-[9px] font-bold text-gray-500 uppercase tracking-tighter opacity-70">Min FPS</p>
|
|
|
- <p className="text-lg font-black theme-text leading-none mt-1">{stats.minFps !== null ? stats.minFps.toFixed(1) : '-'}</p>
|
|
|
+ <div className="text-center py-2 md:py-4 px-1 border-t-0">
|
|
|
+ <p className="text-[8px] md:text-[10px] font-bold text-gray-500 uppercase tracking-tighter opacity-70">Min FPS</p>
|
|
|
+ <p className="text-base md:text-2xl font-black theme-text leading-none mt-1">{stats.minFps !== null ? stats.minFps.toFixed(1) : '-'}</p>
|
|
|
</div>
|
|
|
- <div className="text-center py-2 px-1">
|
|
|
- <p className="text-[9px] font-bold text-gray-500 uppercase tracking-tighter opacity-70">5% Lows</p>
|
|
|
- <p className="text-lg font-black theme-text leading-none mt-1">{stats.fps5PercentLows !== null ? stats.fps5PercentLows.toFixed(1) : '-'}</p>
|
|
|
+ <div className="text-center py-2 md:py-4 px-1 sm:border-t-0 md:border-t-0">
|
|
|
+ <p className="text-[8px] md:text-[10px] font-bold text-gray-500 uppercase tracking-tighter opacity-70">5% Lows</p>
|
|
|
+ <p className="text-base md:text-2xl font-black theme-text leading-none mt-1">{stats.fps5PercentLows !== null ? stats.fps5PercentLows.toFixed(1) : '-'}</p>
|
|
|
</div>
|
|
|
- <div className="text-center py-2 px-1">
|
|
|
- <p className="text-[9px] font-bold text-gray-500 uppercase tracking-tighter opacity-70">≥45 FPS %</p>
|
|
|
- <p className="text-lg font-black theme-text leading-none mt-1">{stats.above45Percent !== null ? `${stats.above45Percent.toFixed(0)}%` : '-'}</p>
|
|
|
+ <div className="text-center py-2 md:py-4 px-1 md:border-t-0">
|
|
|
+ <p className="text-[8px] md:text-[10px] font-bold text-gray-500 uppercase tracking-tighter opacity-70">≥45 FPS %</p>
|
|
|
+ <p className="text-base md:text-2xl font-black theme-text leading-none mt-1">{stats.above45Percent !== null ? `${stats.above45Percent.toFixed(0)}%` : '-'}</p>
|
|
|
</div>
|
|
|
- <div className="text-center py-2 px-1">
|
|
|
- <p className="text-[9px] font-bold text-gray-500 uppercase tracking-tighter opacity-70">Avg Power</p>
|
|
|
- <p className="text-lg font-black theme-text leading-none mt-1">{stats.avgPower !== null ? `${stats.avgPower.toFixed(0)} mW` : '-'}</p>
|
|
|
+ <div className="text-center py-2 md:py-4 px-1 md:border-t-0">
|
|
|
+ <p className="text-[8px] md:text-[10px] font-bold text-gray-500 uppercase tracking-tighter opacity-70">Avg Power</p>
|
|
|
+ <p className="text-base md:text-2xl font-black theme-text leading-none mt-1">{stats.avgPower !== null ? `${stats.avgPower.toFixed(2)} W` : '-'}</p>
|
|
|
</div>
|
|
|
- <div className="text-center py-2 px-1">
|
|
|
- <p className="text-[9px] font-bold text-gray-500 uppercase tracking-tighter opacity-70">Max Temp</p>
|
|
|
- <p className="text-lg font-black text-red-500 leading-none mt-1">{stats.maxTemp !== null ? `${stats.maxTemp.toFixed(1)}°` : '-'}</p>
|
|
|
+ <div className="text-center py-2 md:py-4 px-1 md:border-t-0">
|
|
|
+ <p className="text-[8px] md:text-[10px] font-bold text-gray-500 uppercase tracking-tighter opacity-70">Max Temp</p>
|
|
|
+ <p className="text-base md:text-2xl font-black text-red-500 leading-none mt-1">{stats.maxTemp !== null ? `${stats.maxTemp.toFixed(1)}°` : '-'}</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
)}
|
|
|
@@ -240,21 +248,21 @@ const CsvGraph = ({ rawData }) => {
|
|
|
{/* 1. FPS & Stutter */}
|
|
|
{(groups.fps.fps || groups.fps.jank) && (
|
|
|
<PerformanceCard title="FPS and Stutter (Jank)">
|
|
|
- <ComposedChart data={data} margin={{ top: 10, right: 10, left: 10, bottom: 0 }}>
|
|
|
- <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e5e7eb" />
|
|
|
+ <ComposedChart data={data} margin={{ top: 5, right: 5, left: isMobile ? -20 : 0, bottom: 5 }}>
|
|
|
+ <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e5e7eb" opacity={0.3} />
|
|
|
{renderXAxis()}
|
|
|
- <YAxis domain={[0, 'auto']} tick={{ fontSize: 10 }} stroke="#9ca3af" />
|
|
|
+ <YAxis domain={[0, 'auto']} tick={{ fontSize: isMobile ? 8 : 11 }} stroke="#9ca3af" width={isMobile ? 40 : 50} />
|
|
|
<Tooltip content={<CustomTooltip />} />
|
|
|
- <Legend verticalAlign="top" height={36} />
|
|
|
+ <Legend verticalAlign="top" height={isMobile ? 24 : 40} iconSize={isMobile ? 8 : 12} wrapperStyle={{ fontSize: isMobile ? '9px' : '12px', paddingBottom: '5px' }} />
|
|
|
|
|
|
{groups.fps.fps && (
|
|
|
<Line type="monotone" dataKey={groups.fps.fps} stroke="#3b82f6" strokeWidth={1.5} dot={false} name="FPS" isAnimationActive={false} />
|
|
|
)}
|
|
|
{groups.fps.jank && (
|
|
|
- <Line type="monotone" dataKey={groups.fps.jank} stroke="#f59e0b" strokeWidth={0} dot={{ r: 3, fill: '#f59e0b' }} name="Jank" isAnimationActive={false} />
|
|
|
+ <Line type="monotone" dataKey={groups.fps.jank} stroke="#f59e0b" strokeWidth={0} dot={{ r: 2.5, fill: '#f59e0b' }} name="Jank" isAnimationActive={false} />
|
|
|
)}
|
|
|
{groups.fps.bigJank && (
|
|
|
- <Line type="monotone" dataKey={groups.fps.bigJank} stroke="#ef4444" strokeWidth={0} dot={{ r: 4, fill: '#ef4444', strokeWidth: 2, stroke: '#fff' }} name="BigJank" isAnimationActive={false} />
|
|
|
+ <Line type="monotone" dataKey={groups.fps.bigJank} stroke="#ef4444" strokeWidth={0} dot={{ r: isMobile ? 3 : 4, fill: '#ef4444', strokeWidth: 1.5, stroke: '#fff' }} name="BigJank" isAnimationActive={false} />
|
|
|
)}
|
|
|
</ComposedChart>
|
|
|
</PerformanceCard>
|
|
|
@@ -263,16 +271,26 @@ const CsvGraph = ({ rawData }) => {
|
|
|
{/* 2. CPU Usage & Clocks */}
|
|
|
{(groups.cpu.total || groups.cpu.clocks.length > 0) && (
|
|
|
<PerformanceCard title="CPU Usage and Clock Speeds">
|
|
|
- <ComposedChart data={data} margin={{ top: 10, right: 10, left: 10, bottom: 0 }}>
|
|
|
- <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e5e7eb" />
|
|
|
+ <ComposedChart data={data} margin={{ top: 5, right: 5, left: isMobile ? -20 : 0, bottom: 5 }}>
|
|
|
+ <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e5e7eb" opacity={0.3} />
|
|
|
{renderXAxis()}
|
|
|
- <YAxis yAxisId="left" domain={[0, 100]} tick={{ fontSize: 10 }} stroke="#10b981" name="Usage %" />
|
|
|
- <YAxis yAxisId="right" orientation="right" tick={{ fontSize: 10 }} stroke="#9ca3af" name="Clock (MHz)" />
|
|
|
+ <YAxis yAxisId="left" domain={[0, 100]} tick={{ fontSize: isMobile ? 8 : 11 }} stroke="#10b981" width={isMobile ? 30 : 45} />
|
|
|
+ <YAxis yAxisId="right" orientation="right" tick={{ fontSize: isMobile ? 8 : 11 }} stroke="#9ca3af" width={isMobile ? 30 : 45} />
|
|
|
<Tooltip content={<CustomTooltip />} />
|
|
|
- <Legend verticalAlign="top" height={36} wrapperStyle={{ fontSize: '10px' }} />
|
|
|
+ <Legend
|
|
|
+ verticalAlign="top"
|
|
|
+ height={isMobile ? 30 : 60}
|
|
|
+ iconSize={isMobile ? 6 : 10}
|
|
|
+ wrapperStyle={{
|
|
|
+ fontSize: isMobile ? '8px' : '11px',
|
|
|
+ paddingBottom: '5px',
|
|
|
+ overflowY: 'auto',
|
|
|
+ maxHeight: isMobile ? '40px' : '150px'
|
|
|
+ }}
|
|
|
+ />
|
|
|
|
|
|
{groups.cpu.total && (
|
|
|
- <Line yAxisId="left" type="monotone" dataKey={groups.cpu.total} stroke="#10b981" strokeWidth={2.5} dot={false} name="Total CPU %" isAnimationActive={false} />
|
|
|
+ <Line yAxisId="left" type="monotone" dataKey={groups.cpu.total} stroke="#10b981" strokeWidth={2.5} dot={false} name="Total%" isAnimationActive={false} />
|
|
|
)}
|
|
|
|
|
|
{groups.cpu.clocks.map((clock, idx) => (
|
|
|
@@ -282,9 +300,9 @@ const CsvGraph = ({ rawData }) => {
|
|
|
type="monotone"
|
|
|
dataKey={clock}
|
|
|
stroke={`hsl(${idx * 40}, 60%, 50%)`}
|
|
|
- strokeWidth={0.8}
|
|
|
+ strokeWidth={isMobile ? 0.6 : 1.2}
|
|
|
dot={false}
|
|
|
- name={clock}
|
|
|
+ name={clock.replace('(MHz)', '')}
|
|
|
opacity={0.25}
|
|
|
isAnimationActive={false}
|
|
|
/>
|
|
|
@@ -294,24 +312,24 @@ const CsvGraph = ({ rawData }) => {
|
|
|
)}
|
|
|
|
|
|
{/* 3. Power & Temp & Battery */}
|
|
|
- {(groups.power.power || groups.power.temp || groups.power.level) && (
|
|
|
+ {(groups.power.power || groups.power.level || groups.power.temp) && (
|
|
|
<PerformanceCard title="Power, Thermal and Battery Level">
|
|
|
- <ComposedChart data={data} margin={{ top: 10, right: 10, left: 10, bottom: 0 }}>
|
|
|
- <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e5e7eb" />
|
|
|
+ <ComposedChart data={data} margin={{ top: 5, right: 5, left: isMobile ? -20 : 0, bottom: 5 }}>
|
|
|
+ <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e5e7eb" opacity={0.3} />
|
|
|
{renderXAxis()}
|
|
|
- <YAxis yAxisId="left" tick={{ fontSize: 10 }} stroke="#8b5cf6" name="Power (mW)" />
|
|
|
- <YAxis yAxisId="right" orientation="right" tick={{ fontSize: 10 }} stroke="#ef4444" name="Temp/Level" domain={['auto', 'auto']} />
|
|
|
+ <YAxis yAxisId="left" tick={{ fontSize: isMobile ? 8 : 11 }} stroke="#8b5cf6" width={isMobile ? 40 : 55} />
|
|
|
+ <YAxis yAxisId="right" orientation="right" tick={{ fontSize: isMobile ? 8 : 11 }} stroke="#ef4444" domain={['auto', 'auto']} width={isMobile ? 30 : 45} />
|
|
|
<Tooltip content={<CustomTooltip />} />
|
|
|
- <Legend verticalAlign="top" height={36} />
|
|
|
+ <Legend verticalAlign="top" height={isMobile ? 24 : 40} iconSize={isMobile ? 8 : 12} wrapperStyle={{ fontSize: isMobile ? '9px' : '12px', paddingBottom: '5px' }} />
|
|
|
|
|
|
{groups.power.power && (
|
|
|
<Line yAxisId="left" type="monotone" dataKey={groups.power.power} stroke="#8b5cf6" strokeWidth={1.5} dot={false} name="Power (mW)" isAnimationActive={false} />
|
|
|
)}
|
|
|
{groups.power.temp && (
|
|
|
- <Line yAxisId="right" type="monotone" dataKey={groups.power.temp} stroke="#ef4444" strokeWidth={2} strokeDasharray="4 4" dot={false} name="Battery Temp (°C)" isAnimationActive={false} />
|
|
|
+ <Line yAxisId="right" type="monotone" dataKey={groups.power.temp} stroke="#ef4444" strokeWidth={1.5} strokeDasharray="3 3" dot={false} name="Temp" isAnimationActive={false} />
|
|
|
)}
|
|
|
{groups.power.level && (
|
|
|
- <Line yAxisId="right" type="stepAfter" dataKey={groups.power.level} stroke="#3b82f6" strokeWidth={2} dot={false} name="Battery Level (%)" isAnimationActive={false} />
|
|
|
+ <Line yAxisId="right" type="stepAfter" dataKey={groups.power.level} stroke="#3b82f6" strokeWidth={2} dot={false} name="Batt%" isAnimationActive={false} />
|
|
|
)}
|
|
|
</ComposedChart>
|
|
|
</PerformanceCard>
|
|
|
@@ -335,10 +353,10 @@ const CsvGraph = ({ rawData }) => {
|
|
|
|
|
|
{/* Shared Brush for scrolling */}
|
|
|
{data.length > 0 && (
|
|
|
- <div className="h-12 w-full mt-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg p-2">
|
|
|
+ <div className="h-10 w-full mt-2 bg-gray-50/50 dark:bg-gray-800/20 rounded-lg p-1">
|
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
|
<LineChart data={data}>
|
|
|
- <Brush height={20} stroke="#3b82f6" fill="transparent" />
|
|
|
+ <Brush height={16} stroke="#3b82f6" fill="transparent" tickFormatter={() => ''} />
|
|
|
</LineChart>
|
|
|
</ResponsiveContainer>
|
|
|
</div>
|