More FE changes
This commit is contained in:
parent
f114ada255
commit
a48eb945e0
48 changed files with 4127 additions and 1751 deletions
|
|
@ -56,6 +56,15 @@ function distToRatios(dist: unknown): number[] {
|
|||
return r;
|
||||
}
|
||||
|
||||
function requireEnumPalette(
|
||||
palette: [number, number, number][] | null
|
||||
): [number, number, number][] {
|
||||
if (!palette) {
|
||||
throw new Error('Enum layer requested without an enum color palette');
|
||||
}
|
||||
return palette;
|
||||
}
|
||||
|
||||
export function useDeckLayers({
|
||||
data,
|
||||
postcodeData,
|
||||
|
|
@ -127,9 +136,12 @@ export function useDeckLayers({
|
|||
? colorFeatureMeta.values.length
|
||||
: 0;
|
||||
|
||||
// Per-feature color palette (uses overrides when defined)
|
||||
const enumPaletteRef = useRef(getEnumPaletteForFeature(viewFeature, colorFeatureMeta?.values));
|
||||
enumPaletteRef.current = getEnumPaletteForFeature(viewFeature, colorFeatureMeta?.values);
|
||||
const enumPalette =
|
||||
viewFeature && colorFeatureMeta?.type === 'enum' && colorFeatureMeta.values
|
||||
? getEnumPaletteForFeature(viewFeature, colorFeatureMeta.values)
|
||||
: null;
|
||||
const enumPaletteRef = useRef(enumPalette);
|
||||
enumPaletteRef.current = enumPalette;
|
||||
|
||||
const countRange = useMemo(() => {
|
||||
if (data.length === 0) return { min: 0, max: 1, total: 0 };
|
||||
|
|
@ -256,27 +268,27 @@ export function useDeckLayers({
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const pieProps: any = isEnum
|
||||
? {
|
||||
extensions: [new PieHexExtension(enumPaletteRef.current)],
|
||||
getCenter: (d: HexagonData) => [d.lon, d.lat],
|
||||
getRatios0: (d: HexagonData) => {
|
||||
const r = distToRatios(d[distKey]);
|
||||
return [r[0], r[1], r[2], r[3]];
|
||||
},
|
||||
getRatios1: (d: HexagonData) => {
|
||||
const r = distToRatios(d[distKey]);
|
||||
return [r[4], r[5], r[6], r[7]];
|
||||
},
|
||||
getRatios2: (d: HexagonData) => {
|
||||
const r = distToRatios(d[distKey]);
|
||||
return [r[8], r[9]];
|
||||
},
|
||||
updateTriggers: {
|
||||
getCenter: [colorTrigger, data],
|
||||
getRatios0: [colorTrigger, data],
|
||||
getRatios1: [colorTrigger, data],
|
||||
getRatios2: [colorTrigger, data],
|
||||
},
|
||||
}
|
||||
extensions: [new PieHexExtension(requireEnumPalette(enumPaletteRef.current))],
|
||||
getCenter: (d: HexagonData) => [d.lon, d.lat],
|
||||
getRatios0: (d: HexagonData) => {
|
||||
const r = distToRatios(d[distKey]);
|
||||
return [r[0], r[1], r[2], r[3]];
|
||||
},
|
||||
getRatios1: (d: HexagonData) => {
|
||||
const r = distToRatios(d[distKey]);
|
||||
return [r[4], r[5], r[6], r[7]];
|
||||
},
|
||||
getRatios2: (d: HexagonData) => {
|
||||
const r = distToRatios(d[distKey]);
|
||||
return [r[8], r[9]];
|
||||
},
|
||||
updateTriggers: {
|
||||
getCenter: [colorTrigger, data],
|
||||
getRatios0: [colorTrigger, data],
|
||||
getRatios1: [colorTrigger, data],
|
||||
getRatios2: [colorTrigger, data],
|
||||
},
|
||||
}
|
||||
: {};
|
||||
|
||||
return new H3HexagonLayer<HexagonData>({
|
||||
|
|
@ -568,11 +580,15 @@ export function useDeckLayers({
|
|||
|
||||
const layers = useMemo(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const baseLayers: any[] = usePostcodeView
|
||||
? zoom >= 16
|
||||
? [postcodeLayer, postcodeLabelsLayer, ...poiLayers]
|
||||
: [postcodeLayer, ...poiLayers]
|
||||
: [hexLayer, ...poiLayers];
|
||||
const baseLayers: any[] = [];
|
||||
if (usePostcodeView) {
|
||||
baseLayers.push(postcodeLayer);
|
||||
if (zoom >= 16) baseLayers.push(postcodeLabelsLayer);
|
||||
baseLayers.push(...poiLayers);
|
||||
} else {
|
||||
baseLayers.push(hexLayer);
|
||||
baseLayers.push(...poiLayers);
|
||||
}
|
||||
if (marchingAntsLayer) baseLayers.push(marchingAntsLayer);
|
||||
if (currentLocationLayer) baseLayers.push(currentLocationLayer);
|
||||
return baseLayers;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,23 @@ import {
|
|||
getSpecificCrimeFilterKeyId,
|
||||
normalizeSpecificCrimeFilters,
|
||||
} from '../lib/crime-filter';
|
||||
import {
|
||||
ETHNICITIES_FILTER_NAME,
|
||||
createEthnicityFilterKey,
|
||||
getDefaultEthnicityFeatureName,
|
||||
getEthnicityFeatureName,
|
||||
getEthnicityFilterKeyId,
|
||||
normalizeEthnicityFilters,
|
||||
} from '../lib/ethnicity-filter';
|
||||
import {
|
||||
POI_FILTER_NAMES,
|
||||
createPoiFilterKey,
|
||||
getDefaultPoiFilterFeatureName,
|
||||
getPoiDistanceFeatureName,
|
||||
getPoiDistanceFilterKeyId,
|
||||
normalizePoiDistanceFilters,
|
||||
type PoiFilterName,
|
||||
} from '../lib/poi-distance-filter';
|
||||
|
||||
interface UseFiltersOptions {
|
||||
initialFilters: FeatureFilters;
|
||||
|
|
@ -24,11 +41,19 @@ interface UseFiltersOptions {
|
|||
}
|
||||
|
||||
function normalizeFilters(filters: FeatureFilters): FeatureFilters {
|
||||
return normalizeSpecificCrimeFilters(normalizeSchoolFilters(filters));
|
||||
return normalizePoiDistanceFilters(
|
||||
normalizeEthnicityFilters(normalizeSpecificCrimeFilters(normalizeSchoolFilters(filters)))
|
||||
);
|
||||
}
|
||||
|
||||
function getBackendFeatureName(name: string): string {
|
||||
return getSchoolBackendFeatureName(name) ?? getSpecificCrimeFeatureName(name) ?? name;
|
||||
return (
|
||||
getSchoolBackendFeatureName(name) ??
|
||||
getSpecificCrimeFeatureName(name) ??
|
||||
getEthnicityFeatureName(name) ??
|
||||
getPoiDistanceFeatureName(name) ??
|
||||
name
|
||||
);
|
||||
}
|
||||
|
||||
function dropUnknownFilters(filters: FeatureFilters, features: FeatureMeta[]): FeatureFilters {
|
||||
|
|
@ -85,6 +110,12 @@ export function useFilters({ initialFilters, features }: UseFiltersOptions) {
|
|||
const specificCrimeFilterIdRef = useRef(
|
||||
getNextNumericKeyId(initialFiltersRef.current!, getSpecificCrimeFilterKeyId)
|
||||
);
|
||||
const ethnicityFilterIdRef = useRef(
|
||||
getNextNumericKeyId(initialFiltersRef.current!, getEthnicityFilterKeyId)
|
||||
);
|
||||
const poiDistanceFilterIdRef = useRef(
|
||||
getNextNumericKeyId(initialFiltersRef.current!, getPoiDistanceFilterKeyId)
|
||||
);
|
||||
|
||||
const enabledFeatures = useMemo(() => new Set(Object.keys(filters)), [filters]);
|
||||
|
||||
|
|
@ -117,7 +148,15 @@ export function useFilters({ initialFilters, features }: UseFiltersOptions) {
|
|||
const handleAddFilter = useCallback(
|
||||
(name: string) => {
|
||||
const meta = features.find((f) => f.name === name);
|
||||
if (name !== SCHOOL_FILTER_NAME && name !== SPECIFIC_CRIMES_FILTER_NAME && !meta) return;
|
||||
if (
|
||||
name !== SCHOOL_FILTER_NAME &&
|
||||
name !== SPECIFIC_CRIMES_FILTER_NAME &&
|
||||
name !== ETHNICITIES_FILTER_NAME &&
|
||||
!POI_FILTER_NAMES.includes(name as PoiFilterName) &&
|
||||
!meta
|
||||
) {
|
||||
return;
|
||||
}
|
||||
trackEvent('Filter Add', { feature: name });
|
||||
setFilters((prev) => {
|
||||
undoStackRef.current.push(prev);
|
||||
|
|
@ -159,6 +198,42 @@ export function useFilters({ initialFilters, features }: UseFiltersOptions) {
|
|||
],
|
||||
};
|
||||
}
|
||||
if (name === ETHNICITIES_FILTER_NAME) {
|
||||
const defaultEthnicityFeatureName = getDefaultEthnicityFeatureName(features);
|
||||
const defaultEthnicityFeature = defaultEthnicityFeatureName
|
||||
? features.find((feature) => feature.name === defaultEthnicityFeatureName)
|
||||
: undefined;
|
||||
if (!defaultEthnicityFeatureName) return prev;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[createEthnicityFilterKey(defaultEthnicityFeatureName, ethnicityFilterIdRef.current++)]:
|
||||
[
|
||||
defaultEthnicityFeature?.histogram?.min ?? defaultEthnicityFeature?.min ?? 0,
|
||||
defaultEthnicityFeature?.histogram?.max ?? defaultEthnicityFeature?.max ?? 100,
|
||||
],
|
||||
};
|
||||
}
|
||||
if (POI_FILTER_NAMES.includes(name as PoiFilterName)) {
|
||||
const poiFilterName = name as PoiFilterName;
|
||||
const defaultPoiFeatureName = getDefaultPoiFilterFeatureName(features, poiFilterName);
|
||||
const defaultPoiFeature = defaultPoiFeatureName
|
||||
? features.find((feature) => feature.name === defaultPoiFeatureName)
|
||||
: undefined;
|
||||
if (!defaultPoiFeatureName) return prev;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[createPoiFilterKey(
|
||||
poiFilterName,
|
||||
defaultPoiFeatureName,
|
||||
poiDistanceFilterIdRef.current++
|
||||
)]: [
|
||||
defaultPoiFeature?.histogram?.min ?? defaultPoiFeature?.min ?? 0,
|
||||
defaultPoiFeature?.histogram?.max ?? defaultPoiFeature?.max ?? 5,
|
||||
],
|
||||
};
|
||||
}
|
||||
if (!meta) return prev;
|
||||
if (meta.type === 'enum' && meta.values) {
|
||||
return { ...prev, [name]: [...meta.values!] };
|
||||
|
|
@ -234,6 +309,40 @@ export function useFilters({ initialFilters, features }: UseFiltersOptions) {
|
|||
if (replaced) return normalizeFilters(next);
|
||||
}
|
||||
|
||||
const ethnicityKeyId = getEthnicityFilterKeyId(name);
|
||||
if (ethnicityKeyId != null) {
|
||||
let replaced = false;
|
||||
const next: FeatureFilters = {};
|
||||
for (const [existingName, existingValue] of Object.entries(prev)) {
|
||||
if (getEthnicityFilterKeyId(existingName) === ethnicityKeyId) {
|
||||
if (!replaced) {
|
||||
next[name] = value;
|
||||
replaced = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
next[existingName] = existingValue;
|
||||
}
|
||||
if (replaced) return normalizeFilters(next);
|
||||
}
|
||||
|
||||
const poiDistanceKeyId = getPoiDistanceFilterKeyId(name);
|
||||
if (poiDistanceKeyId != null) {
|
||||
let replaced = false;
|
||||
const next: FeatureFilters = {};
|
||||
for (const [existingName, existingValue] of Object.entries(prev)) {
|
||||
if (getPoiDistanceFilterKeyId(existingName) === poiDistanceKeyId) {
|
||||
if (!replaced) {
|
||||
next[name] = value;
|
||||
replaced = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
next[existingName] = existingValue;
|
||||
}
|
||||
if (replaced) return normalizeFilters(next);
|
||||
}
|
||||
|
||||
return normalizeFilters({ ...prev, [name]: value });
|
||||
});
|
||||
}, []);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import {
|
|||
} from '../lib/api';
|
||||
import { getSchoolBackendFeatureName } from '../lib/school-filter';
|
||||
import { getSpecificCrimeFeatureName } from '../lib/crime-filter';
|
||||
import { getEthnicityFeatureName } from '../lib/ethnicity-filter';
|
||||
import { getPoiDistanceFeatureName } from '../lib/poi-distance-filter';
|
||||
import { POSTCODE_ZOOM_THRESHOLD } from '../lib/consts';
|
||||
import { COLOR_RANGE_LOW_PERCENTILE, COLOR_RANGE_HIGH_PERCENTILE } from '../lib/consts';
|
||||
import { type TravelTimeEntry } from './useTravelTime';
|
||||
|
|
@ -86,7 +88,11 @@ export function useMapData({
|
|||
const usePostcodeView = zoom >= POSTCODE_ZOOM_THRESHOLD;
|
||||
const getBackendFeatureName = useCallback(
|
||||
(name: string) =>
|
||||
getSchoolBackendFeatureName(name) ?? getSpecificCrimeFeatureName(name) ?? name,
|
||||
getSchoolBackendFeatureName(name) ??
|
||||
getSpecificCrimeFeatureName(name) ??
|
||||
getEthnicityFeatureName(name) ??
|
||||
getPoiDistanceFeatureName(name) ??
|
||||
name,
|
||||
[]
|
||||
);
|
||||
const dataViewFeature = useMemo(
|
||||
|
|
@ -279,9 +285,11 @@ export function useMapData({
|
|||
useEffect(() => {
|
||||
if (!bounds) {
|
||||
latestDataRequestKeyRef.current = '';
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
latestDataRequestKeyRef.current = dataRequestKey;
|
||||
setLoading(true);
|
||||
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current);
|
||||
|
|
@ -294,7 +302,6 @@ export function useMapData({
|
|||
abortControllerRef.current = new AbortController();
|
||||
|
||||
const requestKey = dataRequestKey;
|
||||
setLoading(true);
|
||||
try {
|
||||
if (usePostcodeView) {
|
||||
const params = new URLSearchParams({ bounds: boundsParam });
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { Step, CallBackProps } from 'react-joyride';
|
||||
import type { EventData, Step } from 'react-joyride';
|
||||
|
||||
const STORAGE_KEY = 'tutorial_completed';
|
||||
const JOYRIDE_ACTION_CLOSE = 'close';
|
||||
|
|
@ -18,43 +18,35 @@ export function useTutorial(initialLoading: boolean, isMobile: boolean, blocked
|
|||
title: t('tutorial.step1Title'),
|
||||
content: t('tutorial.step1Content'),
|
||||
placement: 'right' as const,
|
||||
disableBeacon: true,
|
||||
skipBeacon: true,
|
||||
},
|
||||
{
|
||||
target: '[data-tutorial="ai-filters"]',
|
||||
title: t('tutorial.step2Title'),
|
||||
content: t('tutorial.step2Content'),
|
||||
placement: 'right' as const,
|
||||
disableBeacon: true,
|
||||
skipBeacon: true,
|
||||
},
|
||||
{
|
||||
target: '[data-tutorial="map"]',
|
||||
target: '[data-tutorial="map-anchor"]',
|
||||
title: t('tutorial.step3Title'),
|
||||
content: t('tutorial.step3Content'),
|
||||
placement: 'bottom' as const,
|
||||
disableBeacon: true,
|
||||
placement: 'top' as const,
|
||||
skipBeacon: true,
|
||||
},
|
||||
{
|
||||
target: '[data-tutorial="search"]',
|
||||
title: t('tutorial.step4Title'),
|
||||
content: t('tutorial.step4Content'),
|
||||
placement: 'bottom' as const,
|
||||
disableBeacon: true,
|
||||
},
|
||||
{
|
||||
target: '[data-tutorial="right-pane"]',
|
||||
title: t('tutorial.step5Title'),
|
||||
content: t('tutorial.step5Content'),
|
||||
placement: 'left' as const,
|
||||
disableBeacon: true,
|
||||
skipBeacon: true,
|
||||
},
|
||||
{
|
||||
target: '[data-tutorial="poi-button"]',
|
||||
title: t('tutorial.step6Title'),
|
||||
content: t('tutorial.step6Content'),
|
||||
placement: 'left' as const,
|
||||
disableBeacon: true,
|
||||
styles: { tooltip: { transform: 'translateY(-50px)' } },
|
||||
skipBeacon: true,
|
||||
},
|
||||
],
|
||||
[t]
|
||||
|
|
@ -67,7 +59,7 @@ export function useTutorial(initialLoading: boolean, isMobile: boolean, blocked
|
|||
|
||||
const shouldRun = run && !initialLoading && !isMobile && !blocked;
|
||||
|
||||
const handleCallback = useCallback((data: CallBackProps) => {
|
||||
const handleCallback = useCallback((data: EventData) => {
|
||||
const { status, action, type } = data;
|
||||
|
||||
if (status === JOYRIDE_STATUS_FINISHED || status === JOYRIDE_STATUS_SKIPPED) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue