Add FPS sparkline chart to target cards, move timing breakdown inline
Replace the three FPS text labels (actual/current/target) with a Chart.js sparkline chart + compact label, matching the dashboard style. FPS history (30 samples) persists across poll rebuilds. Pipeline timing breakdown moved inside the metrics grid directly under the FPS chart. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,46 @@ import { createColorStripCard } from './color-strips.js';
|
||||
// Re-render targets tab when language changes
|
||||
document.addEventListener('languageChanged', () => { if (apiKey) loadTargetsTab(); });
|
||||
|
||||
// --- FPS sparkline history for target cards ---
|
||||
const _TARGET_MAX_FPS_SAMPLES = 30;
|
||||
const _targetFpsHistory = {};
|
||||
|
||||
function _pushTargetFps(targetId, value) {
|
||||
if (!_targetFpsHistory[targetId]) _targetFpsHistory[targetId] = [];
|
||||
const h = _targetFpsHistory[targetId];
|
||||
h.push(value);
|
||||
if (h.length > _TARGET_MAX_FPS_SAMPLES) h.shift();
|
||||
}
|
||||
|
||||
function _createTargetFpsChart(canvasId, history, fpsTarget) {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
if (!canvas) return null;
|
||||
return new Chart(canvas, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: history.map(() => ''),
|
||||
datasets: [{
|
||||
data: [...history],
|
||||
borderColor: '#2196F3',
|
||||
backgroundColor: 'rgba(33,150,243,0.12)',
|
||||
borderWidth: 1.5,
|
||||
tension: 0.3,
|
||||
fill: true,
|
||||
pointRadius: 0,
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
animation: false,
|
||||
plugins: { legend: { display: false }, tooltip: { display: false } },
|
||||
scales: {
|
||||
x: { display: false },
|
||||
y: { display: false, min: 0, max: fpsTarget * 1.15 },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
class TargetEditorModal extends Modal {
|
||||
constructor() {
|
||||
super('target-editor-modal');
|
||||
@@ -410,6 +450,25 @@ export async function loadTargetsTab() {
|
||||
if (!processingKCIds.has(id)) disconnectKCWebSocket(id);
|
||||
});
|
||||
|
||||
// FPS sparkline charts: push samples and init charts after HTML rebuild
|
||||
const allTargets = [...ledTargets, ...kcTargets];
|
||||
const runningIds = new Set();
|
||||
allTargets.forEach(target => {
|
||||
if (target.state && target.state.processing) {
|
||||
runningIds.add(target.id);
|
||||
if (target.state.fps_actual != null) {
|
||||
_pushTargetFps(target.id, target.state.fps_actual);
|
||||
}
|
||||
const history = _targetFpsHistory[target.id] || [];
|
||||
const fpsTarget = target.state.fps_target || 30;
|
||||
_createTargetFpsChart(`target-fps-${target.id}`, history, fpsTarget);
|
||||
}
|
||||
});
|
||||
// Clean up history for targets no longer running
|
||||
Object.keys(_targetFpsHistory).forEach(id => {
|
||||
if (!runningIds.has(id)) delete _targetFpsHistory[id];
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
console.error('Failed to load targets tab:', error);
|
||||
@@ -454,18 +513,34 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap) {
|
||||
<div class="card-content">
|
||||
${isProcessing ? `
|
||||
<div class="metrics-grid">
|
||||
<div class="metric">
|
||||
<div class="metric-label">${t('device.metrics.actual_fps')}</div>
|
||||
<div class="metric-value">${state.fps_actual?.toFixed(1) || '0.0'}</div>
|
||||
<div class="target-fps-row">
|
||||
<div class="target-fps-sparkline">
|
||||
<canvas id="target-fps-${target.id}" data-fps-target="${state.fps_target || 30}"></canvas>
|
||||
</div>
|
||||
<div class="target-fps-label">
|
||||
<span class="metric-value">${state.fps_actual?.toFixed(1) || '0.0'}<span class="target-fps-target">/${state.fps_target || 0}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-label">${t('device.metrics.current_fps')}</div>
|
||||
<div class="metric-value">${state.fps_current ?? '-'}</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="metric-label">${t('device.metrics.target_fps')}</div>
|
||||
<div class="metric-value">${state.fps_target || 0}</div>
|
||||
${state.timing_total_ms != null ? `
|
||||
<div class="timing-breakdown" style="grid-column:1/-1">
|
||||
<div class="timing-header">
|
||||
<div class="metric-label">${t('device.metrics.timing')}</div>
|
||||
<div class="timing-total"><strong>${state.timing_total_ms}ms</strong></div>
|
||||
</div>
|
||||
<div class="timing-bar">
|
||||
${state.timing_extract_ms != null ? `<span class="timing-seg timing-extract" style="flex:${state.timing_extract_ms}" title="extract ${state.timing_extract_ms}ms"></span>` : ''}
|
||||
${state.timing_map_leds_ms != null ? `<span class="timing-seg timing-map" style="flex:${state.timing_map_leds_ms}" title="map ${state.timing_map_leds_ms}ms"></span>` : ''}
|
||||
${state.timing_smooth_ms != null ? `<span class="timing-seg timing-smooth" style="flex:${state.timing_smooth_ms || 0.1}" title="smooth ${state.timing_smooth_ms}ms"></span>` : ''}
|
||||
<span class="timing-seg timing-send" style="flex:${state.timing_send_ms}" title="send ${state.timing_send_ms}ms"></span>
|
||||
</div>
|
||||
<div class="timing-legend">
|
||||
${state.timing_extract_ms != null ? `<span class="timing-legend-item"><span class="timing-dot timing-extract"></span>extract ${state.timing_extract_ms}ms</span>` : ''}
|
||||
${state.timing_map_leds_ms != null ? `<span class="timing-legend-item"><span class="timing-dot timing-map"></span>map ${state.timing_map_leds_ms}ms</span>` : ''}
|
||||
${state.timing_smooth_ms != null ? `<span class="timing-legend-item"><span class="timing-dot timing-smooth"></span>smooth ${state.timing_smooth_ms}ms</span>` : ''}
|
||||
<span class="timing-legend-item"><span class="timing-dot timing-send"></span>send ${state.timing_send_ms}ms</span>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="metric">
|
||||
<div class="metric-label">${t('device.metrics.frames')}</div>
|
||||
<div class="metric-value">${metrics.frames_processed || 0}</div>
|
||||
@@ -479,26 +554,6 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap) {
|
||||
<div class="metric-value">${metrics.errors_count || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
${state.timing_total_ms != null ? `
|
||||
<div class="timing-breakdown">
|
||||
<div class="timing-header">
|
||||
<div class="metric-label">${t('device.metrics.timing')}</div>
|
||||
<div class="timing-total"><strong>${state.timing_total_ms}ms</strong></div>
|
||||
</div>
|
||||
<div class="timing-bar">
|
||||
${state.timing_extract_ms != null ? `<span class="timing-seg timing-extract" style="flex:${state.timing_extract_ms}" title="extract ${state.timing_extract_ms}ms"></span>` : ''}
|
||||
${state.timing_map_leds_ms != null ? `<span class="timing-seg timing-map" style="flex:${state.timing_map_leds_ms}" title="map ${state.timing_map_leds_ms}ms"></span>` : ''}
|
||||
${state.timing_smooth_ms != null ? `<span class="timing-seg timing-smooth" style="flex:${state.timing_smooth_ms || 0.1}" title="smooth ${state.timing_smooth_ms}ms"></span>` : ''}
|
||||
<span class="timing-seg timing-send" style="flex:${state.timing_send_ms}" title="send ${state.timing_send_ms}ms"></span>
|
||||
</div>
|
||||
<div class="timing-legend">
|
||||
${state.timing_extract_ms != null ? `<span class="timing-legend-item"><span class="timing-dot timing-extract"></span>extract ${state.timing_extract_ms}ms</span>` : ''}
|
||||
${state.timing_map_leds_ms != null ? `<span class="timing-legend-item"><span class="timing-dot timing-map"></span>map ${state.timing_map_leds_ms}ms</span>` : ''}
|
||||
${state.timing_smooth_ms != null ? `<span class="timing-legend-item"><span class="timing-dot timing-smooth"></span>smooth ${state.timing_smooth_ms}ms</span>` : ''}
|
||||
<span class="timing-legend-item"><span class="timing-dot timing-send"></span>send ${state.timing_send_ms}ms</span>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
|
||||
Reference in New Issue
Block a user