feat(cluster): show the grid, and flag a new one

Finishes the half of this that was already computing on the backend and reaching
nobody. A Grid column in the Geo group, NEW GRID as a badge in Status, a filled
cell in the marker's own colour, and a filter chip.

new_grid joins lib/spotMarkers rather than getting colours of its own, so the
badge, the cell fill and the chip cannot drift apart - and the per-marker colour
setting will drive it with the rest from one table. Magenta: the last hue in the
categorical set not already spoken for, and one that does not read as a status,
because a new square is never urgent the way a new entity is.

The band map leaves it to the cluster, as it already leaves the prefix. The pill
is 22 px tall and its accent strip stops being readable past three segments.

A row carrying a new grid is no longer "dull", or the dimming would grey out the
one thing worth looking at.

The column is off by default, like the other Geo columns: it is only ever filled
for stations this receiver decoded over the UDP link, so for an operator who
does not run digital it would be a permanently empty column.
This commit is contained in:
2026-08-10 20:05:34 +02:00
parent d273d21f1a
commit 4f87cedc2c
7 changed files with 36 additions and 11 deletions
+4 -3
View File
@@ -48,12 +48,13 @@ type SpotStatusEntry = {
//
// Their colours come from lib/spotMarkers, shared with the cluster list: the
// same fact must not be violet in one panel and green in the next.
// The map shows three of the four. A new PREFIX is left to the cluster list: the
// The map shows three of the five. A new PREFIX and a new GRID are left to the
// cluster list, which has the width to name them: the
// pill is 22 px tall, and a fourth segment turns the strip into a colour code
// nobody can read at a glance. Add it here the day the strip earns more room.
const BMP_MARKERS = SPOT_MARKERS.filter((m) => m.key !== 'new_pfx');
const BMP_MARKERS = SPOT_MARKERS.filter((m) => m.key !== 'new_pfx' && m.key !== 'new_grid');
const markersFor = (e: SpotStatusEntry | undefined) =>
activeMarkers(e).filter((m) => m.key !== 'new_pfx');
activeMarkers(e).filter((m) => m.key !== 'new_pfx' && m.key !== 'new_grid');
// The legend spells the markers out; the cluster list's badges are abbreviated
// ("NEW CTY") because they sit in a narrow cell, and there is room here.
+15 -1
View File
@@ -193,7 +193,7 @@ function statusColor(s: SpotStatusEntry | undefined): string | null {
function isDull(s: SpotStatusEntry | undefined): boolean {
if (!s || !s.status) return false;
if (s.status === 'new' || s.status === 'new-band' || s.status === 'new-mode' || s.status === 'new-slot' || s.status === 'new-call') return false;
return !(s.worked_call || s.new_pota || s.new_county || s.new_pfx);
return !(s.worked_call || s.new_pota || s.new_county || s.new_pfx || s.new_grid);
}
const makeColCatalog = (t: TFn): ColEntry[] => [
@@ -266,6 +266,7 @@ const makeColCatalog = (t: TFn): ColEntry[] => [
if (s?.new_county) parts.push(t('clg2.newCounty'));
if (s?.new_pota) parts.push(t('clg2.newPota'));
if (s?.new_pfx) parts.push(t('clg2.newPfx'));
if (s?.new_grid) parts.push(t('clg2.newGrid'));
return parts.join(' ');
},
cellRenderer: (p: any) => {
@@ -286,6 +287,7 @@ const makeColCatalog = (t: TFn): ColEntry[] => [
if (s?.new_county) parts.push({ text: t('clg2.newCounty'), color: markerColour('new_county') });
if (s?.new_pota) parts.push({ text: t('clg2.newPota'), color: markerColour('new_pota') });
if (s?.new_pfx) parts.push({ text: t('clg2.newPfx'), color: markerColour('new_pfx') });
if (s?.new_grid) parts.push({ text: t('clg2.newGrid'), color: markerColour('new_grid') });
if (parts.length === 0) return <span style={{ color: 'var(--muted-foreground)', fontSize: 10 }}></span>;
return (
<span style={{ whiteSpace: 'nowrap' }}>
@@ -362,6 +364,18 @@ const makeColCatalog = (t: TFn): ColEntry[] => [
: { color: 'var(--muted-foreground)' }) as any,
tooltipValueGetter: (p: any) => (statusFor(p)?.new_pfx ? t('clg2.tipNewPfx') : undefined),
},
{
group: 'Geo', label: t('clg2.c.grid'), colId: 'grid',
headerName: t('clg2.c.grid'), width: 70, cellClass: 'font-mono',
// Not on the spot: the square a station announced in a CQ that THIS receiver
// decoded over the WSJT-X link, so it is filled for the digital watering
// hole being monitored and empty everywhere else. A cluster line carries the
// spotter's grid at best, never the DX's.
valueGetter: (p: any) => statusFor(p)?.grid ?? '',
cellStyle: (p: any) => (statusFor(p)?.new_grid ? fillStyle(markerColour('new_grid')) : null) as any,
cellRenderer: (p: any) => cellText(p.value, null),
tooltipValueGetter: (p: any) => (statusFor(p)?.new_grid ? t('clg2.tipNewGrid') : undefined),
},
{
group: 'Geo', label: t('clg2.c.county'), colId: 'county',
headerName: t('clg2.c.county'), width: 130, cellClass: 'font-mono',