EgoInfinity / js /timeseries.js
VectorW's picture
Initial commit
66d097c
Raw
History Blame Contribute Delete
9.95 kB
// Render per-frame signals (contact_l, contact_r, grasp, motion, trust) into 5 mini SVGs.
// Each SVG is 200x18 viewBox. We scale to 0..1 vertically (motion uses log(1+x)/log(1+max)).
const VB_W = 200, VB_H = 18;
function clearSvg(svg) {
while (svg.firstChild) svg.removeChild(svg.firstChild);
}
function pathFromSeries(values, normalize) {
if (!values || values.length === 0) return '';
const n = values.length;
let pts = [];
for (let i = 0; i < n; i++) {
const x = (i / Math.max(n - 1, 1)) * VB_W;
const v = normalize(values[i]);
const y = VB_H - v * VB_H;
pts.push(`${i === 0 ? 'M' : 'L'}${x.toFixed(2)},${y.toFixed(2)}`);
}
return pts.join(' ');
}
function fillBars(svg, values, normalize, color) {
// For binary trust: render as horizontal bars where True
const n = values.length;
if (n === 0) return;
const w = VB_W / n;
let i = 0;
while (i < n) {
if (values[i]) {
let j = i;
while (j < n && values[j]) j++;
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('x', (i * w).toFixed(2));
rect.setAttribute('y', '4');
rect.setAttribute('width', ((j - i) * w).toFixed(2));
rect.setAttribute('height', String(VB_H - 8));
rect.setAttribute('fill', color);
rect.setAttribute('opacity', '0.7');
svg.appendChild(rect);
i = j;
} else {
i++;
}
}
}
function drawArea(svg, values, normalize, fill, stroke) {
const n = values.length;
if (n === 0) return;
let pts = [`M0,${VB_H}`];
for (let i = 0; i < n; i++) {
const x = (i / Math.max(n - 1, 1)) * VB_W;
const v = Math.max(0, Math.min(1, normalize(values[i])));
const y = VB_H - v * VB_H;
pts.push(`L${x.toFixed(2)},${y.toFixed(2)}`);
}
pts.push(`L${VB_W},${VB_H}Z`);
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', pts.join(' '));
path.setAttribute('fill', fill);
path.setAttribute('stroke', stroke);
path.setAttribute('stroke-width', '0.8');
svg.appendChild(path);
}
function addPlayhead(svg, frac) {
const x = frac * VB_W;
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', x.toFixed(2));
line.setAttribute('x2', x.toFixed(2));
line.setAttribute('y1', '0');
line.setAttribute('y2', String(VB_H));
line.setAttribute('class', 'signal-playhead');
line.setAttribute('stroke', 'var(--accent)');
line.setAttribute('stroke-width', '1');
svg.appendChild(line);
return line;
}
export class SignalPanel {
constructor() {
this.svgs = {
contact_l: document.getElementById('sig-contact-l'),
contact_r: document.getElementById('sig-contact-r'),
grasp: document.getElementById('sig-grasp'),
motion: document.getElementById('sig-motion'),
trust: document.getElementById('sig-trust'),
};
this.vals = {
contact_l: document.getElementById('sig-val-contact-l'),
contact_r: document.getElementById('sig-val-contact-r'),
grasp: document.getElementById('sig-val-grasp'),
motion: document.getElementById('sig-val-motion'),
trust: document.getElementById('sig-val-trust'),
};
this.playheads = {};
this.data = null;
}
setData(timeseries) {
this.data = timeseries;
if (!timeseries) {
Object.values(this.svgs).forEach(clearSvg);
Object.values(this.vals).forEach(v => v.textContent = '-');
return;
}
const scene = timeseries.scene || {};
this._render('contact_l', scene.contact_l || [], v => v, 'rgba(217,119,87,0.25)', 'oklch(0.68 0.18 40)');
this._render('contact_r', scene.contact_r || [], v => v, 'rgba(106,148,96,0.25)', 'oklch(0.58 0.12 150)');
this._render('grasp', scene.grasp || [], v => v, 'rgba(160,88,72,0.30)', 'oklch(0.58 0.18 30)');
// motion: log scale
const motion = scene.motion || [];
const maxMotion = Math.max(1, ...motion.map(v => v || 0));
this._render('motion', motion, v => Math.log1p(v) / Math.log1p(maxMotion), 'rgba(120,120,160,0.22)', 'oklch(0.55 0.10 240)');
// trust: union over per-object trust booleans (if any object trusted that frame, mark trusted)
const perObj = timeseries.per_object || {};
const n = timeseries.n_frames || (motion.length || 0);
const trustUnion = new Array(n).fill(false);
Object.values(perObj).forEach(o => {
const arr = o.trust || [];
for (let i = 0; i < n; i++) if (arr[i]) trustUnion[i] = true;
});
clearSvg(this.svgs.trust);
fillBars(this.svgs.trust, trustUnion, v => v ? 1 : 0, 'oklch(0.68 0.18 40)');
// playhead overlay
this.playheads.trust = addPlayhead(this.svgs.trust, 0);
this._lastTrust = trustUnion;
}
_render(key, series, normalize, fill, stroke) {
const svg = this.svgs[key];
clearSvg(svg);
drawArea(svg, series, normalize, fill, stroke);
this.playheads[key] = addPlayhead(svg, 0);
}
setPlayheadFrame(frameIdx) {
if (!this.data) return;
const n = this.data.n_frames || 1;
const frac = Math.max(0, Math.min(1, frameIdx / Math.max(n - 1, 1)));
Object.values(this.playheads).forEach(line => {
if (!line) return;
const x = (frac * VB_W).toFixed(2);
line.setAttribute('x1', x);
line.setAttribute('x2', x);
});
// update value labels
const scene = this.data.scene || {};
const f = Math.max(0, Math.min(n - 1, Math.round(frameIdx)));
const fmt = (v) => (v == null ? '-' : (typeof v === 'number' ? v.toFixed(2) : String(v)));
this.vals.contact_l.textContent = fmt(scene.contact_l && scene.contact_l[f]);
this.vals.contact_r.textContent = fmt(scene.contact_r && scene.contact_r[f]);
this.vals.grasp.textContent = fmt(scene.grasp && scene.grasp[f]);
this.vals.motion.textContent = fmt(scene.motion && scene.motion[f]);
this.vals.trust.textContent = (this._lastTrust && this._lastTrust[f]) ? 'yes' : 'no';
}
}
// ─── Per-object state timeline (static / grasped / moving) ────────
// Rendered into #state-timeline-rows from signals.json.per_object[oid].state
// One row per object: prompt label + horizontal SVG strip + per-frame value.
// Colors must match the .state-chip classes in styles.css.
const STATE_COLOR = {
static: 'oklch(0.78 0.01 240)', // light gray
grasped_l: 'oklch(0.70 0.16 50)', // warm orange = left hand
grasped_r: 'oklch(0.60 0.20 20)', // red-orange = right hand
grasped_both: 'oklch(0.42 0.20 15)', // deep red = both hands
moving: 'oklch(0.55 0.10 240)', // cool blue
// back-compat: clips processed before the L/R split write plain "grasped"
grasped: 'oklch(0.58 0.18 30)',
};
export class StateTimelinePanel {
constructor() {
this.container = document.getElementById('state-timeline-rows');
this.rows = []; // [{oid, svg, valEl, playhead, states}]
this.nFrames = 0;
}
setData(timeseries, scene) {
if (!this.container) return;
this.container.innerHTML = '';
this.rows = [];
this.nFrames = (timeseries && timeseries.n_frames) || 0;
const perObj = (timeseries && timeseries.per_object) || {};
const objs = (scene && scene.reconstruction && scene.reconstruction.objects) || [];
// Need at least one object with a state array to render anything
const haveState = Object.values(perObj).some(o => Array.isArray(o.state));
if (!haveState) {
const empty = document.createElement('div');
empty.style.cssText = 'font-size:10px;color:var(--ink-3);padding:4px 0;';
empty.textContent = 'No state data; clip may pre-date the state-machine refresh.';
this.container.appendChild(empty);
return;
}
objs.forEach(o => {
const oidKey = String(o.id);
const po = perObj[oidKey] || {};
const states = po.state || [];
if (!states.length) return;
const row = document.createElement('div');
row.className = 'state-row';
const label = document.createElement('div');
label.className = 'state-label';
label.style.color = o.color_hex || 'inherit';
label.title = o.prompt || `obj ${o.id}`;
label.textContent = (o.prompt || `obj ${o.id}`).slice(0, 18);
row.appendChild(label);
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('preserveAspectRatio', 'none');
svg.setAttribute('viewBox', `0 0 ${VB_W} ${VB_H}`);
// Paint segments
const n = states.length;
let i = 0;
const w = VB_W / Math.max(n, 1);
while (i < n) {
const s = states[i];
let j = i;
while (j < n && states[j] === s) j++;
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('x', (i * w).toFixed(2));
rect.setAttribute('y', '2');
rect.setAttribute('width', ((j - i) * w).toFixed(2));
rect.setAttribute('height', String(VB_H - 4));
rect.setAttribute('fill', STATE_COLOR[s] || 'oklch(0.78 0.01 240)');
svg.appendChild(rect);
i = j;
}
const playhead = addPlayhead(svg, 0);
row.appendChild(svg);
const valEl = document.createElement('div');
valEl.className = 'state-value';
valEl.textContent = '-';
row.appendChild(valEl);
this.container.appendChild(row);
this.rows.push({ oid: o.id, svg, valEl, playhead, states });
});
}
setPlayheadFrame(frameIdx) {
if (!this.rows.length) return;
const n = this.nFrames || 1;
const frac = Math.max(0, Math.min(1, frameIdx / Math.max(n - 1, 1)));
const x = (frac * VB_W).toFixed(2);
const f = Math.max(0, Math.min(n - 1, Math.round(frameIdx)));
for (const r of this.rows) {
if (r.playhead) {
r.playhead.setAttribute('x1', x);
r.playhead.setAttribute('x2', x);
}
r.valEl.textContent = r.states[f] || '-';
}
}
}