From e17e36a4c9ec181dfd910cc815fdad4d01ee18f1 Mon Sep 17 00:00:00 2001 From: grigo Date: Mon, 22 Jun 2026 10:38:05 +0300 Subject: [PATCH] fix --- server/static/index.html | 2 +- server/static/quality-viz.js | 116 ++++++++++++++++++++--------------- 2 files changed, 68 insertions(+), 50 deletions(-) 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(); }); }