Fix radar jitter by using band-only positioning

Replace continuous estimated_distance_m-based radius with proximity band
snapping (immediate/near/far/unknown → fixed radius ratios of 0.15/0.40/
0.70/0.90). The proximity_band is computed server-side from rssi_ema which
is already smoothed, so it changes infrequently — dots now only move when
a device genuinely crosses a band boundary rather than on every RSSI
fluctuation.

Also removes the client-side EMA and positionCache added in the previous
commit, and reverts CSS style.transform back to SVG transform attribute to
avoid coordinate-system mismatch when the SVG is displayed at a scaled size.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-21 14:38:50 +00:00
parent f8a6d0ae70
commit aba4ccd040

View File

@@ -25,14 +25,10 @@ const ProximityRadar = (function() {
newDeviceThreshold: 30, // seconds
};
// Configuration
const POSITION_EMA_ALPHA = 0.25; // lower = more smoothing (0.25 → ~4 updates to reach 68% of a step)
// State
let container = null;
let svg = null;
let devices = new Map();
let positionCache = new Map(); // device_key → { x, y } smoothed position
let isPaused = false;
let activeFilter = null;
let onDeviceClick = null;
@@ -217,24 +213,13 @@ const ProximityRadar = (function() {
// Remove elements for devices no longer in the visible set
devicesGroup.querySelectorAll('.radar-device-wrapper').forEach(el => {
const k = el.getAttribute('data-device-key');
if (!visibleKeys.has(k)) {
positionCache.delete(k);
if (!visibleKeys.has(el.getAttribute('data-device-key'))) {
el.remove();
}
});
visibleDevices.forEach(device => {
// Raw target position from distance/band
const { x: rawX, y: rawY } = calculateDevicePosition(device, center, maxRadius);
// EMA smoothing: blend towards the new position rather than snapping,
// so RSSI noise doesn't translate 1:1 into visible movement.
const cached = positionCache.get(device.device_key);
const x = cached ? cached.x * (1 - POSITION_EMA_ALPHA) + rawX * POSITION_EMA_ALPHA : rawX;
const y = cached ? cached.y * (1 - POSITION_EMA_ALPHA) + rawY * POSITION_EMA_ALPHA : rawY;
positionCache.set(device.device_key, { x, y });
const { x, y } = calculateDevicePosition(device, center, maxRadius);
const confidence = device.distance_confidence || 0.5;
const dotSize = CONFIG.dotMinSize + (CONFIG.dotMaxSize - CONFIG.dotMinSize) * confidence;
const color = getBandColor(device.proximity_band);
@@ -249,7 +234,7 @@ const ProximityRadar = (function() {
if (existing) {
// ── In-place update: mutate attributes, never recreate ──
existing.style.transform = `translate(${x}px, ${y}px)`;
existing.setAttribute('transform', `translate(${x}, ${y})`);
const innerG = existing.querySelector('.radar-device');
if (innerG) {
@@ -306,8 +291,7 @@ const ProximityRadar = (function() {
const wrapperG = document.createElementNS(ns, 'g');
wrapperG.classList.add('radar-device-wrapper');
wrapperG.setAttribute('data-device-key', key);
wrapperG.style.transform = `translate(${x}px, ${y}px)`;
wrapperG.style.transition = 'transform 0.6s ease-out';
wrapperG.setAttribute('transform', `translate(${x}, ${y})`);
const innerG = document.createElementNS(ns, 'g');
innerG.classList.add('radar-device');
@@ -390,22 +374,16 @@ const ProximityRadar = (function() {
* Calculate device position on radar
*/
function calculateDevicePosition(device, center, maxRadius) {
// Calculate radius based on proximity band/distance
// Position is band-only — the band is computed server-side from rssi_ema
// (already smoothed), so it changes infrequently and never jitters.
// Using raw estimated_distance_m caused constant micro-movement as RSSI
// fluctuated on every update cycle.
let radiusRatio;
const band = device.proximity_band || 'unknown';
if (device.estimated_distance_m != null) {
// Use actual distance (log scale)
const maxDistance = 15;
radiusRatio = Math.min(1, Math.log10(device.estimated_distance_m + 1) / Math.log10(maxDistance + 1));
} else {
// Use band-based positioning
switch (band) {
case 'immediate': radiusRatio = 0.15; break;
case 'near': radiusRatio = 0.4; break;
case 'far': radiusRatio = 0.7; break;
default: radiusRatio = 0.9; break;
}
switch (device.proximity_band || 'unknown') {
case 'immediate': radiusRatio = 0.15; break;
case 'near': radiusRatio = 0.40; break;
case 'far': radiusRatio = 0.70; break;
default: radiusRatio = 0.90; break;
}
// Calculate angle based on device key hash (stable positioning)
@@ -462,7 +440,6 @@ const ProximityRadar = (function() {
*/
function clear() {
devices.clear();
positionCache.clear();
selectedDeviceKey = null;
renderDevices();
}