diff --git a/server/static/index.html b/server/static/index.html
index ac5eb74..83d8041 100644
--- a/server/static/index.html
+++ b/server/static/index.html
@@ -332,7 +332,7 @@
-
RX Quality и плотность точек vs расстояние TX↔RX
+
RX Quality vs расстояние TX↔RX (распределение по отрезкам)
diff --git a/server/static/quality-viz.js b/server/static/quality-viz.js
index 6250707..4438b01 100644
--- a/server/static/quality-viz.js
+++ b/server/static/quality-viz.js
@@ -2,6 +2,8 @@
(function (global) {
'use strict';
+ const QUALITY_BUCKET = 10;
+
function niceMeterStep(span) {
if (span <= 50) return 10;
if (span <= 150) return 25;
@@ -28,7 +30,12 @@
return ticks;
}
- function drawMeterAxis(ctx, margin, plotW, plotH, h, minD, span, w) {
+ function qualityBucket(quality) {
+ const q = Math.round(Number(quality) / QUALITY_BUCKET) * QUALITY_BUCKET;
+ return Math.min(100, Math.max(0, q));
+ }
+
+ function drawMeterAxis(ctx, margin, plotW, plotH, h, minD, span) {
const ticks = niceMeterTicks(minD, minD + span);
const minLabelGap = 28;
const maxLabels = Math.max(2, Math.floor(plotW / minLabelGap));
@@ -57,6 +64,15 @@
});
}
+ function histogramInBin(qualities) {
+ const hist = new Map();
+ for (const q of qualities) {
+ const bucket = qualityBucket(q);
+ hist.set(bucket, (hist.get(bucket) || 0) + 1);
+ }
+ return hist;
+ }
+
function drawQualityDistChart(canvas, samples, qualityColor, highlightDist) {
if (!canvas) return;
const ctx = canvas.getContext('2d');
@@ -78,15 +94,16 @@
const minD = Math.min(...dists);
const maxD = Math.max(...dists);
const span = Math.max(maxD - minD, 1);
- const binCount = Math.min(40, Math.max(8, Math.ceil(span / 5)));
- const binW = span / binCount;
+ const meterStep = niceMeterStep(span);
+ const binCount = Math.min(60, Math.max(1, Math.ceil(span / meterStep)));
+ const binWDist = span / binCount;
const bins = Array.from({ length: binCount }, (_, i) => ({
- min: minD + i * binW,
- max: minD + (i + 1) * binW,
+ min: minD + i * binWDist,
+ max: minD + (i + 1) * binWDist,
qualities: [],
}));
for (const s of samples) {
- let idx = Math.floor((s.distM - minD) / binW);
+ let idx = Math.floor((s.distM - minD) / binWDist);
if (idx >= binCount) idx = binCount - 1;
if (idx < 0) idx = 0;
bins[idx].qualities.push(s.quality);
@@ -95,72 +112,73 @@
const margin = { l: 36, r: 8, t: 16, b: 30 };
const plotW = w - margin.l - margin.r;
const plotH = h - margin.t - margin.b;
- const distBandH = Math.round(plotH * 0.25);
- const qualBandH = plotH - distBandH - 4;
- const qualTop = margin.t;
- const distTop = margin.t + qualBandH + 4;
- const axisY = margin.t + plotH;
+ const barW = plotW / binCount;
+ const markerH = Math.max(3, Math.min(6, barW * 0.45));
ctx.strokeStyle = '#333';
ctx.beginPath();
- ctx.moveTo(margin.l, qualTop);
- ctx.lineTo(margin.l, axisY);
- ctx.lineTo(margin.l + plotW, axisY);
+ ctx.moveTo(margin.l, margin.t);
+ ctx.lineTo(margin.l, margin.t + plotH);
+ ctx.lineTo(margin.l + plotW, margin.t + plotH);
ctx.stroke();
ctx.fillStyle = '#888';
ctx.font = '9px system-ui';
- ctx.fillText('0%', 2, distTop - 2);
- ctx.fillText('100%', 2, qualTop + 8);
- ctx.fillStyle = '#666';
- ctx.fillText('точки', 2, distTop + distBandH - 2);
+ ctx.fillText('0%', 2, margin.t + plotH);
+ ctx.fillText('100%', 2, margin.t + 8);
- drawMeterAxis(ctx, margin, plotW, plotH, h, minD, span, w);
-
- const barW = plotW / binCount;
- let maxCount = 1;
- bins.forEach(b => {
- if (b.qualities.length > maxCount) maxCount = b.qualities.length;
- });
+ drawMeterAxis(ctx, margin, plotW, plotH, h, minD, span);
bins.forEach((b, i) => {
- const x = margin.l + i / binCount * plotW;
- const count = b.qualities.length;
+ const x0 = margin.l + i / binCount * plotW;
+ const cx = x0 + barW / 2;
const highlight = highlightDist != null
&& highlightDist >= b.min && highlightDist < b.max;
+ const count = b.qualities.length;
if (count === 0) {
- ctx.fillStyle = 'rgba(255,255,255,0.08)';
- ctx.fillRect(x, distTop + distBandH - 1, barW, 1);
- } else {
- const distBarH = (count / maxCount) * distBandH;
- const intensity = 0.35 + 0.65 * (count / maxCount);
- ctx.fillStyle = `rgba(74, 85, 104, ${intensity})`;
- ctx.fillRect(x, distTop + distBandH - distBarH, barW, distBarH);
+ ctx.fillStyle = 'rgba(255,255,255,0.06)';
+ ctx.fillRect(x0, margin.t + plotH - 1, barW, 1);
+ return;
+ }
- const avg = b.qualities.reduce((a, v) => a + v, 0) / count;
- const qualBarH = (avg / 100) * qualBandH;
- const y = qualTop + qualBandH - qualBarH;
- const col = qualityColor ? qualityColor(avg) : '#888';
+ const hist = histogramInBin(b.qualities);
+ let maxInBin = 1;
+ hist.forEach(c => {
+ if (c > maxInBin) maxInBin = c;
+ });
+
+ if (highlight) {
+ ctx.fillStyle = 'rgba(255,255,255,0.06)';
+ ctx.fillRect(x0, margin.t, barW, plotH);
+ }
+
+ const sortedBuckets = Array.from(hist.keys()).sort((a, b) => a - b);
+ for (const bq of sortedBuckets) {
+ const n = hist.get(bq);
+ const frac = n / maxInBin;
+ const segW = Math.max(2, frac * (barW - 1));
+ const y = margin.t + plotH - (bq / 100) * plotH;
+ const col = qualityColor ? qualityColor(bq) : '#888';
ctx.fillStyle = col;
- ctx.globalAlpha = highlight ? 1 : 0.75;
- ctx.fillRect(x, y, barW, qualBarH);
+ ctx.globalAlpha = highlight ? 0.95 : (0.45 + 0.55 * frac);
+ ctx.fillRect(cx - segW / 2, y - markerH / 2, segW, markerH);
ctx.globalAlpha = 1;
- if (highlight) {
- ctx.strokeStyle = '#fff';
- ctx.lineWidth = 1.5;
- ctx.strokeRect(x, y, barW, qualBarH);
- ctx.strokeRect(x, distTop + distBandH - distBarH, barW, distBarH);
- }
+ }
+
+ if (highlight) {
+ ctx.strokeStyle = 'rgba(255,255,255,0.85)';
+ ctx.lineWidth = 1.5;
+ ctx.strokeRect(x0 + 0.5, margin.t + 0.5, barW - 1, plotH - 1);
}
});
- ctx.fillStyle = 'rgba(255,255,255,0.25)';
+ ctx.fillStyle = 'rgba(255,255,255,0.18)';
samples.forEach(s => {
const x = margin.l + ((s.distM - minD) / span) * plotW;
- const y = qualTop + qualBandH - (s.quality / 100) * qualBandH;
+ const y = margin.t + plotH - (s.quality / 100) * plotH;
ctx.beginPath();
- ctx.arc(x, y, 1.5, 0, Math.PI * 2);
+ ctx.arc(x, y, 1.2, 0, Math.PI * 2);
ctx.fill();
});
}