feat(viewer): merge USV M1+M2 into one graph, add history window control

- USV panel: usv-m1 + usv-m2 → single usv-motors multi-trace (2 lines)
- AUV panel: auv-motors already combined (6 lines), no change
- Add window dropdown (10s/30s/60s/5min/15min/ALL) next to trail control
- updateCursor applies xaxis.range=[T-win, T] on all panel graphs
- Plots before: ~9 USV + ~9 AUV = ~18; after: ~7 USV + ~8 AUV = ~15
This commit is contained in:
Poulpe
2026-04-27 21:54:19 +00:00
parent 6978b36650
commit b962997008

View File

@@ -306,6 +306,15 @@ flowchart LR
</select>
<button id="btn-viewall" onclick="viewAll()">View all</button>
<button id="btn-play"></button>
<label for="window-select" style="font-size:10px;color:#666;white-space:nowrap;">win</label>
<select id="window-select" style="background:#0f3460;border:1px solid #a855f7;color:#a855f7;font-family:monospace;font-size:11px;padding:2px 6px;border-radius:2px;cursor:pointer;">
<option value="10000">10s</option>
<option value="30000">30s</option>
<option value="60000" selected>60s</option>
<option value="300000">5min</option>
<option value="900000">15min</option>
<option value="0">ALL</option>
</select>
</div>
<div id="ctrl-row2">
<span id="cursor-info" style="font-size:10px;color:#888;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></span>
@@ -340,8 +349,7 @@ flowchart LR
<div class="graph-cell" id="usv-gps"></div>
<div class="graph-cell" id="usv-usbl-dist"></div>
<div class="graph-cell" id="usv-usbl-angle"></div>
<div class="graph-cell" id="usv-m1"></div>
<div class="graph-cell" id="usv-m2"></div>
<div class="graph-cell wide" id="usv-motors"></div>
<div class="graph-cell wide" id="usv-status"></div>
</div>
<div class="panel-header" id="auv-panel-header">
@@ -981,11 +989,13 @@ function renderUSV(signals) {
const [at, av] = _pts(signals.usbl_angle);
Plotly.react('usv-usbl-angle', [{x:at, y:av, type:'scatter', mode:'lines', line:{color:'#c77dff',width:1}}], _layout('USBL angle','°'), cfg);
const [m1t, m1v] = _pts(signals.M1);
Plotly.react('usv-m1', [{x:m1t, y:m1v, type:'scatter', mode:'lines', line:{color:'#ef476f',width:1}}], _layout('Motor 1','cmd'), cfg);
const [m2t, m2v] = _pts(signals.M2);
Plotly.react('usv-m2', [{x:m2t, y:m2v, type:'scatter', mode:'lines', line:{color:'#ff6b6b',width:1}}], _layout('Motor 2','cmd'), cfg);
const motorColorsUSV = ['#ef476f','#ff8800'];
const motorTracesUSV = ['M1','M2'].map((mk,i) => {
const [t,v] = _pts(signals[mk]);
return {x:t,y:v,type:'scatter',mode:'lines',name:mk,line:{color:motorColorsUSV[i],width:1}};
});
Plotly.react('usv-motors', motorTracesUSV,
Object.assign(_layout('Motors USV','cmd'), {showlegend:true, legend:{font:{size:8},bgcolor:'transparent',orientation:'h',x:0,y:1}}), cfg);
const armPts = _pts(signals.Armed);
const modePts = _pts(signals.Mode);
@@ -1066,24 +1076,32 @@ function renderAUV(signals) {
const ALL_GRAPH_IDS = [
'chart-depth', 'chart-pwm-auv', 'chart-pwm-usv', 'chart-usbl',
'usv-yaw', 'usv-heading', 'usv-batt', 'usv-gps',
'usv-usbl-dist', 'usv-usbl-angle', 'usv-m1', 'usv-m2', 'usv-status',
'usv-usbl-dist', 'usv-usbl-angle', 'usv-motors', 'usv-status',
'auv-pry', 'auv-depth', 'auv-alt', 'auv-obs',
'auv-usbl-dist', 'auv-usbl-angle', 'auv-batt', 'auv-status', 'auv-motors',
];
function updateCursor(epochSec) {
const ts = new Date(epochSec * 1000).toISOString();
const tMs = epochSec * 1000;
const winMs = +document.getElementById('window-select').value;
const t0 = winMs === 0 ? null : new Date(tMs - winMs).toISOString();
const t1 = new Date(tMs).toISOString();
const shape = {
type: 'line', x0: ts, x1: ts, y0: 0, y1: 1,
yref: 'paper', line: {color: '#e94560', width: 1, dash: 'dot'},
};
const rangeUpdate = t0 ? {'xaxis.range': [t0, t1]} : {'xaxis.autorange': true};
ALL_GRAPH_IDS.forEach(id => {
const el = document.getElementById(id);
if (el && el._fullLayout) {
Plotly.relayout(id, {'shapes': [shape]});
Plotly.relayout(id, {...rangeUpdate, 'shapes': [shape]});
}
});
}
document.getElementById('window-select').addEventListener('change', () => {
if (tNow) updateCursor(tNow / 1000);
});
// == Task 11: loadSortieData + sorties loading + wiring ==
async function loadSortieData(sortieId) {