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:
@@ -519,6 +519,36 @@ ul.section-tip li {
|
|||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Target FPS sparkline row */
|
||||||
|
.target-fps-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
padding: 3px 8px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.target-fps-sparkline {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.target-fps-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.target-fps-target {
|
||||||
|
font-weight: 400;
|
||||||
|
opacity: 0.5;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Timing breakdown bar */
|
/* Timing breakdown bar */
|
||||||
.timing-breakdown {
|
.timing-breakdown {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
|
|||||||
@@ -22,6 +22,46 @@ import { createColorStripCard } from './color-strips.js';
|
|||||||
// Re-render targets tab when language changes
|
// Re-render targets tab when language changes
|
||||||
document.addEventListener('languageChanged', () => { if (apiKey) loadTargetsTab(); });
|
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 {
|
class TargetEditorModal extends Modal {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('target-editor-modal');
|
super('target-editor-modal');
|
||||||
@@ -410,6 +450,25 @@ export async function loadTargetsTab() {
|
|||||||
if (!processingKCIds.has(id)) disconnectKCWebSocket(id);
|
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) {
|
} catch (error) {
|
||||||
if (error.isAuth) return;
|
if (error.isAuth) return;
|
||||||
console.error('Failed to load targets tab:', error);
|
console.error('Failed to load targets tab:', error);
|
||||||
@@ -454,18 +513,34 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap) {
|
|||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
${isProcessing ? `
|
${isProcessing ? `
|
||||||
<div class="metrics-grid">
|
<div class="metrics-grid">
|
||||||
<div class="metric">
|
<div class="target-fps-row">
|
||||||
<div class="metric-label">${t('device.metrics.actual_fps')}</div>
|
<div class="target-fps-sparkline">
|
||||||
<div class="metric-value">${state.fps_actual?.toFixed(1) || '0.0'}</div>
|
<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>
|
||||||
<div class="metric">
|
${state.timing_total_ms != null ? `
|
||||||
<div class="metric-label">${t('device.metrics.current_fps')}</div>
|
<div class="timing-breakdown" style="grid-column:1/-1">
|
||||||
<div class="metric-value">${state.fps_current ?? '-'}</div>
|
<div class="timing-header">
|
||||||
</div>
|
<div class="metric-label">${t('device.metrics.timing')}</div>
|
||||||
<div class="metric">
|
<div class="timing-total"><strong>${state.timing_total_ms}ms</strong></div>
|
||||||
<div class="metric-label">${t('device.metrics.target_fps')}</div>
|
</div>
|
||||||
<div class="metric-value">${state.fps_target || 0}</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="metric">
|
<div class="metric">
|
||||||
<div class="metric-label">${t('device.metrics.frames')}</div>
|
<div class="metric-label">${t('device.metrics.frames')}</div>
|
||||||
<div class="metric-value">${metrics.frames_processed || 0}</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 class="metric-value">${metrics.errors_count || 0}</div>
|
||||||
</div>
|
</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>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
|
|||||||
Reference in New Issue
Block a user