Lots of improvements

This commit is contained in:
Andras Schmelczer 2026-02-22 21:09:07 +00:00
parent 205302dbb8
commit eb02b5832b
39 changed files with 699 additions and 271 deletions

View file

@ -1,6 +1,7 @@
import { useCallback, useRef, useState, useMemo, useEffect } from 'react';
import { H3HexagonLayer } from '@deck.gl/geo-layers';
import { GeoJsonLayer, IconLayer, TextLayer } from '@deck.gl/layers';
import { cellToBoundary } from 'h3-js';
import type { PickingInfo } from '@deck.gl/core';
import type {
HexagonData,
@ -80,13 +81,13 @@ export function useDeckLayers({
// Marching ants animation
const [marchTime, setMarchTime] = useState(0);
const hasPostcodeGeometry = selectedPostcodeGeometry != null;
const hasSelection = selectedPostcodeGeometry != null || selectedHexagonId != null;
useEffect(() => {
if (!hasPostcodeGeometry) return;
if (!hasSelection) return;
setMarchTime(0);
const id = setInterval(() => setMarchTime((t) => t + 0.3), 50);
return () => clearInterval(id);
}, [hasPostcodeGeometry]);
}, [hasSelection]);
const isDark = theme === 'dark';
const densityGradient = isDark ? DENSITY_GRADIENT_DARK : DENSITY_GRADIENT;
@ -332,14 +333,11 @@ export function useDeckLayers({
);
},
getLineColor: (d) => {
if (d.h3 === selectedHexagonIdRef.current)
return [255, 255, 255, 255] as [number, number, number, number];
if (d.h3 === hoveredHexagonIdRef.current)
return [29, 228, 195, 200] as [number, number, number, number];
return [0, 0, 0, 0] as [number, number, number, number];
},
getLineWidth: (d) => {
if (d.h3 === selectedHexagonIdRef.current) return 3;
if (d.h3 === hoveredHexagonIdRef.current) return 2;
return 0;
},
@ -481,15 +479,22 @@ export function useDeckLayers({
[pois, stablePoiHover]
);
// Marching ants highlight layer for selected postcode
// Marching ants highlight layer for selected hexagon or postcode
const marchingAntsLayer = useMemo(() => {
if (!selectedPostcodeGeometry) return null;
let geometry: PostcodeGeometry | null = null;
if (selectedPostcodeGeometry) {
geometry = selectedPostcodeGeometry;
} else if (selectedHexagonId) {
const boundary = cellToBoundary(selectedHexagonId, true);
geometry = { type: 'Polygon', coordinates: [boundary] };
}
if (!geometry) return null;
return new GeoJsonLayer({
id: 'marching-ants',
data: [
{
type: 'Feature' as const,
geometry: selectedPostcodeGeometry,
geometry,
properties: {},
},
],
@ -502,7 +507,7 @@ export function useDeckLayers({
marchTime,
extensions: [new MarchingAntsExtension()],
});
}, [selectedPostcodeGeometry, marchTime]);
}, [selectedPostcodeGeometry, selectedHexagonId, marchTime]);
const layers = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View file

@ -31,6 +31,8 @@ export function useFilters({ initialFilters, features }: UseFiltersOptions) {
if (!meta) return;
if (meta.type === 'enum' && meta.values) {
setFilters((prev) => ({ ...prev, [name]: [...meta.values!] }));
} else if (meta.type === 'numeric' && meta.histogram) {
setFilters((prev) => ({ ...prev, [name]: [meta.histogram!.min, meta.histogram!.max] }));
} else if (meta.min != null && meta.max != null) {
setFilters((prev) => ({ ...prev, [name]: [meta.min!, meta.max!] }));
}

View file

@ -16,6 +16,8 @@ export interface TravelTimeEntry {
slug: string;
label: string;
timeRange: [number, number] | null;
/** Use best-case (5th percentile) travel time instead of median. Transit only. */
useBest: boolean;
}
/** Field key matching the backend response: tt_{mode}_{slug} */
@ -33,7 +35,7 @@ export function useTravelTime(initial?: TravelTimeInitial) {
const handleAddEntry = useCallback((mode: TransportMode) => {
setEntries((prev) => [
...prev,
{ mode, slug: '', label: '', timeRange: null },
{ mode, slug: '', label: '', timeRange: null, useBest: false },
]);
}, []);
@ -63,6 +65,17 @@ export function useTravelTime(initial?: TravelTimeInitial) {
[]
);
const handleToggleBest = useCallback(
(index: number) => {
setEntries((prev) =>
prev.map((entry, i) =>
i === index ? { ...entry, useBest: !entry.useBest, timeRange: null } : entry
)
);
},
[]
);
/** Entries that have a destination selected (slug is set) */
const activeEntries = useMemo(
() => entries.filter((e) => e.slug !== ''),
@ -76,5 +89,6 @@ export function useTravelTime(initial?: TravelTimeInitial) {
handleRemoveEntry,
handleSetDestination,
handleTimeRangeChange,
handleToggleBest,
};
}

View file

@ -9,7 +9,7 @@ const STEPS: Step[] = [
target: '[data-tutorial="filters"]',
title: 'Filter Properties',
content:
'Use filters to narrow down properties by price, energy rating, floor area, and more. Pin a filter to colour the map by that feature.',
'Use filters to narrow down to areas which contain matching properties. Filter by crime rate, number of schools around, or filter to an area with detached houses. Pin a filter with the eye icon to colour the map by that feature.',
placement: 'right',
disableBeacon: true,
},
@ -17,7 +17,7 @@ const STEPS: Step[] = [
target: '[data-tutorial="map"]',
title: 'Explore the Map',
content:
'Pan and zoom to explore property data across the UK. Click any hexagon to see detailed stats and individual properties.',
'Pan and zoom to explore property data across England. Click any area (hexagon or postcode boundary) to see detailed stats of historical or currently sold properties matching your filters.',
placement: 'bottom',
disableBeacon: true,
},
@ -44,6 +44,11 @@ const STEPS: Step[] = [
'Toggle points of interest like schools, shops, and transport stops to see what amenities are nearby.',
placement: 'left',
disableBeacon: true,
styles: {
tooltip: {
transform: 'translateY(-50px)',
},
},
},
];