mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-23 12:12:51 +00:00
## Summary Addresses user feedback on #600 — two improvements to RF Health detail panel charts: ### 1. Auto-scale airtime Y-axis Previously fixed 0-100% which made low-activity nodes unreadable (e.g. 0.1% TX barely visible). Now auto-scales to the actual data range with 20% headroom (minimum 1%), matching how the noise floor chart already works. ### 2. Hover tooltips on all chart data points Invisible SVG `<circle>` elements with native `<title>` tooltips on every data point across all 4 charts: - **Noise floor**: `NF: -112.3 dBm` + UTC timestamp - **Airtime**: `TX: 2.1%` or `RX: 8.3%` + UTC timestamp - **Error rate**: `Err: 0.05%` + UTC timestamp - **Battery**: `Batt: 3.85V` + UTC timestamp Uses native browser SVG tooltips — zero dependencies, accessible, no JS event handlers. ### Design rationale (Tufte) - Auto-scaling increases data-ink ratio by eliminating wasted vertical space - Tooltips provide detail-on-demand without cluttering the chart with labels on every point ### Spec update Added M2 feedback improvements section to `docs/specs/rf-health-dashboard.md`. --------- Co-authored-by: you <you@example.com>
This commit is contained in:
1 parent
30e7e9ae3c
commit
f7000992ca
2 files changed
+44
-5
No files matched your search
@@ -431,6 +431,10 @@ Note: No hardcoded duty cycle limit line on charts. Duty cycle regulations vary
|
||||
- All charts time-aligned, sharing X-axis, reboot markers spanning all charts
|
||||
- Tests: delta computation, reboot handling, counter reset, gap insertion, downsampling, error rate calculation
|
||||
|
||||
#### M2 feedback improvements (post-M2)
|
||||
- **Auto-scale airtime Y-axis**: clamp to min/max of actual data values (20% headroom, min 1%) instead of fixed 0-100%, matching noise floor chart behavior. Increases data-ink ratio for low-activity nodes.
|
||||
- **Hover tooltips on all chart data points**: invisible SVG circles with `<title>` elements on every data point across all 4 charts (noise floor, airtime, error rate, battery). Shows exact value + UTC timestamp on hover. Detail-on-demand without cluttering the chart.
|
||||
|
||||
### M3: Pattern detection
|
||||
- Implement after operators have used raw charts (M1–M2) and provided feedback
|
||||
- Jammer detection (NF spike + RX drop)
|
||||
|
||||
+40
-5
@@ -2945,6 +2945,20 @@ function destroy() { _analyticsData = {}; _channelData = null; if (_ngState && _
|
||||
}
|
||||
|
||||
// Shared helper: render X-axis time labels
|
||||
function rfTooltipCircles(data, sx, sy, label, unit, formatV) {
|
||||
let svg = '';
|
||||
formatV = formatV || (v => v.toFixed(1));
|
||||
data.forEach(d => {
|
||||
const t = new Date(d.t);
|
||||
const x = sx(t.getTime());
|
||||
const y = sy(d.v);
|
||||
const ts = t.toISOString().replace('T', ' ').replace(/\.\d+Z/, ' UTC');
|
||||
const tip = `${label}: ${formatV(d.v)}${unit}\n${ts}`;
|
||||
svg += `<circle cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="8" fill="transparent" stroke="none" pointer-events="all"><title>${tip}</title></circle>`;
|
||||
});
|
||||
return svg;
|
||||
}
|
||||
|
||||
function rfXAxisLabels(data, sx, h, pad) {
|
||||
let svg = '';
|
||||
const xTicks = Math.min(6, data.length);
|
||||
@@ -2967,18 +2981,26 @@ function destroy() { _analyticsData = {}; _channelData = null; if (_ngState && _
|
||||
const minT = sharedMinT, maxT = sharedMaxT;
|
||||
const rangeT = maxT - minT || 1;
|
||||
|
||||
// Auto-scale Y-axis to data range (20% headroom, min 1%)
|
||||
let dataMax = 0;
|
||||
for (let i = 0; i < txData.length; i++) { if (txData[i].v > dataMax) dataMax = txData[i].v; }
|
||||
for (let i = 0; i < rxData.length; i++) { if (rxData[i].v > dataMax) dataMax = rxData[i].v; }
|
||||
const yMax = Math.max(dataMax * 1.2, 1);
|
||||
|
||||
const sx = t => pad.left + ((t - minT) / rangeT) * cw;
|
||||
const sy = v => pad.top + ch - (v / 100) * ch; // 0-100%
|
||||
const sy = v => pad.top + ch - (v / yMax) * ch;
|
||||
|
||||
let svg = `<svg viewBox="0 0 ${w} ${h}" style="width:100%;max-height:${h}px" role="img" aria-label="Airtime chart"><title>Airtime %</title>`;
|
||||
|
||||
// Chart title
|
||||
svg += `<text x="${pad.left}" y="12" font-size="10" fill="var(--text-muted)" font-weight="600">Airtime %</text>`;
|
||||
|
||||
// Y-axis: 0, 25, 50, 75, 100
|
||||
for (let pct = 0; pct <= 100; pct += 25) {
|
||||
const y = sy(pct);
|
||||
svg += `<text x="${pad.left - 4}" y="${(y + 3).toFixed(1)}" text-anchor="end" font-size="9" fill="var(--text-muted)">${pct}</text>`;
|
||||
// Y-axis: 5 ticks from 0 to yMax
|
||||
const yTicks = 4;
|
||||
for (let i = 0; i <= yTicks; i++) {
|
||||
const v = yMax * i / yTicks;
|
||||
const y = sy(v);
|
||||
svg += `<text x="${pad.left - 4}" y="${(y + 3).toFixed(1)}" text-anchor="end" font-size="9" fill="var(--text-muted)">${v.toFixed(1)}</text>`;
|
||||
svg += `<line x1="${pad.left}" y1="${y.toFixed(1)}" x2="${w - pad.right}" y2="${y.toFixed(1)}" stroke="var(--border)" stroke-width="0.3"/>`;
|
||||
}
|
||||
|
||||
@@ -3019,6 +3041,10 @@ function destroy() { _analyticsData = {}; _channelData = null; if (_ngState && _
|
||||
const allData = txData.length >= rxData.length ? txData : rxData;
|
||||
svg += rfXAxisLabels(allData, sx, h, pad);
|
||||
|
||||
// Hover tooltips
|
||||
svg += rfTooltipCircles(txData, sx, sy, 'TX', '%');
|
||||
svg += rfTooltipCircles(rxData, sx, sy, 'RX', '%');
|
||||
|
||||
svg += '</svg>';
|
||||
return svg;
|
||||
}
|
||||
@@ -3068,6 +3094,9 @@ function destroy() { _analyticsData = {}; _channelData = null; if (_ngState && _
|
||||
// X-axis labels
|
||||
svg += rfXAxisLabels(errData, sx, h, pad);
|
||||
|
||||
// Hover tooltips
|
||||
svg += rfTooltipCircles(errData, sx, sy, 'Err', '%', v => v.toFixed(2));
|
||||
|
||||
svg += '</svg>';
|
||||
return svg;
|
||||
}
|
||||
@@ -3126,6 +3155,9 @@ function destroy() { _analyticsData = {}; _channelData = null; if (_ngState && _
|
||||
// X-axis labels
|
||||
svg += rfXAxisLabels(battData, sx, h, pad);
|
||||
|
||||
// Hover tooltips
|
||||
svg += rfTooltipCircles(battData, sx, sy, 'Batt', 'V', v => (v/1000).toFixed(2));
|
||||
|
||||
svg += '</svg>';
|
||||
return svg;
|
||||
}
|
||||
@@ -3183,6 +3215,9 @@ function destroy() { _analyticsData = {}; _channelData = null; if (_ngState && _
|
||||
// Data polyline
|
||||
svg += `<polyline points="${pts}" fill="none" stroke="var(--accent)" stroke-width="1.5"/>`;
|
||||
|
||||
// Hover tooltips
|
||||
svg += rfTooltipCircles(data, sx, sy, 'NF', ' dBm');
|
||||
|
||||
// Direct labels: min and max points
|
||||
const times = data.map(d => new Date(d.t).getTime());
|
||||
const maxIdx = values.indexOf(maxV);
|
||||
|
||||
Reference in new issue
Block a user