This commit is contained in:
Andras Schmelczer 2026-03-12 22:11:00 +00:00
parent 14a3555cf1
commit 7e92bf112e
34 changed files with 1214437 additions and 224 deletions

View file

@ -31,8 +31,8 @@ export default memo(function HoverCard({ x, y, id, isPostcode, data, filters, fe
const results: { name: string; value: string }[] = [];
// Show stats for active filters (up to 4)
for (const name of activeFilterNames.slice(0, 4)) {
// Show stats for active filters (up to 4), excluding Listing status
for (const name of activeFilterNames.filter((n) => n !== 'Listing status').slice(0, 4)) {
const val = data[`avg_${name}`] ?? data[`min_${name}`];
if (val == null || typeof val !== 'number') continue;
const meta = featureMap.get(name);
@ -50,14 +50,31 @@ export default memo(function HoverCard({ x, y, id, isPostcode, data, filters, fe
const displayStats = getDisplayStats();
const count = data?.count;
const cardStyle = {
left: x,
top: y - 12,
transform: 'translate(-50%, -100%)',
};
// Loading state: show skeleton when data hasn't arrived yet
if (!data) {
return (
<div
className="absolute bg-white dark:bg-warm-800 rounded-lg shadow-lg p-3 text-sm pointer-events-none z-50 min-w-[140px]"
style={cardStyle}
>
<div className="animate-pulse space-y-2">
<div className="h-3.5 w-20 bg-warm-200 dark:bg-warm-600 rounded" />
<div className="h-2.5 w-14 bg-warm-100 dark:bg-warm-700 rounded" />
</div>
</div>
);
}
return (
<div
className="absolute bg-white dark:bg-warm-800 rounded-lg shadow-lg p-3 text-sm dark:text-white pointer-events-none z-50 min-w-[180px] max-w-[260px]"
style={{
left: x,
top: y - 12,
transform: 'translate(-50%, -100%)',
}}
style={cardStyle}
>
<div className="relative">
{/* Header */}
@ -89,11 +106,9 @@ export default memo(function HoverCard({ x, y, id, isPostcode, data, filters, fe
)}
{/* Hint */}
{data && (
<div className="text-[10px] text-warm-400 dark:text-warm-400 mt-2 text-center">
Click for details
</div>
)}
<div className="text-[10px] text-warm-400 dark:text-warm-400 mt-2 text-center">
Click for details
</div>
</div>
</div>
);

View file

@ -1,9 +1,47 @@
import { formatValue } from '../../lib/format';
import { FEATURE_GRADIENT, DENSITY_GRADIENT, DENSITY_GRADIENT_DARK } from '../../lib/consts';
import { FEATURE_GRADIENT, DENSITY_GRADIENT, DENSITY_GRADIENT_DARK, ENUM_PALETTE } from '../../lib/consts';
import { gradientToCss } from '../../lib/utils';
import { CloseIcon } from '../ui/icons/CloseIcon';
import { TickerValue } from '../ui/TickerValue';
function EnumSwatches({ values }: { values: string[] }) {
return (
<div className="flex flex-col gap-1">
{values.map((label, i) => {
const color = ENUM_PALETTE[i % ENUM_PALETTE.length];
return (
<div key={label} className="flex items-center gap-1.5">
<div
className="w-3 h-3 rounded-sm shrink-0"
style={{ backgroundColor: `rgb(${color[0]},${color[1]},${color[2]})` }}
/>
<span className="text-warm-600 dark:text-warm-300 truncate">{label}</span>
</div>
);
})}
</div>
);
}
function InlineEnumSwatches({ values }: { values: string[] }) {
return (
<div className="flex items-center gap-2 flex-1 min-w-[40%] flex-wrap">
{values.map((label, i) => {
const color = ENUM_PALETTE[i % ENUM_PALETTE.length];
return (
<div key={label} className="flex items-center gap-1">
<div
className="w-2.5 h-2.5 rounded-sm shrink-0"
style={{ backgroundColor: `rgb(${color[0]},${color[1]},${color[2]})` }}
/>
<span className="text-warm-500 dark:text-warm-400 whitespace-nowrap text-[11px]">{label}</span>
</div>
);
})}
</div>
);
}
export default function MapLegend({
featureLabel,
range,
@ -27,6 +65,7 @@ export default function MapLegend({
suffix?: string;
raw?: boolean;
}) {
const isEnum = enumValues && enumValues.length > 0;
const densityGradient = theme === 'dark' ? DENSITY_GRADIENT_DARK : DENSITY_GRADIENT;
const gradientStyle =
mode === 'density' ? gradientToCss(densityGradient) : gradientToCss(FEATURE_GRADIENT);
@ -36,18 +75,14 @@ export default function MapLegend({
const rangeMin =
mode === 'density' ? (
<TickerValue text={formatValue(range[0])} />
) : enumValues && enumValues.length > 0 ? (
<span>{enumValues[0]}</span>
) : (
) : isEnum ? null : (
<TickerValue text={formatValue(range[0], fmt) + (suffix || '')} />
);
const rangeMax =
mode === 'density' ? (
<TickerValue text={formatValue(range[1])} />
) : enumValues && enumValues.length > 0 ? (
<span>{enumValues[enumValues.length - 1]}</span>
) : (
) : isEnum ? null : (
<TickerValue text={formatValue(range[1], fmt) + (suffix || '')} />
);
@ -66,11 +101,15 @@ export default function MapLegend({
<CloseIcon className="w-3.5 h-3.5" />
</button>
)}
<div className="flex items-center gap-1.5 flex-1 min-w-[40%] text-warm-500 dark:text-warm-400">
{rangeMin}
<div className="h-2.5 rounded flex-1 min-w-[40px]" style={{ background: gradientStyle }} />
{rangeMax}
</div>
{isEnum ? (
<InlineEnumSwatches values={enumValues} />
) : (
<div className="flex items-center gap-1.5 flex-1 min-w-[40%] text-warm-500 dark:text-warm-400">
{rangeMin}
<div className="h-2.5 rounded flex-1 min-w-[40px]" style={{ background: gradientStyle }} />
{rangeMax}
</div>
)}
</div>
);
}
@ -89,11 +128,17 @@ export default function MapLegend({
</button>
)}
</div>
<div className="h-3 rounded" style={{ background: gradientStyle }} />
<div className="flex justify-between mt-1 text-warm-600 dark:text-warm-200">
{rangeMin}
{rangeMax}
</div>
{isEnum ? (
<EnumSwatches values={enumValues} />
) : (
<>
<div className="h-3 rounded" style={{ background: gradientStyle }} />
<div className="flex justify-between mt-1 text-warm-600 dark:text-warm-200">
{rangeMin}
{rangeMax}
</div>
</>
)}
</div>
);
}

View file

@ -86,7 +86,7 @@ export default function MapPage({
useState<Set<string>>(initialPOICategories);
const [leftPaneWidth, leftPaneHandlers] = usePaneResize(384, 200, 600, 'left');
const [rightPaneWidth, rightPaneHandlers] = usePaneResize(288, 200, 500, 'right');
const [rightPaneWidth, rightPaneHandlers] = usePaneResize(384, 200, 500, 'right');
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
const [poiPaneOpen, setPoiPaneOpen] = useState(false);