Format the map

This commit is contained in:
Andras Schmelczer 2026-01-31 13:07:18 +00:00
parent 4c258018c3
commit 0fde087c3d
3 changed files with 64 additions and 29 deletions

View file

@ -66,7 +66,9 @@ export default function App() {
const poiAbortControllerRef = useRef<AbortController | null>(null); const poiAbortControllerRef = useRef<AbortController | null>(null);
// Hexagon properties state // Hexagon properties state
const [selectedHexagon, setSelectedHexagon] = useState<{ h3: string; resolution: number } | null>(null); const [selectedHexagon, setSelectedHexagon] = useState<{ h3: string; resolution: number } | null>(
null
);
const [properties, setProperties] = useState<Property[]>([]); const [properties, setProperties] = useState<Property[]>([]);
const [propertiesTotal, setPropertiesTotal] = useState(0); const [propertiesTotal, setPropertiesTotal] = useState(0);
const [propertiesOffset, setPropertiesOffset] = useState(0); const [propertiesOffset, setPropertiesOffset] = useState(0);
@ -347,9 +349,7 @@ export default function App() {
<div className="flex border-b border-gray-200"> <div className="flex border-b border-gray-200">
<button <button
className={`flex-1 p-3 ${ className={`flex-1 p-3 ${
rightPaneTab === 'pois' rightPaneTab === 'pois' ? 'border-b-2 border-blue-500 font-semibold' : 'text-gray-600'
? 'border-b-2 border-blue-500 font-semibold'
: 'text-gray-600'
}`} }`}
onClick={() => setRightPaneTab('pois')} onClick={() => setRightPaneTab('pois')}
> >

View file

@ -31,7 +31,6 @@ function emojiToTwemojiUrl(emoji: string): string {
return `${TWEMOJI_BASE}${hex}.png`; return `${TWEMOJI_BASE}${hex}.png`;
} }
const INITIAL_VIEW: ViewState = { const INITIAL_VIEW: ViewState = {
longitude: -1.5, longitude: -1.5,
latitude: 53.5, latitude: 53.5,
@ -144,7 +143,16 @@ function countToColor(t: number): [number, number, number] {
return [r, g, b]; return [r, g, b];
} }
export default function Map({ data, pois, onViewChange, activeFeature, dragValue, features, selectedHexagonId, onHexagonClick }: MapProps) { export default function Map({
data,
pois,
onViewChange,
activeFeature,
dragValue,
features,
selectedHexagonId,
onHexagonClick,
}: MapProps) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const [viewState, setViewState] = useState<ViewState>(INITIAL_VIEW); const [viewState, setViewState] = useState<ViewState>(INITIAL_VIEW);
const [dimensions, setDimensions] = useState<Dimensions>({ width: 0, height: 0 }); const [dimensions, setDimensions] = useState<Dimensions>({ width: 0, height: 0 });
@ -180,7 +188,8 @@ export default function Map({ data, pois, onViewChange, activeFeature, dragValue
}, []); }, []);
// Make place labels more legible over the colored hexagons // Make place labels more legible over the colored hexagons
const handleMapLoad = useCallback((evt: { target: MapRef['getMap'] extends () => infer M ? M : never }) => { const handleMapLoad = useCallback(
(evt: { target: MapRef['getMap'] extends () => infer M ? M : never }) => {
const map = evt.target; const map = evt.target;
for (const layer of map.getStyle().layers || []) { for (const layer of map.getStyle().layers || []) {
if (layer.type !== 'symbol') continue; if (layer.type !== 'symbol') continue;
@ -188,7 +197,9 @@ export default function Map({ data, pois, onViewChange, activeFeature, dragValue
map.setPaintProperty(layer.id, 'text-halo-width', 2); map.setPaintProperty(layer.id, 'text-halo-width', 2);
map.setPaintProperty(layer.id, 'text-color', '#222'); map.setPaintProperty(layer.id, 'text-color', '#222');
} }
}, []); },
[]
);
// Popup state for POI hover // Popup state for POI hover
const [popupInfo, setPopupInfo] = useState<{ const [popupInfo, setPopupInfo] = useState<{
@ -226,7 +237,9 @@ export default function Map({ data, pois, onViewChange, activeFeature, dragValue
}, [data]); }, [data]);
// Determine color mode // Determine color mode
const colorFeatureMeta = activeFeature ? features.find((f) => f.name === activeFeature) || null : null; const colorFeatureMeta = activeFeature
? features.find((f) => f.name === activeFeature) || null
: null;
const handleHexagonClick = useCallback( const handleHexagonClick = useCallback(
(info: PickingInfo<HexagonData>) => { (info: PickingInfo<HexagonData>) => {
@ -258,7 +271,13 @@ export default function Map({ data, pois, onViewChange, activeFeature, dragValue
const t = (c - countRange.min) / (countRange.max - countRange.min); const t = (c - countRange.min) / (countRange.max - countRange.min);
return countToColor(Math.max(0, Math.min(1, t))); return countToColor(Math.max(0, Math.min(1, t)));
}, },
getLineColor: (d) => (d.h3 === selectedHexagonId ? [255, 255, 255, 255] : [0, 0, 0, 0]) as [number, number, number, number], getLineColor: (d) =>
(d.h3 === selectedHexagonId ? [255, 255, 255, 255] : [0, 0, 0, 0]) as [
number,
number,
number,
number,
],
getLineWidth: (d) => (d.h3 === selectedHexagonId ? 2 : 0), getLineWidth: (d) => (d.h3 === selectedHexagonId ? 2 : 0),
lineWidthUnits: 'pixels', lineWidthUnits: 'pixels',
updateTriggers: { updateTriggers: {
@ -290,7 +309,17 @@ export default function Map({ data, pois, onViewChange, activeFeature, dragValue
onHover: handlePoiHover, onHover: handlePoiHover,
}), }),
], ],
[data, pois, handlePoiHover, handleHexagonClick, activeFeature, dragValue, countRange, colorFeatureMeta, selectedHexagonId] [
data,
pois,
handlePoiHover,
handleHexagonClick,
activeFeature,
dragValue,
countRange,
colorFeatureMeta,
selectedHexagonId,
]
); );
const getTooltip = useCallback( const getTooltip = useCallback(
@ -305,8 +334,14 @@ export default function Map({ data, pois, onViewChange, activeFeature, dragValue
const minVal = hex[`min_${f.name}`]; const minVal = hex[`min_${f.name}`];
const maxVal = hex[`max_${f.name}`]; const maxVal = hex[`max_${f.name}`];
if (minVal != null && maxVal != null) { if (minVal != null && maxVal != null) {
const minStr = typeof minVal === 'number' ? minVal.toLocaleString(undefined, { maximumFractionDigits: 1 }) : String(minVal); const minStr =
const maxStr = typeof maxVal === 'number' ? maxVal.toLocaleString(undefined, { maximumFractionDigits: 1 }) : String(maxVal); typeof minVal === 'number'
? minVal.toLocaleString(undefined, { maximumFractionDigits: 1 })
: String(minVal);
const maxStr =
typeof maxVal === 'number'
? maxVal.toLocaleString(undefined, { maximumFractionDigits: 1 })
: String(maxVal);
const highlight = f.name === activeFeature ? 'font-weight: bold;' : ''; const highlight = f.name === activeFeature ? 'font-weight: bold;' : '';
lines.push(`<div style="${highlight}">${f.label}: ${minStr} - ${maxStr}</div>`); lines.push(`<div style="${highlight}">${f.label}: ${minStr} - ${maxStr}</div>`);
} }

View file

@ -31,9 +31,7 @@ export function PropertiesPane({
case 'size': case 'size':
return ((b.total_floor_area as number) || 0) - ((a.total_floor_area as number) || 0); return ((b.total_floor_area as number) || 0) - ((a.total_floor_area as number) || 0);
case 'energy': case 'energy':
return (a.current_energy_rating || 'Z').localeCompare( return (a.current_energy_rating || 'Z').localeCompare(b.current_energy_rating || 'Z');
b.current_energy_rating || 'Z'
);
} }
}); });
}, [properties, sortBy]); }, [properties, sortBy]);
@ -142,7 +140,8 @@ function PropertyCard({ property }: { property: Property }) {
)} )}
{property.total_floor_area && ( {property.total_floor_area && (
<div> <div>
<span className="text-gray-600">Area:</span> {formatNumber(property.total_floor_area as number)}m² <span className="text-gray-600">Area:</span>{' '}
{formatNumber(property.total_floor_area as number)}m²
</div> </div>
)} )}
{property.number_habitable_rooms && ( {property.number_habitable_rooms && (
@ -163,7 +162,8 @@ function PropertyCard({ property }: { property: Property }) {
)} )}
{property.construction_age_band !== undefined && ( {property.construction_age_band !== undefined && (
<div> <div>
<span className="text-gray-600">Built (age):</span> {formatNumber(property.construction_age_band as number)} <span className="text-gray-600">Built (age):</span>{' '}
{formatNumber(property.construction_age_band as number)}
</div> </div>
)} )}