diff --git a/src/lib/components/RssiAxis.svelte b/src/lib/components/RssiAxis.svelte
index 12d3601..5008773 100644
--- a/src/lib/components/RssiAxis.svelte
+++ b/src/lib/components/RssiAxis.svelte
@@ -15,51 +15,62 @@
W,
PAD_L,
PAD_R,
- BASELINE,
PLOT_TOP,
- DB_GRID,
- DB_LABELED,
+ RSSI_MIN,
+ RSSI_MAX,
+ PLOT_H,
RSSI_GOOD,
RSSI_WEAK,
FS_AXIS,
- rssiToY,
+ gridFor,
+ rssiToYIn,
+ type RssiBereich,
} 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 yWeak = rssiToY(RSSI_WEAK);
+ const grund = $derived(PLOT_TOP + bereich.hoehe);
+ 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));
{#if zonen}
-
-
+
+
{/if}
-{#each DB_GRID as db (db)}
- {@const y = rssiToY(db)}
+{#each grid as db (db)}
+ {@const y = rssiToYIn(db, bereich)}
- {#if DB_LABELED.has(db)}
-
- {db}
-
- {/if}
+
+ {db}
+
{/each}
@@ -74,4 +85,4 @@
-
+
diff --git a/src/lib/components/WifiChannelChart.svelte b/src/lib/components/WifiChannelChart.svelte
index 3edd4c3..28e88c6 100644
--- a/src/lib/components/WifiChannelChart.svelte
+++ b/src/lib/components/WifiChannelChart.svelte
@@ -35,8 +35,6 @@
W,
PAD_L,
PAD_R,
- PLOT_W,
- BASELINE,
PLOT_TOP,
FS_AXIS,
FS_LABEL,
@@ -46,7 +44,8 @@
occupiedRange,
thinTicks,
textWidth,
- rssiToY,
+ rssiBereich,
+ rssiToYIn,
netColor,
kuerzeSsid,
} from '$lib/wifi/chart';
@@ -62,20 +61,32 @@
highlightBssid?: string;
} = $props();
- /** Höhe: Plot + Kanalbeschriftung + Blockunterschrift */
- const H = BASELINE + 34;
/**
- * Ein Block ohne Netze braucht keine volle dBm-Skala — er wird flach
- * gezeichnet. Sonst kostet jedes leere Teilband so viel Platz wie ein
- * belegtes, und bei vier 5-GHz-Blöcken müsste man an leeren Flächen
- * vorbeiscrollen, um die belegten zu finden.
+ * Ein Block ohne Netze braucht keine dBm-Skala — er wird flach gezeichnet.
+ * Sonst kostet jedes leere Teilband so viel Platz wie ein belegtes, und bei
+ * vier 5-GHz-Blöcken müsste man an leeren Flächen vorbeiscrollen, um die
+ * belegten zu finden.
*/
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 segs = BAND_SEGMENTS[band] ?? [];
const scale = scaleForBand(band);
const inBand = networks.filter((n) => n.band === band);
+ const ber = bereich;
+ const base = PLOT_TOP + ber.hoehe;
return segs.map((seg) => {
const spanMhz = seg.maxMhz - seg.minMhz;
@@ -98,7 +109,7 @@
const gekapptLinks = x(from) < PAD_L - 0.5;
const gekapptRechts = x(to) > PAD_L + plotW + 0.5;
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
// bei 20 und 160 MHz gleich aussieht statt mit der Breite zu skalieren.
const schulter = Math.min(breite / 3, 2 * scale);
@@ -114,10 +125,10 @@
isSelf,
farbe: netColor(n.bssid, isSelf),
pfad:
- `M ${xFrom.toFixed(1)},${BASELINE} ` +
+ `M ${xFrom.toFixed(1)},${base} ` +
`L ${(xFrom + 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
* hängt am Messwert, und reale Nachbarnetze liegen fast alle zwischen
- * −65 und −85 dBm — also in einem Band von nur ~40 Einheiten. Dort
- * drängeln sich bei einer Wohnhaus-Nachbarschaft zehn und mehr Namen
- * übereinander und werden unlesbar.
+ * −65 und −85 dBm. Dort drängeln sich bei einer Wohnhaus-Nachbarschaft
+ * zehn und mehr Namen übereinander und werden unlesbar.
*
- * Die Blöcke dürfen dabei NICHT verschoben werden — ihre Höhe IST der
- * Messwert, sie zu spreizen würde die dBm-Achse zur Falschaussage
- * machen. Verschoben wird ausschließlich die Beschriftung; eine dünne
- * Zuordnungslinie führt zum zugehörigen Block.
+ * Der eigentliche Hebel dagegen ist die gespreizte Y-Achse (DB_SCALE in
+ * chart.ts): aus 40 Einheiten für den dichten Bereich werden 100, damit
+ * lösen sich die meisten Überschneidungen von selbst. Anmerkung zur
+ * 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
* bekommt deshalb die beste Position), dann für jedes weitere Label vom
@@ -174,31 +193,52 @@
const frei = (y: number, x1: number, x2: number) =>
y >= PLOT_TOP + 2 &&
- y <= BASELINE - 2 &&
+ y <= base - 2 &&
!belegteLabel.some(
(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)) {
- const text = 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 name = kuerzeSsid(b.n.ssid || b.n.bssid.slice(-8));
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;
- // abwechselnd nach oben und unten suchen — nach oben zuerst, weil
- // dort der Platz über den Blöcken frei ist
- for (let stufe = 1; y === null && 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; }
+ for (const fassung of [text, name]) {
+ text = fassung;
+ breite = textWidth(text, FS_LABEL);
+ // waagerecht im Segment halten, damit nichts aus dem Bild läuft
+ cx = Math.min(
+ PAD_L + plotW - breite / 2 - 1,
+ Math.max(PAD_L + breite / 2 + 1, b.mitte),
+ );
+ 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) {
ohneLabel++;
@@ -257,7 +297,7 @@
{:else}
diff --git a/src/lib/components/WifiTimelineChart.svelte b/src/lib/components/WifiTimelineChart.svelte
index bab1f84..9227803 100644
--- a/src/lib/components/WifiTimelineChart.svelte
+++ b/src/lib/components/WifiTimelineChart.svelte
@@ -13,7 +13,7 @@
*/
import type { WifiSurvey } from '$lib/types';
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 {
surveys,
@@ -23,7 +23,16 @@
connectedBssid?: string;
} = $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 sorted = [...surveys].sort((a, b) => a.createdAt - b.createdAt);
@@ -65,7 +74,7 @@
continue;
}
const px = x(p.ts);
- const py = rssiToY(p.rssi);
+ const py = rssiToYIn(p.rssi, bereich);
punkte.push({ x: px, y: py });
current.push(`${px.toFixed(1)},${py.toFixed(1)}`);
}
@@ -96,7 +105,7 @@
{#if view}