WLAN-Diagramme: Y-Achse gespreizt statt Namen zusammenzuquetschen

Eddy hatte vorgeschlagen, die Netze "weiter auseinander zu ziehen". Das wurde
mit dem Argument abgelehnt, die Blockhoehe SEI der Messwert und duerfe nicht
verschoben werden. Das war ein Denkfehler: ein Netz von seinem Messwert
wegzuschieben waere falsch, den Massstab der Achse zu dehnen dagegen voellig
unbedenklich - die Achsenbeschriftung wandert mit, jeder Wert bleibt exakt
ablesbar. Genau so loest es auch WiFiAnalyzer (Einstellung "Maximum Y-axis
Value" plus Pinch-Zoom).

- DB_SCALE 2 -> 5 Einheiten je dB. Der dichte Bereich (-65...-85 dBm) waechst
  von 40 auf 100 Einheiten: Platz fuer neun Namenszeilen statt dreieinhalb.
- Achse zeigt nur den belegten Bereich (rssiBereich), auf 10 dB gerastert mit
  5 dB Luft nach oben. Ohne das waere 5 GHz mit vier Bloecken ueberwiegend
  leere Flaeche zum Scrollen. Gerastert, damit die Achse nicht bei jedem
  RSSI-Zucken springt und zwei Momentaufnahmen vergleichbar bleiben.
  Ergebnis: trotz 2,5-facher Spreizung 30 % kuerzer als vorher.
- Alle Bloecke EINES Bandes teilen sich den Bereich - sonst laese man zwei
  gleich hohe Balken als "gleich stark", obwohl 20 dB dazwischen liegen.
  Gleiches gilt im Zeitverlauf ueber alle Momentaufnahmen.
- Jede 10-dB-Linie beschriftet (vorher nur jede zweite, da war kein Platz).
- dBm-Wert am Netznamen ("FRITZ!Box 7590 AX -48"). Beantwortet Eddys erste
  Meldung ("ich sehe nicht die Sendestaerke in dB") ohne Blickwechsel zur
  Achse - und bleibt richtig, wenn ein Name verschoben werden musste. Passt
  der Text nicht, wird der Wert geopfert, nicht der Name: den Wert kann man
  an der Achse ablesen, den Namen nirgends.

Geprueft mit tools/render-chart.mjs (dicht24: 15/15 Netze beschriftet,
voll5: 25/25) und im Emulator auf der echten Kanalanalyse-Seite.
This commit is contained in:
Eduard Wisch 2026-08-16 17:48:43 +02:00
parent f671f2605c
commit 9eef6177ba
4 changed files with 212 additions and 78 deletions

View file

@ -15,51 +15,62 @@
W, W,
PAD_L, PAD_L,
PAD_R, PAD_R,
BASELINE,
PLOT_TOP, PLOT_TOP,
DB_GRID, RSSI_MIN,
DB_LABELED, RSSI_MAX,
PLOT_H,
RSSI_GOOD, RSSI_GOOD,
RSSI_WEAK, RSSI_WEAK,
FS_AXIS, FS_AXIS,
rssiToY, gridFor,
rssiToYIn,
type RssiBereich,
} from '$lib/wifi/chart'; } from '$lib/wifi/chart';
let { zonen = true }: { zonen?: boolean } = $props(); let {
zonen = true,
bereich = { min: RSSI_MIN, max: RSSI_MAX, hoehe: PLOT_H },
}: {
zonen?: boolean;
/** sichtbarer dBm-Ausschnitt; Vorgabe ist die volle Skala (Zeitverlauf) */
bereich?: RssiBereich;
} = $props();
const yGood = rssiToY(RSSI_GOOD); const grund = $derived(PLOT_TOP + bereich.hoehe);
const yWeak = rssiToY(RSSI_WEAK); const grid = $derived(gridFor(bereich));
// Zonen an den Ausschnitt kappen: liegt die Grenze außerhalb, wird die Zone
// ganz oder gar nicht gezeigt statt über den Rand hinauszulaufen.
const yGood = $derived(rssiToYIn(RSSI_GOOD, bereich));
const yWeak = $derived(rssiToYIn(RSSI_WEAK, bereich));
</script> </script>
{#if zonen} {#if zonen}
<!-- Praxis-Zonen: oberhalb -67 dBm zuverlässig, unterhalb -75 dBm kritisch. <!-- Praxis-Zonen: oberhalb -67 dBm zuverlässig, unterhalb -75 dBm kritisch.
Beantwortet "reicht das hier?" ohne Kopfrechnen. --> Beantwortet "reicht das hier?" ohne Kopfrechnen. -->
<rect x={PAD_L} y={PLOT_TOP} width={W - PAD_L - PAD_R} height={yGood - PLOT_TOP} class="fill-emerald-500/[0.07]" /> <rect x={PAD_L} y={PLOT_TOP} width={W - PAD_L - PAD_R} height={Math.max(0, yGood - PLOT_TOP)} class="fill-emerald-500/[0.07]" />
<rect x={PAD_L} y={yWeak} width={W - PAD_L - PAD_R} height={BASELINE - yWeak} class="fill-red-500/[0.07]" /> <rect x={PAD_L} y={yWeak} width={W - PAD_L - PAD_R} height={Math.max(0, grund - yWeak)} class="fill-red-500/[0.07]" />
{/if} {/if}
{#each DB_GRID as db (db)} {#each grid as db (db)}
{@const y = rssiToY(db)} {@const y = rssiToYIn(db, bereich)}
<line <line
x1={PAD_L} x1={PAD_L}
y1={y} y1={y}
x2={W - PAD_R} x2={W - PAD_R}
y2={y} y2={y}
stroke="currentColor" stroke="currentColor"
class={DB_LABELED.has(db) ? 'text-zinc-700' : 'text-zinc-800'} class="text-zinc-700"
stroke-width="0.5" stroke-width="0.5"
/> />
{#if DB_LABELED.has(db)} <text
<text x={PAD_L - 4}
x={PAD_L - 4} y={y + 3.5}
y={y + 3.5} text-anchor="end"
text-anchor="end" class="fill-zinc-300"
class="fill-zinc-300" style="font-size: {FS_AXIS}px"
style="font-size: {FS_AXIS}px" >
> {db}
{db} </text>
</text>
{/if}
{/each} {/each}
<!-- Achseneinheit — ohne sie ist nicht klar, was die Zahlen bedeuten --> <!-- Achseneinheit — ohne sie ist nicht klar, was die Zahlen bedeuten -->
@ -74,4 +85,4 @@
</text> </text>
<!-- Grundlinie --> <!-- Grundlinie -->
<line x1={PAD_L} y1={BASELINE} x2={W - PAD_R} y2={BASELINE} stroke="currentColor" class="text-zinc-600" stroke-width="1" /> <line x1={PAD_L} y1={grund} x2={W - PAD_R} y2={grund} stroke="currentColor" class="text-zinc-600" stroke-width="1" />

View file

@ -35,8 +35,6 @@
W, W,
PAD_L, PAD_L,
PAD_R, PAD_R,
PLOT_W,
BASELINE,
PLOT_TOP, PLOT_TOP,
FS_AXIS, FS_AXIS,
FS_LABEL, FS_LABEL,
@ -46,7 +44,8 @@
occupiedRange, occupiedRange,
thinTicks, thinTicks,
textWidth, textWidth,
rssiToY, rssiBereich,
rssiToYIn,
netColor, netColor,
kuerzeSsid, kuerzeSsid,
} from '$lib/wifi/chart'; } from '$lib/wifi/chart';
@ -62,20 +61,32 @@
highlightBssid?: string; highlightBssid?: string;
} = $props(); } = $props();
/** Höhe: Plot + Kanalbeschriftung + Blockunterschrift */
const H = BASELINE + 34;
/** /**
* Ein Block ohne Netze braucht keine volle dBm-Skala — er wird flach * Ein Block ohne Netze braucht keine dBm-Skala — er wird flach gezeichnet.
* gezeichnet. Sonst kostet jedes leere Teilband so viel Platz wie ein * Sonst kostet jedes leere Teilband so viel Platz wie ein belegtes, und bei
* belegtes, und bei vier 5-GHz-Blöcken müsste man an leeren Flächen * vier 5-GHz-Blöcken müsste man an leeren Flächen vorbeiscrollen, um die
* vorbeiscrollen, um die belegten zu finden. * belegten zu finden.
*/ */
const H_LEER = 52; const H_LEER = 52;
/**
* dBm-Ausschnitt EINMAL für das gesamte Band — nicht je Block. Zwei Blöcke
* mit unterschiedlicher Achse würde man unweigerlich als „gleich stark"
* fehllesen, weil beide Balken gleich hoch aussähen.
*/
const bereich = $derived(
rssiBereich(networks.filter((n) => n.band === band).map((n) => n.rssi)),
);
const grund = $derived(PLOT_TOP + bereich.hoehe);
/** Höhe: Plot + Kanalbeschriftung + Blockunterschrift */
const H = $derived(grund + 34);
const segmente = $derived.by(() => { const segmente = $derived.by(() => {
const segs = BAND_SEGMENTS[band] ?? []; const segs = BAND_SEGMENTS[band] ?? [];
const scale = scaleForBand(band); const scale = scaleForBand(band);
const inBand = networks.filter((n) => n.band === band); const inBand = networks.filter((n) => n.band === band);
const ber = bereich;
const base = PLOT_TOP + ber.hoehe;
return segs.map((seg) => { return segs.map((seg) => {
const spanMhz = seg.maxMhz - seg.minMhz; const spanMhz = seg.maxMhz - seg.minMhz;
@ -98,7 +109,7 @@
const gekapptLinks = x(from) < PAD_L - 0.5; const gekapptLinks = x(from) < PAD_L - 0.5;
const gekapptRechts = x(to) > PAD_L + plotW + 0.5; const gekapptRechts = x(to) > PAD_L + plotW + 0.5;
const breite = Math.max(3, xTo - xFrom); const breite = Math.max(3, xTo - xFrom);
const y = rssiToY(n.rssi); const y = rssiToYIn(n.rssi, ber);
// Schulter als feste Frequenzmenge (2 MHz), damit die Flankenneigung // Schulter als feste Frequenzmenge (2 MHz), damit die Flankenneigung
// bei 20 und 160 MHz gleich aussieht statt mit der Breite zu skalieren. // bei 20 und 160 MHz gleich aussieht statt mit der Breite zu skalieren.
const schulter = Math.min(breite / 3, 2 * scale); const schulter = Math.min(breite / 3, 2 * scale);
@ -114,10 +125,10 @@
isSelf, isSelf,
farbe: netColor(n.bssid, isSelf), farbe: netColor(n.bssid, isSelf),
pfad: pfad:
`M ${xFrom.toFixed(1)},${BASELINE} ` + `M ${xFrom.toFixed(1)},${base} ` +
`L ${(xFrom + schulter).toFixed(1)},${y.toFixed(1)} ` + `L ${(xFrom + schulter).toFixed(1)},${y.toFixed(1)} ` +
`L ${(xFrom + breite - schulter).toFixed(1)},${y.toFixed(1)} ` + `L ${(xFrom + breite - schulter).toFixed(1)},${y.toFixed(1)} ` +
`L ${(xFrom + breite).toFixed(1)},${BASELINE} Z`, `L ${(xFrom + breite).toFixed(1)},${base} Z`,
}; };
}); });
@ -143,14 +154,22 @@
* *
* Das Kernproblem, gemeldet vom Anwender: die Wunsch-Höhe eines Labels * Das Kernproblem, gemeldet vom Anwender: die Wunsch-Höhe eines Labels
* hängt am Messwert, und reale Nachbarnetze liegen fast alle zwischen * hängt am Messwert, und reale Nachbarnetze liegen fast alle zwischen
* 65 und 85 dBm — also in einem Band von nur ~40 Einheiten. Dort * 65 und 85 dBm. Dort drängeln sich bei einer Wohnhaus-Nachbarschaft
* drängeln sich bei einer Wohnhaus-Nachbarschaft zehn und mehr Namen * zehn und mehr Namen übereinander und werden unlesbar.
* übereinander und werden unlesbar.
* *
* Die Blöcke dürfen dabei NICHT verschoben werden — ihre Höhe IST der * Der eigentliche Hebel dagegen ist die gespreizte Y-Achse (DB_SCALE in
* Messwert, sie zu spreizen würde die dBm-Achse zur Falschaussage * chart.ts): aus 40 Einheiten für den dichten Bereich werden 100, damit
* machen. Verschoben wird ausschließlich die Beschriftung; eine dünne * lösen sich die meisten Überschneidungen von selbst. Anmerkung zur
* Zuordnungslinie führt zum zugehörigen Block. * Entstehung, weil der Irrtum hier festgehalten gehört: die Spreizung
* wurde zunächst mit dem Argument abgelehnt, die Blockhöhe SEI der
* Messwert und dürfe nicht verschoben werden. Das verwechselte zwei
* Dinge — ein Netz von seinem Messwert wegzuschieben wäre falsch, den
* Maßstab der Achse zu dehnen dagegen völlig unbedenklich, weil die
* Achsenbeschriftung mitwandert und jeder Wert ablesbar bleibt.
*
* Was danach noch übereinander liegt, wird als Beschriftung verschoben —
* nur der Text, nie der Block; eine dünne Zuordnungslinie in der Farbe
* des Netzes führt zurück zum zugehörigen Block.
* *
* Verfahren: stärkstes Netz zuerst (das interessiert am meisten und * Verfahren: stärkstes Netz zuerst (das interessiert am meisten und
* bekommt deshalb die beste Position), dann für jedes weitere Label vom * bekommt deshalb die beste Position), dann für jedes weitere Label vom
@ -174,31 +193,52 @@
const frei = (y: number, x1: number, x2: number) => const frei = (y: number, x1: number, x2: number) =>
y >= PLOT_TOP + 2 && y >= PLOT_TOP + 2 &&
y <= BASELINE - 2 && y <= base - 2 &&
!belegteLabel.some( !belegteLabel.some(
(l) => Math.abs(l.y - y) < ZEILE && x1 < l.x2 + 2 && x2 > l.x1 - 2, (l) => Math.abs(l.y - y) < ZEILE && x1 < l.x2 + 2 && x2 > l.x1 - 2,
); );
for (const b of [...bloecke].sort((a, c) => c.n.rssi - a.n.rssi)) { for (const b of [...bloecke].sort((a, c) => c.n.rssi - a.n.rssi)) {
const text = kuerzeSsid(b.n.ssid || b.n.bssid.slice(-8)); const name = kuerzeSsid(b.n.ssid || b.n.bssid.slice(-8));
const breite = textWidth(text, FS_LABEL);
// waagerecht im Segment halten, damit nichts aus dem Bild läuft
const cx = Math.min(
PAD_L + plotW - breite / 2 - 1,
Math.max(PAD_L + breite / 2 + 1, b.mitte),
);
const x1 = cx - breite / 2;
const x2 = cx + breite / 2;
const wunschY = b.y - 4; const wunschY = b.y - 4;
/*
* Zwei Textfassungen, lange zuerst: „FRITZ!Box 7590 48" statt nur
* „FRITZ!Box 7590". Der angehängte Messwert war die ursprüngliche
* Meldung des Anwenders („ich sehe nicht die Sendestärke in dB in den
* Diagrammen"). Die dBm-Achse beantwortet das inzwischen, aber am Namen
* steht der Wert ohne Blickwechsel zur Achse — und vor allem auch dann
* noch richtig, wenn das Label verschoben werden musste und damit nicht
* mehr auf der Höhe seines Blocks steht.
*
* Reicht der Platz nicht, wird der Wert geopfert, nicht der Name: den
* Wert kann man an der Achse ablesen, den Namen nirgends.
*/
let text = `${name} ${b.n.rssi}`;
let breite = textWidth(text, FS_LABEL);
let cx = 0, x1 = 0, x2 = 0;
let y: number | null = null;
let y: number | null = frei(wunschY, x1, x2) ? wunschY : null; for (const fassung of [text, name]) {
// abwechselnd nach oben und unten suchen — nach oben zuerst, weil text = fassung;
// dort der Platz über den Blöcken frei ist breite = textWidth(text, FS_LABEL);
for (let stufe = 1; y === null && stufe <= 12; stufe++) { // waagerecht im Segment halten, damit nichts aus dem Bild läuft
const hoch = wunschY - stufe * ZEILE; cx = Math.min(
if (frei(hoch, x1, x2)) { y = hoch; break; } PAD_L + plotW - breite / 2 - 1,
const runter = wunschY + stufe * ZEILE; Math.max(PAD_L + breite / 2 + 1, b.mitte),
if (frei(runter, x1, x2)) { y = runter; break; } );
x1 = cx - breite / 2;
x2 = cx + breite / 2;
if (frei(wunschY, x1, x2)) { y = wunschY; break; }
// abwechselnd nach oben und unten suchen — nach oben zuerst, weil
// dort der Platz über den Blöcken frei ist
for (let stufe = 1; stufe <= 12; stufe++) {
const hoch = wunschY - stufe * ZEILE;
if (frei(hoch, x1, x2)) { y = hoch; break; }
const runter = wunschY + stufe * ZEILE;
if (frei(runter, x1, x2)) { y = runter; break; }
}
if (y !== null) break;
} }
if (y === null) { if (y === null) {
ohneLabel++; ohneLabel++;
@ -257,7 +297,7 @@
</svg> </svg>
{:else} {:else}
<svg viewBox="0 0 {W} {H}" class="w-full" role="img" aria-label="Kanalbelegung {s.seg.name}"> <svg viewBox="0 0 {W} {H}" class="w-full" role="img" aria-label="Kanalbelegung {s.seg.name}">
<RssiAxis /> <RssiAxis {bereich} />
<!-- Blöcke: schwächste zuerst, damit starke oben liegen --> <!-- Blöcke: schwächste zuerst, damit starke oben liegen -->
{#each [...s.bloecke].sort((a, b) => a.n.rssi - b.n.rssi) as b (b.n.bssid)} {#each [...s.bloecke].sort((a, b) => a.n.rssi - b.n.rssi) as b (b.n.bssid)}
@ -303,9 +343,9 @@
{#each s.ticks as t (t.channel)} {#each s.ticks as t (t.channel)}
<line <line
x1={t.x} x1={t.x}
y1={BASELINE} y1={grund}
x2={t.x} x2={t.x}
y2={BASELINE + (t.label ? 4 : 2)} y2={grund + (t.label ? 4 : 2)}
stroke="currentColor" stroke="currentColor"
class={t.belegt ? 'text-zinc-400' : 'text-zinc-600'} class={t.belegt ? 'text-zinc-400' : 'text-zinc-600'}
stroke-width="1" stroke-width="1"
@ -313,7 +353,7 @@
{#if t.label} {#if t.label}
<text <text
x={t.x} x={t.x}
y={BASELINE + 14} y={grund + 14}
text-anchor="middle" text-anchor="middle"
class={t.belegt ? 'fill-zinc-200' : 'fill-zinc-500'} class={t.belegt ? 'fill-zinc-200' : 'fill-zinc-500'}
style="font-size: {FS_AXIS}px" style="font-size: {FS_AXIS}px"
@ -325,11 +365,11 @@
<!-- Blockunterschrift. Unbeschriftete Netze werden ausgewiesen — sonst <!-- Blockunterschrift. Unbeschriftete Netze werden ausgewiesen — sonst
entstünde der Eindruck, es wären weniger Netze als tatsächlich da. --> entstünde der Eindruck, es wären weniger Netze als tatsächlich da. -->
<text x={PAD_L} y={BASELINE + 28} class="fill-zinc-500" style="font-size: {FS_LABEL}px"> <text x={PAD_L} y={grund + 28} class="fill-zinc-500" style="font-size: {FS_LABEL}px">
{s.seg.name}{s.seg.dfs ? ' · DFS' : ''} · {s.anzahl} {s.seg.name}{s.seg.dfs ? ' · DFS' : ''} · {s.anzahl}
{s.anzahl === 1 ? 'Netz' : 'Netze'}{s.ohneLabel > 0 ? ` · ${s.ohneLabel} ohne Namen` : ''} {s.anzahl === 1 ? 'Netz' : 'Netze'}{s.ohneLabel > 0 ? ` · ${s.ohneLabel} ohne Namen` : ''}
</text> </text>
<text x={W - PAD_R} y={BASELINE + 28} text-anchor="end" class="fill-zinc-600" style="font-size: {FS_LABEL}px"> <text x={W - PAD_R} y={grund + 28} text-anchor="end" class="fill-zinc-600" style="font-size: {FS_LABEL}px">
Kanal Kanal
</text> </text>
</svg> </svg>

View file

@ -13,7 +13,7 @@
*/ */
import type { WifiSurvey } from '$lib/types'; import type { WifiSurvey } from '$lib/types';
import RssiAxis from './RssiAxis.svelte'; import RssiAxis from './RssiAxis.svelte';
import { W, PAD_L, PAD_R, BASELINE, FS_LABEL, rssiToY, netColor, kuerzeSsid } from '$lib/wifi/chart'; import { W, PAD_L, PAD_R, PLOT_TOP, FS_LABEL, rssiBereich, rssiToYIn, netColor, kuerzeSsid } from '$lib/wifi/chart';
let { let {
surveys, surveys,
@ -23,7 +23,16 @@
connectedBssid?: string; connectedBssid?: string;
} = $props(); } = $props();
const H = BASELINE + 16; /**
* dBm-Ausschnitt über ALLE Momentaufnahmen hinweg — nicht je Aufnahme. Eine
* mitwandernde Achse würde eine Veränderung vortäuschen, wo keine ist: die
* Linie bliebe waagerecht, obwohl der Pegel gefallen wäre.
*/
const bereich = $derived(
rssiBereich(surveys.flatMap((s) => s.networks.map((n) => n.rssi))),
);
const grund = $derived(PLOT_TOP + bereich.hoehe);
const H = $derived(grund + 16);
const view = $derived.by(() => { const view = $derived.by(() => {
const sorted = [...surveys].sort((a, b) => a.createdAt - b.createdAt); const sorted = [...surveys].sort((a, b) => a.createdAt - b.createdAt);
@ -65,7 +74,7 @@
continue; continue;
} }
const px = x(p.ts); const px = x(p.ts);
const py = rssiToY(p.rssi); const py = rssiToYIn(p.rssi, bereich);
punkte.push({ x: px, y: py }); punkte.push({ x: px, y: py });
current.push(`${px.toFixed(1)},${py.toFixed(1)}`); current.push(`${px.toFixed(1)},${py.toFixed(1)}`);
} }
@ -96,7 +105,7 @@
{#if view} {#if view}
<svg viewBox="0 0 {W} {H}" class="w-full" role="img" aria-label="Empfangspegel über die Momentaufnahmen"> <svg viewBox="0 0 {W} {H}" class="w-full" role="img" aria-label="Empfangspegel über die Momentaufnahmen">
<RssiAxis /> <RssiAxis {bereich} />
{#each view.series as s (s.bssid)} {#each view.series as s (s.bssid)}
{#each s.segments as seg, i (i)} {#each s.segments as seg, i (i)}
<polyline points={seg} fill="none" stroke="currentColor" stroke-width="1.5" class={s.farbe.stroke} /> <polyline points={seg} fill="none" stroke="currentColor" stroke-width="1.5" class={s.farbe.stroke} />

View file

@ -26,21 +26,95 @@ export const PLOT_W = W - PAD_L - PAD_R;
/* --- dBm-Skala (Empfangspegel) --- */ /* --- dBm-Skala (Empfangspegel) --- */
export const RSSI_MIN = -95; export const RSSI_MIN = -95;
export const RSSI_MAX = -30; export const RSSI_MAX = -30;
/** Einheiten je dB — bewusst glatt, siehe Kopfkommentar */ /**
export const DB_SCALE = 2; * Einheiten je dB. Bestimmt, wie hoch das Diagramm wird und damit, wieviel
export const PLOT_H = (RSSI_MAX - RSSI_MIN) * DB_SCALE; // 130 * Platz die Netznamen untereinander haben.
*
* Stand vorher: 2 Einheiten je dB, also 130 Einheiten Plothöhe. Bei einer
* realen Nachbarschaft liegen fast alle Netze zwischen 65 und 85 dBm; das
* sind bei diesem Maßstab nur 40 Einheiten, in denen sich zehn und mehr Namen
* drängen mussten. Genau das hat der Anwender gemeldet (kaum lesbar muss
* weiter auseinander gezogen werden") und er hatte recht: die Y-Achse gibt
* vor, wo 50 dBm liegt, man kann den Maßstab also einfach dehnen, ohne dass
* ein einziger Wert falsch wird. Die Achsenbeschriftung wandert mit.
*
* 5 Einheiten je dB 325 Einheiten Plothöhe (2,5-fach). Aus den 40 Einheiten
* im dichten Bereich werden 100 Platz für neun Namenszeilen statt dreieinhalb.
* Vorbild ist die WiFi-Analyzer-App, die den Bildschirm vertikal ausnutzt
* statt alles zusammenzuquetschen.
*/
export const DB_SCALE = 5;
export const PLOT_H = (RSSI_MAX - RSSI_MIN) * DB_SCALE; // 325
export const PLOT_TOP = 20; export const PLOT_TOP = 20;
export const BASELINE = PLOT_TOP + PLOT_H; // 150 export const BASELINE = PLOT_TOP + PLOT_H; // 345
/** dBm → y-Koordinate */ /**
* Sichtbarer dBm-Bereich eines Bandes.
*
* Mit dem gespreizten Maßstab (5 Einheiten je dB) wäre der volle Bereich
* 9530 immer 325 Einheiten hoch auch dann, wenn alle Netze zwischen 55
* und 80 liegen und die obere Hälfte leer bleibt. Bei vier 5-GHz-Blöcken
* untereinander ergäbe das reine Leerfläche zum Scrollen.
*
* Deshalb umfasst die Achse nur den belegten Bereich, auf 10 dB gerastert und
* mit einer Reserve nach oben und unten. Gerastert deshalb, damit sie sich
* nicht bei jedem RSSI-Zucken verschiebt zwei Momentaufnahmen desselben
* Standorts bleiben so vergleichbar. Das Vorbild ist die einstellbare Y-Achse
* der WiFi-Analyzer-App.
*
* WICHTIG: Alle Blöcke EINES Bandes teilen sich diesen Bereich, sonst wären
* die Diagramme untereinander nicht vergleichbar.
*/
export interface RssiBereich {
min: number;
max: number;
/** Höhe der Zeichenfläche in Einheiten */
hoehe: number;
}
export function rssiBereich(rssis: number[]): RssiBereich {
if (rssis.length === 0) {
// ohne Netze ein schmaler Standardbereich statt der vollen Höhe
return { min: -90, max: -50, hoehe: 40 * DB_SCALE };
}
const stk = Math.max(...rssis);
const schwach = Math.min(...rssis);
// Oben mindestens 5 dB Luft — dort steht der Name des stärksten Netzes, und
// liegt dessen Pegel zufällig genau auf einer Rasterlinie, hätte er sonst
// keinen Platz. Unten genügt das Abrunden (bis zu 9 dB Reserve).
const max = Math.min(RSSI_MAX, Math.ceil((stk + 5) / 10) * 10);
const min = Math.max(RSSI_MIN, Math.floor((schwach - 3) / 10) * 10);
// mindestens 30 dB zeigen, damit ein einzelnes Netz keine Briefmarke wird
const spanne = Math.max(30, max - min);
return { min: max - spanne, max, hoehe: spanne * DB_SCALE };
}
/** dBm → y-Koordinate innerhalb eines Bereichs */
export function rssiToYIn(rssi: number, b: RssiBereich): number {
const clamped = Math.min(b.max, Math.max(b.min, rssi));
return PLOT_TOP + b.hoehe - (clamped - b.min) * DB_SCALE;
}
/** dBm → y-Koordinate auf der festen Vollskala (Zeitverlauf) */
export function rssiToY(rssi: number): number { export function rssiToY(rssi: number): number {
const clamped = Math.min(RSSI_MAX, Math.max(RSSI_MIN, rssi)); const clamped = Math.min(RSSI_MAX, Math.max(RSSI_MIN, rssi));
return BASELINE - (clamped - RSSI_MIN) * DB_SCALE; return BASELINE - (clamped - RSSI_MIN) * DB_SCALE;
} }
/** Rasterlinien alle 10 dB; nur jede zweite wird beschriftet (größer = schneller erfassbar) */ /** Rasterlinien-Werte innerhalb eines Bereichs (alle 10 dB) */
export function gridFor(b: RssiBereich): number[] {
const out: number[] = [];
for (let v = Math.ceil(b.min / 10) * 10; v <= b.max; v += 10) out.push(v);
return out;
}
/**
* Rasterlinien alle 10 dB. Seit der Achsenspreizung (DB_SCALE 5) liegen sie
* 50 Einheiten auseinander genug Platz, um jede zu beschriften. Vorher waren
* es 20 Einheiten und nur jede zweite bekam eine Zahl.
*/
export const DB_GRID = [-90, -80, -70, -60, -50, -40, -30]; export const DB_GRID = [-90, -80, -70, -60, -50, -40, -30];
export const DB_LABELED = new Set([-90, -70, -50, -30]); export const DB_LABELED = new Set(DB_GRID);
/** Praxis-Schwellen für die farbigen Hintergrundzonen */ /** Praxis-Schwellen für die farbigen Hintergrundzonen */
export const RSSI_GOOD = -67; export const RSSI_GOOD = -67;