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);
// 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 [propertiesTotal, setPropertiesTotal] = useState(0);
const [propertiesOffset, setPropertiesOffset] = useState(0);
@ -347,9 +349,7 @@ export default function App() {
<div className="flex border-b border-gray-200">
<button
className={`flex-1 p-3 ${
rightPaneTab === 'pois'
? 'border-b-2 border-blue-500 font-semibold'
: 'text-gray-600'
rightPaneTab === 'pois' ? 'border-b-2 border-blue-500 font-semibold' : 'text-gray-600'
}`}
onClick={() => setRightPaneTab('pois')}
>

View file

@ -31,7 +31,6 @@ function emojiToTwemojiUrl(emoji: string): string {
return `${TWEMOJI_BASE}${hex}.png`;
}
const INITIAL_VIEW: ViewState = {
longitude: -1.5,
latitude: 53.5,
@ -43,10 +42,10 @@ const MAP_STYLE = 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json
// Gradient stops for normalized [0,1] values
const GRADIENT: { t: number; color: [number, number, number] }[] = [
{ t: 0, color: [46, 204, 113] }, // Green
{ t: 0.33, color: [241, 196, 15] }, // Yellow
{ t: 0.66, color: [231, 76, 60] }, // Red
{ t: 1, color: [142, 68, 173] }, // Purple
{ t: 0, color: [46, 204, 113] }, // Green
{ t: 0.33, color: [241, 196, 15] }, // Yellow
{ t: 0.66, color: [231, 76, 60] }, // Red
{ t: 1, color: [142, 68, 173] }, // Purple
];
function normalizedToColor(t: number): [number, number, number] {
@ -144,7 +143,16 @@ function countToColor(t: number): [number, number, number] {
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 [viewState, setViewState] = useState<ViewState>(INITIAL_VIEW);
const [dimensions, setDimensions] = useState<Dimensions>({ width: 0, height: 0 });
@ -180,15 +188,18 @@ export default function Map({ data, pois, onViewChange, activeFeature, dragValue
}, []);
// Make place labels more legible over the colored hexagons
const handleMapLoad = useCallback((evt: { target: MapRef['getMap'] extends () => infer M ? M : never }) => {
const map = evt.target;
for (const layer of map.getStyle().layers || []) {
if (layer.type !== 'symbol') continue;
map.setPaintProperty(layer.id, 'text-halo-color', 'rgba(255,255,255,1)');
map.setPaintProperty(layer.id, 'text-halo-width', 2);
map.setPaintProperty(layer.id, 'text-color', '#222');
}
}, []);
const handleMapLoad = useCallback(
(evt: { target: MapRef['getMap'] extends () => infer M ? M : never }) => {
const map = evt.target;
for (const layer of map.getStyle().layers || []) {
if (layer.type !== 'symbol') continue;
map.setPaintProperty(layer.id, 'text-halo-color', 'rgba(255,255,255,1)');
map.setPaintProperty(layer.id, 'text-halo-width', 2);
map.setPaintProperty(layer.id, 'text-color', '#222');
}
},
[]
);
// Popup state for POI hover
const [popupInfo, setPopupInfo] = useState<{
@ -226,7 +237,9 @@ export default function Map({ data, pois, onViewChange, activeFeature, dragValue
}, [data]);
// 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(
(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);
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),
lineWidthUnits: 'pixels',
updateTriggers: {
@ -290,7 +309,17 @@ export default function Map({ data, pois, onViewChange, activeFeature, dragValue
onHover: handlePoiHover,
}),
],
[data, pois, handlePoiHover, handleHexagonClick, activeFeature, dragValue, countRange, colorFeatureMeta, selectedHexagonId]
[
data,
pois,
handlePoiHover,
handleHexagonClick,
activeFeature,
dragValue,
countRange,
colorFeatureMeta,
selectedHexagonId,
]
);
const getTooltip = useCallback(
@ -305,8 +334,14 @@ export default function Map({ data, pois, onViewChange, activeFeature, dragValue
const minVal = hex[`min_${f.name}`];
const maxVal = hex[`max_${f.name}`];
if (minVal != null && maxVal != null) {
const minStr = typeof minVal === 'number' ? minVal.toLocaleString(undefined, { maximumFractionDigits: 1 }) : String(minVal);
const maxStr = typeof maxVal === 'number' ? maxVal.toLocaleString(undefined, { maximumFractionDigits: 1 }) : String(maxVal);
const minStr =
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;' : '';
lines.push(`<div style="${highlight}">${f.label}: ${minStr} - ${maxStr}</div>`);
}

View file

@ -31,9 +31,7 @@ export function PropertiesPane({
case 'size':
return ((b.total_floor_area as number) || 0) - ((a.total_floor_area as number) || 0);
case 'energy':
return (a.current_energy_rating || 'Z').localeCompare(
b.current_energy_rating || 'Z'
);
return (a.current_energy_rating || 'Z').localeCompare(b.current_energy_rating || 'Z');
}
});
}, [properties, sortBy]);
@ -142,7 +140,8 @@ function PropertyCard({ property }: { property: Property }) {
)}
{property.total_floor_area && (
<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>
)}
{property.number_habitable_rooms && (
@ -163,7 +162,8 @@ function PropertyCard({ property }: { property: Property }) {
)}
{property.construction_age_band !== undefined && (
<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>
)}