feat: upload to external services clublog qrz
This commit is contained in:
@@ -160,13 +160,47 @@ export function BandMap({ band, spots, spotStatus, currentFreqHz, onSpotClick, o
|
||||
filtered.push(s);
|
||||
}
|
||||
filtered.sort((a, b) => b.freq_khz - a.freq_khz);
|
||||
|
||||
// Desired pill-CENTRE Y for each spot = its true frequency's Y.
|
||||
const desired = filtered.map(
|
||||
(s) => TOP_PAD + (1 - (s.freq_khz - lo) / span) * innerH,
|
||||
);
|
||||
|
||||
// Non-overlapping label placement via isotonic regression (pool-
|
||||
// adjacent-violators). We want centres c_0 ≤ c_1 ≤ … with
|
||||
// c_{i+1} − c_i ≥ PILL_H, minimising the squared displacement from each
|
||||
// label's desired centre. Substituting q_i = c_i − i·PILL_H turns the
|
||||
// gap constraint into "q non-decreasing", which PAVA solves exactly in
|
||||
// one pass. The win over the old greedy push-down: a tight cluster is
|
||||
// centred on its mean, so its labels fan out symmetrically ABOVE and
|
||||
// below the frequency (Log4OM style) instead of all spilling downward.
|
||||
const n = filtered.length;
|
||||
type Block = { sum: number; count: number; start: number };
|
||||
const blocks: Block[] = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
// e_i = desired_i − i·PILL_H is the target for the substituted q.
|
||||
let cur: Block = { sum: desired[i] - i * PILL_H, count: 1, start: i };
|
||||
while (blocks.length > 0) {
|
||||
const prev = blocks[blocks.length - 1];
|
||||
if (prev.sum / prev.count <= cur.sum / cur.count) break;
|
||||
blocks.pop();
|
||||
cur = { sum: prev.sum + cur.sum, count: prev.count + cur.count, start: prev.start };
|
||||
}
|
||||
blocks.push(cur);
|
||||
}
|
||||
const centers = new Array<number>(n);
|
||||
for (const b of blocks) {
|
||||
const mean = b.sum / b.count; // optimal q for the whole block
|
||||
for (let i = b.start; i < b.start + b.count; i++) centers[i] = mean + i * PILL_H;
|
||||
}
|
||||
// Centres are non-decreasing, so centers[0] is the topmost. Shift the
|
||||
// whole set down by any overflow above the band edge so the first label
|
||||
// isn't clipped (preserves the ≥ PILL_H spacing).
|
||||
const shift = n > 0 ? Math.max(0, TOP_PAD - (centers[0] - PILL_H / 2)) : 0;
|
||||
|
||||
const out: Placed[] = [];
|
||||
let prevY = -Infinity;
|
||||
for (const s of filtered) {
|
||||
const fy = TOP_PAD + (1 - (s.freq_khz - lo) / span) * innerH;
|
||||
const ly = Math.max(fy, prevY + PILL_H);
|
||||
out.push({ spot: s, freqY: fy, labelY: ly });
|
||||
prevY = ly;
|
||||
for (let i = 0; i < n; i++) {
|
||||
out.push({ spot: filtered[i], freqY: desired[i], labelY: centers[i] + shift - PILL_H / 2 });
|
||||
}
|
||||
const lastLabelBottom = out.length ? out[out.length - 1].labelY + PILL_H : 0;
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user