Skip to content

Commit 4f641cb

Browse files
committed
feat(visual): improve OFDM spectrum channel grouping and add per-graph actual/avg controls
- standardize GetCapture-OFDM layout with header row, Device Info block, and modernized chart/table styling - remove fake sysDescr fallbacks and use N/A-safe device info extraction/merge - add combined spectrum overlay graph while preserving individual per-channel graphs - map captures to measurement_stats by frequency-range keys and use measurement channel_stats OFDM channel IDs for correct per-channel grouping - remove redundant per-channel MacAddress display (shown once in Device Info) - add per-channel Actual / Moving Avg radio toggle (combined graph stays actual-only for simpler legend muting) - style per-channel Actual as purple and Moving Avg as solid red for clearer comparison - use fixed-height Chart.js 2 containers to avoid Postman resize issues - update CODING_AGENTS.md with raw-vs-moving-average chart toggle guidance - regenerate visual docs/previews for GetCapture-OFDM - 2026-02-23 00:33:58
1 parent bc11278 commit 4f641cb

4 files changed

Lines changed: 168 additions & 40 deletions

File tree

CODING_AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
- Default UI frequency labels/ticks to whole-number `MHz` (no decimal floats) unless precision is required for the specific visual.
5050
- Include units in graph titles/axis labels for measured values (for example `Magnitude (dB)` instead of `Magnitude`).
5151
- Center graph/panel titles for scanability and consistent visual layout.
52+
- When a visual can plot both raw values and moving-average/smoothed values, prefer a per-graph `Actual / Moving Avg` radio toggle instead of showing both at once by default (reduces clutter).
53+
- For `Moving Avg` overlays, use the same base color as `Actual` with a dashed line style.
5254
- Avoid redundant repetition of values already shown in `Device Info` (for example, do not repeat `MacAddress` in the channel header if it is already in the device table).
5355
- If `system_description` is missing/partial, render `N/A` for missing fields instead of vendor-specific fallback values.
5456
- JSON responses may contain multiple upstream/downstream channels; each channel must render as its own graph for the selected graph type.

docs/visual-previews/SingleCapture/SpectrumAnalysis/GetCapture-OFDM.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/visual/SingleCapture/SpectrumAnalysis/GetCapture-OFDM.md

Lines changed: 82 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,28 @@ var template = `
205205
font-size: 14px;
206206
font-weight: 700;
207207
}
208+
.chart-mode-controls {
209+
display: flex;
210+
justify-content: center;
211+
gap: 14px;
212+
align-items: center;
213+
margin: 0 0 8px 0;
214+
color: #d6deef;
215+
font-size: 12px;
216+
}
217+
.chart-mode-controls label {
218+
display: inline-flex;
219+
align-items: center;
220+
gap: 6px;
221+
cursor: pointer;
222+
}
223+
.chart-mode-controls input[type="radio"] {
224+
accent-color: #5a6fd8;
225+
cursor: pointer;
226+
}
208227
.chart-canvas-wrap {
209228
position: relative;
210-
height: calc(100% - 26px);
229+
height: calc(100% - 58px);
211230
}
212231
.chart-canvas {
213232
display: block;
@@ -344,6 +363,10 @@ var template = `
344363

345364
<div class="chart-container">
346365
<div class="chart-title">Spectrum Magnitude by Capture (dB)</div>
366+
<div class="chart-mode-controls" data-chart-mode="spectrumChart-ch{{channelId}}">
367+
<label><input type="radio" name="mode-spect-ch{{channelId}}" value="actual" checked> Actual</label>
368+
<label><input type="radio" name="mode-spect-ch{{channelId}}" value="avg"> Moving Avg</label>
369+
</div>
347370
<div class="chart-canvas-wrap">
348371
<canvas id="spectrumChart-ch{{channelId}}" class="chart-canvas"></canvas>
349372
</div>
@@ -391,6 +414,26 @@ var template = `
391414
return colors[i % colors.length];
392415
}
393416

417+
function setChartMode(chart, mode) {
418+
if (!chart || !chart.data || !Array.isArray(chart.data.datasets)) return;
419+
chart.data.datasets.forEach(function (ds) {
420+
if (!ds || !ds._seriesMode) return;
421+
ds.hidden = (ds._seriesMode !== mode);
422+
});
423+
chart.update(0);
424+
}
425+
426+
function bindChartModeControls(canvasId, chart) {
427+
var group = document.querySelector('.chart-mode-controls[data-chart-mode="' + canvasId + '"]');
428+
if (!group) return;
429+
var radios = group.querySelectorAll('input[type="radio"]');
430+
for (var i = 0; i < radios.length; i++) {
431+
radios[i].addEventListener('change', function (ev) {
432+
if (ev && ev.target && ev.target.checked) setChartMode(chart, ev.target.value);
433+
});
434+
}
435+
}
436+
394437
pm.getData(function (err, value) {
395438
if (err || !value || !Array.isArray(value.channels)) return;
396439

@@ -404,13 +447,16 @@ var template = `
404447
var freqs = Array.isArray(sig.frequencies) ? sig.frequencies : [];
405448
var mags = Array.isArray(sig.magnitudes) ? sig.magnitudes : [];
406449
if (!freqs.length || !mags.length) return;
407-
var n = Math.min(freqs.length, mags.length);
408-
var pts = [];
409-
for (var i = 0; i < n; i++) pts.push({ x: freqs[i], y: mags[i] });
410450
var c = palette(dsIdx++);
451+
function points(values) {
452+
var n = Math.min(freqs.length, values.length);
453+
var out = [];
454+
for (var i = 0; i < n; i++) out.push({ x: freqs[i], y: values[i] });
455+
return out;
456+
}
411457
combinedDatasets.push({
412458
label: 'Ch ' + ch.channelId + ' · ' + cap.captureLabel,
413-
data: pts,
459+
data: points(mags),
414460
borderColor: c,
415461
backgroundColor: 'transparent',
416462
fill: false,
@@ -421,7 +467,7 @@ var template = `
421467
});
422468
});
423469

424-
new Chart(combinedEl.getContext('2d'), {
470+
var combinedChart = new Chart(combinedEl.getContext('2d'), {
425471
type: 'line',
426472
data: { datasets: combinedDatasets },
427473
options: {
@@ -475,28 +521,44 @@ var template = `
475521
var sig = cap.signal_analysis || {};
476522
var freqs = Array.isArray(sig.frequencies) ? sig.frequencies : [];
477523
var mags = Array.isArray(sig.magnitudes) ? sig.magnitudes : [];
478-
if (!freqs.length || !mags.length) return;
524+
var avg = (sig.window_average && Array.isArray(sig.window_average.magnitudes)) ? sig.window_average.magnitudes : [];
525+
if (!freqs.length || (!mags.length && !avg.length)) return;
479526

480-
var pts = [];
481-
var n = Math.min(freqs.length, mags.length);
482-
for (var i = 0; i < n; i++) {
483-
pts.push({ x: freqs[i], y: mags[i] });
527+
var actualColor = ((ch.captures || []).length === 1) ? '#7e57c2' : palette(idx);
528+
var avgColor = '#c62828';
529+
function points(values) {
530+
var n = Math.min(freqs.length, values.length);
531+
var out = [];
532+
for (var i = 0; i < n; i++) out.push({ x: freqs[i], y: values[i] });
533+
return out;
484534
}
485-
486-
var c = palette(idx);
487-
datasets.push({
488-
label: cap.captureLabel,
489-
data: pts,
490-
borderColor: c,
535+
if (mags.length) datasets.push({
536+
label: cap.captureLabel + ' · Actual',
537+
data: points(mags),
538+
borderColor: actualColor,
539+
backgroundColor: 'transparent',
540+
fill: false,
541+
borderWidth: 1.6,
542+
pointRadius: 0,
543+
lineTension: 0,
544+
_seriesMode: 'actual',
545+
hidden: false
546+
});
547+
if (avg.length) datasets.push({
548+
label: cap.captureLabel + ' · Moving Avg',
549+
data: points(avg),
550+
borderColor: avgColor,
491551
backgroundColor: 'transparent',
492552
fill: false,
493553
borderWidth: 1.6,
494554
pointRadius: 0,
495-
lineTension: 0
555+
lineTension: 0,
556+
_seriesMode: 'avg',
557+
hidden: true
496558
});
497559
});
498560

499-
new Chart(ctx, {
561+
var channelChart = new Chart(ctx, {
500562
type: 'line',
501563
data: { datasets: datasets },
502564
options: {
@@ -543,6 +605,7 @@ var template = `
543605
}
544606
}
545607
});
608+
bindChartModeControls('spectrumChart-ch' + ch.channelId, channelChart);
546609
});
547610
});
548611
})();

visual/PyPNM/SingleCapture/SpectrumAnalysis/GetCapture-OFDM.html

Lines changed: 83 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,28 @@
188188
font-size: 14px;
189189
font-weight: 700;
190190
}
191+
.chart-mode-controls {
192+
display: flex;
193+
justify-content: center;
194+
gap: 14px;
195+
align-items: center;
196+
margin: 0 0 8px 0;
197+
color: #d6deef;
198+
font-size: 12px;
199+
}
200+
.chart-mode-controls label {
201+
display: inline-flex;
202+
align-items: center;
203+
gap: 6px;
204+
cursor: pointer;
205+
}
206+
.chart-mode-controls input[type="radio"] {
207+
accent-color: #5a6fd8;
208+
cursor: pointer;
209+
}
191210
.chart-canvas-wrap {
192211
position: relative;
193-
height: calc(100% - 26px);
212+
height: calc(100% - 58px);
194213
}
195214
.chart-canvas {
196215
display: block;
@@ -327,6 +346,10 @@
327346

328347
<div class="chart-container">
329348
<div class="chart-title">Spectrum Magnitude by Capture (dB)</div>
349+
<div class="chart-mode-controls" data-chart-mode="spectrumChart-ch{{channelId}}">
350+
<label><input type="radio" name="mode-spect-ch{{channelId}}" value="actual" checked> Actual</label>
351+
<label><input type="radio" name="mode-spect-ch{{channelId}}" value="avg"> Moving Avg</label>
352+
</div>
330353
<div class="chart-canvas-wrap">
331354
<canvas id="spectrumChart-ch{{channelId}}" class="chart-canvas"></canvas>
332355
</div>
@@ -374,6 +397,26 @@
374397
return colors[i % colors.length];
375398
}
376399

400+
function setChartMode(chart, mode) {
401+
if (!chart || !chart.data || !Array.isArray(chart.data.datasets)) return;
402+
chart.data.datasets.forEach(function (ds) {
403+
if (!ds || !ds._seriesMode) return;
404+
ds.hidden = (ds._seriesMode !== mode);
405+
});
406+
chart.update(0);
407+
}
408+
409+
function bindChartModeControls(canvasId, chart) {
410+
var group = document.querySelector('.chart-mode-controls[data-chart-mode="' + canvasId + '"]');
411+
if (!group) return;
412+
var radios = group.querySelectorAll('input[type="radio"]');
413+
for (var i = 0; i < radios.length; i++) {
414+
radios[i].addEventListener('change', function (ev) {
415+
if (ev && ev.target && ev.target.checked) setChartMode(chart, ev.target.value);
416+
});
417+
}
418+
}
419+
377420
pm.getData(function (err, value) {
378421
if (err || !value || !Array.isArray(value.channels)) return;
379422

@@ -387,13 +430,16 @@
387430
var freqs = Array.isArray(sig.frequencies) ? sig.frequencies : [];
388431
var mags = Array.isArray(sig.magnitudes) ? sig.magnitudes : [];
389432
if (!freqs.length || !mags.length) return;
390-
var n = Math.min(freqs.length, mags.length);
391-
var pts = [];
392-
for (var i = 0; i < n; i++) pts.push({ x: freqs[i], y: mags[i] });
393433
var c = palette(dsIdx++);
434+
function points(values) {
435+
var n = Math.min(freqs.length, values.length);
436+
var out = [];
437+
for (var i = 0; i < n; i++) out.push({ x: freqs[i], y: values[i] });
438+
return out;
439+
}
394440
combinedDatasets.push({
395441
label: 'Ch ' + ch.channelId + ' · ' + cap.captureLabel,
396-
data: pts,
442+
data: points(mags),
397443
borderColor: c,
398444
backgroundColor: 'transparent',
399445
fill: false,
@@ -404,7 +450,7 @@
404450
});
405451
});
406452

407-
new Chart(combinedEl.getContext('2d'), {
453+
var combinedChart = new Chart(combinedEl.getContext('2d'), {
408454
type: 'line',
409455
data: { datasets: combinedDatasets },
410456
options: {
@@ -458,28 +504,44 @@
458504
var sig = cap.signal_analysis || {};
459505
var freqs = Array.isArray(sig.frequencies) ? sig.frequencies : [];
460506
var mags = Array.isArray(sig.magnitudes) ? sig.magnitudes : [];
461-
if (!freqs.length || !mags.length) return;
462-
463-
var pts = [];
464-
var n = Math.min(freqs.length, mags.length);
465-
for (var i = 0; i < n; i++) {
466-
pts.push({ x: freqs[i], y: mags[i] });
507+
var avg = (sig.window_average && Array.isArray(sig.window_average.magnitudes)) ? sig.window_average.magnitudes : [];
508+
if (!freqs.length || (!mags.length && !avg.length)) return;
509+
510+
var actualColor = ((ch.captures || []).length === 1) ? '#7e57c2' : palette(idx);
511+
var avgColor = '#c62828';
512+
function points(values) {
513+
var n = Math.min(freqs.length, values.length);
514+
var out = [];
515+
for (var i = 0; i < n; i++) out.push({ x: freqs[i], y: values[i] });
516+
return out;
467517
}
468-
469-
var c = palette(idx);
470-
datasets.push({
471-
label: cap.captureLabel,
472-
data: pts,
473-
borderColor: c,
518+
if (mags.length) datasets.push({
519+
label: cap.captureLabel + ' · Actual',
520+
data: points(mags),
521+
borderColor: actualColor,
522+
backgroundColor: 'transparent',
523+
fill: false,
524+
borderWidth: 1.6,
525+
pointRadius: 0,
526+
lineTension: 0,
527+
_seriesMode: 'actual',
528+
hidden: false
529+
});
530+
if (avg.length) datasets.push({
531+
label: cap.captureLabel + ' · Moving Avg',
532+
data: points(avg),
533+
borderColor: avgColor,
474534
backgroundColor: 'transparent',
475535
fill: false,
476536
borderWidth: 1.6,
477537
pointRadius: 0,
478-
lineTension: 0
538+
lineTension: 0,
539+
_seriesMode: 'avg',
540+
hidden: true
479541
});
480542
});
481543

482-
new Chart(ctx, {
544+
var channelChart = new Chart(ctx, {
483545
type: 'line',
484546
data: { datasets: datasets },
485547
options: {
@@ -526,6 +588,7 @@
526588
}
527589
}
528590
});
591+
bindChartModeControls('spectrumChart-ch' + ch.channelId, channelChart);
529592
});
530593
});
531594
})();

0 commit comments

Comments
 (0)