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.
134 lines
5.1 KiB
Svelte
134 lines
5.1 KiB
Svelte
<script lang="ts">
|
||
/**
|
||
* Empfangspegel mehrerer Netze über die gespeicherten Momentaufnahmen.
|
||
*
|
||
* Bewusst eine eigene Komponente (und nicht in der Seite eingebettet): so
|
||
* lässt sie sich mit `tools/render-chart.mjs` ohne Gerät rendern und
|
||
* ansehen. Genau dieser fehlende Blick aufs fertige Bild war die Ursache
|
||
* dafür, dass die erste Fassung der Diagramme unlesbar ausgeliefert wurde.
|
||
*
|
||
* Nutzt dieselbe dBm-Achse wie der Kanalgraph (RssiAxis) — vorher hatte
|
||
* dieses Diagramm eine eigene Skala (−100…−20) ganz ohne Beschriftung, die
|
||
* Werte waren also weder ablesbar noch mit dem Kanalgraph vergleichbar.
|
||
*/
|
||
import type { WifiSurvey } from '$lib/types';
|
||
import RssiAxis from './RssiAxis.svelte';
|
||
import { W, PAD_L, PAD_R, PLOT_TOP, FS_LABEL, rssiBereich, rssiToYIn, netColor, kuerzeSsid } from '$lib/wifi/chart';
|
||
|
||
let {
|
||
surveys,
|
||
connectedBssid = undefined,
|
||
}: {
|
||
surveys: WifiSurvey[];
|
||
connectedBssid?: string;
|
||
} = $props();
|
||
|
||
/**
|
||
* 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 sorted = [...surveys].sort((a, b) => a.createdAt - b.createdAt);
|
||
if (sorted.length < 2) return null;
|
||
|
||
// Meistgesehene Netze zuerst, auf 6 begrenzt — mehr Linien wären auf einem
|
||
// Handy nicht mehr auseinanderzuhalten. Schlüssel ist das BSSID, damit die
|
||
// Farbe dieselbe ist wie im Kanalgraph und in der Netzliste.
|
||
const counts = new Map<string, { bssid: string; name: string; n: number }>();
|
||
for (const s of sorted) {
|
||
for (const net of s.networks) {
|
||
const e = counts.get(net.bssid);
|
||
if (e) e.n++;
|
||
else counts.set(net.bssid, { bssid: net.bssid, name: net.ssid || net.bssid, n: 1 });
|
||
}
|
||
}
|
||
const top = [...counts.values()].sort((a, b) => b.n - a.n).slice(0, 6);
|
||
|
||
const t0 = sorted[0].createdAt;
|
||
const t1 = sorted[sorted.length - 1].createdAt;
|
||
const span = Math.max(1, t1 - t0);
|
||
const x = (ts: number) => PAD_L + ((ts - t0) / span) * (W - PAD_L - PAD_R);
|
||
|
||
const series = top.map((netz) => {
|
||
const points = sorted.map((s) => {
|
||
const match = s.networks.find((n) => n.bssid === netz.bssid);
|
||
return { ts: s.createdAt, rssi: match ? match.rssi : null };
|
||
});
|
||
// In Segmente teilen — war ein Netz in einer Aufnahme nicht sichtbar,
|
||
// wird die Linie NICHT durchgezogen: das würde ein stabiles Signal
|
||
// vortäuschen, wo tatsächlich ein Aussetzer war.
|
||
const segments: string[] = [];
|
||
const punkte: { x: number; y: number }[] = [];
|
||
let current: string[] = [];
|
||
for (const p of points) {
|
||
if (p.rssi == null) {
|
||
if (current.length > 1) segments.push(current.join(' '));
|
||
current = [];
|
||
continue;
|
||
}
|
||
const px = x(p.ts);
|
||
const py = rssiToYIn(p.rssi, bereich);
|
||
punkte.push({ x: px, y: py });
|
||
current.push(`${px.toFixed(1)},${py.toFixed(1)}`);
|
||
}
|
||
if (current.length > 1) segments.push(current.join(' '));
|
||
const letzter = points.filter((p) => p.rssi != null).at(-1);
|
||
return {
|
||
bssid: netz.bssid,
|
||
name: kuerzeSsid(netz.name),
|
||
segments,
|
||
punkte,
|
||
letzterWert: letzter?.rssi ?? null,
|
||
farbe: netColor(netz.bssid, netz.bssid === connectedBssid),
|
||
};
|
||
});
|
||
|
||
return { series, tFirst: t0, tLast: t1 };
|
||
});
|
||
|
||
function fmtDateTime(ts: number): string {
|
||
return new Date(ts).toLocaleString('de-DE', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
});
|
||
}
|
||
</script>
|
||
|
||
{#if view}
|
||
<svg viewBox="0 0 {W} {H}" class="w-full" role="img" aria-label="Empfangspegel über die Momentaufnahmen">
|
||
<RssiAxis {bereich} />
|
||
{#each view.series as s (s.bssid)}
|
||
{#each s.segments as seg, i (i)}
|
||
<polyline points={seg} fill="none" stroke="currentColor" stroke-width="1.5" class={s.farbe.stroke} />
|
||
{/each}
|
||
<!-- Messpunkte sichtbar machen: eine Serie mit nur EINEM Wert hat kein
|
||
Liniensegment und wäre sonst komplett unsichtbar. -->
|
||
{#each s.punkte as p, i (i)}
|
||
<circle cx={p.x} cy={p.y} r="1.8" class={s.farbe.text} />
|
||
{/each}
|
||
{/each}
|
||
<text x={PAD_L} y={H - 3} class="fill-zinc-500" style="font-size: {FS_LABEL}px">
|
||
{fmtDateTime(view.tFirst)}
|
||
</text>
|
||
<text x={W - PAD_R} y={H - 3} text-anchor="end" class="fill-zinc-500" style="font-size: {FS_LABEL}px">
|
||
{fmtDateTime(view.tLast)}
|
||
</text>
|
||
</svg>
|
||
<div class="mt-1 flex flex-wrap gap-x-3 gap-y-1">
|
||
{#each view.series as s (s.bssid)}
|
||
<span class="flex items-center gap-1 text-[10px] text-zinc-400">
|
||
<span class="h-1.5 w-1.5 rounded-full {s.farbe.dot}"></span>
|
||
{s.name}{s.letzterWert != null ? ` ${s.letzterWert} dBm` : ''}
|
||
</span>
|
||
{/each}
|
||
</div>
|
||
{/if}
|